External API — Partner Integration Guide
Create invoices, query status, share payment links, and receive signed webhooks.
| Environment | Base URL |
|---|---|
| Production | https://integration.rsompay.com/api/external/v1 |
| Sandbox | https://demo-integration.rsompay.com/api/external/v1 |
| Environment | Payment URL pattern |
|---|---|
| Production | https://payment.rsompay.com/pay/invoice/{token} |
| Sandbox | https://demo-payment.rsompay.com/pay/invoice/{token} |
| Item | Description |
|---|---|
| External client account | Provisioned by RsomPay for B2B integration |
| Integration API token | Bearer token named client-external-api |
| Webhook signing secret | Provided by RsomPay IT on onboarding — used with header X-Rsom-Signature (see §7.5) |
flowchart TB
subgraph partner [Partner system]
ERP[ERP backend]
WH[Webhook endpoint]
RET[return_url handler]
end
subgraph rsom [RsomPay]
INT[integration.rsompay.com]
PAY[payment.rsompay.com]
end
ERP -->|POST /invoices| INT
INT -->|payment_url| ERP
ERP -->|share link| PAY
PAY -->|browser redirect first| RET
RET -->|GET by-reference| INT
INT -.->|webhook later async| WH
Send the integration token on every request:
Authorization: Bearer YOUR_CLIENT_EXTERNAL_API_TOKEN Content-Type: application/json Accept: application/json
client-external-api.403.client_invoices.create, client_invoices.view. Settlement endpoints additionally require client_settlements.view — already granted by default, no extra setup needed.curl -X POST "https://integration.rsompay.com/api/external/v1/invoices" \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Content-Type: application/json" \ -d @invoice.json
RsomPay also signs every webhook it sends to you (payment/invoice status updates) with a secret specific to your account, so you can verify the notification is genuine. See §7.5 for the webhook signature — this is separate from the access token above and is not something you need to send on your requests to RsomPay.
Successful responses:
{
"status": true,
"message": "Mission completed successfully",
"data": { }
}
Paginated GET /transactions adds meta: total, per_page, current_page, last_page.
POST /invoices
| Field | Required | Type | Description |
|---|---|---|---|
external_id | Yes | string | Partner idempotency key. Max 255. Unique per client; duplicates return the same invoice. |
notification_url | Yes | URL | HTTPS webhook endpoint. Publicly reachable; no localhost/private IPs. Max 2048. |
return_url | Yes | URL | HTTPS customer redirect after checkout. Max 2048. |
customer | Yes | object | Customer block — see table below. |
items | Yes | array | Line items — min 1 object; see table below. |
currency_code | No | string | Currency code. Max 8. Default SAR. |
due_date | No | date | Due date YYYY-MM-DD. |
issued_date | No | date | Issue date YYYY-MM-DD. |
payment_method | No | string | Preferred payment method: mada, credit_card (Visa/Mastercard), apple_pay, or tamara. When set, the hosted checkout page skips its own payment-method screen and opens directly into that method's form. Requesting tamara for an account without Tamara enabled returns 422. mada/credit_card/apple_pay requested for a disabled method fall back to showing the picker instead. |
notes | No | string | Invoice notes. Max 2000. |
customer)| Field | Required | Type | Description |
|---|---|---|---|
customer.fullname | Yes | string | Full name on payment page. Max 255. |
customer.id | Yes | string | Your stable customer reference (e.g. CUST-9). Max 255. |
customer.email | No | string | Valid email. Max 255. |
customer.phone | No | string | Phone (e.g. +966501234567). Max 32. |
customer.saved_card_token | No | string | Reuse a card this customer saved on an earlier invoice — see section 14. Omit for a new customer or any invoice that shouldn't offer a saved card. |
items[])Each array element is one line item. At least one item is required.
| Field | Required | Type | Description |
|---|---|---|---|
items[].description | Yes | string | Line description. Max 255. |
items[].quantity | No | number | Qty. Min 0.001. Default 1. |
items[].unit_price | Yes | number | Unit price before discount and tax. Min 0.01. Invoice total is computed from all lines. |
items[].discount_amount | No | number | Line discount amount. Min 0. |
items[].tax_rate | No | number | Tax % (e.g. 15 = 15% VAT). |
items[].item_type | No | string | Category label (e.g. service). Max 32. |
items[].reference_id | No | string | Your line reference. Max 128. |
external_id → 201, same invoice, no duplicate.issued.amount field — each line requires unit_price. data.amount and data.totals.grand_total are computed from lines (must be > 0).One line: qty 1, unit 100, discount 10, tax 15% → grand_total: 103.5
{
"external_id": "INV-PARTNER-001",
"notification_url": "https://partner.example.com/webhooks/rsom",
"return_url": "https://partner.example.com/payment/return",
"currency_code": "SAR",
"due_date": "2026-12-31",
"issued_date": "2026-05-18",
"payment_method": "credit_card",
"notes": "Optional invoice note",
"customer": {
"id": "CUST-9",
"fullname": "Ahmed Ali",
"email": "customer@example.com",
"phone": "+966501234567"
},
"items": [
{
"description": "Service fee",
"item_type": "service",
"quantity": 1,
"unit_price": 100,
"discount_amount": 10,
"tax_rate": 15,
"reference_id": "LINE-1"
}
]
}
{
"status": true,
"message": "Mission completed successfully",
"data": {
"external_id": "INV-PARTNER-001",
"reference_number": "INV-42-001",
"status": "issued",
"amount": 103.5,
"payment_url": "https://payment.rsompay.com/pay/invoice/...",
"customer": {
"id": "CUST-9",
"fullname": "Ahmed Ali",
"email": "customer@example.com",
"phone": "+966501234567"
},
"totals": {
"subtotal": 100,
"discount_total": 10,
"tax_total": 13.5,
"grand_total": 103.5
},
"items": [
{
"description": "Service fee",
"item_type": "service",
"quantity": 1,
"unit_price": 100,
"discount_amount": 10,
"tax_rate": 15,
"tax_amount": 13.5,
"total_amount": 103.5,
"reference_id": "LINE-1"
}
]
}
}
GET /invoices/by-reference?{parameter}={value}
Look up an invoice by your reference. Use this after webhooks or before fulfilling an order.
Provide exactly one query parameter:
| Parameter | Description |
|---|---|
external_id | Your idempotency key (recommended) |
reference_number | RsomPay invoice number (e.g. INV-42-001) |
GET /invoices/by-reference?external_id=INV-PARTNER-001 GET /invoices/by-reference?reference_number=INV-42-001
data object is the same as POST /invoices (create invoice). Only HTTP status differs: 200 OK here vs 201 Created on create. Fields such as status reflect the current invoice state (e.g. issued before payment, paid after).
{
"status": true,
"message": "Mission completed successfully",
"data": {
"external_id": "INV-PARTNER-001",
"reference_number": "INV-42-001",
"status": "paid",
"amount": 103.5,
"payment_url": "https://payment.rsompay.com/pay/invoice/...",
"customer": {
"id": "CUST-9",
"fullname": "Ahmed Ali",
"email": "customer@example.com",
"phone": "+966501234567"
},
"totals": {
"subtotal": 100,
"discount_total": 10,
"tax_total": 13.5,
"grand_total": 103.5
},
"items": [
{
"description": "Service fee",
"item_type": "service",
"quantity": 1,
"unit_price": 100,
"discount_amount": 10,
"tax_rate": 15,
"tax_amount": 13.5,
"total_amount": 103.5,
"reference_id": "LINE-1"
}
]
}
}
| HTTP | Cause |
|---|---|
422 | No query parameter — provide external_id or reference_number |
404 | Invoice not found for your client |
List customer payments across your invoices.
GET /transactions?customer_id={id}&page=1&per_page=20
| Parameter | Required | Default | Max |
|---|---|---|---|
customer_id | Yes | — | 255 |
page | No | 1 | — |
per_page | No | 20 | 100 |
Same envelope as other endpoints (status, message, data). data is an array of payment records; paginated responses include meta. payment_method reflects how the customer paid — e.g. card or tamara. refund_status is only present once a refund has been requested on that transaction — poll it to track asynchronous refund confirmation.
{
"status": true,
"message": "Mission completed successfully",
"data": [
{
"reference": "TX-REF-1",
"status": "completed",
"refund_status": null,
"amount": 103.5,
"currency_code": "SAR",
"payment_method": "card",
"invoice_external_id": "INV-PARTNER-001",
"captured_at": "2026-05-18 12:00:00",
"created_at": "2026-05-18 11:55:00"
},
{
"reference": "TX-REF-2",
"status": "completed",
"refund_status": "confirmed",
"amount": 250,
"currency_code": "SAR",
"payment_method": "tamara",
"invoice_external_id": "INV-PARTNER-002",
"captured_at": "2026-05-17 15:30:00",
"created_at": "2026-05-17 15:28:00"
}
],
"meta": {
"total": 2,
"per_page": 20,
"current_page": 1,
"last_page": 1
}
}
RsomPay POSTs to your notification_url after payment activity. No webhook is sent when the invoice is created. Delivery is asynchronous — respond with HTTP 2xx quickly.
| Event | When |
|---|---|
invoice.status_changed | Status changes after payment (previous_status in payload) |
payment.completed | Payment succeeded |
payment.failed | Payment failed (declined by gateway, cancelled by customer, etc. — see payment.reason) |
payment.expired | The checkout session expired before the customer completed payment |
payment.refunded | A refund on a completed payment was confirmed |
settlement.updated | A settlement batch changed state (e.g. moved to transfer_pending or failed) |
settlement.paid | A settlement batch was paid out in full |
settlement.* events are sent to your account-level notification URL (configured once for your integration), not to a per-invoice notification_url — a settlement batch aggregates many transactions, not a single invoice. Ask your integration contact to register this URL.
POST {notification_url}
Content-Type: application/json
X-Rsom-Signature: a1b2c3d4e5f6...
Event details are in the JSON body. For authenticity, read header X-Rsom-Signature (see §7.5).
{
"event": "payment.completed",
"event_id": "550e8400-e29b-41d4-a716-446655440000",
"occurred_at": "2026-05-18T12:00:00+00:00",
"customer": { "id": "CUST-9", "fullname": "Ahmed Ali", ... },
"totals": { "subtotal": 100, "discount_total": 10, "tax_total": 13.5, "grand_total": 103.5 },
"invoice": {
"external_id": "INV-PARTNER-001",
"reference_number": "INV-42-001",
"status": "paid",
"amount": 103.5,
"payment_url": "https://payment.rsompay.com/pay/invoice/..."
},
"payment": {
"uuid": "660e8400-e29b-41d4-a716-446655440001",
"payment_method": "card",
"status": "completed",
"amount": 103.5,
"paid_at": "2026-05-18 12:00:00"
}
}
payment_method reflects how the customer paid — e.g. card (Mada/Visa/Mastercard), tamara (Tamara BNPL). Card payments may also return a specific brand when available (e.g. mada, visa).
payment.failed and payment.expired both carry an additional payment.reason field so you can distinguish a gateway decline from a checkout-session expiry from a customer cancellation:
{
"event": "payment.expired",
"event_id": "770e8400-e29b-41d4-a716-446655440002",
"occurred_at": "2026-08-03T12:00:00+00:00",
"invoice": { "external_id": "INV-PARTNER-001", "reference_number": "INV-42-001", "status": "failed", ... },
"payment": {
"uuid": "660e8400-e29b-41d4-a716-446655440001",
"payment_method": "card",
"status": "failed",
"amount": 103.5,
"paid_at": "2026-08-03 12:00:00",
"reason": "dhamen_invoice_payment_expired"
}
}
payment.refunded carries a transaction object instead of payment — the refund state lives on the underlying transaction, not the invoice status:
{
"event": "payment.refunded",
"event_id": "880e8400-e29b-41d4-a716-446655440003",
"occurred_at": "2026-08-03T12:00:00+00:00",
"invoice": { "external_id": "INV-PARTNER-001", "reference_number": "INV-42-001", "status": "issued", ... },
"transaction": {
"uuid": "990e8400-e29b-41d4-a716-446655440004",
"reference": "TXN-42-001",
"status": "completed",
"settlement_status": "reversed",
"gross_amount": 103.5,
"net_amount": 101.5
}
}
settlement.updated/settlement.paid carry a settlement object instead of invoice/payment (no single invoice is involved):
{
"event": "settlement.paid",
"event_id": "aa0e8400-e29b-41d4-a716-446655440005",
"occurred_at": "2026-08-03T12:00:00+00:00",
"settlement": {
"uuid": "bb0e8400-e29b-41d4-a716-446655440006",
"batch_no": "STL-20260803120000-AB12",
"status": "settled",
"channel": "dhamen",
"currency_code": "SAR",
"net_total": 980.00,
"transactions_count": 12,
"settled_at": "2026-08-03 12:00:00",
"failure_reason": null
}
}
2xx within a few seconds.event_id in the JSON body.X-Rsom-Signature before trusting the body (see §7.5).X-Rsom-Signature)RsomPay includes a signature on every webhook so you can confirm the request is genuine, computed with a signing secret unique to your account.
| Header | Value |
|---|---|
X-Rsom-Timestamp |
Unix timestamp (seconds) of when RsomPay sent the webhook. |
X-Rsom-Signature |
Hex-encoded HMAC-SHA256 signature. Read this header from the incoming request and verify it on your server before processing the JSON body. |
Verification:
expected_signature = HMAC_SHA256(your_signing_secret, timestamp + "." + raw_request_body) # reject the webhook if expected_signature != X-Rsom-Signature
Your signing secret is generated for your account and shown (masked, with a copy button) right next to your access token on the "Integration Settings" page in the RsomPay client dashboard. It is independent from the access token — regenerating one does not affect the other. If you rotate it, update your verification code first, since new webhooks are signed with the new secret immediately.
event_id across retries — deduplicate on your side.payment_url.payment.rsompay.com.return_url with query parameters (UX only).GET /invoices/by-reference (primary) — call from your return_url handler or right after redirect.payment.completed / payment.failed arrives later, asynchronously on notification_url (after the redirect).return_url first. RsomPay sends the webhook to notification_url afterward (async). Do not wait for the webhook on the return page — confirm with GET /invoices/by-reference first.
On your return_url page, use GET /invoices/by-reference as your main check before you fulfill an order:
GET /invoices/by-reference?external_id=INV-PARTNER-001
Confirm data.status is paid. Poll briefly if status is still issued or pending (payment may still be confirming).
| Channel | When | Role |
|---|---|---|
return_url | Right after checkout | UX — show success/failure; not proof of payment alone |
GET /invoices/by-reference | On return page (primary) | Authoritative — confirm paid before fulfilling |
Webhook payment.completed | Later (async) | Backup — optional reconciliation; still use by-reference if webhook is delayed |
| Parameter | Description |
|---|---|
status | success, failed, or pending |
external_id | Your invoice id |
reference_number | RsomPay invoice number (e.g. INV-42-001) |
payment_reference | Payment reference (if available) |
amount_paid | Amount for this payment |
currency_code | e.g. SAR |
payment_method | How the customer paid — e.g. card, tamara, or card brand (mada, visa, …) when available |
https://partner.example.com/payment/return?status=success&external_id=INV-PARTNER-001&reference_number=INV-42-001&payment_reference=GW-123&amount_paid=103.5¤cy_code=SAR&payment_method=card
On data.status and webhook invoice.status for external API invoices:
| Status | Meaning |
|---|---|
issued | Issued — awaiting payment |
paid | Paid in full |
canceled | Cancelled — do not collect |
failed | Last payment attempt failed; customer may try again via payment_url |
refunded | Fully refunded — the full amount was returned to the customer |
partially_refunded | Partially refunded — some but not all of the paid amount was returned |
Refund completeness is also visible per-transaction via refund_status on GET /transactions (gateway confirmation state — requested/pending_gateway/confirmed/stuck/submit_failed — not the same thing as full vs. partial).
| HTTP | Typical cause |
|---|---|
401 | Missing or invalid token |
403 | Wrong token type or suspended client |
404 | Invoice not found |
422 | Validation error |
500 | Server error |
| Issue | Fix |
|---|---|
| URL rejected | notification_url / return_url must be HTTPS and publicly reachable |
| by-reference 422 | Provide external_id or reference_number |
customer_id on by-reference | Not supported — use GET /transactions instead |
payment_method: "tamara" rejected (422 on payment_method) | Tamara isn't enabled for your account — omit the field, use mada/credit_card/apple_pay, or contact RsomPay to enable Tamara |
Validation messages may be localized — rely on HTTP status and field keys.
client-external-api token from RsomPay.X-Rsom-Signature (see §7.5).POST /invoices with a unique external_id — save payment_url.GET /invoices/by-reference?external_id=... — confirm status is issued.payment_url.return_url with query parameters (happens before webhook).GET /invoices/by-reference from your return handler — status is paid.payment.completed webhook on notification_url arrives later (if delivered).Refund a previously captured Dhamen payment on one of your invoices — in full or in part.
POST /invoices/{reference}/refund
{reference} is your invoice's external_id or its RsomPay reference_number (same identifiers accepted by GET /invoices/by-reference).
| Field | Required | Type | Description |
|---|---|---|---|
amount | No | number | Omit for a full refund of the remaining captured amount. Provide for a partial refund (minimum 1). |
reason | No | string | Free-text reason, max 500 characters — recorded for audit purposes. |
403 until settlement finishes.200 response below means "refund request accepted", not "refund completed." RsomPay submits the refund to the payment gateway and waits for the gateway's own confirmation before the refund is final. Poll GET /transactions and check the refund_status field on the matching transaction to see the outcome:
refund_status | Meaning |
|---|---|
pending_gateway | Submitted, awaiting gateway confirmation — not refunded yet. |
confirmed | Refund confirmed by the gateway — money has moved. |
stuck | No confirmation received after an extended period — under investigation by RsomPay. Contact support if you see this. |
submit_failed | The gateway rejected the request outright (see the error response below) — safe to correct and retry. |
{
"amount": 50.00,
"reason": "Customer requested partial refund"
}
{
"status": true,
"message": "Mission completed successfully",
"data": {
"reference": "DHAMEN-REFUND-9f2c1e6a-...",
"refund_status": "pending_gateway",
"amount": 50.00,
"currency_code": "SAR",
"created_at": "2026-07-28 10:15:00"
}
}
| HTTP | Cause |
|---|---|
403 | Refunds not enabled for your account, or the payment is currently mid-settlement / already settled |
404 | Invoice not found for your client, or no completed Dhamen payment exists on it |
422 | Refund amount invalid or exceeds the remaining refundable amount, or the gateway rejected the request |
Read-only settlement/payout status for reconciliation. Settlement batches are created and processed internally by RsomPay — this endpoint only exposes their status and constituent transactions, scoped to your own account. Requires the client_settlements.view permission (already granted on all external-partner accounts).
GET /settlements?status=settled&page=1&per_page=15
| Parameter | Required | Notes |
|---|---|---|
status | No | pending, in_progress, transfer_pending, settled, failed, canceled |
date_from / date_to | No | Filter by batch creation date (ISO date) |
page / per_page | No | Default 15, max 100 |
{
"status": true,
"message": "Mission completed successfully",
"data": [
{
"id": 42,
"reference": "DHM-INS-12-0007",
"status": "settled",
"period_from": "2026-07-01",
"period_to": "2026-07-15",
"currency_code": "SAR",
"gross_total": 1500,
"fee_total": 75,
"net_total": 1425,
"transactions_count": 12,
"bank_transfer_reference": "BANK-REF-001",
"settled_at": "2026-07-18 09:00:00",
"created_at": "2026-07-16 08:00:00"
}
],
"meta": { "total": 1, "per_page": 15, "current_page": 1, "last_page": 1 }
}
GET /settlements/{id}
id is the numeric id returned by the list endpoint. Returns 404 if the settlement doesn't exist or doesn't belong to your account — settlements are never shared across clients.
{
"status": true,
"message": "Mission completed successfully",
"data": {
"id": 42,
"reference": "DHM-INS-12-0007",
"status": "settled",
"period_from": "2026-07-01",
"period_to": "2026-07-15",
"currency_code": "SAR",
"gross_total": 1500,
"fee_total": 75,
"net_total": 1425,
"transactions_count": 12,
"bank_transfer_reference": "BANK-REF-001",
"settled_at": "2026-07-18 09:00:00",
"failure_reason": null,
"items": [
{
"invoice_reference": "INV-42-000123",
"status": "settled",
"gross_amount": 125,
"fee_amount": 6.25,
"net_amount": 118.75,
"settled_at": "2026-07-18 09:00:00",
"transaction": {
"reference": "TX-REF-1",
"status": "completed",
"payment_method": "card",
"captured_at": "2026-07-16 08:05:00"
}
}
],
"created_at": "2026-07-16 08:00:00"
}
}
No Dhamen/Zoho internal identifiers, ledger references, or platform balance snapshots are exposed on this endpoint by design — only the fields your reconciliation needs. There is currently no outbound webhook for settlement status changes — poll this endpoint periodically (e.g. daily) until one is introduced.
Let a returning customer reuse a card they saved on an earlier invoice, instead of entering it again. The saved card itself is always scoped to a customer identifier (the 10-digit national ID / CR you pass as customer.id) — the retrieval token described here is what proves you are the integration that customer's card actually belongs to, before RsomPay will ever surface or charge it again.
customer.id. A customer identifier alone is not a secret — it is a national ID / CR number your own systems already hold for other reasons. RsomPay will never disclose or charge a previously saved card on the strength of that identifier alone. Only an invoice created with a valid saved_card_token can ever see or use the card again.
saved_card_token — there is nothing to reuse yet.recurring.card_saved to your notification_url (no secret in that payload).POST /customers/{customer_external_id}/saved-card-token explicitly (below), or simply make your next authenticated call to GET /invoices/by-reference / POST /invoices for that customer — see "Auto-surfaced on status checks" below. Store it against your own customer record; either way, the plaintext is only ever returned once.customer.saved_card_token. RsomPay verifies it, and the hosted checkout page now offers the saved card — or skip the checkout page entirely and charge it yourself server-to-server with POST /invoices/{reference}/charge-saved-card (below).POST /customers/1055512345/saved-card-token
Requires permission client_saved_cards.manage on your access token, and at least one active saved card for this customer under your account.
recurring.card_saved arrives; call it again only to rotate (e.g. if you suspect the stored copy leaked).
{
"status": true,
"message": "Mission completed successfully",
"data": {
"saved_card_token": "rsp_sct_8f3a1c9d4e2b7f60a1c9d4e2b7f60a1c9d4e2b7f60a1c"
}
}
Store the value as-is against your own customer record. Treat it like any other credential — keep it out of logs and never send it to the customer's browser; it only ever travels over your own server-to-server calls to RsomPay.
The moment a card is saved (the same moment recurring.card_saved fires), RsomPay also pre-issues a token and holds it for you briefly. Your next authenticated GET /invoices/by-reference or POST /invoices response for that customer carries it automatically under a saved_card key — removing the need to call the issue endpoint above at all for the common case:
{
"status": true,
"data": {
"external_id": "INV-PARTNER-002",
"...": "...",
"saved_card": {
"saved_card_token": "rsp_sct_8f3a1c9d4e2b7f60a1c9d4e2b7f60a1c9d4e2b7f60a1c",
"brand": "MADA",
"last4": "4621"
}
}
}
saved_card is null on every call once it has been claimed (or after roughly 24h unclaimed). It surfaces on any invoice of that customer's, not only the one that triggered the save — unless your customer.id isn't a clean 10-digit national ID / CR number, in which case it narrows to only the specific invoice that was paid. This never appears in the recurring.card_saved webhook body itself — webhook payloads are durably logged for retry/audit, so the secret only ever travels over an authenticated request you make.
You can still call POST /customers/{customer_external_id}/saved-card-token any time to explicitly (re)issue one — the two mechanisms share the same underlying token, and either one rotates it.
POST /invoices/{reference}/charge-saved-cardMerchant-initiated, server-to-server — no customer visit to checkout required. Create the invoice normally first (section 4, no saved_card_token needed at creation for this path), then charge it directly:
POST /invoices/INV-PARTNER-002/charge-saved-card
Requires permission client_invoices.charge on your access token.
| Field | Required | Type | Description |
|---|---|---|---|
idempotency_key | Yes | string | Your own dedupe key. Max 80. A repeat call with the same key returns the same charge without charging again. |
saved_card_token | Yes | string | A token previously returned by the issue endpoint or auto-surfaced on a status check. Must belong to this invoice's own customer. |
description | No | string | Max 255. For your own records. |
Always charges the invoice's full remaining balance — partial amounts aren't supported here, same as everywhere else in this API.
{
"idempotency_key": "chg-2026-09-001",
"saved_card_token": "rsp_sct_8f3a1c9d4e2b7f60a1c9d4e2b7f60a1c9d4e2b7f60a1c"
}
{
"status": true,
"message": "Mission completed successfully",
"data": {
"external_id": "INV-PARTNER-002",
"status": "issued",
"...": "..."
},
"charge": {
"reference": "sub-1234567890-AbCdEfGh",
"idempotency_key": "chg-2026-09-001",
"status": "pending"
}
}
202 means accepted, not paid yet. data.status stays issued in this same response. Confirmation follows later via the payment.completed / payment.failed webhook (section 7) — or poll GET /invoices/by-reference (section 5) the same way you would after any other payment. Exactly the same confirmation rules as section 8 apply.
customer.saved_card_token on invoice creationAdd this one field to the customer object on POST /invoices (section 4):
| Field | Required | Type | Description |
|---|---|---|---|
customer.saved_card_token | No | string | A token previously returned by the endpoint above. When present, RsomPay verifies it and the checkout page for this invoice offers the saved card; customer.id is still required and must match the customer the token was issued for. |
{
"external_id": "INV-PARTNER-002",
"notification_url": "https://partner.example.com/webhooks/rsom",
"return_url": "https://partner.example.com/payment/return",
"customer": {
"id": "1055512345",
"fullname": "Sara Ali",
"saved_card_token": "rsp_sct_8f3a1c9d4e2b7f60a1c9d4e2b7f60a1c9d4e2b7f60a1c"
},
"items": [{ "description": "Monthly service fee", "unit_price": 100 }]
}
Omit the field entirely for any customer you have not issued a token for — the invoice is created exactly as before, and no saved card is offered.
Signed exactly like the invoice webhooks — see section 7 for header and HMAC verification.
{
"event": "recurring.card_saved",
"event_id": "cc0e8400-e29b-41d4-a716-446655440007",
"occurred_at": "2026-09-11T12:00:00+03:00",
"customer_external_id": "1055512345",
"mandate_uuid": "9f2b3c1a-7cfa-41fe-9b42-eb334f96c8e4",
"brand": "MADA",
"last4": "4621"
}
| Event | When |
|---|---|
recurring.card_saved | A customer's card was saved for the first time. Fires once, on activation — never again on a later reuse of the same card. Carries no secret; call the issue endpoint above to get the actual retrieval token. |
403 — missing the client_saved_cards.manage permission (issue endpoint) or client_invoices.charge (charge-saved-card endpoint).404 — no active saved card exists for this customer under your account yet.422 — saved_card_token is invalid, or does not match the customer it's being used for.422 — (charge-saved-card only) the invoice is already paid, canceled, or refunded, or a charge is already in progress for it.Bill a customer a fixed amount on a fixed cadence using a card they save once. RsomPay stores the card on the first payment, then charges every cycle automatically — or on your call.
client_subscriptions.view / client_subscriptions.manage permissions.
incomplete — created; waiting for the customer to complete the first payment via first_payment_url.trialing — first payment done, in the free trial window (only when trial_days > 0).active — being billed each cycle.past_due — a cycle charge failed; RsomPay is retrying (automatic mode) or waiting for you (merchant_managed).paused — stopped after dunning was exhausted, the saved card became invalid, or you paused it. Resume with POST /{id}/resume.canceled — stopped for good.completed — reached its total_cycles / ends_at limit.billing_mode | Who triggers each charge |
|---|---|
automatic (default) | RsomPay's scheduler charges every cycle when it falls due and runs dunning (retries on days 1, 3, 5, 7, then pauses). |
merchant_managed | You call POST /{id}/charge for every charge. RsomPay executes, records and notifies, but never schedules and never auto-retries. |
Within automatic, collection_method is either auto (charge the saved card) or manual (email the customer an invoice link each cycle). The first payment is always a hosted link, in every mode.
POST /subscriptions
Idempotent on external_id per client — calling again with the same external_id returns the existing subscription with HTTP 200 instead of creating a duplicate (201 the first time).
| Field | Required | Type | Description |
|---|---|---|---|
external_id | Yes | string | Your own id for this subscription. Idempotency key. |
customer.id_number | Yes | string | Customer national ID / CR — exactly 10 digits, not starting with 0. |
customer.name / customer.email / customer.phone | No | string | Contact details. email receives the payment link and notices. |
title | Yes | string | Plan name shown to the customer. |
amount | Yes | number | Charge per cycle, in SAR (minimum 1). |
currency | No | string | SAR only (default). |
interval.unit | Yes | string | day · week · month · year. |
interval.count | No | integer | Every N units (default 1). e.g. { "unit": "month", "count": 3 } = quarterly. |
billing_anchor_day | No | integer | 1–28. Pins monthly/yearly charges to a fixed day of month. |
billing_mode | No | string | automatic (default) or merchant_managed. |
collection_method | No | string | auto (default) or manual. Only meaningful for automatic. |
trial_days | No | integer | Free days added before the first interval. The first payment still charges amount now. |
start_at | No | datetime | Defaults to now. |
total_cycles / ends_at | No | integer / datetime | Fixed term — the subscription completeds once either limit is reached. |
notification_url | No | string (https) | Per-subscription webhook target. Falls back to your account's default. |
{
"external_id": "SUB-PARTNER-001",
"customer": { "name": "Sara Ali", "id_number": "1055512345", "email": "sara@example.com" },
"title": "Pro plan — monthly",
"amount": 149.00,
"currency": "SAR",
"interval": { "unit": "month", "count": 1 },
"billing_mode": "automatic",
"collection_method": "auto",
"trial_days": 14,
"notification_url": "https://partner.example.com/webhooks/rsom"
}
{
"status": true,
"message": "Mission completed successfully",
"data": {
"id": "9f2b3c1a-7cfa-41fe-9b42-eb334f96c8e4",
"external_id": "SUB-PARTNER-001",
"status": "incomplete",
"billing_mode": "automatic",
"collection_method": "auto",
"customer": { "name": "Sara Ali", "id_number": "1055512345", "email": "sara@example.com", "phone": null },
"title": "Pro plan — monthly",
"amount": 149.0,
"currency": "SAR",
"interval": { "unit": "month", "count": 1 },
"trial_days": 14,
"current_period_end": null,
"next_action_at": null,
"cycles_completed": 0,
"total_cycles": null,
"cancel_at_period_end": false,
"created_at": "2026-09-10T10:00:00+03:00"
},
"first_payment": {
"first_payment_url": "https://payment.rsompay.com/pay/checkout/2b1a9f3c-...",
"first_payment_expires_at": "2026-09-13T10:00:00+03:00"
}
}
Send first_payment.first_payment_url to the customer. Once they pay, the subscription becomes active (or trialing) and the subscription.activated webhook fires.
| Method & path | Purpose |
|---|---|
GET /subscriptions | List. Filters: status, billing_mode, external_id. |
GET /subscriptions/{id} | One subscription by its RsomPay id. |
GET /subscriptions/by-external-id/{external_id} | One subscription by your id. |
GET /subscriptions/{id}/cycles | Every billing cycle (period, status, amount, paid_via). |
GET /subscriptions/{id}/payment-link | A hosted checkout URL for the current open (due / failed) cycle — surface it in your own portal so the customer can pay a failed month themselves. 204 when nothing is due. |
PATCH /subscriptions/{id} | Change amount, interval, collection_method or billing_mode. Applies from the next cycle, never the running period. |
POST /subscriptions/{id}/charge | merchant_managed only. See below. |
POST /subscriptions/{id}/pause · /resume | Pause / resume billing. |
POST /subscriptions/{id}/cancel | Body { "at_period_end": true|false }. false = stop now; true = stop at the end of the paid period. |
POST /subscriptions/{id}/payment-method | Body { "mandate_ref": "<saved-card-id>" } to switch cards, or { "send_link": true } to get a fresh link for the customer to add one. |
Triggers one off-session charge on the saved card. Only valid when billing_mode = merchant_managed and the subscription is active or past_due.
| Field | Required | Type | Description |
|---|---|---|---|
idempotency_key | Yes | string | A repeat call with the same key returns the same cycle without charging again. |
amount | No | number | Defaults to the subscription amount. Bounded server-side to a sensible multiple of it. |
description | No | string | Line on the cycle's invoice. |
period_start / period_end | No | datetime | The period this charge covers (recorded on the cycle). |
POST /subscriptions/9f2b3c1a-.../charge
{ "idempotency_key": "chg-2026-09-001", "amount": 149.00, "description": "September" }
Responds 202 Accepted with the created cycle. The charge is not yet confirmed — the subscription.renewed webhook (success) or subscription.payment_failed webhook (failure) follows. On failure, the subscription goes past_due; call /charge again with a new key, or surface the pay link.
Every state change POSTs to the subscription's notification_url (or your account default), signed exactly like the invoice webhooks — see section 7 for header and HMAC verification (X-Rsom-Event, X-Rsom-Timestamp, X-Rsom-Signature).
{
"event": "subscription.payment_failed",
"event_id": "5c1a...",
"occurred_at": "2026-10-01T09:00:00+03:00",
"data": {
"subscription": {
"id": "9f2b3c1a-...",
"external_id": "SUB-PARTNER-001",
"status": "past_due",
"billing_mode": "automatic",
"customer_identifier": "1055512345",
"amount": 149.0,
"currency": "SAR",
"interval": { "unit": "month", "count": 1 },
"current_period_end": "2026-10-01T00:00:00+03:00",
"next_action_at": "2026-10-02T00:00:00+03:00",
"cycles_completed": 1
},
"event_data": { "sequence": 2, "attempt": 1, "reason": "gateway_rejected" }
}
}
| Event | When |
|---|---|
subscription.created | Subscription created (still incomplete). |
subscription.first_payment_link_issued | A first-payment link was generated. |
subscription.activated | First payment confirmed — now active or trialing. |
subscription.trial_started / .trial_ended | Trial window opened / closed. |
subscription.renewed | A cycle was paid (auto charge or the customer paid a link). |
subscription.payment_failed | A cycle charge failed. event_data carries sequence, attempt, reason. |
subscription.retry_scheduled | Dunning scheduled the next retry (automatic only). |
subscription.paused / .resumed | Billing paused / resumed. |
subscription.amount_updated / .interval_updated / .billing_mode_changed / .payment_method_updated | A PATCH / payment-method change took effect. |
subscription.canceled | Canceled (event_data.at_period_end tells you immediate vs deferred). |
subscription.completed | Fixed term reached. |
subscription.cycle_refunded | A cycle's invoice was refunded (the subscription itself is unaffected). |
403 — subscriptions not enabled for your account, or missing permission.404 — subscription id / external_id not found for your account.422 — validation error, or an operation not allowed from the current status (e.g. charge on an automatic subscription, resume when the saved card is inactive, charge amount out of bounds).Guide version v1.4.0 — API base path /api/external/v1 (unchanged). See the Changelog for full version history.
| Method | Path | Purpose |
|---|---|---|
POST | /invoices | Create invoice |
GET | /invoices/by-reference | Query status (external_id or reference_number) |
POST | /invoices/{reference}/refund | Refund a captured payment, full or partial (requires refunds enabled on your account) |
GET | /transactions | Customer transactions (customer_id required) |
GET | /settlements | List settlement/payout batches |
GET | /settlements/{id} | Settlement detail with settled items |
POST | /subscriptions | Create a subscription, plus 10 more endpoints (list/detail/cycles/payment-link/update/charge/pause/resume/cancel/payment-method) — see §15. Subscriptions |
POST | /customers/{customer_external_id}/saved-card-token | Issue a one-time retrieval token for a customer's saved card (also auto-surfaced on status checks) — see §14. Saved cards |
POST | /invoices/{reference}/charge-saved-card | Charge an existing invoice with a saved-card token, server-to-server, no checkout visit — see §14. Saved cards |
| Production | Sandbox | |
|---|---|---|
| Integration API | integration.rsompay.com | demo-integration.rsompay.com |
| Payment page | payment.rsompay.com | demo-payment.rsompay.com |
Download Postman collection (JSON)
customer_id in Postman is only for GET /transactions, not invoice lookup.
Machine-generated from the live route/validation definitions (Scribe) — the authoritative parameter shapes and types behind this hand-written guide. Regenerate after any External API change: php artisan scribe:generate in backend-api, then copy storage/app/private/scribe/openapi.yaml here.
Technical support: tech@bseeds.sa
Website: https://rsompay.com
Full version history with categorized details: Changelog.
| Date | Notes |
|---|---|
| 2026-09-11 | Guide v1.4.0 — added Subscriptions (POST /subscriptions and 10 more endpoints — recurring billing, automatic or merchant-managed billing modes, dunning retries, lifecycle webhooks) and Saved Cards (POST /customers/{customer_external_id}/saved-card-token plus a new customer.saved_card_token field on POST /invoices; the retrieval token — plus card brand/last4 — is now also auto-surfaced on your next GET /invoices/by-reference or POST /invoices response after a card is saved, exactly once, with no extra call needed; added POST /invoices/{reference}/charge-saved-card to charge an existing invoice with a saved-card token directly from your server, no customer visit to checkout, confirmed asynchronously via payment.completed/payment.failed, new permission client_invoices.charge); also added payment.expired, payment.refunded, settlement.updated, settlement.paid webhook events and a payment.reason field; and added/fixed data.status values refunded and partially_refunded for invoices (previously a refunded invoice incorrectly showed issued). |
| 2026-08-02 | Guide v1.3.0 — added GET /settlements and GET /settlements/{id} for read-only settlement/payout reconciliation, scoped to your own account (client_settlements.view). |
| 2026-07-28 | Guide v1.2.0 — added POST /invoices/{reference}/refund (full/partial refunds, disabled by default, requires an enabled account) and a refund_status field on GET /transactions to track asynchronous refund confirmation. |
| 2026-07-13 | Guide v1.1.0 — added optional payment_method field to POST /invoices to pre-select mada, credit_card (Visa/Mastercard), apple_pay, or tamara, and skip the payment-method screen on the hosted checkout page. |
| 2026-05-19 | Guide v1.0.1 — partner-facing fields, payment flow, webhook signature docs |
| 2026-05-18 | Initial partner guide; docs site split to static hosting (docs.rsompay.com) |