Payments
Cash payments
Vouchers, the amount to pay, deadlines, the 100 MAD minimum, where your customers pay, and what to do when a code expires.
On this page
With cash, your customer receives a payment code, on a voucher, and pays it in cash at a partner payment agent before a deadline. You learn the result from a webhook, as for a card. Cash lets you sell to customers who have no card, or prefer not to use one online.
To try it in a few minutes, follow the cash quickstart.
How a cash payment moves
- requires_payment_method → requires_action: code issued
- requires_action → succeeded: paid at an agent, confirmed
- requires_action → requires_payment_method: expired or refused
- requires_action → canceled: you cancel
- Your customer chooses cash on the hosted page, or you confirm a payment intent with cash. They
receive a voucher, and the payment intent is
requires_action. - They go to a partner payment agent, give the code, pay in cash and keep their receipt.
- A status check sees the payment: the payment intent becomes
succeededand you receivepayment_intent.succeeded. In live mode the check runs every 2 minutes, so the event usually comes within minutes of the payment; in test mode it runs at once after a simulated payment.
Until the payment is confirmed, the payment intent stays requires_action, possibly for days.
The rules
- A cash payment is at least 100.00 MAD (
10000centimes), in test mode as in live mode. A smaller amount is refused withamount_too_small. - Your customer's full name and city are required. The hosted page asks for them; with the API
you send
payer.nameandpayer.citywhen you confirm. A phone number is optional. - The currency is
mad, and amounts are integers in centimes, as everywhere in the API.
The voucher
The voucher is in the payment intent's next_action.display_voucher:
| Field | What it holds |
|---|---|
code | The payment code your customer gives at the agent. Show it as it is: never change its characters. |
amount_to_pay | What your customer hands over: your amount plus a small stamp fee, which your customer pays (0.25 MAD in test mode). Show this figure as the voucher gives it; never compute it. The fee is not taken from your amount. |
expires_at | The deadline, in Unix seconds: about three days after the code is issued. Show it in Morocco time. |
slip_url | A printable slip. |
hosted_voucher_url | Our voucher page, which you can send to your customer. |
Our voucher page shows the code with a Copy button, the amount to hand over, the deadline in Morocco time, where and how to pay, and buttons to print it, share it by WhatsApp or SMS, and I've paid: check now. It is in your colours (Settings → Branding) and works on any phone.
Where your customers pay
At a partner payment agent: your customer gives the code, pays the amount to hand over in cash, before the deadline, and keeps the agent's receipt. The voucher page says which agents accept the code and how to find one nearby.
In test mode no agent accepts a code: you simulate the payment instead, on the voucher page, on the dashboard or with the test helpers. Every outcome is listed in Test cash scenarios.
When it does not go to plan
| What happens | What you receive | What you do |
|---|---|---|
| The deadline passes unpaid | payment_intent.payment_failed, with last_payment_error.code set to voucher_expired; back to requires_payment_method | Free the order or the stock, or let your customer get a new code: on the hosted page while the session is open, or by confirming the payment intent again. |
| The payment network refuses the code | payment_intent.payment_failed with voucher_refused | The same: your customer can get a new code. |
| The code is put on hold at the network | Nothing until it is paid or not | Wait. |
| Your customer pays another amount, or pays after the deadline or after you canceled | review.opened; nothing is credited | Wait: a person on our side looks at it. See When something does not add up. |
Cancel a code you no longer want
If the order is canceled before your customer pays, cancel the payment intent with
POST /v1/payment_intents/{id}/cancel. The code can no longer be paid; you receive
payment_intent.canceled.
If your customer pays it anyway, nothing is credited: the payment gets a review, and a person on our side looks at it. Tell your customer when you cancel.
curl -X POST \
https://pay-api.adpromax.com/v1/payment_intents/pi_01k7e6t4v8x2z5b9c3d7f1g6hq/cancel \
-u "$APX_SECRET_KEY:"const id = 'pi_01k7e6t4v8x2z5b9c3d7f1g6hq'
const response = await fetch(`https://pay-api.adpromax.com/v1/payment_intents/${id}/cancel`, {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.APX_SECRET_KEY}` },
})
const intent = await response.json()
console.log(intent.status) // canceledinterface PaymentIntent {
id: string
object: 'payment_intent'
status: string
}
const id = 'pi_01k7e6t4v8x2z5b9c3d7f1g6hq'
const response = await fetch(`https://pay-api.adpromax.com/v1/payment_intents/${id}/cancel`, {
method: 'POST',
headers: { Authorization: `Bearer ${process.env.APX_SECRET_KEY}` },
})
const intent = (await response.json()) as PaymentIntent
console.log(intent.status) // canceledimport os
import requests
intent_id = "pi_01k7e6t4v8x2z5b9c3d7f1g6hq"
response = requests.post(
f"https://pay-api.adpromax.com/v1/payment_intents/{intent_id}/cancel",
auth=(os.environ["APX_SECRET_KEY"], ""),
)
print(response.json()["status"]) # canceled<?php
$id = 'pi_01k7e6t4v8x2z5b9c3d7f1g6hq';
$ch = curl_init("https://pay-api.adpromax.com/v1/payment_intents/{$id}/cancel");
curl_setopt_array($ch, [
CURLOPT_USERPWD => getenv('APX_SECRET_KEY') . ':',
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
]);
$intent = json_decode(curl_exec($ch), true);
echo $intent['status'], PHP_EOL; // canceledrequire 'json'
require 'net/http'
id = 'pi_01k7e6t4v8x2z5b9c3d7f1g6hq'
uri = URI("https://pay-api.adpromax.com/v1/payment_intents/#{id}/cancel")
request = Net::HTTP::Post.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
puts JSON.parse(response.body)['status'] # canceledpackage main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"os"
)
type PaymentIntent struct {
ID string `json:"id"`
Status string `json:"status"`
}
func main() {
id := "pi_01k7e6t4v8x2z5b9c3d7f1g6hq"
req, err := http.NewRequest(http.MethodPost,
"https://pay-api.adpromax.com/v1/payment_intents/"+id+"/cancel", nil)
if err != nil {
log.Fatal(err)
}
req.SetBasicAuth(os.Getenv("APX_SECRET_KEY"), "")
res, err := http.DefaultClient.Do(req)
if err != nil {
log.Fatal(err)
}
defer res.Body.Close()
var intent PaymentIntent
if err := json.NewDecoder(res.Body).Decode(&intent); err != nil {
log.Fatal(err)
}
fmt.Println(intent.Status) // canceled
}import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class Cancel {
public static void main(String[] args) throws Exception {
String id = "pi_01k7e6t4v8x2z5b9c3d7f1g6hq";
HttpRequest request = HttpRequest.newBuilder(
URI.create("https://pay-api.adpromax.com/v1/payment_intents/" + id + "/cancel"))
.header("Authorization", "Bearer " + System.getenv("APX_SECRET_KEY"))
.POST(HttpRequest.BodyPublishers.noBody())
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
// The payment intent as JSON: its status is now "canceled".
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"));
var id = "pi_01k7e6t4v8x2z5b9c3d7f1g6hq";
using var response = await http.PostAsync(
$"https://pay-api.adpromax.com/v1/payment_intents/{id}/cancel", null);
var intent = await response.Content.ReadFromJsonAsync<PaymentIntent>(json);
Console.WriteLine(intent!.Status); // canceled
record PaymentIntent(string Id, string Status);// Cargo.toml: reqwest = { version = "0.12", features = ["blocking", "json"] }
// serde = { version = "1", features = ["derive"] }
use serde::Deserialize;
#[derive(Deserialize)]
struct PaymentIntent {
status: String,
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let id = "pi_01k7e6t4v8x2z5b9c3d7f1g6hq";
let intent: PaymentIntent = reqwest::blocking::Client::new()
.post(format!("https://pay-api.adpromax.com/v1/payment_intents/{id}/cancel"))
.bearer_auth(std::env::var("APX_SECRET_KEY")?)
.send()?
.json()?;
println!("{}", intent.status); // canceled
Ok(())
}Cash is not refunded
A succeeded cash payment cannot be refunded through AdProMaxPay, neither in cash nor to a card: see Refunds for why, and what to do instead.
From the dashboard
You can also take a cash payment without code:
- Create → Cash voucher: an amount from 100.00 MAD, a description, a customer if you want, your customer's name and city, and a phone number if you have it. The payment's page then lets you share the voucher by WhatsApp, SMS or a link.
- Payment links can offer cash next to card: each person who opens the link chooses.
Your own page
You can show the code on your own page rather than ours: create a payment intent, confirm it with
cash, then display next_action.display_voucher. See
Show the code on your own page.
Good practice
- Hold the order or the stock until the deadline, then free it on
voucher_expired. - Show
amount_to_payand the deadline exactly as the voucher gives them, with the time zone. - Do not promise an instant confirmation: tell your customer the order is confirmed once their payment is.
- Remind your customer before the deadline if you can: most unpaid codes simply expire.