Connect your back end to Reconaxe. Plain JSON, signed webhooks.
Your back end sends customers, cases, KYC files and the customer's replies. Reconaxe sends back case references, due dates and every update your app should show. One API key, plain JSON and CSV, and signed webhooks.
- OpenAPI 3.1 contracts
- Checked against the service on every build
- Safe retries, nothing created twice
- Delivery retried for 24 hours
curl -X POST "$SANAD_URL/v1/cases" \
-H "X-Api-Key: $SANAD_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"submission_id": "app-7731",
"customer_no": "SME-100245",
"request_type": "COMPLAINT",
"topic": "COLLECTION_CALLS",
"title": "Collection calls after I paid"
}'
# 201 delivered
# { "status": "delivered", "case": { "caseId": "CS-261027-K2P9",
# "priority": "p1", "respondBy": "…", "dueAt": "…" } }
const res = await fetch(`${process.env.SANAD_URL}/v1/cases`, {
method: 'POST',
headers: { 'X-Api-Key': process.env.SANAD_API_KEY, 'Content-Type': 'application/json' },
body: JSON.stringify({
submission_id: 'app-7731', // your id: sending it again never makes a second case
customer_no: 'SME-100245',
request_type: 'COMPLAINT',
topic: 'COLLECTION_CALLS',
title: 'Collection calls after I paid',
}),
});
const answer = await res.json();
// 201: show answer.case.caseId and answer.case.dueAt in the app
// 202: queued, Reconaxe delivers it for you; 422: answer.errors names the fields
import os, requests
res = requests.post(f"{os.environ['SANAD_URL']}/v1/cases",
headers={"X-Api-Key": os.environ["SANAD_API_KEY"]},
json={
"submission_id": "app-7731", # your id: safe to retry
"customer_no": "SME-100245",
"request_type": "COMPLAINT",
"topic": "COLLECTION_CALLS",
"title": "Collection calls after I paid",
}, timeout=15)
answer = res.json()
# 201: answer["case"]["caseId"] is the reference to show the customer
Two APIs, one contract each
Partners integrate with Reconaxe Connection. The Reconaxe CRM API is the API behind the Reconaxe app itself, for in-house tools and reporting.
Partner API
The only door from your systems into Reconaxe. Customer profiles, cases raised in your app, evidence files, customer replies, the end-of-day KYC file and the nightly customer list. Your own field names and codes, translated by your connector profile.
- API key
- 10 calls
- 2 signed webhooks
- JSON and CSV
Reconaxe CRM API
Everything the Reconaxe app does: cases and their SLA clocks, customers, operations tasks with maker-checker approval, the inbox, leads, reports and the audit trail. Signed in through your own identity provider.
- OIDC bearer token
- 105 endpoints
- Role-based permissions
- RFC 7807 errors
Quickstart
Four steps from an API key to a case on an agent's screen and its update back in your app. Every example uses the demo connector profile's field names; at onboarding you get the same contract in your own.
-
Get a key and an address
At onboarding Reconaxe gives your team an API key and the address of your test environment. Send the key in
X-Api-Key(orAuthorization: Bearer). Reconaxe keeps only its SHA-256, and can limit calls to your networks.Running Reconaxe yourself? The local stack answers on port 8090 with the demo key below.
Your shellexport SANAD_URL=http://localhost:8090 # or your test environment export SANAD_API_KEY=demo-partner-key-not-for-production -
Send a customer profile
Send the full current profile when a business finishes onboarding and whenever it changes. Include the time it changed: an older change than the last one Reconaxe applied is ignored, so messages can arrive out of order safely.
The answer says what happened:
created,updated,unchanged,linkedorignored_older.PUT /v1/customers/{customerNo}curl -X PUT "$SANAD_URL/v1/customers/SME-100245" \ -H "X-Api-Key: $SANAD_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "legal_name_en": "Al-Waha Trading Est.", "cr_number": "1010000419", "mobile": "+966 11 000 0419", "segment": "SMALL", "city_code": "RUH", "language": "AR", "account_status": "A", "updated_at": "2026-10-27T09:30:00Z" }'await fetch(`${process.env.SANAD_URL}/v1/customers/SME-100245`, { method: 'PUT', headers: { 'X-Api-Key': process.env.SANAD_API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ legal_name_en: 'Al-Waha Trading Est.', cr_number: '1010000419', mobile: '+966 11 000 0419', segment: 'SMALL', city_code: 'RUH', language: 'AR', account_status: 'A', updated_at: '2026-10-27T09:30:00Z', }), });requests.put(f"{os.environ['SANAD_URL']}/v1/customers/SME-100245", headers={"X-Api-Key": os.environ["SANAD_API_KEY"]}, json={ "legal_name_en": "Al-Waha Trading Est.", "cr_number": "1010000419", "mobile": "+966 11 000 0419", "segment": "SMALL", "city_code": "RUH", "language": "AR", "account_status": "A", "updated_at": "2026-10-27T09:30:00Z", }, timeout=15) -
Raise a case from your app
Name each case with your own
submission_id. Sending it again returns the first case, so retry freely after a timeout. Reconaxe answers with the case reference and the dates SAMA's rules set; the priority follows the topic.A case can even arrive before its profile: it gets its reference at once and moves to the customer when the profile lands.
201 · the answer{ "messageId": "9b2f…", "status": "delivered", "receivedAt": "2026-10-27T10:02:11Z", "attempts": 1, "case": { "caseId": "CS-261027-K2P9", // show this to the customer "customerLinked": true, "priority": "p1", // follows the topic "respondBy": "…", // the customer hears from Reconaxe by then "dueAt": "2026-11-03T10:02:11Z" // 5 SAMA working days } } -
Hear back through a signed webhook
When an agent replies, needs more information, resolves or reopens the case, Reconaxe posts
case.updatedto your webhook. Changes an agent makes to a customer arrive ascustomer.changed.- Check
X-Sanad-Signatureand refuse atolder than 5 minutes. X-Sanad-Message-Idrepeats on a retry: ignore ids you have stored.- Apply an update only if its
sequenceis higher than the last one. - Answer
2xxonce stored. Anything else is retried for 24 hours.
Verify X-Sanad-Signatureimport crypto from 'node:crypto'; // rawBody: the exact bytes received, e.g. express.raw({ type: 'application/json' }) // header: "t=1761559200,v1=5f0c…" export function isFromSanad(rawBody, header, secret) { const parts = Object.fromEntries(header.split(',').map((p) => p.split('='))); const t = Number(parts.t); if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false; const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest(); const given = Buffer.from(parts.v1 ?? '', 'hex'); return given.length === expected.length && crypto.timingSafeEqual(given, expected); }import hmac, hashlib, time # raw_body: the exact bytes received; header: "t=1761559200,v1=5f0c…" def is_from_sanad(raw_body: bytes, header: str, secret: str) -> bool: parts = dict(p.split("=", 1) for p in header.split(",")) t = int(parts.get("t", "0")) if not t or abs(time.time() - t) > 300: return False expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest() return hmac.compare_digest(expected, parts.get("v1", "")) - Check
The nine flows
Everything a partner exchanges with Reconaxe, in the order most teams build it. Each one is a call in the reference, with examples, every answer and every error.
| Flow | Direction | Call | When |
|---|---|---|---|
| 1 Customer profile | to Reconaxe | PUT/v1/customers/{customerNo} | A business finishes onboarding, or its profile changes |
| 2 New case | to Reconaxe | POST/v1/cases | A customer raises a complaint, question or request in your app |
| 3 KYC file | to Reconaxe | POST/v1/kyc-files CSV | Once a day, before the deadline set in Reconaxe (default 23:00 Riyadh) |
| 4 Changes made in Reconaxe | to you | webhook customer.changed | Within a minute of an agent changing one of your customers |
| 5 Nightly customer list | to Reconaxe | POST/v1/reconciliations CSV | Once a night; Reconaxe answers with its comparison |
| 6 Read full details | to Reconaxe | GET/v1/customers/{customerNo} | When you need Reconaxe's current record (off unless allowed) |
| 7 Case updates | to you | webhook case.updated | Received, more information needed, reply, resolved, closed, reopened |
| 8 Evidence file | to Reconaxe | POST/v1/cases/{submissionId}/files | The customer attaches a PDF, JPG or PNG (up to 10 MB) |
| 9 Customer reply | to Reconaxe | POST/v1/cases/{submissionId}/replies | The customer answers in your app |
Rules you can build on
The behaviour your code can rely on, so you write less of it.
delivered Reconaxe applied it; its reply is attached.queued Stored. Reconaxe delivers it for you and you can ask about it.rejected errors names each field, in your own field names.Profiles carry their time of change, cases a submission_id, files an uploadId and replies a reply_id. Sending the same message twice never creates anything twice.
Fields Reconaxe does not keep, such as a bank account number, are dropped and listed by name in droppedFields. Messages are stored encrypted and cleared once delivered.
Your connector profile maps your field names and code values to Reconaxe's. You never rename a column to fit us, and errors come back in your names.
Every evidence file is scanned for malware. A file that fails is blocked, its content deleted, and your app hears file_blocked with a request for a clean copy.
Problems found before a message is accepted (missing key, wrong address, unreadable input) are RFC 9457 problem details with a stable code.
Downloads
Import the contract into your own tools: generate a client, mock Reconaxe in your tests, or run the flows in order from Postman.
Planning an integration?
Talk to the team that built it. We will map your fields, set up your connector profile and test environment, and send you the contract in your own names.