Wasl

Developer docs

Connect your systems to Wasl with the REST API and webhooks. Create a key under Settings → Integrations and call the API in minutes.

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:

http
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:

json
{
  "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:

bash
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.

bash
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

bash
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

bash
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 branches
  • GET /branch-groups · POST /branch-groups — activity-based branch groups (Enterprise, feature chain.rollup)
  • GET /branch-groups/:id/overview · /leaderboard · /timeseries — group sales dashboard
  • GET /banks · GET /banks/tree — Sudanese bank registry
  • GET /recurring-invoices — recurring invoice templates
  • GET /analytics/summary plus /analytics/* chart endpoints
  • POST /reports/preview · POST /reports/export
  • GET /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

bash
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

bash
curl -X PUT "$UPLOAD_URL" \
  -H "Content-Type: image/jpeg" \
  --data-binary "@receipt.jpg"

3) Register the upload

bash
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:

bash
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.

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

bash
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

bash
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 confirmed
  • invoice.paid — invoice paid (full or partial)
  • alert.raised — alert (duplicate, fraud, …)

Request headers

  • X-Wasl-Event — event name
  • X-Wasl-Delivery — delivery id
  • X-Wasl-Signature — t=<unix>,v1=<hmac>
  • User-Agent: Wasl-Webhooks/1.0

Payload shape

json
{
  "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.

javascript
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 session
  • 403 — insufficient role for that route
  • 404 — resource not in your organization
  • 422 — 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.