المدفوعات

البيانات الوصفية

أرفق مراجعك الخاصة بالمدفوعات والجلسات والعملاء وعمليات الاسترداد، واعثر عليها من جديد في إشعارات webhook وفي لوحة التحكم.

فتح بصيغة Markdown
في هذه الصفحة
  1. أضف بيانات وصفية إلى دفعة
  2. الحدود
  3. الكائنات التي تقبل البيانات الوصفية
  4. تبقى البيانات الوصفية حيث وضعتها
  5. أين تراها
  6. ما لا يوضع أبدًا في البيانات الوصفية

البيانات الوصفية (metadata) مجموعة من مفاتيحك وقيمك الخاصة على كائن: رقم طلبية، أو معرّف سلة، أو المتجر أو القناة التي جاء منها البيع. يخزّنها AdProMaxPay ويعيدها كما هي أينما ظهر الكائن. ولا تغيّر أبدًا طريقة عمل الدفعة.

أضف بيانات وصفية إلى دفعة

أرسل metadata ككائن من السلاسل النصية عند إنشاء نية الدفع (payment intent). تعود مراجعك عندئذ في استجابات API وفي كل حدث يخص هذه الدفعة: عندما يتلقى الـ webhook لديك payment_intent.succeeded، يذكر data.object.metadata.order_id الطلبية المراد تنفيذها.

  • curl
  • Node.js
  • TypeScript
  • Python
  • PHP
  • Ruby
  • Go
  • Java
  • C#
  • Rust
metadata.mjsmetadata.tsmetadata.pymetadata.phpmetadata.rbmetadata.goMetadata.javaMetadata.csmain.rs
curl https://pay-api.adpromax.com/v1/payment_intents \
  -u "$APX_SECRET_KEY:" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-1045-create" \
  -d '{
    "amount": 25000,
    "currency": "mad",
    "payment_method_types": ["card", "cash"],
    "metadata": { "order_id": "1045", "channel": "whatsapp" }
  }'
const response = await fetch('https://pay-api.adpromax.com/v1/payment_intents', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.APX_SECRET_KEY}`,
    'Content-Type': 'application/json',
    'Idempotency-Key': 'order-1045-create',
  },
  body: JSON.stringify({
    amount: 25000,
    currency: 'mad',
    payment_method_types: ['card', 'cash'],
    metadata: { order_id: '1045', channel: 'whatsapp' },
  }),
})
const intent = await response.json()
if (!response.ok)
  throw new Error(intent.error.message)
console.log(intent.id, intent.metadata.order_id)
interface PaymentIntent {
  id: string
  object: 'payment_intent'
  amount: number
  currency: string
  status: 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/payment_intents', {
  method: 'POST',
  headers: {
    'Authorization': `Bearer ${process.env.APX_SECRET_KEY}`,
    'Content-Type': 'application/json',
    'Idempotency-Key': 'order-1045-create',
  },
  body: JSON.stringify({
    amount: 25000,
    currency: 'mad',
    payment_method_types: ['card', 'cash'],
    metadata: { order_id: '1045', channel: 'whatsapp' },
  }),
})
if (!response.ok) {
  const { error } = (await response.json()) as ApiError
  throw new Error(`${error.code}: ${error.message}`)
}
const intent = (await response.json()) as PaymentIntent
console.log(intent.id, intent.metadata.order_id)
import os

import requests

response = requests.post(
    "https://pay-api.adpromax.com/v1/payment_intents",
    auth=(os.environ["APX_SECRET_KEY"], ""),
    headers={"Idempotency-Key": "order-1045-create"},
    json={
        "amount": 25000,
        "currency": "mad",
        "payment_method_types": ["card", "cash"],
        "metadata": {"order_id": "1045", "channel": "whatsapp"},
    },
)
intent = response.json()
response.raise_for_status()
print(intent["id"], intent["metadata"]["order_id"])
<?php
$ch = curl_init('https://pay-api.adpromax.com/v1/payment_intents');
curl_setopt_array($ch, [
    CURLOPT_USERPWD => getenv('APX_SECRET_KEY') . ':',
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => [
        'Content-Type: application/json',
        'Idempotency-Key: order-1045-create',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'amount' => 25000,
        'currency' => 'mad',
        'payment_method_types' => ['card', 'cash'],
        'metadata' => ['order_id' => '1045', 'channel' => 'whatsapp'],
    ]),
    CURLOPT_RETURNTRANSFER => true,
]);
$intent = json_decode(curl_exec($ch), true);
echo $intent['id'], ' ', $intent['metadata']['order_id'], PHP_EOL;
require 'json'
require 'net/http'

uri = URI('https://pay-api.adpromax.com/v1/payment_intents')
request = Net::HTTP::Post.new(uri)
request.basic_auth(ENV.fetch('APX_SECRET_KEY'), '')
request['Content-Type'] = 'application/json'
request['Idempotency-Key'] = 'order-1045-create'
request.body = JSON.generate(
  amount: 25000,
  currency: 'mad',
  payment_method_types: %w[card cash],
  metadata: { order_id: '1045', channel: 'whatsapp' }
)

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

puts "#{intent['id']} #{intent['metadata']['order_id']}"
package main

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

type PaymentIntent struct {
	ID       string            `json:"id"`
	Status   string            `json:"status"`
	Metadata map[string]string `json:"metadata"`
}

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

func main() {
	metadata := map[string]string{"order_id": "1045", "channel": "whatsapp"}
	body, err := json.Marshal(map[string]any{
		"amount":               25000,
		"currency":             "mad",
		"payment_method_types": []string{"card", "cash"},
		"metadata":             metadata,
	})
	if err != nil {
		log.Fatal(err)
	}
	req, err := http.NewRequest(http.MethodPost,
		"https://pay-api.adpromax.com/v1/payment_intents", 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", "order-1045-create")

	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 intent PaymentIntent
	if err := json.NewDecoder(res.Body).Decode(&intent); err != nil {
		log.Fatal(err)
	}
	fmt.Println(intent.ID, intent.Metadata["order_id"])
}
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;

public class Metadata {
    public static void main(String[] args) throws Exception {
        // Your own references, as strings.
        String metadata = """
            { "order_id": "1045", "channel": "whatsapp" }""";
        String body = """
            {
              "amount": 25000,
              "currency": "mad",
              "payment_method_types": ["card", "cash"],
              "metadata": %s
            }
            """.formatted(metadata);
        HttpRequest request = HttpRequest.newBuilder(
                URI.create("https://pay-api.adpromax.com/v1/payment_intents"))
            .header("Authorization", "Bearer " + System.getenv("APX_SECRET_KEY"))
            .header("Content-Type", "application/json")
            .header("Idempotency-Key", "order-1045-create")
            .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 payment intent as JSON: its metadata comes back as you sent 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 request = new HttpRequestMessage(
    HttpMethod.Post, "https://pay-api.adpromax.com/v1/payment_intents");
request.Headers.Add("Idempotency-Key", "order-1045-create");
request.Content = JsonContent.Create(new
{
    amount = 25000,
    currency = "mad",
    payment_method_types = new[] { "card", "cash" },
    metadata = new { order_id = "1045", channel = "whatsapp" },
});

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 intent = await response.Content.ReadFromJsonAsync<PaymentIntent>(json);
Console.WriteLine($"{intent!.Id} {intent.Metadata["order_id"]}");

record PaymentIntent(string Id, string Status, Dictionary<string, string> Metadata);
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 std::collections::HashMap;

use serde::Deserialize;
use serde_json::json;

#[derive(Deserialize)]
struct PaymentIntent {
    id: String,
    metadata: HashMap<String, 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/payment_intents")
        .bearer_auth(std::env::var("APX_SECRET_KEY")?)
        .header("Idempotency-Key", "order-1045-create")
        .json(&json!({
            "amount": 25000,
            "currency": "mad",
            "payment_method_types": ["card", "cash"],
            "metadata": { "order_id": "1045", "channel": "whatsapp" }
        }))
        .send()?;
    if !response.status().is_success() {
        let failure: ApiError = response.json()?;
        return Err(failure.error.message.into());
    }
    let intent: PaymentIntent = response.json()?;
    println!("{} {}", intent.id, intent.metadata["order_id"]);
    Ok(())
}

الحدود

  • حتى 50 مفتاحًا في الكائن الواحد.
  • المفتاح حتى 40 حرفًا؛ والقيمة حتى 500 حرف.
  • القيم سلاسل نصية فقط. أرسل الرقم كنص: "1045" لا 1045.

يُرفض الطلب الذي يتجاوز هذه الحدود بـ 400، parameter_invalid، مع param يذكر المفتاح المخالف، مثل metadata.order_id. ولا يُنشأ شيء. اقرأ param لتعرف المفتاح: فحقل message جملة عامة لا تذكر أي حد جرى تجاوزه. في هذه الاستجابة، المفتاح أطول من 40 حرفًا.

الاستجابة400 Bad Request
{
  "error": {
    "type": "invalid_request_error",
    "code": "parameter_invalid",
    "param": "metadata.customer_reference_from_our_previous_system",
    "message": "This value is not valid.",
    "doc_url": "https://pay-docs.adpromax.com/errors#parameter_invalid",
    "request_id": "req_01k7e5s3d7f1g5h9j3k7m1n5pz"
  }
}

الكائنات التي تقبل البيانات الوصفية

الكائنالتعيينالتغيير
نية الدفعPOST /v1/payment_intentsPOST /v1/payment_intents/{id}، ما دامت الدفعة requires_payment_method
جلسة الدفعPOST /v1/checkout/sessions—
العميلPOST /v1/customersPOST /v1/customers/{id}، في أي وقت
الاستردادPOST /v1/refunds—
نقطة استقبال الـ webhookPOST /v1/webhook_endpointsPOST /v1/webhook_endpoints/{id}

تغيير البيانات الوصفية لنقطة استقبال webhook يُدمج مع ما تحمله: تُعيَّن المفاتيح التي ترسلها، وتبقى الأخرى، ويُحذف المفتاح المرسل بقيمة فارغة ("").

تبقى البيانات الوصفية حيث وضعتها

تنتمي البيانات الوصفية إلى الكائن الذي وضعتها عليه، ولا تُنسخ أبدًا إلى كائن آخر. تكون metadata جلسة الدفع (checkout session) على الجلسة وفي أحداثها checkout.session.*؛ ولا تحملها نية الدفع التي تنشئها الجلسة.

مع صفحة الدفع المستضافة، احتفظ بمعرّف نية الدفع مع طلبيتك، كما يفعل البدء السريع للبطاقة، أو اقرأ الجلسة عند وصول checkout.session.completed.

أين تراها

  • في API: كل استجابة تحمل الكائن.
  • في إشعارات الـ webhook: يحمل data.object في الحدث البيانات الوصفية للكائن كما كانت عند وقوع الحدث.
  • في لوحة التحكم: تعرض صفحة الدفعة بياناتها الوصفية، للقراءة فقط، ويمكن تصفية قائمة المدفوعات حسب البيانات الوصفية. وتُعدَّل البيانات الوصفية للعميل في صفحة العميل.

ما لا يوضع أبدًا في البيانات الوصفية

البيانات الوصفية للمراجع، لا للأسرار. يراها فريقك كله في لوحة التحكم، وتتلقاها نقاط استقبال الـ webhook لديك.

  • لا أرقام بطاقات. لا تمر أرقام البطاقات عبر API أبدًا: تُرفض أي قيمة تشبه رقم بطاقة بالرمز parameter_invalid.
  • لا كلمات مرور ولا مفاتيح ولا رموز وصول.
  • لا بيانات شخصية حساسة، مثل أرقام الهوية أو المعلومات الصحية. لتعرف من دفع، استخدم عميلًا واحتفظ بالباقي في أنظمتك، تحت معرّفك.