# 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.

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](https://pay.adpromax.com/auth/signup): 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.

```bash title="Terminal"
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.

```bash
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" }
  }'
```

```js title="checkout.mjs"
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)
```

```ts title="checkout.ts"
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)
```

```python title="checkout.py"
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 title="checkout.php"
<?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;
```

```ruby title="checkout.rb"
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']}"
```

```go title="checkout.go"
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)
}
```

```java title="Checkout.java"
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());
    }
}
```

```csharp title="Checkout.cs"
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);
```

```rust title="main.rs"
// 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](https://pay-docs.adpromax.com/en/sandbox/cash-scenarios).

```bash
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:"
```

```js title="simulate.mjs"
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`)
```

```ts title="simulate.ts"
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`)
```

```python title="simulate.py"
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 title="simulate.php"
<?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");
```

```ruby title="simulate.rb"
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")
```

```go title="simulate.go"
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)
	}
}
```

```java title="Simulate.java"
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");
    }
}
```

```csharp title="Simulate.cs"
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);
```

```rust title="main.rs"
// 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](https://pay-docs.adpromax.com/en/get-started/quickstart-card#5-create-a-webhook-endpoint), then
[handle the event](https://pay-docs.adpromax.com/en/get-started/quickstart-card#6-handle-the-event). A cash payment is never
refunded in cash: see [Refunds](https://pay-docs.adpromax.com/en/payments/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`.

```bash
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"}}'
```

```js title="voucher.mjs"
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)
```

```ts title="voucher.ts"
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)
```

```python title="voucher.py"
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 title="voucher.php"
<?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;
```

```ruby title="voucher.rb"
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']}"
```

```go title="voucher.go"
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)
}
```

```java title="Voucher.java"
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);
    }
}
```

```csharp title="Voucher.cs"
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);
```

```rust title="main.rs"
// 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(())
}
```

```json title="200 OK" response
{
  "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](https://pay-docs.adpromax.com/en/get-started/quickstart-card).
* [Test cash scenarios](https://pay-docs.adpromax.com/en/sandbox/cash-scenarios): expired, refused, paid late, paid another amount.
* [Sandbox and live: the differences](https://pay-docs.adpromax.com/en/sandbox/differences): what changes for cash in live mode.
