Complete REST API reference for integrating Esaalat's payment verification into your own server. Create payment intents, attach the buyer's wallet reference, and get verified in real time via webhooks.
https://api.esaalat.com/api/v1/merchantPOST /token · GET /wallets/ · POST /intents/ · GET/PATCH /intents/{id}/
The Merchant REST API is how your own server integrates with Esaalat. You create a payment intent with the exact amount and the wallet you collect into. When the buyer pays, their wallet shows a transaction reference. You attach that reference to the intent, and Esaalat verifies the payment and notifies you with a signed webhook.
| Method | Path |
|---|---|
| POST | /token |
| GET | /wallets/ |
| POST | /intents/ |
| GET | /intents/ |
| GET | /intents/{id}/ |
| PATCH | /intents/{id}/ |
Authenticate with your client credentials to obtain a short-lived JWT. Credentials are created in Dashboard → API Keys.
/api/v1/merchant/token| Name | Type | Required | Description |
|---|---|---|---|
client_id | 32 hex chars | Required | Public identifier, safe to embed. |
client_secret | urlsafe string | Required | Shown exactly once at creation and never retrievable afterwards. Rotate the key in the dashboard if it leaks. |
curl -sS -X POST https://api.esaalat.com/api/v1/merchant/token \
-H "ESAALAT-CLIENT-ID: <client_id>" \
-H "ESAALAT-CLIENT-SECRET: <client_secret>"Before creating an intent, fetch the wallets you can collect into. Only active wallets with a monitoring device switched ON are returned. A wallet nobody is monitoring can never verify a payment. The dedicated testing wallet is excluded unless you opt in with ?include_test=1.
/api/v1/merchant/wallets/| Name | Type | Required | Description |
|---|---|---|---|
id | UUID string | Optional | The wallet UUID. Pass it as wallet when creating an intent. |
name / account_number | string | Optional | What the buyer sees / sends money to. |
sender_name | string | Optional | The sender identifier this wallet's payment alerts come from. |
currency | string | Optional | The wallet's currency (YER today). |
provider | object | Optional | The wallet provider's branding: display name, theme color, logo URL. |
is_test | boolean | Optional | true for the account's dedicated testing wallet (only returned with ?include_test=1). |
curl -sS https://api.esaalat.com/api/v1/merchant/wallets/ \
-H "Authorization: Bearer <access_token>"/api/v1/merchant/intents/| Name | Type | Required | Description |
|---|---|---|---|
wallet | UUID string | Required | A wallet of your account that is set up for payment monitoring in the dashboard. Only monitored wallets can receive payments. |
amount | decimal string | Optional | Total to collect, e.g. "1234.56". Required if items is absent. |
items | array of objects | Optional | Order line items; the total is computed from them. Required if amount is absent. If both are sent, amount is ignored. |
currency | string | Optional | Only YER is supported. Defaults to the wallet's currency and must match it. |
external_order_id | string ≤128 | Optional | Your order reference, echoed back in responses and webhooks. |
notes | string ≤500 | Optional | Free-form note, stored on the intent. |
curl -sS -X POST https://api.esaalat.com/api/v1/merchant/intents/ \
-H "Authorization: Bearer <access_token>" \
-H "Content-Type: application/json" \
-H "ESAALAT-IDEMPOTENCY-KEY: a1b2c3d4-0000-4000-8000-000000000001" \
-d '{
"wallet": "b48249d2-9b34-4414-b20e-ffd8dfedaf89",
"amount": "1234.56",
"currency": "YER",
"external_order_id": "order-12345",
"notes": "Optional note to the merchant"
}'{
"wallet": "b48249d2-9b34-4414-b20e-ffd8dfedaf89",
"amount": "1234.56",
"currency": "YER",
"external_order_id": "order-12345",
"notes": "Optional note to the merchant"
}{
"name": "Product name", // required, ≤100 chars
"description": "Optional", // ≤500 chars
"price": "1500.00", // decimal string, required
"quantity": 1 // integer ≥ 1, default 1
}/api/v1/merchant/intents/{id}/| Name | Type | Required | Description |
|---|---|---|---|
reference | string ≤128 | Required | The wallet transaction reference, trimmed of whitespace. |
curl -sS -X PATCH https://api.esaalat.com/api/v1/merchant/intents/<intent_id>/ \
-H "Authorization: Bearer <access_token>" \
-H "Content-Type: application/json" \
-d '{"reference": "12345678"}'{
"reference": "12345678"
}List your intents, newest first, always scoped to your client account. 20 per page by default; ?page_size= (max 100) and ?page= control pagination.
/api/v1/merchant/intents/| Name | Type | Required | Description |
|---|---|---|---|
status | comma list | Optional | PENDING, VERIFIED, EXPIRED, CANCELLED, WRONG_AMOUNT. e.g. ?status=VERIFIED,WRONG_AMOUNT. Invalid value → 400. |
code | string | Optional | Exact payment_id, e.g. ?code=P7K4X92 |
reference | string | Optional | Exact wallet reference, e.g. ?reference=12345678 |
external_order_id | string | Optional | Your own order reference (exact match). |
wallet | UUID | Optional | Restrict to one wallet. |
created_after | ISO 8601 | Optional | Inclusive (>=). |
created_before | ISO 8601 | Optional | Inclusive (<=). |
search | string | Optional | Free text matching reference, code and external_order_id. |
ordering | string | Optional | created_at, verified_at, payable_amount, status. Prefix with - to reverse. |
curl -sS "https://api.esaalat.com/api/v1/merchant/intents/?status=VERIFIED&created_after=2026-09-09T00:00:00Z&page_size=100" \
-H "Authorization: Bearer <access_token>"Get the current status and full detail of one intent. An intent of another account returns 404.
/api/v1/merchant/intents/{id}/| Name | Type | Required | Description |
|---|---|---|---|
status | string | Optional | Your source of truth: one of the statuses below. |
paid_amount | decimal string | Optional | Set only when status is WRONG_AMOUNT, to show what the buyer actually sent. |
matched_event | object | Optional | The raw SMS event that resolved the intent (its reference, amount, sender). |
device | object | Optional | The device that reported the payment. id stays the same even when the device code is regenerated. |
manual_override / collected_manually | boolean | Optional | true when the merchant booked the payment manually from the unmatched queue. |
curl -sS https://api.esaalat.com/api/v1/merchant/intents/<intent_id>/ \
-H "Authorization: Bearer <access_token>"{
"id": "dd157a06-a065-475d-8723-d98a61698aa0",
"code": "P7K4X92",
"external_order_id": "order-12345",
"reference": "12345678",
"status": "VERIFIED",
"base_amount": "1234.56",
"payable_amount": "1234.56",
"paid_amount": null,
"currency": "YER",
"items": [
{
"name": "Product name",
"description": "",
"price": "1234.56",
"quantity": 1,
"total_price": "1234.56"
}
],
"notes": "",
"wallet": {
"id": "b48249d2-9b34-4414-b20e-ffd8dfedaf89",
"name": "Kuraimi - Main",
"sender_name": "777777777",
"is_test": false,
"sim_slot": "SIM 1",
"account_number": "777777777"
},
"provider": {
"id": "3f0a4b1e-0000-4000-8000-000000000002",
"name": "KURAIMI",
"display_name": "Kuraimi",
"theme_color": "#0b8f4c",
"logo_url": "https://<cdn>/kuraimi_256.png",
"provider_status": "ACTIVE"
},
"expires_at": "2026-09-09T14:30:00+03:00",
"created_at": "2026-09-09T13:30:00.123456+03:00",
"verified_at": "2026-09-09T13:41:22.654321+03:00",
"manual_override": false,
"collected_manually": false,
"matched_event": {
"id": "32fd9972-71fd-4b87-82ce-501061ab7c5d",
"amount": "1234.56",
"sender": "777777777",
"reference": "12345678",
"created_at": "2026-09-09T13:41:21+03:00"
},
"device": {
"id": "5a1f6c3d-0000-4000-8000-000000000004",
"name": "Shop phone 1"
},
"subscription": null
}| Name | Description | Terminal |
|---|---|---|
PENDING | Created, awaiting the buyer's payment + reference. | — |
VERIFIED | Payment detected and matched (wallet + reference + exact amount). | Terminal |
WRONG_AMOUNT | The reference matched but the paid amount differs from payable_amount. paid_amount records what arrived. It never auto-verifies, so the buyer must contact you. | Terminal |
EXPIRED | The 1-hour window passed without verification. | Terminal |
CANCELLED | Cancelled by the merchant (dashboard). | Terminal |
Webhooks are the primary channel for verification; use the list/retrieve endpoints for backfill and reconciliation. Configure endpoints in Dashboard → Webhooks, optionally scoped to a specific wallet.
Every request carries an X-Esaalat-Signature header: t=<unix-ts>,v1=<hex>. v1 is HMAC-SHA256 of "{ts}.{raw_body}" using your endpoint's secret. Always compare with hmac.compare_digest, and optionally reject timestamps older than ~5 minutes (replay protection).
import hashlib, hmac
def verify_signature(secret: str, body: bytes, signature: str) -> bool:
ts, v1 = (p.split("=", 1)[1] for p in signature.split(","))
digest = hmac.new(
secret.encode(), f"{ts}.{body.decode()}".encode(), hashlib.sha256
).hexdigest()
return hmac.compare_digest(digest, v1)The happy path: a payment was verified, either automatically or collected manually.
{
"event": "payment.verified",
"payment_id": "P7K4X92",
"intent_id": "dd157a06-a065-475d-8723-d98a61698aa0",
"order_id": "order-12345",
"reference": "12345678",
"base_amount": "1234.56",
"amount": "1234.56",
"currency": "YER",
"status": "VERIFIED",
"verified_at": "2026-09-09T13:41:22+03:00",
"manual_override": false
}Money landed on a real wallet but no intent claimed it. Only delivered if the endpoint explicitly opts into payment.unmatched. Never fires for the testing wallet.
{
"event": "payment.unmatched",
"event_id": "32fd9972-71fd-4b87-82ce-501061ab7c5d",
"reference": "12345678",
"amount": "1234.56",
"currency": "YER",
"wallet": "Kuraimi - Main",
"received_at": "2026-09-09T13:41:21+03:00",
"is_test": false
}Fires for any test-wallet payment arrival, even when no intent was created. Lets you exercise your integration end-to-end with the testing wallet.
{
"event": "payment.received",
"event_id": "32fd9972-71fd-4b87-82ce-501061ab7c5d",
"reference": "12345678",
"amount": "1234.56",
"currency": "YER",
"resolution": "UNMATCHED",
"received_at": "2026-09-09T13:41:21+03:00",
"is_test": true
}Requests are rate-limited per API credential. When a limit is exceeded you receive 429 with a Retry-After header. Always handle 429 gracefully.
| Path | Burst | Sustained |
|---|---|---|
POST /token | 10/min | 500/day |
POST /intents/, GET/PATCH /intents/{id}/ | 240/min (~4 req/s) | 50,000/day |
GET /intents/ (list) | 60/min | 50,000/day |
Every error uses a standardized envelope:
{
"type": "validation_error",
"errors": [
{ "code": "required", "detail": "This field is required.", "attr": "wallet" }
]
}| Name | Description |
|---|---|
400 | Bad payload, unknown wallet, invalid filter, wallet not set up for payment monitoring, reference on a non-pending intent, quota exceeded. |
401 | Missing, invalid or expired Bearer token. |
403 | A dashboard (user) token used on merchant endpoints. A credential token is required. |
404 | Unknown intent id (or another account's intent). |
429 | Rate limit exceeded. Honor Retry-After. |
500 | Something broke. Report it. |