البدء

البدء السريع: الدفع بالبطاقة عبر صفحة الدفع المستضافة

أنشئ جلسة دفع، وادفعها ببطاقة اختبار، واعرف النتيجة من webhook. نحو عشر دقائق، في وضع الاختبار.

فتح بصيغة Markdown
في هذه الصفحة
  1. 1. اضبط مفتاح الاختبار
  2. 2. أنشئ جلسة دفع
  3. 3. أرسل عميلك إلى الصفحة
  4. 4. ادفع ببطاقة اختبار
  5. 5. أنشئ نقطة استقبال webhook
  6. 6. عالج الحدث
  7. بلا كود: رابط دفع
  8. الخطوات التالية

ينشئ خادمك جلسة دفع (checkout session)، ويدفع عميلك في صفحة الدفع المستضافة، ويعرف خادمك النتيجة من webhook. كل ما هنا يعمل في وضع الاختبار: لا تتحرك أي أموال حقيقية، وصفحة البطاقة محاكاة تحمل علامة TEST.

تحتاج إلى حساب في بيئة الاختبار تم التحقق من بريده الإلكتروني. أنشئ حسابًا، ثم أنشئ مفاتيح الاختبار.

1. اضبط مفتاح الاختبار

يبدأ مفتاحك السري للاختبار بـ apx_sk_test_. تنشئه من لوحة التحكم، في المطوّرون ← مفاتيح API، ويُعرض مرة واحدة فقط، عند إنشائه. ضعه في متغير بيئة على الجهاز الذي يشغّل الأمثلة.

تقرؤه أمثلة هذه الصفحة من APX_SECRET_KEY. وللتحقق من المفتاح أولًا، اسأل API من أنت: انظر مفاتيح API الخاصة بك.

Terminal
export APX_SECRET_KEY="apx_sk_test_…"

2. أنشئ جلسة دفع

عندما ينقر عميلك على ادفع، ينشئ خادمك جلسة دفع تتضمن:

  • amount، بالسنتيم: 25000 تساوي 250.00 MAD؛
  • currency: دائمًا mad؛
  • payment_method_types: ["card"] هنا؛ أضف "cash" ليختار عميلك؛
  • success_url وcancel_url: العنوانان اللذان تعيد إليهما الصفحة المستضافة عميلك؛
  • metadata: مراجعك الخاصة، مثل رقم الطلبية.

ترويسة Idempotency-Key إلزامية. إذا فشل الطلب بسبب الشبكة، فأرسله من جديد بالمفتاح نفسه: تحصل على الجلسة نفسها، لا على جلسة ثانية أبدًا. انظر عدم التكرار (idempotency).

تتضمن الاستجابة url الجلسة وpayment_intent الخاص بها. احفظ معرّف نية الدفع (payment intent) مع طلبيتك: يذكره الـ webhook في الخطوة 6.

  • curl
  • Node.js
  • TypeScript
  • Python
  • PHP
  • Ruby
  • Go
  • Java
  • C#
  • Rust
checkout.mjscheckout.tscheckout.pycheckout.phpcheckout.rbcheckout.goCheckout.javaCheckout.csmain.rs
curl https://pay-api.adpromax.com/v1/checkout/sessions \
  -u "$APX_SECRET_KEY:" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: checkout-order-1042" \
  -d '{
    "amount": 25000,
    "currency": "mad",
    "payment_method_types": ["card"],
    "success_url": "https://shop.example.com/thanks",
    "cancel_url": "https://shop.example.com/cart",
    "metadata": { "order_id": "1042" }
  }'
const response = await fetch('https://pay-api.adpromax.com/v1/checkout/sessions', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.APX_SECRET_KEY}`,
    'Content-Type': 'application/json',
    'Idempotency-Key': 'checkout-order-1042',
  },
  body: JSON.stringify({
    amount: 25000,
    currency: 'mad',
    payment_method_types: ['card'],
    success_url: 'https://shop.example.com/thanks',
    cancel_url: 'https://shop.example.com/cart',
    metadata: { order_id: '1042' },
  }),
})
const session = await response.json()
if (!response.ok)
  throw new Error(session.error.message)
console.log(session.payment_intent, session.url)
interface CheckoutSession {
  id: string
  object: 'checkout.session'
  amount: number
  currency: string
  payment_intent: string
  status: string
  url: string
  metadata: Record<string, string>
}

interface ApiError {
  error: { type: string, code: string, message: string, request_id: string }
}

const response = await fetch('https://pay-api.adpromax.com/v1/checkout/sessions', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.APX_SECRET_KEY}`,
    'Content-Type': 'application/json',
    'Idempotency-Key': 'checkout-order-1042',
  },
  body: JSON.stringify({
    amount: 25000,
    currency: 'mad',
    payment_method_types: ['card'],
    success_url: 'https://shop.example.com/thanks',
    cancel_url: 'https://shop.example.com/cart',
    metadata: { order_id: '1042' },
  }),
})
if (!response.ok) {
  const { error } = (await response.json()) as ApiError
  throw new Error(`${error.code}: ${error.message}`)
}
const session = (await response.json()) as CheckoutSession
console.log(session.payment_intent, session.url)
import os

import requests

response = requests.post(
    "https://pay-api.adpromax.com/v1/checkout/sessions",
    auth=(os.environ["APX_SECRET_KEY"], ""),
    headers={"Idempotency-Key": "checkout-order-1042"},
    json={
        "amount": 25000,
        "currency": "mad",
        "payment_method_types": ["card"],
        "success_url": "https://shop.example.com/thanks",
        "cancel_url": "https://shop.example.com/cart",
        "metadata": {"order_id": "1042"},
    },
)
session = response.json()
if not response.ok:
    raise RuntimeError(session["error"]["message"])
print(session["payment_intent"], session["url"])
<?php
$ch = curl_init('https://pay-api.adpromax.com/v1/checkout/sessions');
curl_setopt_array($ch, [
    CURLOPT_USERPWD => getenv('APX_SECRET_KEY') . ':',
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => [
        'Content-Type: application/json',
        'Idempotency-Key: checkout-order-1042',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'amount' => 25000,
        'currency' => 'mad',
        'payment_method_types' => ['card'],
        'success_url' => 'https://shop.example.com/thanks',
        'cancel_url' => 'https://shop.example.com/cart',
        'metadata' => ['order_id' => '1042'],
    ]),
    CURLOPT_RETURNTRANSFER => true,
]);
$session = json_decode(curl_exec($ch), true);
if (curl_getinfo($ch, CURLINFO_RESPONSE_CODE) >= 400) {
    throw new RuntimeException($session['error']['message']);
}
echo $session['payment_intent'], ' ', $session['url'], PHP_EOL;
require 'json'
require 'net/http'

uri = URI('https://pay-api.adpromax.com/v1/checkout/sessions')
request = Net::HTTP::Post.new(uri)
request.basic_auth(ENV.fetch('APX_SECRET_KEY'), '')
request['Content-Type'] = 'application/json'
request['Idempotency-Key'] = 'checkout-order-1042'
request.body = JSON.generate(
  amount: 25000,
  currency: 'mad',
  payment_method_types: ['card'],
  success_url: 'https://shop.example.com/thanks',
  cancel_url: 'https://shop.example.com/cart',
  metadata: { order_id: '1042' }
)

response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
  http.request(request)
end
session = JSON.parse(response.body)
raise session.dig('error', 'message') unless response.is_a?(Net::HTTPSuccess)

puts "#{session['payment_intent']} #{session['url']}"
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"os"
)

type CheckoutSession struct {
	ID            string `json:"id"`
	PaymentIntent string `json:"payment_intent"`
	Status        string `json:"status"`
	URL           string `json:"url"`
}

type APIError struct {
	Error struct {
		Code    string `json:"code"`
		Message string `json:"message"`
	} `json:"error"`
}

func main() {
	body, err := json.Marshal(map[string]any{
		"amount":               25000,
		"currency":             "mad",
		"payment_method_types": []string{"card"},
		"success_url":          "https://shop.example.com/thanks",
		"cancel_url":           "https://shop.example.com/cart",
		"metadata":             map[string]string{"order_id": "1042"},
	})
	if err != nil {
		log.Fatal(err)
	}
	req, err := http.NewRequest(http.MethodPost,
		"https://pay-api.adpromax.com/v1/checkout/sessions", bytes.NewReader(body))
	if err != nil {
		log.Fatal(err)
	}
	req.SetBasicAuth(os.Getenv("APX_SECRET_KEY"), "")
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Idempotency-Key", "checkout-order-1042")

	res, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer res.Body.Close()

	if res.StatusCode >= 400 {
		var apiErr APIError
		json.NewDecoder(res.Body).Decode(&apiErr)
		log.Fatal(apiErr.Error.Message)
	}
	var session CheckoutSession
	if err := json.NewDecoder(res.Body).Decode(&session); err != nil {
		log.Fatal(err)
	}
	fmt.Println(session.PaymentIntent, session.URL)
}
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class Checkout {
    public static void main(String[] args) throws Exception {
        String body = """
            {
              "amount": 25000,
              "currency": "mad",
              "payment_method_types": ["card"],
              "success_url": "https://shop.example.com/thanks",
              "cancel_url": "https://shop.example.com/cart",
              "metadata": { "order_id": "1042" }
            }
            """;
        HttpRequest request = HttpRequest.newBuilder(
                URI.create("https://pay-api.adpromax.com/v1/checkout/sessions"))
            .header("Authorization", "Bearer " + System.getenv("APX_SECRET_KEY"))
            .header("Content-Type", "application/json")
            .header("Idempotency-Key", "checkout-order-1042")
            .POST(HttpRequest.BodyPublishers.ofString(body))
            .build();
        HttpResponse<String> response = HttpClient.newHttpClient()
            .send(request, HttpResponse.BodyHandlers.ofString());
        if (response.statusCode() >= 400) {
            throw new IllegalStateException(response.body());
        }
        // The session as JSON: read payment_intent and url with a JSON library.
        System.out.println(response.statusCode() + " " + response.body());
    }
}
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;

var json = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower };
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
    "Bearer", Environment.GetEnvironmentVariable("APX_SECRET_KEY"));

using var request = new HttpRequestMessage(
    HttpMethod.Post, "https://pay-api.adpromax.com/v1/checkout/sessions");
request.Headers.Add("Idempotency-Key", "checkout-order-1042");
request.Content = JsonContent.Create(new
{
    amount = 25000,
    currency = "mad",
    payment_method_types = new[] { "card" },
    success_url = "https://shop.example.com/thanks",
    cancel_url = "https://shop.example.com/cart",
    metadata = new { order_id = "1042" },
});

using var response = await http.SendAsync(request);
if (!response.IsSuccessStatusCode)
{
    var failure = await response.Content.ReadFromJsonAsync<ApiError>(json);
    throw new InvalidOperationException(failure?.Error.Message);
}
var session = await response.Content.ReadFromJsonAsync<CheckoutSession>(json);
Console.WriteLine($"{session!.PaymentIntent} {session.Url}");

record CheckoutSession(string Id, string PaymentIntent, string Status, string Url);
record ApiError(ApiErrorBody Error);
record ApiErrorBody(string Code, string Message);
// Cargo.toml: reqwest = { version = "0.12", features = ["blocking", "json"] }
//             serde = { version = "1", features = ["derive"] }
//             serde_json = "1"
use serde::Deserialize;
use serde_json::json;

#[derive(Deserialize)]
struct CheckoutSession {
    payment_intent: String,
    url: String,
}

#[derive(Deserialize)]
struct ApiError {
    error: ErrorBody,
}

#[derive(Deserialize)]
struct ErrorBody {
    message: String,
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let response = reqwest::blocking::Client::new()
        .post("https://pay-api.adpromax.com/v1/checkout/sessions")
        .bearer_auth(std::env::var("APX_SECRET_KEY")?)
        .header("Idempotency-Key", "checkout-order-1042")
        .json(&json!({
            "amount": 25000,
            "currency": "mad",
            "payment_method_types": ["card"],
            "success_url": "https://shop.example.com/thanks",
            "cancel_url": "https://shop.example.com/cart",
            "metadata": { "order_id": "1042" }
        }))
        .send()?;
    if !response.status().is_success() {
        let failure: ApiError = response.json()?;
        return Err(failure.error.message.into());
    }
    let session: CheckoutSession = response.json()?;
    println!("{} {}", session.payment_intent, session.url);
    Ok(())
}
الاستجابة200 OK
{
  "id": "cs_01k7d2h6w4q9m3x8v5c2n7b1rt",
  "amount": 25000,
  "currency": "mad",
  "payment_method_types": ["card"],
  "payment_intent": "pi_01k7d2h6w0f3r8t5y2v9n4p6qa",
  "status": "open",
  "url": "https://pay.example.com/c/cs_01k7d2h6w4q9m3x8v5c2n7b1rt#…",
  "success_url": "https://shop.example.com/thanks",
  "cancel_url": "https://shop.example.com/cart",
  "expires_at": 1790937600,
  "metadata": { "order_id": "1042" },
  "livemode": false
}

3. أرسل عميلك إلى الصفحة

أعد توجيه متصفح عميلك إلى url الجلسة، برمز HTTP 303. احتفظ بالعنوان كاملًا، بما فيه الجزء الذي يلي #: فهو وسيلة وصول عميلك إلى هذه الجلسة.

تعرض الصفحة المستضافة المبلغ واسم نشاطك التجاري وطرق الدفع. تبقى الجلسة مفتوحة 24 ساعة ما لم تحدد expires_at (بين 30 دقيقة و24 ساعة).

  • curl
  • Node.js
  • TypeScript
  • Python
  • PHP
  • Ruby
  • Go
  • Java
  • C#
  • Rust
express.mjsexpress.tsapp.pycheckout.phpapp.rbhandler.goCheckoutController.javaProgram.csmain.rs
# curl does not follow your customer: read where your server sends them.
curl -si https://shop.example.com/checkout | grep -i '^location:'
res.redirect(303, session.url)
app.post('/checkout', async (req: Request, res: Response) => {
  const session = await createCheckoutSession(req.body.orderId)
  res.redirect(303, session.url)
})
return redirect(session["url"], code=303)
header('Location: ' . $session['url'], true, 303);
redirect session['url'], 303
http.Redirect(w, r, session.URL, http.StatusSeeOther)
return ResponseEntity.status(HttpStatus.SEE_OTHER)
    .location(URI.create(session.url()))
    .build();
// Results.Redirect answers 302: set the 303 yourself.
context.Response.Headers.Location = session.Url;
context.Response.StatusCode = StatusCodes.Status303SeeOther;
// axum's Redirect::to answers 303 See Other.
Redirect::to(&session.url)

4. ادفع ببطاقة اختبار

افتح url الجلسة في متصفح واختر البطاقة. صفحة الدفع في وضع الاختبار محاكاة تحمل علامة TEST؛ اكتب فيها رقم البطاقة هذا. وإذا طلبت تاريخ انتهاء أو رمز أمان، فاستخدم أي تاريخ في المستقبل وأي ثلاثة أرقام.

بعد الدفع، تعيدك الصفحة إلى success_url.

أرقام أخرى تُرفض، أو تطلب 3-D Secure، أو تجيب ببطء: انظر بطاقات الاختبار. وللدفع دون متصفح، في الاختبارات الآلية، استخدم أدوات الاختبار المساعدة.

العودة إلى success_url ليست دليلًا على الدفع: قد يغلق عميلك علامة التبويب قبل العودة، أو يفتح ذلك العنوان يدويًا. نفّذ الطلبية عند وصول الـ webhook في الخطوات التالية، أو بعد قراءة status نية الدفع.

رقم البطاقةتاريخ الانتهاءأي تاريخ في المستقبلCVCأي 3 أرقامالسيناريوالنتيجة
4242 4242 4242 4242تاريخ الانتهاء12/34CVC123نجاح

تنجح فورًا.

5. أنشئ نقطة استقبال webhook

عند نجاح الدفعة، يرسل AdProMaxPay الحدث payment_intent.succeeded إلى نقطة استقبال الـ webhook لديك. أنشئ نقطة الاستقبال مرة واحدة، لوضع الاختبار، من لوحة التحكم (المطوّرون ← Webhooks) أو عبر API كما هنا. تعرض الاستجابة سرّ توقيع نقطة الاستقبال (apx_whsec_…) هذه المرة فقط: احفظه في APX_WEBHOOK_SECRET.

يجب أن يكون العنوان عامًا وأن يستخدم https. ولاستقبال الأحداث على حاسوبك، اجعله متاحًا عبر نفق HTTPS واستخدم عنوان النفق. استبدل shop.example.com في المثال بمضيفك: يُرفض العنوان الذي لا يمكن حل اسم مضيفه بالرمز url_not_allowed.

  • curl
  • Node.js
  • TypeScript
  • Python
  • PHP
  • Ruby
  • Go
  • Java
  • C#
  • Rust
endpoint.mjsendpoint.tsendpoint.pyendpoint.phpendpoint.rbendpoint.goEndpoint.javaEndpoint.csmain.rs
curl https://pay-api.adpromax.com/v1/webhook_endpoints \
  -u "$APX_SECRET_KEY:" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://shop.example.com/webhooks/payments",
    "enabled_events": [
      "payment_intent.succeeded",
      "payment_intent.payment_failed"
    ]
  }'
const response = await fetch('https://pay-api.adpromax.com/v1/webhook_endpoints', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.APX_SECRET_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    url: 'https://shop.example.com/webhooks/payments',
    enabled_events: [
      'payment_intent.succeeded',
      'payment_intent.payment_failed',
    ],
  }),
})
const endpoint = await response.json()
if (!response.ok)
  throw new Error(endpoint.error.message)
// The secret is shown this once: store it.
console.log(endpoint.id, endpoint.secret)
interface WebhookEndpoint {
  id: string
  object: 'webhook_endpoint'
  url: string
  enabled_events: string[]
  status: 'enabled' | 'disabled'
  livemode: boolean
  /** Only in the answer that created the endpoint. */
  secret?: string
}

interface ApiError {
  error: { type: string, code: string, message: string, request_id: string }
}

const response = await fetch('https://pay-api.adpromax.com/v1/webhook_endpoints', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.APX_SECRET_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    url: 'https://shop.example.com/webhooks/payments',
    enabled_events: [
      'payment_intent.succeeded',
      'payment_intent.payment_failed',
    ],
  }),
})
if (!response.ok) {
  const { error } = (await response.json()) as ApiError
  throw new Error(`${error.code}: ${error.message}`)
}
const endpoint = (await response.json()) as WebhookEndpoint
// The secret is shown this once: store it.
console.log(endpoint.id, endpoint.secret)
import os

import requests

response = requests.post(
    "https://pay-api.adpromax.com/v1/webhook_endpoints",
    auth=(os.environ["APX_SECRET_KEY"], ""),
    json={
        "url": "https://shop.example.com/webhooks/payments",
        "enabled_events": [
            "payment_intent.succeeded",
            "payment_intent.payment_failed",
        ],
    },
)
endpoint = response.json()
if not response.ok:
    raise RuntimeError(endpoint["error"]["message"])
# The secret is shown this once: store it.
print(endpoint["id"], endpoint["secret"])
<?php
$ch = curl_init('https://pay-api.adpromax.com/v1/webhook_endpoints');
curl_setopt_array($ch, [
    CURLOPT_USERPWD => getenv('APX_SECRET_KEY') . ':',
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
    CURLOPT_POSTFIELDS => json_encode([
        'url' => 'https://shop.example.com/webhooks/payments',
        'enabled_events' => [
            'payment_intent.succeeded',
            'payment_intent.payment_failed',
        ],
    ]),
    CURLOPT_RETURNTRANSFER => true,
]);
$endpoint = json_decode(curl_exec($ch), true);
if (curl_getinfo($ch, CURLINFO_RESPONSE_CODE) >= 400) {
    throw new RuntimeException($endpoint['error']['message']);
}
// The secret is shown this once: store it.
echo $endpoint['id'], ' ', $endpoint['secret'], PHP_EOL;
require 'json'
require 'net/http'

uri = URI('https://pay-api.adpromax.com/v1/webhook_endpoints')
request = Net::HTTP::Post.new(uri)
request.basic_auth(ENV.fetch('APX_SECRET_KEY'), '')
request['Content-Type'] = 'application/json'
request.body = JSON.generate(
  url: 'https://shop.example.com/webhooks/payments',
  enabled_events: [
    'payment_intent.succeeded',
    'payment_intent.payment_failed'
  ]
)

response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
  http.request(request)
end
endpoint = JSON.parse(response.body)
raise endpoint.dig('error', 'message') unless response.is_a?(Net::HTTPSuccess)

# The secret is shown this once: store it.
puts "#{endpoint['id']} #{endpoint['secret']}"
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"os"
)

type WebhookEndpoint struct {
	ID            string   `json:"id"`
	URL           string   `json:"url"`
	EnabledEvents []string `json:"enabled_events"`
	Status        string   `json:"status"`
	Secret        string   `json:"secret"`
}

type APIError struct {
	Error struct {
		Code    string `json:"code"`
		Message string `json:"message"`
	} `json:"error"`
}

func main() {
	body, err := json.Marshal(map[string]any{
		"url": "https://shop.example.com/webhooks/payments",
		"enabled_events": []string{
			"payment_intent.succeeded",
			"payment_intent.payment_failed",
		},
	})
	if err != nil {
		log.Fatal(err)
	}
	req, err := http.NewRequest(http.MethodPost,
		"https://pay-api.adpromax.com/v1/webhook_endpoints", bytes.NewReader(body))
	if err != nil {
		log.Fatal(err)
	}
	req.SetBasicAuth(os.Getenv("APX_SECRET_KEY"), "")
	req.Header.Set("Content-Type", "application/json")

	res, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer res.Body.Close()

	if res.StatusCode >= 400 {
		var apiErr APIError
		json.NewDecoder(res.Body).Decode(&apiErr)
		log.Fatal(apiErr.Error.Message)
	}
	var endpoint WebhookEndpoint
	if err := json.NewDecoder(res.Body).Decode(&endpoint); err != nil {
		log.Fatal(err)
	}
	// The secret is shown this once: store it.
	fmt.Println(endpoint.ID, endpoint.Secret)
}
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class Endpoint {
    public static void main(String[] args) throws Exception {
        String body = """
            {
              "url": "https://shop.example.com/webhooks/payments",
              "enabled_events": [
                "payment_intent.succeeded",
                "payment_intent.payment_failed"
              ]
            }
            """;
        HttpRequest request = HttpRequest.newBuilder(
                URI.create("https://pay-api.adpromax.com/v1/webhook_endpoints"))
            .header("Authorization", "Bearer " + System.getenv("APX_SECRET_KEY"))
            .header("Content-Type", "application/json")
            .POST(HttpRequest.BodyPublishers.ofString(body))
            .build();
        HttpResponse<String> response = HttpClient.newHttpClient()
            .send(request, HttpResponse.BodyHandlers.ofString());
        if (response.statusCode() >= 400) {
            throw new IllegalStateException(response.body());
        }
        // The secret is shown this once: store it.
        System.out.println(response.statusCode() + " " + response.body());
    }
}
using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json;

var json = new JsonSerializerOptions { PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower };
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
    "Bearer", Environment.GetEnvironmentVariable("APX_SECRET_KEY"));

using var response = await http.PostAsJsonAsync(
    "https://pay-api.adpromax.com/v1/webhook_endpoints",
    new
    {
        url = "https://shop.example.com/webhooks/payments",
        enabled_events = new[]
        {
            "payment_intent.succeeded",
            "payment_intent.payment_failed",
        },
    });
if (!response.IsSuccessStatusCode)
{
    var failure = await response.Content.ReadFromJsonAsync<ApiError>(json);
    throw new InvalidOperationException(failure?.Error.Message);
}
var endpoint = await response.Content.ReadFromJsonAsync<WebhookEndpoint>(json);
// The secret is shown this once: store it.
Console.WriteLine($"{endpoint!.Id} {endpoint.Secret}");

record WebhookEndpoint(
    string Id, string Url, string[] EnabledEvents, string Status, string? Secret);
record ApiError(ApiErrorBody Error);
record ApiErrorBody(string Code, string Message);
// Cargo.toml: reqwest = { version = "0.12", features = ["blocking", "json"] }
//             serde = { version = "1", features = ["derive"] }
//             serde_json = "1"
use serde::Deserialize;
use serde_json::json;

#[derive(Deserialize)]
struct WebhookEndpoint {
    id: String,
    /// Only in the answer that created the endpoint.
    secret: Option<String>,
}

#[derive(Deserialize)]
struct ApiError {
    error: ErrorBody,
}

#[derive(Deserialize)]
struct ErrorBody {
    message: String,
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let response = reqwest::blocking::Client::new()
        .post("https://pay-api.adpromax.com/v1/webhook_endpoints")
        .bearer_auth(std::env::var("APX_SECRET_KEY")?)
        .json(&json!({
            "url": "https://shop.example.com/webhooks/payments",
            "enabled_events": [
                "payment_intent.succeeded",
                "payment_intent.payment_failed"
            ]
        }))
        .send()?;
    if !response.status().is_success() {
        let failure: ApiError = response.json()?;
        return Err(failure.error.message.into());
    }
    let endpoint: WebhookEndpoint = response.json()?;
    // The secret is shown this once: store it.
    println!("{} {}", endpoint.id, endpoint.secret.unwrap_or_default());
    Ok(())
}
الاستجابة200 OK
{
  "id": "we_01k7d3a9r5t2y8v4j6p1p3s7dc",
  "object": "webhook_endpoint",
  "url": "https://shop.example.com/webhooks/payments",
  "description": null,
  "enabled_events": [
    "payment_intent.succeeded",
    "payment_intent.payment_failed"
  ],
  "api_version": "2026-11-01",
  "status": "enabled",
  "livemode": false,
  "metadata": {},
  "created": 1790851200,
  "secret": "apx_whsec_…"
}

6. عالج الحدث

يتحقق معالجك من التوقيع على نص الطلب الخام، ثم ينفّذ الطلبية التي تحمل معرّف نية الدفع. تأتي الدالة verifySignature من التحقق من التوقيعات.

أجب بـ 200 سريعًا، ثم نفّذ العمل البطيء. قد يصل الحدث أكثر من مرة: انظر إعادة المحاولة والترتيب وإزالة التكرار.

  • curl
  • Node.js
  • TypeScript
  • Python
  • PHP
  • Ruby
  • Go
  • Java
  • C#
  • Rust
webhook.mjswebhook.tswebhook.pywebhook.phpwebhook.rbwebhook.goWebhook.javaProgram.csmain.rs
# curl receives nothing. From a terminal, send your handler a signed
# test delivery: 200 means it verified the signature.
SECRET="$APX_WEBHOOK_SECRET"
BODY='{"id":"evt_handler_test","object":"event","type":"payment_intent.succeeded","data":{"object":{"id":"pi_handler_test"}}}'
T=$(date +%s)
SIG=$(printf '%s.%s' "$T" "$BODY" \
  | openssl dgst -sha256 -hmac "$SECRET" | sed 's/^.*= //')

curl -i http://localhost:4242/webhooks/payments \
  -H "Content-Type: application/json" \
  -H "X-AdProMaxPay-Signature: t=$T,v1=$SIG" \
  --data-binary "$BODY"
import { Buffer } from 'node:buffer'
import { createServer } from 'node:http'
import { verifySignature } from './verify-signature.mjs'

createServer(async (request, response) => {
  const chunks = []
  for await (const chunk of request)
    chunks.push(chunk)
  // The exact bytes: verify them before parsing.
  const rawBody = Buffer.concat(chunks)
  const header = request.headers['x-adpromaxpay-signature'] ?? ''
  const secret = process.env.APX_WEBHOOK_SECRET
  if (!verifySignature(rawBody, header, secret)) {
    response.writeHead(400).end()
    return
  }
  const event = JSON.parse(rawBody.toString('utf8'))
  if (event.type === 'payment_intent.succeeded') {
    const paymentIntent = event.data.object
    // Fulfil the order that holds this payment intent, once.
    console.log('paid', paymentIntent.id)
  }
  response.writeHead(200).end()
}).listen(4242)
import { Buffer } from 'node:buffer'
import { createServer } from 'node:http'
import { verifySignature } from './verify-signature.ts'

interface WebhookEvent {
  id: string
  type: string
  data: { object: { id: string, status: string } }
}

createServer(async (request, response) => {
  const chunks: Buffer[] = []
  for await (const chunk of request)
    chunks.push(chunk as Buffer)
  // The exact bytes: verify them before parsing.
  const rawBody = Buffer.concat(chunks)
  const header = request.headers['x-adpromaxpay-signature']
  const secret = process.env.APX_WEBHOOK_SECRET ?? ''
  if (typeof header !== 'string' || !verifySignature(rawBody, header, secret)) {
    response.writeHead(400).end()
    return
  }
  const event = JSON.parse(rawBody.toString('utf8')) as WebhookEvent
  if (event.type === 'payment_intent.succeeded') {
    const paymentIntent = event.data.object
    // Fulfil the order that holds this payment intent, once.
    console.log('paid', paymentIntent.id)
  }
  response.writeHead(200).end()
}).listen(4242)
import json
import os
from http.server import BaseHTTPRequestHandler, HTTPServer

from verify_signature import verify_signature


class Webhook(BaseHTTPRequestHandler):
    def do_POST(self):
        # The exact bytes: verify them before parsing.
        length = int(self.headers.get("Content-Length", 0))
        raw_body = self.rfile.read(length)
        header = self.headers.get("X-AdProMaxPay-Signature", "")
        secret = os.environ["APX_WEBHOOK_SECRET"]
        if not verify_signature(raw_body, header, secret):
            self.send_response(400)
            self.end_headers()
            return
        event = json.loads(raw_body)
        if event["type"] == "payment_intent.succeeded":
            payment_intent = event["data"]["object"]
            # Fulfil the order that holds this payment intent, once.
            print("paid", payment_intent["id"], flush=True)
        self.send_response(200)
        self.end_headers()


HTTPServer(("", 4242), Webhook).serve_forever()
<?php
require_once __DIR__ . '/verify_signature.php';

// The exact bytes: verify them before parsing.
$rawBody = file_get_contents('php://input');
$header = $_SERVER['HTTP_X_ADPROMAXPAY_SIGNATURE'] ?? '';
$secret = getenv('APX_WEBHOOK_SECRET');
if (!verify_signature($rawBody, $header, $secret)) {
    http_response_code(400);
    exit;
}
$event = json_decode($rawBody, true);
if ($event['type'] === 'payment_intent.succeeded') {
    $paymentIntent = $event['data']['object'];
    // Fulfil the order that holds this payment intent, once.
    error_log('paid ' . $paymentIntent['id']);
}
http_response_code(200);
require 'json'
require 'webrick' # Ruby 3.0 or later: gem install webrick
require_relative 'verify_signature'

server = WEBrick::HTTPServer.new(Port: 4242)
server.mount_proc '/webhooks/payments' do |request, response|
  # The exact bytes: verify them before parsing.
  raw_body = request.body.to_s
  header = request['X-AdProMaxPay-Signature'].to_s
  secret = ENV.fetch('APX_WEBHOOK_SECRET')
  unless verify_signature(raw_body, header, secret)
    response.status = 400
    next
  end
  event = JSON.parse(raw_body)
  if event['type'] == 'payment_intent.succeeded'
    payment_intent = event['data']['object']
    # Fulfil the order that holds this payment intent, once.
    puts "paid #{payment_intent['id']}"
  end
  response.status = 200
end
trap('INT') { server.shutdown }
server.start
package main

import (
	"encoding/json"
	"io"
	"log"
	"net/http"
	"os"
	"time"
)

type Event struct {
	Type string `json:"type"`
	Data struct {
		Object struct {
			ID string `json:"id"`
		} `json:"object"`
	} `json:"data"`
}

func webhook(w http.ResponseWriter, r *http.Request) {
	// The exact bytes: verify them before parsing.
	rawBody, err := io.ReadAll(r.Body)
	if err != nil {
		w.WriteHeader(http.StatusBadRequest)
		return
	}
	header := r.Header.Get("X-AdProMaxPay-Signature")
	secret := os.Getenv("APX_WEBHOOK_SECRET")
	if !VerifySignature(rawBody, header, secret, time.Now()) {
		w.WriteHeader(http.StatusBadRequest)
		return
	}
	var event Event
	if err := json.Unmarshal(rawBody, &event); err != nil {
		w.WriteHeader(http.StatusBadRequest)
		return
	}
	if event.Type == "payment_intent.succeeded" {
		// Fulfil the order that holds this payment intent, once.
		log.Println("paid", event.Data.Object.ID)
	}
	w.WriteHeader(http.StatusOK)
}

func main() {
	http.HandleFunc("POST /webhooks/payments", webhook)
	log.Fatal(http.ListenAndServe(":4242", nil))
}
import com.sun.net.httpserver.HttpServer;
import java.net.InetSocketAddress;
import java.security.GeneralSecurityException;

public class Webhook {
    public static void main(String[] args) throws Exception {
        String secret = System.getenv("APX_WEBHOOK_SECRET");
        HttpServer server = HttpServer.create(new InetSocketAddress(4242), 0);
        server.createContext("/webhooks/payments", exchange -> {
            // The exact bytes: verify them before parsing.
            byte[] rawBody = exchange.getRequestBody().readAllBytes();
            String header = exchange.getRequestHeaders()
                .getFirst("X-AdProMaxPay-Signature");
            boolean valid;
            try {
                valid = header != null
                    && WebhookSignature.verify(rawBody, header, secret);
            } catch (GeneralSecurityException e) {
                valid = false;
            }
            if (valid) {
                // Read rawBody with your JSON library; on
                // payment_intent.succeeded, fulfil the order once.
                System.out.println("verified " + rawBody.length + " bytes");
            }
            exchange.sendResponseHeaders(valid ? 200 : 400, -1);
            exchange.close();
        });
        server.start();
    }
}
// dotnet new web, with WebhookSignature.cs beside this file.
using System.Text.Json;

var app = WebApplication.Create(args);
var secret = Environment.GetEnvironmentVariable("APX_WEBHOOK_SECRET")!;

app.MapPost("/webhooks/payments", async (HttpRequest request) =>
{
    // The exact bytes: verify them before parsing.
    using var buffer = new MemoryStream();
    await request.Body.CopyToAsync(buffer);
    var rawBody = buffer.ToArray();
    var header = request.Headers["X-AdProMaxPay-Signature"].ToString();
    if (!WebhookSignature.Verify(rawBody, header, secret))
        return Results.BadRequest();

    using var json = JsonDocument.Parse(rawBody);
    var root = json.RootElement;
    if (root.GetProperty("type").GetString() == "payment_intent.succeeded")
    {
        var paymentIntent = root.GetProperty("data").GetProperty("object");
        // Fulfil the order that holds this payment intent, once.
        Console.WriteLine($"paid {paymentIntent.GetProperty("id").GetString()}");
    }
    return Results.Ok();
});

app.Run("http://localhost:4242");
// Cargo.toml: axum = "0.8", tokio = { version = "1", features = ["full"] },
//             serde_json = "1", hmac = "0.12", sha2 = "0.10", hex = "0.4"
mod signature; // signature.rs, from "Verifying signatures"

use std::time::{SystemTime, UNIX_EPOCH};

use axum::body::Bytes;
use axum::http::{HeaderMap, StatusCode};
use axum::routing::post;
use axum::Router;

async fn webhook(headers: HeaderMap, body: Bytes) -> StatusCode {
    // The exact bytes: verify them before parsing.
    let header = headers
        .get("x-adpromaxpay-signature")
        .and_then(|value| value.to_str().ok())
        .unwrap_or("");
    let secret = std::env::var("APX_WEBHOOK_SECRET").unwrap_or_default();
    let now = SystemTime::now()
        .duration_since(UNIX_EPOCH)
        .map_or(0, |elapsed| elapsed.as_secs() as i64);
    if !signature::verify_signature(&body, header, &secret, now) {
        return StatusCode::BAD_REQUEST;
    }
    let Ok(event) = serde_json::from_slice::<serde_json::Value>(&body) else {
        return StatusCode::BAD_REQUEST;
    };
    if event["type"] == "payment_intent.succeeded" {
        let id = event["data"]["object"]["id"].as_str().unwrap_or_default();
        // Fulfil the order that holds this payment intent, once.
        println!("paid {id}");
    }
    StatusCode::OK
}

#[tokio::main]
async fn main() {
    let app = Router::new().route("/webhooks/payments", post(webhook));
    let listener = tokio::net::TcpListener::bind("0.0.0.0:4242")
        .await
        .expect("port 4242 is free");
    axum::serve(listener, app).await.expect("the server runs");
}

بلا كود: رابط دفع

يُنشأ رابط الدفع من لوحة التحكم، في روابط الدفع ← جديد: عنوان ومبلغ وطرق الدفع. شاركه كرابط أو رمز QR أو رسالة واتساب. كل من يفتحه يحصل على الصفحة المستضافة نفسها وعلى دفعة خاصة به، ويتلقى الـ webhook لديك الأحداث نفسها.

في وضع الاختبار، لا يقبل الرابط إلا بطاقات الاختبار المذكورة في الخطوة 4.

الخطوات التالية