Payments
Metadata
Attach your own references to payments, sessions, customers and refunds, and find them again in webhooks and on the dashboard.
On this page
Metadata is a set of your own keys and values on an object: an order number, a cart ID, the shop or the channel a sale came from. AdProMaxPay stores it and gives it back, unchanged, wherever the object appears. It never changes how a payment works.
Add metadata to a payment
Send metadata as an object of strings when you create the payment intent. Your references then come
back in the API's answers and in every event about this payment: when your webhook receives
payment_intent.succeeded, data.object.metadata.order_id names the order to fulfil.
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(())
}The limits
- Up to 50 keys on one object.
- A key is up to 40 characters; a value up to 500 characters.
- Values are strings only. Send a number as text:
"1045", not1045.
A request beyond these limits is refused with 400, parameter_invalid, and param naming the key
at fault, such as metadata.order_id. Nothing is created. Read param to find the key: the
message is a general sentence and does not say which limit was passed. In this answer, the key is
longer than 40 characters.
{
"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"
}
}Objects that take metadata
| Object | Set it | Change it |
|---|---|---|
| Payment intent | POST /v1/payment_intents | POST /v1/payment_intents/{id}, while the payment is requires_payment_method |
| Checkout session | POST /v1/checkout/sessions | — |
| Customer | POST /v1/customers | POST /v1/customers/{id}, at any time |
| Refund | POST /v1/refunds | — |
| Webhook endpoint | POST /v1/webhook_endpoints | POST /v1/webhook_endpoints/{id} |
A change to a webhook endpoint's metadata merges with what it holds: the keys you send are set,
the others stay, and a key sent with an empty value ("") is removed.
Metadata stays where you put it
Metadata belongs to the object you set it on, and is never copied to another one. A checkout
session's metadata is on the session and in its checkout.session.* events; the payment intent the
session creates does not carry it.
With hosted checkout, keep the payment intent's ID with your order, as the
card quickstart does, or read the session when
checkout.session.completed arrives.
Where you see it
- In the API: every answer that holds the object.
- In webhooks: the event's
data.objectcarries the object's metadata as it was when the event happened. - On the dashboard: a payment's page shows its metadata, read-only, and the payments list can be filtered by metadata. A customer's metadata is edited on the customer's page.
What never goes in metadata
Metadata is for references, not for secrets. Your whole team sees it on the dashboard, and your webhook endpoints receive it.
- No card numbers. Card numbers never travel through the API: a value that looks like a card
number is refused with
parameter_invalid. - No passwords, keys or tokens.
- No sensitive personal data, such as identity numbers or health details. To know who paid, use a customer and keep the rest in your own systems, under your ID.