Sandbox
Test helpers API
Play the payer's part from your own tests, without a browser: complete a card payment, pay or expire a cash code.
On this page
The test helpers let an automated test do what a person does in test mode: type a test card on the
payment page, or pay a cash code at an agent. They live under /v1/test_helpers and accept a test
secret key only. With a live key they answer 403, permission_error, test_mode_only.
A helper acts on an attempt (ch_…): the latest_charge of a payment intent that is waiting for
its payer, in requires_action. It plays the payer's or the network's part, nothing more: the payment
then changes the way it would in live mode, through the provider's notification or a status check,
usually within seconds. Read the payment intent again, or wait for the webhook.
Complete a card payment
POST /v1/test_helpers/charges/{charge}/complete submits a card number on the payment page of a
waiting card attempt, as your customer would.
card_number: one of the test cards, with or without spaces. Any other number is refused.three_d_secure, optional: for the 3-D Secure card, the answer to the simulated challenge (see the API reference).
curl https://pay-api.adpromax.com/v1/test_helpers/charges/ch_01k7d2j8m3n6b9v2c5x8z1a4qd/complete \
-u "$APX_SECRET_KEY:" \
-H "Content-Type: application/json" \
-d '{"card_number": "4242424242424242"}'const charge = 'ch_01k7d2j8m3n6b9v2c5x8z1a4qd'
const path = `/v1/test_helpers/charges/${charge}/complete`
await fetch(`https://pay-api.adpromax.com${path}`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.APX_SECRET_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ card_number: '4242424242424242' }),
})interface CompleteCharge {
/** One of the test cards, with or without spaces. */
card_number: string
}
const charge = 'ch_01k7d2j8m3n6b9v2c5x8z1a4qd'
const path = `/v1/test_helpers/charges/${charge}/complete`
const body: CompleteCharge = { card_number: '4242424242424242' }
await fetch(`https://pay-api.adpromax.com${path}`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.APX_SECRET_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
})import os
import requests
charge = "ch_01k7d2j8m3n6b9v2c5x8z1a4qd"
requests.post(
f"https://pay-api.adpromax.com/v1/test_helpers/charges/{charge}/complete",
auth=(os.environ["APX_SECRET_KEY"], ""),
json={"card_number": "4242424242424242"},
)<?php
$charge = 'ch_01k7d2j8m3n6b9v2c5x8z1a4qd';
$path = "/v1/test_helpers/charges/{$charge}/complete";
$ch = curl_init('https://pay-api.adpromax.com' . $path);
curl_setopt_array($ch, [
CURLOPT_USERPWD => getenv('APX_SECRET_KEY') . ':',
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode(['card_number' => '4242424242424242']),
CURLOPT_RETURNTRANSFER => true,
]);
curl_exec($ch);require 'json'
require 'net/http'
charge = 'ch_01k7d2j8m3n6b9v2c5x8z1a4qd'
uri = URI("https://pay-api.adpromax.com/v1/test_helpers/charges/#{charge}/complete")
request = Net::HTTP::Post.new(uri)
request.basic_auth(ENV.fetch('APX_SECRET_KEY'), '')
request['Content-Type'] = 'application/json'
request.body = JSON.generate(card_number: '4242424242424242')
Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
http.request(request)
endpackage main
import (
"log"
"net/http"
"os"
"strings"
)
func main() {
charge := "ch_01k7d2j8m3n6b9v2c5x8z1a4qd"
req, err := http.NewRequest(http.MethodPost,
"https://pay-api.adpromax.com/v1/test_helpers/charges/"+charge+"/complete",
strings.NewReader(`{"card_number": "4242424242424242"}`))
if err != nil {
log.Fatal(err)
}
req.SetBasicAuth(os.Getenv("APX_SECRET_KEY"), "")
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
log.Fatal(err)
}
res.Body.Close()
}import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class Complete {
public static void main(String[] args) throws Exception {
String charge = "ch_01k7d2j8m3n6b9v2c5x8z1a4qd";
String body = """
{"card_number": "4242424242424242"}
""";
HttpRequest request = HttpRequest.newBuilder(URI.create(
"https://pay-api.adpromax.com/v1/test_helpers/charges/" + charge + "/complete"))
.header("Authorization", "Bearer " + System.getenv("APX_SECRET_KEY"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.discarding());
}
}using System.Net.Http.Headers;
using System.Net.Http.Json;
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
"Bearer", Environment.GetEnvironmentVariable("APX_SECRET_KEY"));
var charge = "ch_01k7d2j8m3n6b9v2c5x8z1a4qd";
using var response = await http.PostAsJsonAsync(
$"https://pay-api.adpromax.com/v1/test_helpers/charges/{charge}/complete",
new { card_number = "4242424242424242" });// Cargo.toml: reqwest = { version = "0.12", features = ["blocking", "json"] }
// serde_json = "1"
use serde_json::json;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let charge = "ch_01k7d2j8m3n6b9v2c5x8z1a4qd";
reqwest::blocking::Client::new()
.post(format!(
"https://pay-api.adpromax.com/v1/test_helpers/charges/{charge}/complete"
))
.bearer_auth(std::env::var("APX_SECRET_KEY")?)
.json(&json!({ "card_number": "4242424242424242" }))
.send()?;
Ok(())
}Pay, expire, refuse or suspend a cash code
Four helpers act on a waiting cash attempt, as the payment network would. Each one is a POST with
no body, except pay:
| Helper | What happens |
|---|---|
/v1/test_helpers/cash_vouchers/{charge}/pay | The code is paid. amount, optional, in centimes: pay another amount than the voucher's. |
/v1/test_helpers/cash_vouchers/{charge}/expire | The code reaches its deadline unpaid. |
/v1/test_helpers/cash_vouchers/{charge}/refuse | The network refuses the code. |
/v1/test_helpers/cash_vouchers/{charge}/suspend | The code is put on hold; pay it afterwards. |
What each outcome does to the payment is in Test cash scenarios.
CH=ch_01k7d5f3a1s5d9f2g6h4j8k3nb
curl -X POST https://pay-api.adpromax.com/v1/test_helpers/cash_vouchers/$CH/pay \
-u "$APX_SECRET_KEY:"
# or pay another amount: half of a 150.00 MAD payment
curl https://pay-api.adpromax.com/v1/test_helpers/cash_vouchers/$CH/pay \
-u "$APX_SECRET_KEY:" \
-H "Content-Type: application/json" \
-d '{"amount": 7500}'const charge = 'ch_01k7d5f3a1s5d9f2g6h4j8k3nb'
const helper = action =>
`https://pay-api.adpromax.com/v1/test_helpers/cash_vouchers/${charge}/${action}`
const headers = {
'Authorization': `Bearer ${process.env.APX_SECRET_KEY}`,
'Content-Type': 'application/json',
}
await fetch(helper('pay'), { method: 'POST', headers })
// or: helper('expire'), helper('refuse'), helper('suspend'),
// or pay another amount:
// fetch(helper('pay'), { method: 'POST', headers,
// body: JSON.stringify({ amount: 7500 }) })type CashAction = 'pay' | 'expire' | 'refuse' | 'suspend'
const charge = 'ch_01k7d5f3a1s5d9f2g6h4j8k3nb'
const helper = (action: CashAction): string =>
`https://pay-api.adpromax.com/v1/test_helpers/cash_vouchers/${charge}/${action}`
const headers = {
'Authorization': `Bearer ${process.env.APX_SECRET_KEY}`,
'Content-Type': 'application/json',
}
await fetch(helper('pay'), { method: 'POST', headers })
// or: helper('expire'), helper('refuse'), helper('suspend'),
// or pay another amount:
// fetch(helper('pay'), { method: 'POST', headers,
// body: JSON.stringify({ amount: 7500 }) })import os
import requests
AUTH = (os.environ["APX_SECRET_KEY"], "")
charge = "ch_01k7d5f3a1s5d9f2g6h4j8k3nb"
def cash_helper(action, body=None):
url = f"https://pay-api.adpromax.com/v1/test_helpers/cash_vouchers/{charge}/{action}"
return requests.post(url, auth=AUTH, json=body or {})
cash_helper("pay")
# or "expire", "refuse", "suspend"; another amount:
# cash_helper("pay", {"amount": 7500})<?php
function cash_helper(string $charge, string $action, array $body = []): void
{
$path = "/v1/test_helpers/cash_vouchers/{$charge}/{$action}";
$ch = curl_init('https://pay-api.adpromax.com' . $path);
curl_setopt_array($ch, [
CURLOPT_USERPWD => getenv('APX_SECRET_KEY') . ':',
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode((object) $body),
CURLOPT_RETURNTRANSFER => true,
]);
curl_exec($ch);
}
cash_helper('ch_01k7d5f3a1s5d9f2g6h4j8k3nb', 'pay');
// or 'expire', 'refuse', 'suspend'; another amount:
// cash_helper('ch_…', 'pay', ['amount' => 7500]);require 'json'
require 'net/http'
CHARGE = 'ch_01k7d5f3a1s5d9f2g6h4j8k3nb'
def cash_helper(action, body = {})
uri = URI("https://pay-api.adpromax.com/v1/test_helpers/cash_vouchers/#{CHARGE}/#{action}")
request = Net::HTTP::Post.new(uri)
request.basic_auth(ENV.fetch('APX_SECRET_KEY'), '')
request['Content-Type'] = 'application/json'
request.body = JSON.generate(body)
Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == 'https') do |http|
http.request(request)
end
end
cash_helper('pay')
# or 'expire', 'refuse', 'suspend'; another amount:
# cash_helper('pay', amount: 7500)package main
import (
"bytes"
"encoding/json"
"log"
"net/http"
"os"
)
const charge = "ch_01k7d5f3a1s5d9f2g6h4j8k3nb"
func cashHelper(action string, body map[string]any) error {
payload, err := json.Marshal(body)
if err != nil {
return err
}
req, err := http.NewRequest(http.MethodPost,
"https://pay-api.adpromax.com/v1/test_helpers/cash_vouchers/"+charge+"/"+action,
bytes.NewReader(payload))
if err != nil {
return err
}
req.SetBasicAuth(os.Getenv("APX_SECRET_KEY"), "")
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
return res.Body.Close()
}
func main() {
if err := cashHelper("pay", map[string]any{}); err != nil {
log.Fatal(err)
}
// or "expire", "refuse", "suspend"; another amount:
// cashHelper("pay", map[string]any{"amount": 7500})
}import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class Cash {
static final String CHARGE = "ch_01k7d5f3a1s5d9f2g6h4j8k3nb";
static void cashHelper(String action, String body) throws Exception {
HttpRequest request = HttpRequest.newBuilder(URI.create(
"https://pay-api.adpromax.com/v1/test_helpers/cash_vouchers/"
+ CHARGE + "/" + action))
.header("Authorization", "Bearer " + System.getenv("APX_SECRET_KEY"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.discarding());
}
public static void main(String[] args) throws Exception {
cashHelper("pay", "{}");
// or "expire", "refuse", "suspend"; another amount:
// cashHelper("pay", "{\"amount\": 7500}");
}
}using System.Net.Http.Headers;
using System.Net.Http.Json;
using var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
"Bearer", Environment.GetEnvironmentVariable("APX_SECRET_KEY"));
const string charge = "ch_01k7d5f3a1s5d9f2g6h4j8k3nb";
Task<HttpResponseMessage> CashHelper(string action, object? body = null) =>
http.PostAsJsonAsync(
$"https://pay-api.adpromax.com/v1/test_helpers/cash_vouchers/{charge}/{action}",
body ?? new { });
await CashHelper("pay");
// or "expire", "refuse", "suspend"; another amount:
// await CashHelper("pay", new { amount = 7500 });// Cargo.toml: reqwest = { version = "0.12", features = ["blocking", "json"] }
// serde_json = "1"
use serde_json::{json, Value};
const CHARGE: &str = "ch_01k7d5f3a1s5d9f2g6h4j8k3nb";
fn cash_helper(action: &str, body: Value) -> Result<(), Box<dyn std::error::Error>> {
reqwest::blocking::Client::new()
.post(format!(
"https://pay-api.adpromax.com/v1/test_helpers/cash_vouchers/{CHARGE}/{action}"
))
.bearer_auth(std::env::var("APX_SECRET_KEY")?)
.json(&body)
.send()?;
Ok(())
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
cash_helper("pay", json!({}))?;
// or "expire", "refuse", "suspend"; another amount:
// cash_helper("pay", json!({ "amount": 7500 }))?;
Ok(())
}A whole card payment, without a browser
A test of your integration can run a payment end to end: create a payment intent, confirm it for a
card, complete its attempt with a test card, then wait for succeeded. Each creation and
confirmation carries its own Idempotency-Key; a fresh one per test run.
API=https://pay-api.adpromax.com/v1
RUN=$(date +%s)
PI=$(curl -s $API/payment_intents -u "$APX_SECRET_KEY:" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: test-$RUN-create" \
-d '{"amount": 50000, "currency": "mad",
"payment_method_types": ["card"]}' | jq -r .id)
curl -s $API/payment_intents/$PI/confirm -u "$APX_SECRET_KEY:" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: test-$RUN-confirm" \
-d '{"payment_method_type": "card",
"return_url": "https://shop.example.com/done"}' > /dev/null
CH=$(curl -s $API/payment_intents/$PI -u "$APX_SECRET_KEY:" \
| jq -r .latest_charge)
curl -s $API/test_helpers/charges/$CH/complete -u "$APX_SECRET_KEY:" \
-H "Content-Type: application/json" \
-d '{"card_number": "4242424242424242"}' > /dev/null
for i in 1 2 3 4 5 6 7 8 9 10; do
STATUS=$(curl -s $API/payment_intents/$PI -u "$APX_SECRET_KEY:" \
| jq -r .status)
[ "$STATUS" = succeeded ] && break
sleep 1
done
echo "$PI $STATUS"import { randomUUID } from 'node:crypto'
const api = 'https://pay-api.adpromax.com/v1'
const auth = `Bearer ${process.env.APX_SECRET_KEY}`
const run = randomUUID()
async function call(method, path, body, key) {
const headers = { Authorization: auth }
if (body !== undefined)
headers['Content-Type'] = 'application/json'
if (key !== undefined)
headers['Idempotency-Key'] = `${run}-${key}`
const response = await fetch(`${api}${path}`, {
method,
headers,
body: body === undefined ? undefined : JSON.stringify(body),
})
const answer = await response.json()
if (!response.ok)
throw new Error(answer.error.message)
return answer
}
const intent = await call('POST', '/payment_intents', {
amount: 50000,
currency: 'mad',
payment_method_types: ['card'],
}, 'create')
await call('POST', `/payment_intents/${intent.id}/confirm`, {
payment_method_type: 'card',
return_url: 'https://shop.example.com/done',
}, 'confirm')
const charge = (await call('GET', `/payment_intents/${intent.id}`)).latest_charge
await call('POST', `/test_helpers/charges/${charge}/complete`, {
card_number: '4242424242424242',
})
let status
for (let i = 0; i < 10 && status !== 'succeeded'; i++) {
await new Promise(resolve => setTimeout(resolve, 1000))
status = (await call('GET', `/payment_intents/${intent.id}`)).status
}
console.log(intent.id, status)import { randomUUID } from 'node:crypto'
interface PaymentIntent {
id: string
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 auth = `Bearer ${process.env.APX_SECRET_KEY}`
const run = randomUUID()
async function call<T = PaymentIntent>(
method: 'GET' | 'POST',
path: string,
body?: object,
key?: string,
): Promise<T> {
const headers: Record<string, string> = { Authorization: auth }
if (body !== undefined)
headers['Content-Type'] = 'application/json'
if (key !== undefined)
headers['Idempotency-Key'] = `${run}-${key}`
const response = await fetch(`${api}${path}`, {
method,
headers,
body: body === undefined ? undefined : 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 T
}
const intent = await call('POST', '/payment_intents', {
amount: 50000,
currency: 'mad',
payment_method_types: ['card'],
}, 'create')
await call('POST', `/payment_intents/${intent.id}/confirm`, {
payment_method_type: 'card',
return_url: 'https://shop.example.com/done',
}, 'confirm')
const charge = (await call('GET', `/payment_intents/${intent.id}`)).latest_charge
await call<unknown>('POST', `/test_helpers/charges/${charge}/complete`, {
card_number: '4242424242424242',
})
let status: string | undefined
for (let i = 0; i < 10 && status !== 'succeeded'; i++) {
await new Promise(resolve => setTimeout(resolve, 1000))
status = (await call('GET', `/payment_intents/${intent.id}`)).status
}
console.log(intent.id, status)import os
import time
import uuid
import requests
API = "https://pay-api.adpromax.com/v1"
AUTH = (os.environ["APX_SECRET_KEY"], "")
RUN = uuid.uuid4().hex
def call(method, path, body=None, key=None):
headers = {"Idempotency-Key": f"{RUN}-{key}"} if key else {}
response = requests.request(
method, f"{API}{path}", auth=AUTH, json=body, headers=headers
)
answer = response.json()
response.raise_for_status()
return answer
intent = call("POST", "/payment_intents", {
"amount": 50000,
"currency": "mad",
"payment_method_types": ["card"],
}, "create")
call("POST", f"/payment_intents/{intent['id']}/confirm", {
"payment_method_type": "card",
"return_url": "https://shop.example.com/done",
}, "confirm")
charge = call("GET", f"/payment_intents/{intent['id']}")["latest_charge"]
call("POST", f"/test_helpers/charges/{charge}/complete", {
"card_number": "4242424242424242",
})
status = None
for _ in range(10):
time.sleep(1)
status = call("GET", f"/payment_intents/{intent['id']}")["status"]
if status == "succeeded":
break
print(intent["id"], status)<?php
function call(
string $method,
string $path,
?array $body = null,
?string $key = null
): array {
$headers = ['Content-Type: application/json'];
if ($key !== null) {
$headers[] = "Idempotency-Key: {$key}";
}
$ch = curl_init('https://pay-api.adpromax.com/v1' . $path);
curl_setopt_array($ch, [
CURLOPT_USERPWD => getenv('APX_SECRET_KEY') . ':',
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_RETURNTRANSFER => true,
]);
if ($body !== null) {
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
}
$answer = json_decode(curl_exec($ch), true);
return $answer;
}
$run = bin2hex(random_bytes(8));
$intent = call('POST', '/payment_intents', [
'amount' => 50000,
'currency' => 'mad',
'payment_method_types' => ['card'],
], "{$run}-create");
call('POST', "/payment_intents/{$intent['id']}/confirm", [
'payment_method_type' => 'card',
'return_url' => 'https://shop.example.com/done',
], "{$run}-confirm");
$charge = call('GET', "/payment_intents/{$intent['id']}")['latest_charge'];
call('POST', "/test_helpers/charges/{$charge}/complete", [
'card_number' => '4242424242424242',
]);
for ($i = 0, $status = null; $i < 10 && $status !== 'succeeded'; $i++) {
sleep(1);
$status = call('GET', "/payment_intents/{$intent['id']}")['status'];
}
echo $intent['id'], ' ', $status, PHP_EOL;require 'json'
require 'minitest/autorun'
require 'net/http'
require 'securerandom'
class CardPaymentTest < Minitest::Test
API = 'https://pay-api.adpromax.com/v1'
RUN = SecureRandom.uuid
def api(method, path, body = nil, key = nil)
uri = URI("#{API}#{path}")
request = (method == :get ? Net::HTTP::Get : Net::HTTP::Post).new(uri)
request.basic_auth(ENV.fetch('APX_SECRET_KEY'), '')
request['Idempotency-Key'] = "#{RUN}-#{key}" if key
if body
request['Content-Type'] = 'application/json'
request.body = JSON.generate(body)
end
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
def test_a_card_payment_succeeds
intent = api(:post, '/payment_intents', {
amount: 50_000,
currency: 'mad',
payment_method_types: ['card']
}, 'create')
api(:post, "/payment_intents/#{intent['id']}/confirm", {
payment_method_type: 'card',
return_url: 'https://shop.example.com/done'
}, 'confirm')
charge = api(:get, "/payment_intents/#{intent['id']}")['latest_charge']
api(:post, "/test_helpers/charges/#{charge}/complete",
{ card_number: '4242424242424242' })
status = nil
10.times do
sleep 1
status = api(:get, "/payment_intents/#{intent['id']}")['status']
break if status == 'succeeded'
end
assert_equal 'succeeded', status
end
endpackage main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"testing"
"time"
)
const api = "https://pay-api.adpromax.com/v1"
type paymentIntent struct {
ID string `json:"id"`
Status string `json:"status"`
LatestCharge string `json:"latest_charge"`
}
func call(t *testing.T, method, path string, body any, key string) paymentIntent {
t.Helper()
var payload io.Reader
if body != nil {
encoded, err := json.Marshal(body)
if err != nil {
t.Fatal(err)
}
payload = bytes.NewReader(encoded)
}
req, err := http.NewRequest(method, api+path, payload)
if err != nil {
t.Fatal(err)
}
req.SetBasicAuth(os.Getenv("APX_SECRET_KEY"), "")
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
if key != "" {
req.Header.Set("Idempotency-Key", key)
}
res, err := http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
}
defer res.Body.Close()
if res.StatusCode >= 400 {
t.Fatalf("%s %s: %s", method, path, res.Status)
}
var answer paymentIntent
if err := json.NewDecoder(res.Body).Decode(&answer); err != nil {
t.Fatal(err)
}
return answer
}
func TestCardPayment(t *testing.T) {
run := fmt.Sprint(time.Now().UnixNano())
intent := call(t, "POST", "/payment_intents", map[string]any{
"amount": 50000,
"currency": "mad",
"payment_method_types": []string{"card"},
}, run+"-create")
path := "/payment_intents/" + intent.ID
call(t, "POST", path+"/confirm", map[string]any{
"payment_method_type": "card",
"return_url": "https://shop.example.com/done",
}, run+"-confirm")
charge := call(t, "GET", path, nil, "").LatestCharge
call(t, "POST", "/test_helpers/charges/"+charge+"/complete",
map[string]any{"card_number": "4242424242424242"}, "")
status := ""
for i := 0; i < 10 && status != "succeeded"; i++ {
time.Sleep(time.Second)
status = call(t, "GET", path, nil, "").Status
}
if status != "succeeded" {
t.Fatalf("%s is %q, want succeeded", intent.ID, status)
}
}import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class CardPaymentTest {
static final String API = "https://pay-api.adpromax.com/v1";
static final String RUN = UUID.randomUUID().toString();
static final HttpClient HTTP = HttpClient.newHttpClient();
static String call(String method, String path, String body, String key)
throws Exception {
HttpRequest.Builder request = HttpRequest.newBuilder(URI.create(API + path))
.header("Authorization", "Bearer " + System.getenv("APX_SECRET_KEY"))
.header("Content-Type", "application/json")
.method(method, body == null
? HttpRequest.BodyPublishers.noBody()
: HttpRequest.BodyPublishers.ofString(body));
if (key != null) {
request.header("Idempotency-Key", RUN + "-" + key);
}
HttpResponse<String> response = HTTP.send(
request.build(), HttpResponse.BodyHandlers.ofString());
if (response.statusCode() >= 400) {
throw new IllegalStateException(response.body());
}
return response.body();
}
// No JSON library here: a small regex reads the first field of that name.
static String field(String json, String name) {
Matcher match = Pattern
.compile("\"" + name + "\"\\s*:\\s*\"([^\"]*)\"")
.matcher(json);
if (!match.find()) {
throw new IllegalStateException("no " + name + " in " + json);
}
return match.group(1);
}
public static void main(String[] args) throws Exception {
String intent = field(call("POST", "/payment_intents", """
{
"amount": 50000,
"currency": "mad",
"payment_method_types": ["card"]
}
""", "create"), "id");
String path = "/payment_intents/" + intent;
call("POST", path + "/confirm", """
{
"payment_method_type": "card",
"return_url": "https://shop.example.com/done"
}
""", "confirm");
String charge = field(call("GET", path, null, null), "latest_charge");
call("POST", "/test_helpers/charges/" + charge + "/complete", """
{"card_number": "4242424242424242"}
""", null);
String status = null;
for (int i = 0; i < 10 && !"succeeded".equals(status); i++) {
Thread.sleep(1000);
status = field(call("GET", path, null, null), "status");
}
if (!"succeeded".equals(status)) {
throw new AssertionError(intent + " is " + status);
}
System.out.println(intent + " " + status);
}
}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"));
var api = "https://pay-api.adpromax.com/v1";
var run = Guid.NewGuid().ToString();
async Task<PaymentIntent> Call(
HttpMethod method, string path, object? body = null, string? key = null)
{
using var request = new HttpRequestMessage(method, api + path);
if (body is not null)
request.Content = JsonContent.Create(body);
if (key is not null)
request.Headers.Add("Idempotency-Key", $"{run}-{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))!;
}
var intent = await Call(HttpMethod.Post, "/payment_intents", new
{
amount = 50000,
currency = "mad",
payment_method_types = new[] { "card" },
}, "create");
var path = $"/payment_intents/{intent.Id}";
await Call(HttpMethod.Post, $"{path}/confirm", new
{
payment_method_type = "card",
return_url = "https://shop.example.com/done",
}, "confirm");
var charge = (await Call(HttpMethod.Get, path)).LatestCharge;
await Call(HttpMethod.Post, $"/test_helpers/charges/{charge}/complete",
new { card_number = "4242424242424242" });
string? status = null;
for (var i = 0; i < 10 && status != "succeeded"; i++)
{
await Task.Delay(1000);
status = (await Call(HttpMethod.Get, path)).Status;
}
if (status != "succeeded")
throw new InvalidOperationException($"{intent.Id} is {status}");
Console.WriteLine($"{intent.Id} {status}");
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_json = "1"
// Run it with `cargo test`.
fn main() {}
#[cfg(test)]
mod tests {
use std::thread;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use reqwest::{blocking::Client, Method};
use serde_json::{json, Value};
const API: &str = "https://pay-api.adpromax.com/v1";
type Error = Box<dyn std::error::Error>;
fn call(
method: Method,
path: &str,
body: Option<Value>,
key: Option<String>,
) -> Result<Value, Error> {
let mut request = Client::new()
.request(method, format!("{API}{path}"))
.bearer_auth(std::env::var("APX_SECRET_KEY")?);
if let Some(body) = body {
request = request.json(&body);
}
if let Some(key) = key {
request = request.header("Idempotency-Key", key);
}
let response = request.send()?;
let failed = !response.status().is_success();
let answer: Value = response.json()?;
if failed {
let message = answer["error"]["message"].as_str();
return Err(message.unwrap_or("request failed").into());
}
Ok(answer)
}
#[test]
fn a_card_payment_succeeds() -> Result<(), Error> {
let run = SystemTime::now().duration_since(UNIX_EPOCH)?.as_nanos();
let intent = call(
Method::POST,
"/payment_intents",
Some(json!({
"amount": 50000,
"currency": "mad",
"payment_method_types": ["card"]
})),
Some(format!("{run}-create")),
)?;
let id = intent["id"].as_str().ok_or("no id")?;
let path = format!("/payment_intents/{id}");
call(
Method::POST,
&format!("{path}/confirm"),
Some(json!({
"payment_method_type": "card",
"return_url": "https://shop.example.com/done"
})),
Some(format!("{run}-confirm")),
)?;
let waiting = call(Method::GET, &path, None, None)?;
let charge = waiting["latest_charge"].as_str().ok_or("no charge")?;
call(
Method::POST,
&format!("/test_helpers/charges/{charge}/complete"),
Some(json!({ "card_number": "4242424242424242" })),
None,
)?;
let mut status = String::new();
for _ in 0..10 {
thread::sleep(Duration::from_secs(1));
let current = call(Method::GET, &path, None, None)?;
status = current["status"].as_str().unwrap_or_default().to_owned();
if status == "succeeded" {
break;
}
}
assert_eq!(status, "succeeded", "{id}");
Ok(())
}
}Elsewhere in test mode
- The voucher page your customer sees carries Simulate payment and the other cash outcomes.
- On the dashboard, a test payment's page has a test helpers card with the same actions.
- Neither exists in live mode, where only a real payment moves a payment.