Odit Verify

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.

curl -sX POST https://v.odit.et/api/verify \
  -H "x-api-key: vk_live_..." \
  -H "content-type: application/json" \
  -d '{
    "url": "https://transactioninfo.ethiotelecom.et/receipt/ABCD1234EF",
    "waitMs": 3000
  }'

Completed inside the wait window — 200

{
  "ok": true,
  "processingStatus": "completed",
  "requestId": "550e8400-e29b-41d4-a716-446655440000",
  "providerKey": "telebirr",
  "resolvedUrl": "...",
  "receipt": { "...": "..." },
  "cached": false,
  "httpStatus": 200,
  "error": null
}

Still running at the deadline — 202

{
  "processingStatus": "queued",
  "requestId": "550e8400-e29b-41d4-a716-446655440000",
  "statusUrl": "/api/verify/550e8400-e29b-41d4-a716-446655440000",
  "eventsUrl": "/api/verify/550e8400-e29b-41d4-a716-446655440000/events"
}

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 true; do
  RESP=$(curl -s -H "x-api-key: vk_live_..." \
    "https://v.odit.et/api/verify/$REQUEST_ID")
  STATUS=$(jq -r '.processingStatus' <<< "$RESP")
  if [ "$STATUS" = "completed" ] || [ "$STATUS" = "failed" ]; then
    echo "$RESP" | jq .
    break
  fi
  sleep 2
done

While the request is still queued — 202

{
  "processingStatus": "queued",
  "requestId": "550e8400-e29b-41d4-a716-446655440000",
  "statusUrl": "/api/verify/...",
  "eventsUrl": "/api/verify/.../events"
}

Once the row resolves — 200 / 502

Same envelope as the sync flow, with processingStatus and requestId added:

{
  "ok": true,
  "processingStatus": "completed",
  "requestId": "550e8400-e29b-41d4-a716-446655440000",
  "providerKey": "telebirr",
  "receipt": { "...": "..." },
  "cached": false,
  "httpStatus": 200,
  "error": null
}

Cadence suggestion: 1.5–3 s between polls. Faster than 1/s burns rate-limit quota for no benefit (the server-side verification is sequential, not parallelisable).

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

curl -N \
  -H "Accept: text/event-stream" \
  "https://v.odit.et/api/verify/$REQUEST_ID/events?x-api-key=vk_live_..."

EventSource from the browser:

const es = new EventSource(
  `/api/verify/${requestId}/events?x-api-key=${apiKey}`,
);
 
es.addEventListener('status', (e) => {
  const d = JSON.parse(e.data);
  console.log('status:', d.status);
});
 
es.addEventListener('completed', (e) => {
  const d = JSON.parse(e.data);
  console.log('done', d.receipt);
  es.close();
});
 
es.addEventListener('failed', (e) => {
  const d = JSON.parse(e.data);
  console.error('failed', d.error);
  es.close();
});
 
es.addEventListener('timeout', () => {
  // The server caps the stream at 5 min. Reconnect to pick up where you
  // left off — pending state is replayed on connect.
  es.close();
});

Event types

EventWhenPayload
statusWhen the row is created (pending) and on any intermediate transition{ type: 'status', status: 'pending' | 'processing', ts }
completedVerification finished successfully. Stream closes after.{ type: 'completed', ok: true, cached, receipt, httpStatus, ts }
failedVerification finished with an error. Stream closes after.{ type: 'failed', error, httpStatus, ts }
timeoutThe 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.

curl -X POST https://v.odit.et/api/verify \
  -H "x-api-key: vk_live_..." \
  -H "Idempotency-Key: 7e1a4c34-..." \
  -H "content-type: application/json" \
  -d '{"url":"...","waitMs":2000}'

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

FlowBest forTrade-off
Sync (default)One-off verifications triggered by user interaction; cached receiptsHolds the HTTP request open up to ~20 s
Short waitCached-most-of-the-time receipts where you accept queue fallback when it isn't cachedUp to 30 s of held connection in the worst case
PollingBackground jobs, queue workers, mobile networksSlightly stale; uses more HTTP requests
SSEBrowser UIs that want the result instantlyNeeds 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.

On this page