Bien démarrer
Démarrage rapide : paiement en espèces
Donnez à votre client un code qu’il paie en espèces, simulez le paiement en mode test et apprenez le résultat par un webhook.
Sur cette page
En espèces, votre client reçoit un code de paiement et le règle en espèces chez un agent de paiement partenaire, avant une échéance. Vous créez le paiement exactement comme pour une carte ; seul le moyen de paiement change. Tout se passe ici en mode test : un code de test ne peut pas être payé chez un agent, alors vous simulez le paiement.
Il vous faut un compte bac à sable. Créez-en un : ses clés de test fonctionnent tout de suite.
1. Définir votre clé de test
Placez votre clé secrète de test (apx_sk_test_…, reçue à l’inscription ou sous
Développeurs → Clés d’API sur le tableau de bord) dans la variable d’environnement
APX_SECRET_KEY.
export APX_SECRET_KEY="apx_sk_test_…"2. Créer une session de paiement en espèces
Créez une session de paiement avec "cash" dans payment_method_types. Ajoutez aussi "card" pour
laisser votre client choisir sur la page.
Un paiement en espèces vaut au moins 100,00 MAD (10000 centimes). Un montant inférieur est
refusé avec amount_too_small, en mode test exactement comme en production.
L’en-tête Idempotency-Key est obligatoire, comme pour toute requête qui crée un paiement.
curl https://pay-api.adpromax.com/v1/checkout/sessions \
-u "$APX_SECRET_KEY:" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: checkout-order-1043" \
-d '{
"amount": 15000,
"currency": "mad",
"payment_method_types": ["cash"],
"success_url": "https://shop.example.com/thanks",
"cancel_url": "https://shop.example.com/cart",
"metadata": { "order_id": "1043" }
}'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-1043',
},
body: JSON.stringify({
amount: 15000,
currency: 'mad',
payment_method_types: ['cash'],
success_url: 'https://shop.example.com/thanks',
cancel_url: 'https://shop.example.com/cart',
metadata: { order_id: '1043' },
}),
})
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
}
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-1043',
},
body: JSON.stringify({
amount: 15000,
currency: 'mad',
payment_method_types: ['cash'],
success_url: 'https://shop.example.com/thanks',
cancel_url: 'https://shop.example.com/cart',
metadata: { order_id: '1043' },
}),
})
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-1043"},
json={
"amount": 15000,
"currency": "mad",
"payment_method_types": ["cash"],
"success_url": "https://shop.example.com/thanks",
"cancel_url": "https://shop.example.com/cart",
"metadata": {"order_id": "1043"},
},
)
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-1043',
],
CURLOPT_POSTFIELDS => json_encode([
'amount' => 15000,
'currency' => 'mad',
'payment_method_types' => ['cash'],
'success_url' => 'https://shop.example.com/thanks',
'cancel_url' => 'https://shop.example.com/cart',
'metadata' => ['order_id' => '1043'],
]),
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-1043'
request.body = JSON.generate(
amount: 15000,
currency: 'mad',
payment_method_types: ['cash'],
success_url: 'https://shop.example.com/thanks',
cancel_url: 'https://shop.example.com/cart',
metadata: { order_id: '1043' }
)
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"`
URL string `json:"url"`
}
type APIError struct {
Error struct {
Code string `json:"code"`
Message string `json:"message"`
} `json:"error"`
}
func main() {
body, _ := json.Marshal(map[string]any{
"amount": 15000,
"currency": "mad",
"payment_method_types": []string{"cash"},
"success_url": "https://shop.example.com/thanks",
"cancel_url": "https://shop.example.com/cart",
"metadata": map[string]string{"order_id": "1043"},
})
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-1043")
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": 15000,
"currency": "mad",
"payment_method_types": ["cash"],
"success_url": "https://shop.example.com/thanks",
"cancel_url": "https://shop.example.com/cart",
"metadata": { "order_id": "1043" }
}
""";
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-1043")
.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 your JSON library.
System.out.println(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")
{
Content = JsonContent.Create(new
{
Amount = 15000,
Currency = "mad",
PaymentMethodTypes = new[] { "cash" },
SuccessUrl = "https://shop.example.com/thanks",
CancelUrl = "https://shop.example.com/cart",
Metadata = new Dictionary<string, string> { ["order_id"] = "1043" },
}, options: json),
};
request.Headers.Add("Idempotency-Key", "checkout-order-1043");
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 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-1043")
.json(&json!({
"amount": 15000,
"currency": "mad",
"payment_method_types": ["cash"],
"success_url": "https://shop.example.com/thanks",
"cancel_url": "https://shop.example.com/cart",
"metadata": { "order_id": "1043" },
}))
.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(())
}3. Votre client reçoit un code de paiement
Envoyez votre client vers l’url de la session, comme pour une carte. Sur la page, il indique son
nom et sa ville, puis reçoit un bon de paiement :
- le code à donner à l’agent ;
- le montant à payer : votre montant plus un petit droit de timbre, payé par votre client (en mode test, 0,25 MAD). Affichez toujours le montant donné par le bon ; ne le calculez jamais vous-même ;
- l’échéance : environ trois jours, donnée par le bon.
La page du bon s’imprime ou s’enregistre, et votre client peut y revenir. Il paie en espèces chez un
agent de paiement partenaire, en donnant le code, avant l’échéance. Jusque-là, le statut du paiement
reste requires_action.
4. Simuler le paiement
En mode test, la page du bon porte un bouton Simuler le paiement. Appuyez dessus : le code est payé comme si votre client avait payé chez un agent.
Dans des tests automatisés, appelez plutôt l’assistant de test. Il prend la tentative du paiement,
le latest_charge de son intention de paiement. Les autres issues (expiré, refusé, autre montant)
sont décrites dans Scénarios de test en espèces.
PI=pi_01k7d4c8n2v6b1m5x9z3q7w4ea # the session's payment_intent
CH=$(curl -s https://pay-api.adpromax.com/v1/payment_intents/$PI \
-u "$APX_SECRET_KEY:" | jq -r .latest_charge)
curl -X POST https://pay-api.adpromax.com/v1/test_helpers/cash_vouchers/$CH/pay \
-u "$APX_SECRET_KEY:"const api = 'https://pay-api.adpromax.com/v1'
const headers = { Authorization: `Bearer ${process.env.APX_SECRET_KEY}` }
const id = 'pi_01k7d4c8n2v6b1m5x9z3q7w4ea' // the session's payment_intent
async function call(method, path) {
const response = await fetch(`${api}${path}`, { method, headers })
const answer = await response.json()
if (!response.ok)
throw new Error(answer.error.message)
return answer
}
const intent = await call('GET', `/payment_intents/${id}`)
const charge = intent.latest_charge
await call('POST', `/test_helpers/cash_vouchers/${charge}/pay`)interface PaymentIntent {
id: string
object: 'payment_intent'
status: string
latest_charge: string | null
}
interface ApiError {
error: { type: string, code: string, message: string, request_id: string }
}
const api = 'https://pay-api.adpromax.com/v1'
const headers = { Authorization: `Bearer ${process.env.APX_SECRET_KEY}` }
const id = 'pi_01k7d4c8n2v6b1m5x9z3q7w4ea' // the session's payment_intent
async function call<T>(method: 'GET' | 'POST', path: string): Promise<T> {
const response = await fetch(`${api}${path}`, { method, headers })
if (!response.ok) {
const { error } = (await response.json()) as ApiError
throw new Error(`${error.code}: ${error.message}`)
}
return (await response.json()) as T
}
const intent = await call<PaymentIntent>('GET', `/payment_intents/${id}`)
const charge = intent.latest_charge
await call('POST', `/test_helpers/cash_vouchers/${charge}/pay`)import os
import requests
API = "https://pay-api.adpromax.com/v1"
AUTH = (os.environ["APX_SECRET_KEY"], "")
intent_id = "pi_01k7d4c8n2v6b1m5x9z3q7w4ea" # the session's payment_intent
def call(method, path):
response = requests.request(method, f"{API}{path}", auth=AUTH)
answer = response.json()
if not response.ok:
raise RuntimeError(answer["error"]["message"])
return answer
intent = call("GET", f"/payment_intents/{intent_id}")
charge = intent["latest_charge"]
call("POST", f"/test_helpers/cash_vouchers/{charge}/pay")<?php
function api(string $method, string $path): array
{
$ch = curl_init('https://pay-api.adpromax.com/v1' . $path);
curl_setopt_array($ch, [
CURLOPT_USERPWD => getenv('APX_SECRET_KEY') . ':',
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_RETURNTRANSFER => true,
]);
$answer = json_decode(curl_exec($ch), true);
if (curl_getinfo($ch, CURLINFO_RESPONSE_CODE) >= 400) {
throw new RuntimeException($answer['error']['message']);
}
return $answer;
}
// the session's payment_intent
$intent = api('GET', '/payment_intents/pi_01k7d4c8n2v6b1m5x9z3q7w4ea');
$charge = $intent['latest_charge'];
api('POST', "/test_helpers/cash_vouchers/{$charge}/pay");require 'json'
require 'net/http'
API = 'https://pay-api.adpromax.com/v1'
intent_id = 'pi_01k7d4c8n2v6b1m5x9z3q7w4ea' # the session's payment_intent
def call(request_class, path)
uri = URI("#{API}#{path}")
request = request_class.new(uri)
request.basic_auth(ENV.fetch('APX_SECRET_KEY'), '')
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
http.request(request)
end
answer = JSON.parse(response.body)
raise answer.dig('error', 'message') unless response.is_a?(Net::HTTPSuccess)
answer
end
intent = call(Net::HTTP::Get, "/payment_intents/#{intent_id}")
charge = intent['latest_charge']
call(Net::HTTP::Post, "/test_helpers/cash_vouchers/#{charge}/pay")package main
import (
"encoding/json"
"errors"
"log"
"net/http"
"os"
)
const api = "https://pay-api.adpromax.com/v1"
type PaymentIntent struct {
ID string `json:"id"`
Status string `json:"status"`
LatestCharge string `json:"latest_charge"`
}
type APIError struct {
Error struct {
Code string `json:"code"`
Message string `json:"message"`
} `json:"error"`
}
func call(method, path string, answer any) error {
req, err := http.NewRequest(method, api+path, nil)
if err != nil {
return err
}
req.SetBasicAuth(os.Getenv("APX_SECRET_KEY"), "")
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer res.Body.Close()
if res.StatusCode >= 400 {
var apiErr APIError
json.NewDecoder(res.Body).Decode(&apiErr)
return errors.New(apiErr.Error.Message)
}
return json.NewDecoder(res.Body).Decode(answer)
}
func main() {
id := "pi_01k7d4c8n2v6b1m5x9z3q7w4ea" // the session's payment_intent
var intent PaymentIntent
if err := call(http.MethodGet, "/payment_intents/"+id, &intent); err != nil {
log.Fatal(err)
}
pay := "/test_helpers/cash_vouchers/" + intent.LatestCharge + "/pay"
var charge map[string]any
if err := call(http.MethodPost, pay, &charge); err != nil {
log.Fatal(err)
}
}import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.regex.Pattern;
public class Simulate {
static final String API = "https://pay-api.adpromax.com/v1";
static final HttpClient HTTP = HttpClient.newHttpClient();
static String call(String method, String path) throws Exception {
HttpRequest request = HttpRequest.newBuilder(URI.create(API + path))
.header("Authorization", "Bearer " + System.getenv("APX_SECRET_KEY"))
.method(method, HttpRequest.BodyPublishers.noBody())
.build();
HttpResponse<String> response =
HTTP.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() >= 400) {
throw new IllegalStateException(response.body());
}
return response.body();
}
public static void main(String[] args) throws Exception {
String id = "pi_01k7d4c8n2v6b1m5x9z3q7w4ea"; // the session's payment_intent
String intent = call("GET", "/payment_intents/" + id);
// The JDK has no JSON parser: a regex reads latest_charge here.
var charge = Pattern.compile("\"latest_charge\"\\s*:\\s*\"([^\"]+)\"")
.matcher(intent);
if (!charge.find()) {
throw new IllegalStateException("No latest_charge: " + intent);
}
call("POST", "/test_helpers/cash_vouchers/" + charge.group(1) + "/pay");
}
}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 { BaseAddress = new Uri("https://pay-api.adpromax.com/v1/") };
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
"Bearer", Environment.GetEnvironmentVariable("APX_SECRET_KEY"));
var id = "pi_01k7d4c8n2v6b1m5x9z3q7w4ea"; // the session's payment_intent
using var found = await http.GetAsync($"payment_intents/{id}");
await EnsureSuccess(found);
var intent = await found.Content.ReadFromJsonAsync<PaymentIntent>(json);
using var paid = await http.PostAsync(
$"test_helpers/cash_vouchers/{intent!.LatestCharge}/pay", null);
await EnsureSuccess(paid);
async Task EnsureSuccess(HttpResponseMessage response)
{
if (response.IsSuccessStatusCode)
return;
var failure = await response.Content.ReadFromJsonAsync<ApiError>(json);
throw new InvalidOperationException(failure?.Error.Message);
}
record PaymentIntent(string Id, string Status, string? LatestCharge);
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 reqwest::blocking::{Client, RequestBuilder};
use serde::de::DeserializeOwned;
use serde::Deserialize;
const API: &str = "https://pay-api.adpromax.com/v1";
#[derive(Deserialize)]
struct PaymentIntent {
latest_charge: String,
}
#[derive(Deserialize)]
struct ApiError {
error: ErrorBody,
}
#[derive(Deserialize)]
struct ErrorBody {
message: String,
}
fn send<T: DeserializeOwned>(
request: RequestBuilder,
) -> Result<T, Box<dyn std::error::Error>> {
let key = std::env::var("APX_SECRET_KEY")?;
let response = request.bearer_auth(key).send()?;
if !response.status().is_success() {
let failure: ApiError = response.json()?;
return Err(failure.error.message.into());
}
Ok(response.json()?)
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new();
let id = "pi_01k7d4c8n2v6b1m5x9z3q7w4ea"; // the session's payment_intent
let url = format!("{API}/payment_intents/{id}");
let intent: PaymentIntent = send(client.get(url))?;
let charge = intent.latest_charge;
let pay = format!("{API}/test_helpers/cash_vouchers/{charge}/pay");
let _charge: serde_json::Value = send(client.post(pay))?;
Ok(())
}5. Recevoir le résultat
Quand le paiement est confirmé, AdProMaxPay envoie payment_intent.succeeded à votre endpoint de
webhook : quelques secondes après un paiement simulé. En production, il arrive une fois le paiement
chez l’agent confirmé, en général en quelques minutes.
Si l’échéance passe sans paiement, vous recevez payment_intent.payment_failed, avec
last_payment_error.code à voucher_expired. Le paiement revient à requires_payment_method :
votre client peut obtenir un nouveau code tant que la session est ouverte.
L’endpoint et le gestionnaire sont ceux du démarrage rapide carte : créer l’endpoint, puis traiter l’événement. Un paiement en espèces n’est jamais remboursé en espèces : voir Remboursements.
Afficher le code sur votre propre page
Vous pouvez aussi vous passer de la page hébergée : créez une intention de paiement, puis
confirmez-la en espèces avec le nom et la ville de votre client. Le next_action.display_voucher de
la réponse contient le bon : le code, l’amount_to_pay, expires_at (secondes Unix), un
slip_url imprimable, et hosted_voucher_url, notre page du bon, que vous pouvez envoyer à votre
client à la place.
Chacune des deux requêtes a besoin de sa propre Idempotency-Key.
PI=$(curl -s https://pay-api.adpromax.com/v1/payment_intents \
-u "$APX_SECRET_KEY:" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-1044-create" \
-d '{"amount": 15000, "currency": "mad",
"payment_method_types": ["cash"]}' | jq -r .id)
curl https://pay-api.adpromax.com/v1/payment_intents/$PI/confirm \
-u "$APX_SECRET_KEY:" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-1044-confirm" \
-d '{"payment_method_type": "cash",
"payer": {"name": "Salma Idrissi", "city": "Casablanca"}}'const api = 'https://pay-api.adpromax.com/v1'
const auth = `Bearer ${process.env.APX_SECRET_KEY}`
async function post(path, key, body) {
const response = await fetch(`${api}${path}`, {
method: 'POST',
headers: {
'Authorization': auth,
'Content-Type': 'application/json',
'Idempotency-Key': key,
},
body: JSON.stringify(body),
})
const answer = await response.json()
if (!response.ok)
throw new Error(answer.error.message)
return answer
}
const intent = await post('/payment_intents', 'order-1044-create', {
amount: 15000,
currency: 'mad',
payment_method_types: ['cash'],
})
const confirmed = await post(`/payment_intents/${intent.id}/confirm`,
'order-1044-confirm', {
payment_method_type: 'cash',
payer: { name: 'Salma Idrissi', city: 'Casablanca' },
})
const voucher = confirmed.next_action.display_voucher
console.log(voucher.code, voucher.amount_to_pay, voucher.expires_at)interface Voucher {
code: string
amount_to_pay: number
currency: string
expires_at: number
slip_url: string
hosted_voucher_url: string
}
interface PaymentIntent {
id: string
object: 'payment_intent'
status: string
next_action: { type: 'display_voucher', display_voucher: Voucher } | null
}
interface ApiError {
error: { type: string, code: string, message: string, request_id: string }
}
const api = 'https://pay-api.adpromax.com/v1'
const auth = `Bearer ${process.env.APX_SECRET_KEY}`
async function post(path: string, key: string, body: object): Promise<PaymentIntent> {
const response = await fetch(`${api}${path}`, {
method: 'POST',
headers: {
'Authorization': auth,
'Content-Type': 'application/json',
'Idempotency-Key': key,
},
body: JSON.stringify(body),
})
if (!response.ok) {
const { error } = (await response.json()) as ApiError
throw new Error(`${error.code}: ${error.message}`)
}
return (await response.json()) as PaymentIntent
}
const intent = await post('/payment_intents', 'order-1044-create', {
amount: 15000,
currency: 'mad',
payment_method_types: ['cash'],
})
const confirmed = await post(`/payment_intents/${intent.id}/confirm`,
'order-1044-confirm', {
payment_method_type: 'cash',
payer: { name: 'Salma Idrissi', city: 'Casablanca' },
})
const voucher = confirmed.next_action?.display_voucher
console.log(voucher?.code, voucher?.amount_to_pay, voucher?.expires_at)import os
import requests
API = "https://pay-api.adpromax.com/v1"
AUTH = (os.environ["APX_SECRET_KEY"], "")
def post(path, key, body):
response = requests.post(
f"{API}{path}", auth=AUTH, json=body,
headers={"Idempotency-Key": key},
)
answer = response.json()
if not response.ok:
raise RuntimeError(answer["error"]["message"])
return answer
intent = post("/payment_intents", "order-1044-create", {
"amount": 15000,
"currency": "mad",
"payment_method_types": ["cash"],
})
confirmed = post(f"/payment_intents/{intent['id']}/confirm",
"order-1044-confirm", {
"payment_method_type": "cash",
"payer": {"name": "Salma Idrissi", "city": "Casablanca"},
})
voucher = confirmed["next_action"]["display_voucher"]
print(voucher["code"], voucher["amount_to_pay"], voucher["expires_at"])<?php
function post(string $path, string $key, array $body): array
{
$ch = curl_init('https://pay-api.adpromax.com/v1' . $path);
curl_setopt_array($ch, [
CURLOPT_USERPWD => getenv('APX_SECRET_KEY') . ':',
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Idempotency-Key: ' . $key,
],
CURLOPT_POSTFIELDS => json_encode($body),
CURLOPT_RETURNTRANSFER => true,
]);
$answer = json_decode(curl_exec($ch), true);
if (curl_getinfo($ch, CURLINFO_RESPONSE_CODE) >= 400) {
throw new RuntimeException($answer['error']['message']);
}
return $answer;
}
$intent = post('/payment_intents', 'order-1044-create', [
'amount' => 15000,
'currency' => 'mad',
'payment_method_types' => ['cash'],
]);
$confirmed = post("/payment_intents/{$intent['id']}/confirm",
'order-1044-confirm', [
'payment_method_type' => 'cash',
'payer' => ['name' => 'Salma Idrissi', 'city' => 'Casablanca'],
]);
$voucher = $confirmed['next_action']['display_voucher'];
echo $voucher['code'], ' ', $voucher['amount_to_pay'], ' ',
$voucher['expires_at'], PHP_EOL;require 'json'
require 'net/http'
API = 'https://pay-api.adpromax.com/v1'
def post(path, key, body)
uri = URI("#{API}#{path}")
request = Net::HTTP::Post.new(uri)
request.basic_auth(ENV.fetch('APX_SECRET_KEY'), '')
request['Content-Type'] = 'application/json'
request['Idempotency-Key'] = key
request.body = JSON.generate(body)
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
http.request(request)
end
answer = JSON.parse(response.body)
raise answer.dig('error', 'message') unless response.is_a?(Net::HTTPSuccess)
answer
end
intent = post('/payment_intents', 'order-1044-create',
amount: 15000, currency: 'mad', payment_method_types: ['cash'])
confirmed = post("/payment_intents/#{intent['id']}/confirm", 'order-1044-confirm',
payment_method_type: 'cash',
payer: { name: 'Salma Idrissi', city: 'Casablanca' })
voucher = confirmed.dig('next_action', 'display_voucher')
puts "#{voucher['code']} #{voucher['amount_to_pay']} #{voucher['expires_at']}"package main
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"log"
"net/http"
"os"
)
const api = "https://pay-api.adpromax.com/v1"
type Voucher struct {
Code string `json:"code"`
AmountToPay int64 `json:"amount_to_pay"`
ExpiresAt int64 `json:"expires_at"`
HostedVoucherURL string `json:"hosted_voucher_url"`
}
type PaymentIntent struct {
ID string `json:"id"`
Status string `json:"status"`
NextAction struct {
DisplayVoucher Voucher `json:"display_voucher"`
} `json:"next_action"`
}
type APIError struct {
Error struct {
Code string `json:"code"`
Message string `json:"message"`
} `json:"error"`
}
func post(path, key string, body any) (*PaymentIntent, error) {
payload, _ := json.Marshal(body)
req, err := http.NewRequest(http.MethodPost, api+path, bytes.NewReader(payload))
if err != nil {
return nil, err
}
req.SetBasicAuth(os.Getenv("APX_SECRET_KEY"), "")
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", key)
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
if res.StatusCode >= 400 {
var apiErr APIError
json.NewDecoder(res.Body).Decode(&apiErr)
return nil, errors.New(apiErr.Error.Message)
}
var intent PaymentIntent
return &intent, json.NewDecoder(res.Body).Decode(&intent)
}
func main() {
intent, err := post("/payment_intents", "order-1044-create", map[string]any{
"amount": 15000,
"currency": "mad",
"payment_method_types": []string{"cash"},
})
if err != nil {
log.Fatal(err)
}
confirmed, err := post("/payment_intents/"+intent.ID+"/confirm",
"order-1044-confirm", map[string]any{
"payment_method_type": "cash",
"payer": map[string]string{
"name": "Salma Idrissi",
"city": "Casablanca",
},
})
if err != nil {
log.Fatal(err)
}
voucher := confirmed.NextAction.DisplayVoucher
fmt.Println(voucher.Code, voucher.AmountToPay, voucher.ExpiresAt)
}import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.regex.Pattern;
public class Voucher {
static final String API = "https://pay-api.adpromax.com/v1";
static final HttpClient HTTP = HttpClient.newHttpClient();
static String post(String path, String key, String body) throws Exception {
HttpRequest request = HttpRequest.newBuilder(URI.create(API + path))
.header("Authorization", "Bearer " + System.getenv("APX_SECRET_KEY"))
.header("Content-Type", "application/json")
.header("Idempotency-Key", key)
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse<String> response =
HTTP.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() >= 400) {
throw new IllegalStateException(response.body());
}
return response.body();
}
public static void main(String[] args) throws Exception {
String intent = post("/payment_intents", "order-1044-create", """
{"amount": 15000, "currency": "mad", "payment_method_types": ["cash"]}
""");
// The JDK has no JSON parser: a regex reads the id here.
var id = Pattern.compile("\"id\"\\s*:\\s*\"(pi_[^\"]+)\"").matcher(intent);
if (!id.find()) {
throw new IllegalStateException("No id: " + intent);
}
String confirmed = post("/payment_intents/" + id.group(1) + "/confirm",
"order-1044-confirm", """
{"payment_method_type": "cash",
"payer": {"name": "Salma Idrissi", "city": "Casablanca"}}
""");
// next_action.display_voucher: code, amount_to_pay, expires_at
System.out.println(confirmed);
}
}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 { BaseAddress = new Uri("https://pay-api.adpromax.com/v1/") };
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
"Bearer", Environment.GetEnvironmentVariable("APX_SECRET_KEY"));
var intent = await Post("payment_intents", "order-1044-create", new
{
Amount = 15000,
Currency = "mad",
PaymentMethodTypes = new[] { "cash" },
});
var confirm = $"payment_intents/{intent.Id}/confirm";
var confirmed = await Post(confirm, "order-1044-confirm", new
{
PaymentMethodType = "cash",
Payer = new { Name = "Salma Idrissi", City = "Casablanca" },
});
var voucher = confirmed.NextAction!.DisplayVoucher;
Console.WriteLine($"{voucher.Code} {voucher.AmountToPay} {voucher.ExpiresAt}");
async Task<PaymentIntent> Post(string path, string key, object body)
{
using var request = new HttpRequestMessage(HttpMethod.Post, path)
{
Content = JsonContent.Create(body, options: json),
};
request.Headers.Add("Idempotency-Key", key);
using var response = await http.SendAsync(request);
if (!response.IsSuccessStatusCode)
{
var failure = await response.Content.ReadFromJsonAsync<ApiError>(json);
throw new InvalidOperationException(failure?.Error.Message);
}
return (await response.Content.ReadFromJsonAsync<PaymentIntent>(json))!;
}
record PaymentIntent(string Id, string Status, NextAction? NextAction);
record NextAction(string Type, Voucher DisplayVoucher);
record Voucher(string Code, long AmountToPay, long ExpiresAt, string HostedVoucherUrl);
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 reqwest::blocking::Client;
use serde::Deserialize;
use serde_json::{json, Value};
const API: &str = "https://pay-api.adpromax.com/v1";
#[derive(Deserialize)]
struct PaymentIntent {
id: String,
next_action: Option<NextAction>,
}
#[derive(Deserialize)]
struct NextAction {
display_voucher: Voucher,
}
#[derive(Deserialize)]
struct Voucher {
code: String,
amount_to_pay: i64,
expires_at: i64,
}
#[derive(Deserialize)]
struct ApiError {
error: ErrorBody,
}
#[derive(Deserialize)]
struct ErrorBody {
message: String,
}
fn post(
client: &Client,
path: &str,
key: &str,
body: Value,
) -> Result<PaymentIntent, Box<dyn std::error::Error>> {
let response = client
.post(format!("{API}{path}"))
.bearer_auth(std::env::var("APX_SECRET_KEY")?)
.header("Idempotency-Key", key)
.json(&body)
.send()?;
if !response.status().is_success() {
let failure: ApiError = response.json()?;
return Err(failure.error.message.into());
}
Ok(response.json()?)
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new();
let intent = post(&client, "/payment_intents", "order-1044-create", json!({
"amount": 15000,
"currency": "mad",
"payment_method_types": ["cash"],
}))?;
let confirm = format!("/payment_intents/{}/confirm", intent.id);
let confirmed = post(&client, &confirm, "order-1044-confirm", json!({
"payment_method_type": "cash",
"payer": { "name": "Salma Idrissi", "city": "Casablanca" },
}))?;
let next_action = confirmed.next_action.ok_or("no voucher")?;
let voucher = next_action.display_voucher;
println!("{} {} {}", voucher.code, voucher.amount_to_pay, voucher.expires_at);
Ok(())
}{
"id": "pi_01k7d5f2k8m4n9b3v7c1x6z2ra",
"object": "payment_intent",
"amount": 15000,
"currency": "mad",
"status": "requires_action",
"payment_method_types": ["cash"],
"latest_charge": "ch_01k7d5f3a1s5d9f2g6h4j8k3nb",
"next_action": {
"type": "display_voucher",
"display_voucher": {
"code": "48210937",
"amount_to_pay": 15025,
"currency": "mad",
"expires_at": 1791112200,
"slip_url": "https://pay.example.com/…",
"hosted_voucher_url": "https://pay.example.com/v/ch_01k7d5f3a1…#…"
}
},
"livemode": false
}Et ensuite
- Démarrage rapide : paiement par carte avec la page hébergée.
- Scénarios de test en espèces : expiré, refusé, payé en retard, payé d’un autre montant.
- Bac à sable et production : les différences : ce qui change pour les espèces en production.