Integration flows
Three ways to consume /api/verify (short wait, polling, and SSE) plus idempotency replay. Pick whichever matches how your app handles latency.
POST /api/verify defaults to synchronous: the call blocks until the upstream finishes, then returns the receipt. For most callers (a single
on-demand verification triggered by a user click) that's fine.
When you can't or don't want to hold the request open (mobile network, server with strict request budgets, queue-of-many architecture), use one of the three async-aware flows below. All three share the same submit endpoint and result envelope; what differs is how the client waits.
Short Wait
Submit with waitMs to ask the server to block up to waitMs milliseconds. If the verification finishes before the deadline, you get the same 200/502 response a sync call would return. If it doesn't, you get 202 + a request id and the URLs you'd use to poll or stream.
Completed inside the wait window: 200
Still running at the deadline: 202
The verification keeps running on the server. Follow up via polling or SSE; both work against the same requestId.
waitMs is capped at 30 000 ms server-side; anything higher is silently clamped.
Polling
Use when your environment can't keep a long-lived connection open but you can call back every few seconds.
While the request is still queued: 202
Once the row resolves: 200 / 502
Same envelope as the sync flow, with processingStatus and requestId added:
Cadence suggestion: 1.5 to 3 s between polls. Faster than one per second buys nothing, because the verification runs at its own pace regardless.
A poll reads an existing row, so it counts against your per-minute request rate but never against your plan's verification cap. Only the original submit can spend an uncached verification, and only if the receipt was not already cached. See rate limits for the difference.
Server-Sent Events (SSE)
Use when the client can keep a connection open and wants the result as soon as it's ready (instead of polling on a timer).
EventSource from the browser:
Event types
| Event | When | Payload |
|---|---|---|
status | When the row is created (pending) and on any intermediate transition | { type: 'status', status: 'pending' | 'processing', ts } |
completed | Verification finished successfully. Stream closes after. | { type: 'completed', ok: true, cached, receipt, httpStatus, ts } |
failed | Verification finished with an error. Stream closes after. | { type: 'failed', error, httpStatus, ts } |
timeout | The server is closing the stream at the 5-minute lifetime cap. | { type: 'timeout', message } |
The stream also sends an unnamed comment (: ping) every 15 s to keep proxies from idling the connection.
Replay-on-reconnect
If the verification resolves between your submit call and the moment you subscribe to the SSE stream, you don't miss the terminal event; the server replays the last-seen event on connect for ~5 minutes. After that, the in-memory replay is evicted (the row stays in Postgres; the GET polling endpoint still returns the resolution forever).
Idempotency
Add an Idempotency-Key header to POST /api/verify to make retries safe. The first call with a given key (per API key) creates a request and returns its id. Any subsequent call with the same key replays the current state of that same request; even if the URL differs in the retry, you get the original.
A 200 / 202 / 502 will look identical to the first response, including the same requestId, for as long as the row exists. Use a UUID (or any stable string up to 256 chars) generated by the client per logical operation. Idempotency rows are kept indefinitely; there's no TTL today.
When to use which
| Flow | Best for | Trade-off |
|---|---|---|
| Sync (default) | One-off verifications triggered by user interaction; cached receipts | Holds the HTTP request open. Instant on a cache hit, but see below for the worst case |
| Short wait | Cached-most-of-the-time receipts where you accept queue fallback when it isn't cached | Up to 30 s of held connection in the worst case |
| Polling | Background jobs, queue workers, mobile networks | Slightly stale; uses more HTTP requests |
| SSE | Browser UIs that want the result instantly | Needs a long-lived connection; harder to put behind some proxies |
You can mix and match. A common pattern is: submit with waitMs: 2000. If 202, open SSE for the result. If your SSE connection drops, fall back to polling once a second.
What a busy bank does to a sync call
A cached receipt returns immediately. An uncached one has to reach the bank, and we are a polite client: each upstream host gets a token bucket per egress identity, so when a bank is saturated your call waits for a slot before the fetch even starts.
That makes the sync worst case much longer than one fetch timeout:
| Stage | Bound |
|---|---|
| Waiting for an upstream slot | 30 s, longer on a deliberately slow host, hard ceiling 120 s |
| The fetch itself | 20 s per attempt |
| One retry, on a different egress identity | 20 s |
So a sync call against a struggling bank can hold the connection for well over a minute. There is one retry, never more, and none at all for a metered link like Siinqee.
If the queue is full or the wait runs out, the call gives up rather than
hanging, and the error reads like gave up waiting 34s for a slot on transactioninfo.ethiotelecom.et. Today that returns HTTP 400, because the
response never got an upstream status code to report. It is a "come back
later", not a complaint about your request, so retry it with backoff the way
you would a 502.
This is the reason waitMs exists. Submit with a deadline you are happy to
hold, take the 202, and pick the result up by polling or SSE instead of
holding a socket open through a bank outage.