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.

Open as Markdown
On this page
  1. How a cash payment moves
  2. The rules
  3. The voucher
  4. Where your customers pay
  5. When it does not go to plan
  6. Cancel a code you no longer want
  7. Cash is not refunded
  8. From the dashboard
  9. Your own page
  10. Good practice

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

A cash payment, from the code to the money
  • 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
  1. 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.
  2. They go to a partner payment agent, give the code, pay in cash and keep their receipt.
  3. A status check sees the payment: the payment intent becomes succeeded and you receive payment_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 (10000 centimes), in test mode as in live mode. A smaller amount is refused with amount_too_small.
  • Your customer's full name and city are required. The hosted page asks for them; with the API you send payer.name and payer.city when 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:

FieldWhat it holds
codeThe payment code your customer gives at the agent. Show it as it is: never change its characters.
amount_to_payWhat 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_atThe deadline, in Unix seconds: about three days after the code is issued. Show it in Morocco time.
slip_urlA printable slip.
hosted_voucher_urlOur 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 happensWhat you receiveWhat you do
The deadline passes unpaidpayment_intent.payment_failed, with last_payment_error.code set to voucher_expired; back to requires_payment_methodFree 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 codepayment_intent.payment_failed with voucher_refusedThe same: your customer can get a new code.
The code is put on hold at the networkNothing until it is paid or notWait.
Your customer pays another amount, or pays after the deadline or after you canceledreview.opened; nothing is creditedWait: 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
  • Node.js
  • TypeScript
  • Python
  • PHP
  • Ruby
  • Go
  • Java
  • C#
  • Rust
cancel.mjscancel.tscancel.pycancel.phpcancel.rbcancel.goCancel.javaCancel.csmain.rs
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) // canceled
interface 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) // canceled
import 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; // canceled
require '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'] # canceled
package 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_pay and 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.