Payments
Initiate payments and collect money through the API
The Payments API allows you to collect payments through various payment methods including mobile money (MTN, Moov, Orange) and card payments (Stripe).
All API requests require a Bearer token. Include your API token in the Authorization header:
Authorization: Bearer YOUR_API_TOKEN
Note: The payment verification endpoint (/api/payments/verify/{reference}) is publicly accessible and does not require authentication.
| Method | Endpoint | Description |
|---|
POST | /api/payments/initiate | Initiate a new payment |
GET | /api/payments/verify/{reference} | Verify payment status (public) |
GET | /api/payments/by-order/{order_id} | Get payment by order ID |
| Environment | URL |
|---|
| Demo | https://gsp-api.demo.tindorah.com |
| Production | https://gsp-api.tindorah.com |
curl --request POST \
--url https://gsp-api.demo.tindorah.com/api/payments/initiate \
--header 'Accept: application/json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data '{
"order_id": "ORDER-1001",
"currency": "XAF",
"country_code": "cm",
"payment_method": "mtn_cm",
"return_url": "https://example.com/payments/return",
"items": [
{
"title": "Starter plan",
"price": 25000,
"quantity": 1
}
],
"user": {
"email": "customer@example.com",
"name": "John Doe"
}
}'
| Field | Type | Required | Description |
|---|
order_id | string | Yes | Your unique order identifier (max 255 chars) |
items | array | Yes* | Payment items (required if no service_code) |
items[].title | string | Yes | Item title (max 255 chars) |
items[].price | number | Yes | Unit price (must be > 0) |
items[].quantity | integer | Yes | Quantity (min 1) |
items[].description | string | No | Item description (max 255 chars) |
currency | string | Yes* | Currency code (required if no service_code) |
country_code | string | No | ISO country code (e.g., bj, cm, bf) |
payment_method | string | Yes | Payment method code (see below) |
return_url | string | Yes | URL to redirect after payment (max 500 chars) |
invoice_email | string | No | Email for invoice delivery |
payment_phone_number | string | No | Mobile money phone number |
user_id | integer | No** | Existing user ID |
user | object | No** | New user data |
user.email | string | Yes | User email |
user.name | string | No | User name |
metadata | object | No | Custom data (stored with payment) |
service_code | string | No | Pre-configured service code |
*Not required if service_code is provided
**Either user_id or user object is required
{
"status": 201,
"message": "Payment initiated successfully",
"data": {
"id": "9c8f7e6d-5a4b-3c2d-1e0f-9a8b7c6d5e4f",
"reference": "01234567-89ab-cdef-0123-456789abcdef",
"order_id": "order-12345",
"amount": 5000,
"currency": "XOF",
"payment_method": "mtn_open",
"status": "pending",
"invoice_email": "customer@example.com",
"invoice_url": "https://api.example.com/invoices/download/signed-url",
"payment_url": null,
"return_url": "https://yourapp.com/checkout/complete",
"metadata": {
"subscription_id": "sub-456",
"plan": "premium"
},
"initiated_at": "2024-06-17T10:30:00.000000Z",
"items": [
{
"title": "Premium Subscription",
"unit_price": 5000,
"quantity": 1
}
]
}
}
This endpoint is publicly accessible and does not require authentication.
curl https://gsp-api.demo.tindorah.com/api/payments/verify/{reference}
{
"status": 200,
"message": "Payment verified successfully",
"data": {
"id": "9c8f7e6d-5a4b-3c2d-1e0f-9a8b7c6d5e4f",
"reference": "01234567-89ab-cdef-0123-456789abcdef",
"order_id": "order-12345",
"amount": 5000,
"currency": "XOF",
"payment_method": "mtn_open",
"status": "success",
"initiated_at": "2024-06-17T10:30:00.000000Z"
}
}
async function checkPaymentStatus(reference) {
const response = await fetch(
`https://gsp-api.demo.tindorah.com/api/payments/verify/${reference}`
);
const data = await response.json();
if (data.data.status === "success") {
showSuccessPage();
} else if (data.data.status === "failed" || data.data.status === "canceled") {
showErrorPage();
} else {
// Still pending, check again later
setTimeout(() => checkPaymentStatus(reference), 5000);
}
}
| Code | Description | Currency | Country |
|---|
mtn_open | MTN Mobile Money | XOF | BJ |
moov | Moov Money | XOF | BJ |
mtn_momo_bj | MTN MoMo Benin | XOF | BJ |
mtn_cm | MTN Cameroon | XAF | CM |
orange_cm | Orange Cameroon | XAF | CM |
orange_bf | Orange Burkina Faso | XOF | BF |
moov_bf | Moov Burkina Faso | XOF | BF |
| Code | Description | Currency |
|---|
stripe | Card Payment (Stripe) | Any |
| Code | Description |
|---|
XOF | West African CFA Franc |
XAF | Central African CFA Franc |
USD | US Dollar |
EUR | Euro |
| Status | Description |
|---|
created | Payment record created, awaiting initiation |
pending | Payment initiated, awaiting customer action |
success | Payment completed successfully |
failed | Payment attempt failed |
canceled | Payment canceled by customer |
declined | Payment declined by provider |
expired | Payment window expired |
refunded | Payment was refunded |
- Call
POST /api/payments/initiate with payment_phone_number
- Customer receives USSD prompt on their phone
- Customer approves payment on their phone
- Poll
GET /api/payments/verify/{reference} to check status
- Call
POST /api/payments/initiate
- Redirect customer to
payment_url
- Customer completes payment on Stripe checkout page
- Customer is redirected to your
return_url with status
After payment completion, customers are redirected to your return_url with query parameters:
https://yourapp.com/checkout/complete?status=success&reference=01234567-89ab...
| Parameter | Description |
|---|
status | Final payment status |
reference | Payment reference (use to verify) |
{
"order_id": "order-12345",
"items": [
{
"title": "Premium Subscription",
"price": 5000,
"quantity": 1,
"description": "Monthly plan"
}
],
"currency": "XOF",
"country_code": "bj",
"payment_method": "mtn_open",
"return_url": "https://yourapp.com/checkout/complete",
"invoice_email": "customer@example.com",
"payment_phone_number": "+22997000000",
"user": {
"email": "customer@example.com",
"name": "John Doe"
},
"metadata": {
"subscription_id": "sub-456",
"plan": "premium"
}
}
{
"order_id": "order-67890",
"items": [
{
"title": "Event Ticket",
"price": 25.00,
"quantity": 2
},
{
"title": "Processing Fee",
"price": 2.50,
"quantity": 1
}
],
"currency": "USD",
"payment_method": "stripe",
"return_url": "https://yourapp.com/tickets/confirm",
"user_id": 42,
"invoice_email": "customer@example.com"
}
{
"order_id": "order-99999",
"service_code": "premium-monthly",
"payment_method": "mtn_cm",
"return_url": "https://yourapp.com/subscription/complete",
"user_id": 42
}
{
"message": "The payment method field is required.",
"errors": {
"payment_method": ["The payment method field is required."]
}
}
{
"message": "The order id has already been taken.",
"errors": {
"order_id": ["The order id has already been taken."]
}
}
{
"message": "Payment not found"
}
| Country | Format | Example |
|---|
| Benin | +229XXXXXXXX | +22997000000 |
| Cameroon | +237XXXXXXXXX | +237650000000 |
| Burkina Faso | +226XXXXXXXX | +22670000000 |
- Always verify payments - Do not trust client-side status alone. Call the
/verify endpoint.
- Store the reference - Save the
reference from initiation response for later verification.
- Handle all statuses - Implement handlers for success, failure, cancellation, and expiry.
- Use idempotent order IDs - Each
order_id can only be used once per tenant.
- Poll responsibly - When polling the verify endpoint, use reasonable intervals (e.g., every 5 seconds).