Payments
Create, fetch and cancel payments; every field, idempotency and the order-reference rule.
| Method | Path | Does |
|---|---|---|
POST |
/api/v1/payments |
Create a payment and get its checkout URL |
GET |
/api/v1/payments/{id} |
Fetch a payment |
POST |
/api/v1/payments/{id}/cancel |
Cancel a pending payment |
POST |
/api/v1/payments/{id}/refunds |
Refund a payment (test mode), see Refunds |
{id} is the payment's id from the create response. Every request needs your API key; see
Authentication.
Create a payment
POST /api/v1/payments
curl -s -X POST "$BRIDGE_URL/api/v1/payments" \
-H "Authorization: Bearer $BRIDGE_KEY" \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-H "Idempotency-Key: 7f9c2b1e-order-1001" \
-d @payment.json
with payment.json:
{
"amount": 25000,
"currency": "IQD",
"method": "aps_card",
"order_reference": "ORD-1001",
"description": "Two coffees",
"customer": {"name": "Ali", "phone": "+9647700000000", "email": "ali@example.com"},
"payer_name": "Ali Hassan",
"payer_phone": "+9647701234567",
"payer_email": "ali.hassan@example.com",
"locale": "en",
"return_url": "https://shop.example/return",
"metadata": {"cart_id": "c-42"}
}
Request fields
| Field | Type | Required | Rules |
|---|---|---|---|
amount |
integer | yes | Whole IQD, at least 250. Your application may also have its own minimum and maximum. |
currency |
string | no | IQD, the only currency today. Default IQD. |
method |
string | no | aps_card. Required only if your application offers more than one method. |
order_reference |
string | yes | Your order id, up to 64 characters. See the rule below. |
description |
string | no | Up to 512 characters. Shown to the customer on the checkout page. |
customer |
object | no | Only name (up to 255), phone (up to 32) and email. |
payer_name |
string | no | Who pays, up to 255 characters. Shown in the dashboards. |
payer_phone |
string | no | Up to 20 characters. |
payer_email |
string | no | A valid email address. |
locale |
string | no | ar or en: the language of the checkout and card pages. Default: your application's language. |
return_url |
string | no | Where the customer is sent afterwards, up to 2048 characters. Default: your application's return URL. Must be https:// with a live key, and its host must equal your application's own return-URL host. See the rule below. |
metadata |
object | no | Any JSON object up to 4 KB. Returned as it was sent. |
Response
201 Created:
{
"id": "01a13c8c-8592-70f4-9ef9-d3767e74d54c",
"status": "pending",
"mode": "test",
"amount": 25000,
"refunded_amount": 0,
"currency": "IQD",
"method": "aps_card",
"order_reference": "ORD-1001",
"description": "Two coffees",
"customer": {"name": "Ali", "phone": "+9647700000000", "email": "ali@example.com"},
"payer": {"name": "Ali Hassan", "phone": "+9647701234567", "email": "ali.hassan@example.com", "ip": "203.0.113.10", "user_agent": "GuzzleHttp/7"},
"locale": "en",
"return_url": "https://shop.example/return",
"metadata": {"cart_id": "c-42"},
"checkout_url": "https://<your-bridge-domain>/checkout/01a13c8c-8592-70f4-9ef9-d3767e74d54c",
"expires_at": "2026-10-14T23:03:24+00:00",
"paid_at": null,
"created_at": "2026-10-14T22:33:24+00:00",
"refunds": []
}
Payment fields
| Field | Type | Meaning |
|---|---|---|
id |
string (UUID) | The payment's id. Use it in every other call. |
status |
string | pending, paid, failed, cancelled, expired, partially_refunded or refunded. See Statuses. |
mode |
string | test or live, from the key that created it. |
amount |
integer | Whole IQD. |
refunded_amount |
integer | Whole IQD refunded so far. |
currency |
string | IQD. |
method |
string | aps_card. |
order_reference |
string | Your order id. |
description |
string or null | As sent. |
customer |
object | name, phone, email, each null when not sent. |
payer |
object | name, phone, email as sent, plus ip and user_agent of the server that created the payment. |
locale |
string | ar or en. |
return_url |
string or null | Where the customer is sent afterwards. |
metadata |
object or null | As sent. |
checkout_url |
string | Send the customer here to pay. |
expires_at |
string | ISO 8601. Unpaid after this time, the payment expires. 30 minutes after creation by default. |
paid_at |
string or null | ISO 8601, set when the payment becomes paid. |
created_at |
string | ISO 8601. |
refunds |
array | Every confirmed refund: id, amount, source, provider_reference, note, created_at. |
refunds[].source is api (a test-mode refund through the API), manual (a live refund
confirmed in the dashboard) or provider_detected (the provider reported a refund the bridge had
no record of).
The order-reference rule
The same order_reference cannot be paid twice. While a payment with that reference is
pending, paid, partially_refunded or refunded, creating another one with the same
reference answers 409 order_reference_already_paid, with the existing payment's id:
{
"error": {
"code": "order_reference_already_paid",
"message": "A payment for this order_reference is still pending. Reuse it or wait until it fails, is cancelled or expires.",
"payment_id": "01a13c8c-8592-70f4-9ef9-d3767e74d54c"
}
}
Send the customer to that payment's checkout_url if it is still pending, or treat the order as
paid. Once a payment is failed, cancelled or expired, you can create a new payment with the
same reference. Test and live references are separate.
The return_url rule
A return_url you send must have the same host as your application's own return URL (any path or
query is fine); otherwise the bridge answers 422 return_url_not_allowed:
{
"error": {
"code": "return_url_not_allowed",
"message": "The return_url host must match this application's own return-URL host."
}
}
Leave return_url out to use your application's own one. This holds in both test and live mode.
If you need a second domain, ask Alphabet to change the return URL on your application.
Idempotency
Network errors happen. To retry POST /api/v1/payments safely, send an Idempotency-Key header
(any string up to 255 characters, unique per order attempt, for example a UUID).
| You send | You get |
|---|---|
| A new key | The payment is created as usual. The answer is stored with the key. |
| The same key and the same body | The stored answer again, with the header Idempotent-Replayed: true. No second payment. |
| The same key and a different body | 409 idempotency_key_reused. |
| The same key while the first request is still running | 409 idempotency_request_in_progress. Retry shortly. |
A 422 validation_failed answer is not stored, so you can fix the body and retry with the same
key. Keys are per application and per mode.
Fetch a payment
GET /api/v1/payments/{id}
curl -s "$BRIDGE_URL/api/v1/payments/01a13c8c-8592-70f4-9ef9-d3767e74d54c" \
-H "Authorization: Bearer $BRIDGE_KEY" \
-H "Accept: application/json"
200 OK with the payment, in the same shape as above. Here it has been paid and partly refunded:
{
"id": "01a13c8c-8592-70f4-9ef9-d3767e74d54c",
"status": "partially_refunded",
"mode": "test",
"amount": 25000,
"refunded_amount": 10000,
"currency": "IQD",
"method": "aps_card",
"order_reference": "ORD-1001",
"description": "Two coffees",
"customer": {"name": "Ali", "phone": "+9647700000000", "email": "ali@example.com"},
"payer": {"name": "Ali Hassan", "phone": "+9647701234567", "email": "ali.hassan@example.com", "ip": "203.0.113.10", "user_agent": "GuzzleHttp/7"},
"locale": "en",
"return_url": "https://shop.example/return",
"metadata": {"cart_id": "c-42"},
"checkout_url": "https://<your-bridge-domain>/checkout/01a13c8c-8592-70f4-9ef9-d3767e74d54c",
"expires_at": "2026-10-14T23:03:24+00:00",
"paid_at": "2026-10-14T22:35:02+00:00",
"created_at": "2026-10-14T22:33:24+00:00",
"refunds": [
{"id": 1, "amount": 10000, "source": "api", "provider_reference": "01a13c8c-8595-723d-90e3-978995c21827", "note": "One item out of stock", "created_at": "2026-10-14T22:40:11+00:00"}
]
}
A payment of another application, of the other mode, or an id that does not exist answers
404 not_found.
Cancel a payment
POST /api/v1/payments/{id}/cancel, no body.
curl -s -X POST "$BRIDGE_URL/api/v1/payments/01a13c8c-85e7-718d-ad61-4dc03208c5f0/cancel" \
-H "Authorization: Bearer $BRIDGE_KEY" \
-H "Accept: application/json"
Only a pending payment can be cancelled. The bridge first asks the provider for the latest
result, so a payment the customer has just paid is not cancelled. On success the answer is
200 OK with the payment, "status": "cancelled", and a payment.cancelled webhook is sent.
If the payment is no longer pending, the answer is 409 with its current status:
{
"error": {
"code": "payment_not_cancellable",
"message": "Only a pending payment can be cancelled; this one is paid.",
"status": "paid"
}
}
A cancelled payment can still turn paid until its original expires_at if the customer
completes the card page that was already open. You then receive payment.paid and must deliver
the order or refund it. See Payment flow.