Initiate a payment
curl --request POST \
--url https://api.cartevo.co/api/v1/payment/collect \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"operator": "mtn",
"country": "CM",
"phone_number": "237670000000",
"amount": 1000,
"currency": "XAF",
"notify_url": "https://example.com/webhook/payment",
"external_id": "<string>",
"reference_id": "<string>",
"lang": "<string>",
"purpose": "<string>"
}
'import requests
url = "https://api.cartevo.co/api/v1/payment/collect"
payload = {
"operator": "mtn",
"country": "CM",
"phone_number": "237670000000",
"amount": 1000,
"currency": "XAF",
"notify_url": "https://example.com/webhook/payment",
"external_id": "<string>",
"reference_id": "<string>",
"lang": "<string>",
"purpose": "<string>"
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
operator: 'mtn',
country: 'CM',
phone_number: '237670000000',
amount: 1000,
currency: 'XAF',
notify_url: 'https://example.com/webhook/payment',
external_id: '<string>',
reference_id: '<string>',
lang: '<string>',
purpose: '<string>'
})
};
fetch('https://api.cartevo.co/api/v1/payment/collect', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.cartevo.co/api/v1/payment/collect",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'operator' => 'mtn',
'country' => 'CM',
'phone_number' => '237670000000',
'amount' => 1000,
'currency' => 'XAF',
'notify_url' => 'https://example.com/webhook/payment',
'external_id' => '<string>',
'reference_id' => '<string>',
'lang' => '<string>',
'purpose' => '<string>'
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.cartevo.co/api/v1/payment/collect"
payload := strings.NewReader("{\n \"operator\": \"mtn\",\n \"country\": \"CM\",\n \"phone_number\": \"237670000000\",\n \"amount\": 1000,\n \"currency\": \"XAF\",\n \"notify_url\": \"https://example.com/webhook/payment\",\n \"external_id\": \"<string>\",\n \"reference_id\": \"<string>\",\n \"lang\": \"<string>\",\n \"purpose\": \"<string>\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.cartevo.co/api/v1/payment/collect")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"operator\": \"mtn\",\n \"country\": \"CM\",\n \"phone_number\": \"237670000000\",\n \"amount\": 1000,\n \"currency\": \"XAF\",\n \"notify_url\": \"https://example.com/webhook/payment\",\n \"external_id\": \"<string>\",\n \"reference_id\": \"<string>\",\n \"lang\": \"<string>\",\n \"purpose\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.cartevo.co/api/v1/payment/collect")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"operator\": \"mtn\",\n \"country\": \"CM\",\n \"phone_number\": \"237670000000\",\n \"amount\": 1000,\n \"currency\": \"XAF\",\n \"notify_url\": \"https://example.com/webhook/payment\",\n \"external_id\": \"<string>\",\n \"reference_id\": \"<string>\",\n \"lang\": \"<string>\",\n \"purpose\": \"<string>\"\n}"
response = http.request(request)
puts response.read_body{
"success": true,
"message": "Collection initiated successfully",
"data": {
"transaction_id": "550e8400-e29b-41d4-a716-446655440000",
"external_id": "COLL-1234567890-abc12345",
"status": "PENDING",
"amount": 1000,
"currency": "XAF",
"operator": "mtn",
"country": "CM",
"phone_number": "237670000000",
"initiated_at": "2025-01-08T12:00:00.000Z"
}
}{
"statusCode": 400,
"message": "Transaction with external_id COLL-123 already exists",
"error": "Bad Request"
}Payment Collection
Initiate Payment Collection
Charge a mobile-money account to credit your company’s pay-in wallet. Supports MTN, Orange, Moov, Wave, M-Pesa, and other African operators.
POST
/
payment
/
collect
Initiate a payment
curl --request POST \
--url https://api.cartevo.co/api/v1/payment/collect \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"operator": "mtn",
"country": "CM",
"phone_number": "237670000000",
"amount": 1000,
"currency": "XAF",
"notify_url": "https://example.com/webhook/payment",
"external_id": "<string>",
"reference_id": "<string>",
"lang": "<string>",
"purpose": "<string>"
}
'import requests
url = "https://api.cartevo.co/api/v1/payment/collect"
payload = {
"operator": "mtn",
"country": "CM",
"phone_number": "237670000000",
"amount": 1000,
"currency": "XAF",
"notify_url": "https://example.com/webhook/payment",
"external_id": "<string>",
"reference_id": "<string>",
"lang": "<string>",
"purpose": "<string>"
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
operator: 'mtn',
country: 'CM',
phone_number: '237670000000',
amount: 1000,
currency: 'XAF',
notify_url: 'https://example.com/webhook/payment',
external_id: '<string>',
reference_id: '<string>',
lang: '<string>',
purpose: '<string>'
})
};
fetch('https://api.cartevo.co/api/v1/payment/collect', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.cartevo.co/api/v1/payment/collect",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'operator' => 'mtn',
'country' => 'CM',
'phone_number' => '237670000000',
'amount' => 1000,
'currency' => 'XAF',
'notify_url' => 'https://example.com/webhook/payment',
'external_id' => '<string>',
'reference_id' => '<string>',
'lang' => '<string>',
'purpose' => '<string>'
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.cartevo.co/api/v1/payment/collect"
payload := strings.NewReader("{\n \"operator\": \"mtn\",\n \"country\": \"CM\",\n \"phone_number\": \"237670000000\",\n \"amount\": 1000,\n \"currency\": \"XAF\",\n \"notify_url\": \"https://example.com/webhook/payment\",\n \"external_id\": \"<string>\",\n \"reference_id\": \"<string>\",\n \"lang\": \"<string>\",\n \"purpose\": \"<string>\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.cartevo.co/api/v1/payment/collect")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"operator\": \"mtn\",\n \"country\": \"CM\",\n \"phone_number\": \"237670000000\",\n \"amount\": 1000,\n \"currency\": \"XAF\",\n \"notify_url\": \"https://example.com/webhook/payment\",\n \"external_id\": \"<string>\",\n \"reference_id\": \"<string>\",\n \"lang\": \"<string>\",\n \"purpose\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.cartevo.co/api/v1/payment/collect")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"operator\": \"mtn\",\n \"country\": \"CM\",\n \"phone_number\": \"237670000000\",\n \"amount\": 1000,\n \"currency\": \"XAF\",\n \"notify_url\": \"https://example.com/webhook/payment\",\n \"external_id\": \"<string>\",\n \"reference_id\": \"<string>\",\n \"lang\": \"<string>\",\n \"purpose\": \"<string>\"\n}"
response = http.request(request)
puts response.read_body{
"success": true,
"message": "Collection initiated successfully",
"data": {
"transaction_id": "550e8400-e29b-41d4-a716-446655440000",
"external_id": "COLL-1234567890-abc12345",
"status": "PENDING",
"amount": 1000,
"currency": "XAF",
"operator": "mtn",
"country": "CM",
"phone_number": "237670000000",
"initiated_at": "2025-01-08T12:00:00.000Z"
}
}{
"statusCode": 400,
"message": "Transaction with external_id COLL-123 already exists",
"error": "Bad Request"
}Overview
POST /payment/collect initiates a pay-in: it charges a mobile-money account and credits the proceeds to your company’s pay-in wallet for the matching country/currency. The call returns immediately with status: PENDING; the final outcome (SUCCESS or FAILED) reaches you asynchronously via webhook (or via notify_url if you supply one).
For some operators, the end user must approve the charge with an OTP or USSD step — see the country/operator matrix in Payment guidelines.
When to use it
- You need to charge an end user’s mobile-money account to settle a bill, top up a balance, or fund a service.
POST /payment/payout.
Prerequisites
- Your company has a wallet for the requested
(country, currency)pair. If not, create it first viaPOST /wallets. - The combination of
country,currency, andoperatoris supported — see Payment guidelines.
Request
Headers
| Name | Required | Description |
|---|---|---|
Authorization | Yes | Bearer <access_token> |
Content-Type | Yes | application/json |
Body
| Field | Type | Required | Constraints / format |
|---|---|---|---|
operator | string | Yes | One of mtn, orange, moov, airtel, mpesa, afrimoney, vodacom, wave, wligdicash, expresso, free, tmoney, celtiis, coris. Lowercase. |
country | string | Yes | ISO 3166-1 alpha-2, uppercase (e.g. CM, CI, SN). Selects the wallet. |
phone_number | string | Yes | Country code + local number, no + (e.g. 237670000000). This format is specific to payments; other endpoints use E.164 with +. |
amount | number | Yes | Minimum 1. Unit is the major currency unit of currency. |
currency | string | Yes | ISO 4217. Must match a wallet you own. Common values: XAF, XOF, CDF, GNF, USD. |
external_id | string | No | Idempotency key. Up to 255 chars. If you supply the same value twice, the second call is rejected with 400. Auto-generated if omitted (you lose idempotency). |
notify_url | string | No | HTTPS URL to receive the final status webhook. If omitted, only the company-level webhook is fired. |
reference_id | string | No | Free-form reference shown in the dashboard. Up to 255 chars. |
lang | string | No | Two-letter language code for any operator-side prompts (e.g. en, fr). |
purpose | string | No | Free-form description of why the collection is happening (e.g. "Invoice payment"). |
otp_code | string | No | One-time code the payer generates on their phone, required by some operators (notably Orange Burkina Faso and Orange Senegal) to validate the collection. Forwarded to the operator as otp_code. See the Orange Money workflow below. |
{
"operator": "mtn",
"country": "CM",
"phone_number": "237670000000",
"amount": 1000,
"currency": "XAF",
"external_id": "ORD-2026-001",
"notify_url": "https://your-app.com/webhooks/cartevo",
"purpose": "Invoice payment for May 2026"
}
Orange Money workflow (Burkina Faso & Senegal)
Orange Money in Burkina Faso and Senegal requires an OTP. The operator does not push a confirmation prompt to the payer — instead the payer must generate the one-time code themselves and give it to you, then you submit it with the collection:- The payer dials the Orange USSD code on their phone to generate the payment OTP:
- Burkina Faso (
country: "BF"):*144*4*6*<amount>#— where<amount>is the collection amount. - Senegal (
country: "SN"):#144*391#
- Burkina Faso (
- The payer gives you the generated OTP.
- You call
POST /payment/collectwithoperator: "orange", the matchingcountry,currency: "XOF", and the OTP inotp_code.
{
"operator": "orange",
"country": "BF",
"phone_number": "22670000000",
"amount": 1000,
"currency": "XOF",
"otp_code": "123456",
"external_id": "ORD-2026-002"
}
The OTP is short-lived (a few minutes) and single-use. If it has expired the
collection fails — the payer must generate a fresh one and you retry with a
new
external_id.Other operators also require an OTP (e.g. Wallet LigdiCash in Burkina
Faso). Whenever the operator requires one, send it in
otp_code. See the
Payment guidelines for the full
country/operator OTP matrix.Idempotency
Always supply your ownexternal_id. Cartevo rejects duplicates with 400 (after the first call), so storing the value before you send the request and reusing it on retry is safe. If you let Cartevo auto-generate one, you lose this protection.
Response
200 — Collection initiated
{
"success": true,
"statusCode": 200,
"message": "Collection initiated successfully",
"data": {
"transaction_id": "550e8400-e29b-41d4-a716-446655440000",
"external_id": "ORD-2026-001",
"status": "PENDING",
"amount": 1000,
"currency": "XAF",
"operator": "mtn",
"country": "CM",
"phone_number": "237670000000",
"initiated_at": "2026-05-09T10:15:00.000Z",
"provider_link": null
}
}
| Field | Type | Description |
|---|---|---|
transaction_id | string | Cartevo transaction ID (UUID). Use this for GET /payment/transactions/{id}/status. |
external_id | string | Echoes your idempotency key (or the auto-generated one). |
status | string | PENDING immediately after this call. Other values: PROCESSING, SUCCESS, FAILED. |
initiated_at | string | ISO 8601 (UTC). |
provider_link | string | null | Link-based operators only (e.g. Wave CI/SN). A payment URL you must redirect the payer to so they can confirm. null for operators that don’t use a link (MTN, Orange…). |
Wave workflow (payment link)
Wave (Côte d’Ivoire, Senegal, …) requires a payment link. Wave does not push a USSD/OTP prompt; instead AfribaPay returns aprovider_link in the
response and the payer must open it to confirm:
- Call
POST /payment/collectwithoperator: "wave". - The response contains a non-null
provider_link(e.g.https://pay.wave.com/c/cos-...). - Redirect the payer to that URL (or open it). They confirm the payment on Wave’s page.
- The transaction stays
PENDINGuntil they confirm; the finalSUCCESS/FAILEDarrives via webhook.
The Wave link is short-lived. If the payer doesn’t confirm in time the
collection fails — retry with a new
external_id to get a fresh link.Lifecycle
PENDING → PROCESSING → SUCCESS
↘ FAILED
PENDING— Cartevo accepted the request; the operator has been notified.PROCESSING— The operator is awaiting user action (OTP / USSD) or processing internally.SUCCESS— Funds have been credited to your pay-in wallet. Thepayment.collectwebhook fires.FAILED— End user rejected, insufficient funds at the operator, OTP timeout, etc. Seeerror_messagein the status endpoint.
PROCESSING can last up to 15 minutes for OTP-required operators. Beyond that, treat as failed and reconcile via the status endpoint.
Error responses
| Status | message example | Trigger |
|---|---|---|
400 | "Validation failed: country must be a valid ISO 3166-1 alpha-2 code" | Field-level validation failed. |
400 | "Duplicate external_id" | An earlier call used the same external_id. |
400 | "No wallet for this country and currency" | You don’t own a wallet for the (country, currency) pair. |
400 | "Operator not available for this country/currency" | See Payment guidelines. |
401 | "Unauthorized" | Missing or expired token. |
Webhooks fired
payment.collect— fires when the collection is initiated, then again on status changes (SUCCESS/FAILED).- If you supplied
notify_url, it receives a copy of each status change.
Code examples
cURL
curl -X POST https://api.cartevo.co/api/v1/payment/collect \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"operator": "mtn",
"country": "CM",
"phone_number": "237670000000",
"amount": 1000,
"currency": "XAF",
"external_id": "ORD-2026-001"
}'
Node.js (axios)
const externalId = `ORD-${Date.now()}`;
await db.savePendingCollection(externalId);
const { data } = await axios.post(
"https://api.cartevo.co/api/v1/payment/collect",
{
operator: "mtn",
country: "CM",
phone_number: "237670000000",
amount: 1000,
currency: "XAF",
external_id: externalId,
notify_url: "https://your-app.com/webhooks/cartevo",
},
{ headers: { Authorization: `Bearer ${token}` } }
);
Related
GET /payment/transactions/{id}/status— poll for the outcome.POST /payment/payout— send money out via mobile money.- Payment guidelines — supported countries, operators, OTP/USSD requirements.
- Webhooks — full webhook catalogue.
Authorizations
Bearer authentication header of the form Bearer <token>, where <token> is your auth token.
Body
application/json
Available options:
mtn, orange, moov, airtel, mpesa, afrimoney, vodacom, wave, wligdicash, expresso, free, tmoney, celtiis, coris Example:
"mtn"
Pattern:
^[A-Z]{2}$Example:
"CM"
Example:
"237670000000"
Required range:
x >= 1Example:
1000
Example:
"XAF"
Example:
"https://example.com/webhook/payment"
Idempotency key (up to 255 chars). Duplicate values are rejected with 400 after the first call. Auto-generated if omitted.
Free-form reference shown in the dashboard (up to 255 chars).
Two-letter language code for operator-side prompts (e.g. en, fr).
Free-form description of the payment's purpose.