1. Overview
Wasl exposes a REST API at https://api.wasl.nami-lab.com/api/v1 so you can pull customers, transfers, and invoices, push transfer screenshots for OCR, and receive signed webhooks when a transfer is verified, an invoice is paid, or an alert is raised.
- Pull data with an API key (server-to-server).
- Push screenshots with a signed upload URL + register call to kick off OCR.
- Receive events with a signed webhook to your HTTPS endpoint.
- Money fields use
amountMinor(smallest currency unit, stringified to avoid overflow).
2. Authentication
In the dashboard open Settings → Integrations, create a key, and copy it once. Keys start with wsl_.
Send it on every request:
Authorization: Bearer wsl_…The key authenticates as the organization owner. Never embed it in mobile apps or browser code — keep it on your server. Revoke it any time from the same page.
Scopes
Every API key carries a subset of scopes. Pick the smallest one the integration needs:
read— every GET route only. Safe for read-only integrations (dashboards, summaries).notifications:write— upload, review, and confirm transfers.invoices:write— create/edit invoices, customers, and refunds.
Keys created before Sprint 35 default to full scope for backwards compatibility. A request missing the required scope returns 403 API key missing required scope.
3. Conventions
Pagination
Every list endpoint accepts page (1-indexed) andpageSize (default 20, max 100). Responses look like:
{
"data": [ /* … page of rows … */ ],
"page": 1,
"pageSize": 20,
"total": 143,
"totalPages": 8,
"hasNext": true,
"hasPrev": false
}Money & currency
Monetary fields are amountMinor, a string in the smallest currency unit. Pass currency as an ISO-4217 code — currently SDG and USD are supported.
Dates & time
All timestamps are ISO-8601 UTC. Render in Africa/Khartoum for your users.
Resource IDs
IDs are string cuid values — never store them as integers.
Idempotency-Key
Networks in Sudan drop connections. Send an Idempotency-Key header on every creation POST — Wasl caches the first successful response for 24 hours and replays it verbatim on any retry with the same key and same payload:
curl -s -X POST "https://api.wasl.nami-lab.com/api/v1/invoices" \
-H "Authorization: Bearer wsl_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-4711" \
-d '{ "customerId": "clx…", "items": [ … ] }'Supported today: POST /invoices, POST /invoices/:id/refunds, POST /customers, POST /uploads/register, and POST /public-receipts/notifications/:id. Reusing the same key with a different payload returns 409 Idempotency-Key reused with a different request payload.
4. Core endpoints
All routes live under /api/v1. Common calls:
Transfers (notifications)
GET /notifications — list transfers with filters for status, branch, customer, and date.
curl -s "https://api.wasl.nami-lab.com/api/v1/notifications?status=VERIFIED&pageSize=20" \
-H "Authorization: Bearer wsl_YOUR_API_KEY" \
-H "Accept: application/json"Customers
GET /customers · POST /customers · GET /customers/:id · customer bank accounts at /customers/:id/bank-accounts
curl -s -X POST "https://api.wasl.nami-lab.com/api/v1/customers" \
-H "Authorization: Bearer wsl_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "محل النور",
"phone": "+249900000000"
}'Invoices
GET /invoices · POST /invoices · GET /invoices/:id · link transfers with POST /invoices/:id/link · unlink with DELETE /invoices/:id/link/:transferId · partial refund with POST /invoices/:id/refunds
curl -s "https://api.wasl.nami-lab.com/api/v1/invoices/{invoiceId}" \
-H "Authorization: Bearer wsl_YOUR_API_KEY" \
-H "Accept: application/json"Other resources
GET /branches— your branchesGET /branch-groups·POST /branch-groups— activity-based branch groups (Enterprise, featurechain.rollup)GET /branch-groups/:id/overview·/leaderboard·/timeseries— group sales dashboardGET /banks·GET /banks/tree— Sudanese bank registryGET /recurring-invoices— recurring invoice templatesGET /analytics/summaryplus/analytics/*chart endpointsPOST /reports/preview·POST /reports/exportGET /reconciliation/unlinked-transfers·/reconciliation/suggestions
5. Upload transfer images
To push screenshots from your own system (POS, WhatsApp bot, …), use the two-step signed-upload flow: request a URL, upload the file, then register it to kick off OCR.
1) Request an upload URL
curl -s -X POST "https://api.wasl.nami-lab.com/api/v1/uploads/sign" \
-H "Authorization: Bearer wsl_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"filename": "receipt.jpg",
"mimeType": "image/jpeg",
"size": 812345
}'The response contains uploadUrl (a direct PUT link) and objectKey.
2) PUT the file to R2
curl -X PUT "$UPLOAD_URL" \
-H "Content-Type: image/jpeg" \
--data-binary "@receipt.jpg"3) Register the upload
curl -s -X POST "https://api.wasl.nami-lab.com/api/v1/uploads/register" \
-H "Authorization: Bearer wsl_YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"objectKey": "org_abc/receipts/2026-08-16/receipt.jpg",
"bankId": null,
"branchId": null
}'The response returns notificationId. Poll GET /notifications/:id or wait for the transfer.verified webhook to see the OCR result.
6. Receiving accounts
To build a “transfer to us” page inside your own app, pull bank accounts for your organization:
curl -s "https://api.wasl.nami-lab.com/api/v1/org-bank-accounts" \
-H "Authorization: Bearer wsl_YOUR_API_KEY" \
-H "Accept: application/json"Each account carries the bank name, account number/holder, and an isActive flag — only show the active ones to customers.
7. Public receipt & invoice links
Wasl mints signed, shareable links so you can hand customers a receipt or a payment page via WhatsApp or SMS. Recipients do not need an account.
Receipt for a verified transfer
curl -s -X POST "https://api.wasl.nami-lab.com/api/v1/public-receipts/notifications/{notificationId}" \
-H "Authorization: Bearer wsl_YOUR_API_KEY"Response returns a token. Public URL: https://api.wasl.nami-lab.com/r/{token}. Revoke all links at once with DELETE /public-receipts/notifications/:id.
Payment page for an invoice
curl -s -X POST "https://api.wasl.nami-lab.com/api/v1/public-invoices/invoices/{invoiceId}" \
-H "Authorization: Bearer wsl_YOUR_API_KEY"Public URL: https://api.wasl.nami-lab.com/i/{token} — shows the line items, receiving accounts, and a payment QR.
8. Webhooks
Under the Webhooks tab in Integrations, add an HTTPS URL and select events. Wasl POSTs JSON within ~10s and retries on transient failures.
Events
transfer.verified— transfer confirmedinvoice.paid— invoice paid (full or partial)alert.raised— alert (duplicate, fraud, …)
Request headers
X-Wasl-Event— event nameX-Wasl-Delivery— delivery idX-Wasl-Signature—t=<unix>,v1=<hmac>User-Agent: Wasl-Webhooks/1.0
Payload shape
{
"id": "evt_01HXYZ…",
"type": "transfer.verified",
"createdAt": "2026-08-16T12:00:00.000Z",
"data": {
"transferId": "clx…",
"amountMinor": "1500000",
"currency": "SDG",
"referenceNumber": "123456789",
"bankId": "clx…",
"customerId": "clx…",
"branchId": "clx…",
"status": "VERIFIED",
"fraudSignals": []
}
}The fraudSignals array carries keys like CROSS_ORG_SENDER or UNKNOWN_SENDER_ACCOUNT — use them to flag your team before releasing goods.
Verify the signature
Compute HMAC-SHA256 of {t}.{rawBody} with your webhook secret and compare to v1 with a timing-safe equal. Reject timestamps older than 5 minutes.
import { createHmac, timingSafeEqual } from 'node:crypto'
export function verifyWaslSignature (secret, rawBody, header, toleranceSec = 300) {
const parts = Object.fromEntries(
header.split(',').map((part) => {
const [k, ...rest] = part.trim().split('=')
return [k, rest.join('=')]
}),
)
if (!parts.t || !parts.v1) return false
const t = Number(parts.t)
if (!Number.isFinite(t)) return false
if (Math.abs(Math.floor(Date.now() / 1000) - t) > toleranceSec) return false
const expected = createHmac('sha256', secret)
.update(`${t}.${rawBody}`)
.digest('hex')
const a = Buffer.from(expected, 'utf8')
const b = Buffer.from(parts.v1, 'utf8')
return a.length === b.length && timingSafeEqual(a, b)
}Respond with 2xx quickly. Non-timeout 4xx responses (except 408/429) are treated as permanent and will not be retried.
9. Errors & limits
401— invalid key or session403— insufficient role for that route404— resource not in your organization422— validation error (details in the body)429— rate limited; back off and retry
Default limit: 60 requests/minute per API key. Need higher? Talk to us. Responses are JSON; monetary fields are strings so large integers stay exact in every language.
10. Interactive reference
Try every endpoint in Swagger UI. The reference only lists routes an API key can actually call — team management, settings, and dashboard-only internal tools are hidden because they are not part of the integration surface. Need help? Contact us.