Get started
Quickstart: cash payments
Give your customer a payment code they pay in cash, simulate the payment in test mode, and learn the result from a webhook.
On this page
With cash, your customer gets a payment code and pays it in cash at a partner payment agent, before a deadline. You create the payment exactly as for a card; only the payment method changes. Everything here runs in test mode: a test code cannot be paid at an agent, so you simulate the payment.
You need a sandbox account. Create one: its test keys work at once.
1. Set your test key
Put your test secret key (apx_sk_test_…, from sign-up or Developers → API keys on the
dashboard) in the APX_SECRET_KEY environment variable.
export APX_SECRET_KEY="apx_sk_test_…"2. Create a checkout session for cash
Create a checkout session with "cash" in payment_method_types. Add "card" as well to let your
customer choose on the page.
A cash payment is at least 100.00 MAD (10000 centimes). A smaller amount is refused with
amount_too_small, in test mode exactly as in live mode.
The Idempotency-Key header is required, as for every request that creates a payment.
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. Your customer gets a payment code
Send your customer to the session's url, as for a card. On the page they give their name and their
city, then receive a voucher:
- the code to give at the agent;
- the amount to pay: your amount plus a small stamp fee, which your customer pays (in test mode, 0.25 MAD). Always show the amount the voucher gives; never compute it yourself;
- the deadline: about three days, given by the voucher.
The voucher page can be printed or saved, and your customer can come back to it. They pay in cash at
a partner payment agent, giving the code, before the deadline. The payment's status stays
requires_action until then.
4. Simulate the payment
In test mode, the voucher page carries a Simulate payment button. Press it: the code is paid as if your customer had paid at an agent.
In automated tests, call the test helper instead. It takes the payment's attempt, the
latest_charge of its payment intent. Other outcomes (expired, refused, another amount) are listed in
Test cash scenarios.
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. Receive the result
When the payment is confirmed, AdProMaxPay sends payment_intent.succeeded to your webhook endpoint:
a few seconds after a simulated payment. In live mode it comes once the agent's payment is
confirmed, usually within minutes.
If the deadline passes unpaid, you receive payment_intent.payment_failed, with
last_payment_error.code set to voucher_expired. The payment goes back to
requires_payment_method: your customer can get a new code while the session is open.
The endpoint and the handler are the ones of the card quickstart: create the endpoint, then handle the event. A cash payment is never refunded in cash: see Refunds.
Show the code on your own page
You can also skip the hosted page: create a payment intent, then confirm it with cash and your
customer's name and city. The answer's next_action.display_voucher holds the voucher: the
code, the amount_to_pay, expires_at (Unix seconds), a printable slip_url, and
hosted_voucher_url, our voucher page, which you can send to your customer instead.
Both requests need their own 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
}Next steps
- Quickstart: card payments with hosted checkout.
- Test cash scenarios: expired, refused, paid late, paid another amount.
- Sandbox and live: the differences: what changes for cash in live mode.