Skip to main content

REST API Examples

REST Examples Until Official SDKs Ship

Register a dashboard webhook, then submit verifications with these REST snippets. Official SDK packages are not published yet. Use polling only when a screen needs to show progress.

WebhooksReceipt imagesTypeScriptPythonPHP

Use REST First

Treat these snippets as small SDK adapters you can place in your own backend. Keep API keys server-side and never expose VERIFY_BANK_ET_your_key_here secrets in browser code.

Base URL: https://verify.et

Suggested path

Register one webhook, save the whsec_ secret, and send a test. Then submit POST /api/verify.

Open Webhooks

Receive the Result

Put this receiver on a public HTTPS URL. Return 204 after the signature check, then do the rest of the work. Test pings set X-Webhook-Test: true. Reply to those without fulfilling an order.

  1. 1

    Add the endpoint

    Add your HTTPS URL and save the whsec_ secret as VERIFY_ET_WEBHOOK_SECRET.Open Webhooks
  2. 2

    Check the raw body

    Sign timestamp + "." + raw body with HMAC-SHA256. Reply 204. Skip an event id you already handled.
  3. 3

    Send a test

    A 2xx test makes the endpoint active. Finished checks arrive as verification.completed.
import crypto from "node:crypto";
import express from "express";

const app = express();
const TOLERANCE_MS = 5 * 60 * 1000;

app.post("/webhooks/verify", express.raw({ type: "application/json" }), (req, res) => {
  const timestamp = req.header("X-Webhook-Timestamp") ?? "";
  const signature = req.header("X-Webhook-Signature") ?? "";
  const eventId = req.header("X-Webhook-Event-Id");
  const rawBody = req.body.toString("utf8");
  const secret = process.env.VERIFY_ET_WEBHOOK_SECRET ?? "";
  const sentAt = Date.parse(timestamp);

  if (!secret) return res.status(401).send("Missing webhook secret");
  if (!Number.isFinite(sentAt) || Math.abs(Date.now() - sentAt) > TOLERANCE_MS) {
    return res.status(401).send("Stale webhook timestamp");
  }

  const expected = crypto.createHmac("sha256", secret).update(`${timestamp}.${rawBody}`).digest("hex");
  const matches = signature.split(",").some((part) => {
    const actual = Buffer.from(part.trim().replace(/^sha256=/, ""));
    const expectedBuf = Buffer.from(expected);
    return actual.length === expectedBuf.length && crypto.timingSafeEqual(actual, expectedBuf);
  });
  if (!matches) return res.status(401).send("Invalid signature");
  if (req.header("X-Webhook-Test") === "true") return res.sendStatus(204);

  const event = JSON.parse(rawBody);
  console.log(eventId ?? event.id, event.requestId, event.data.status);
  return res.sendStatus(204);
});

Submit a Verification

These helpers omit webhookUrl. An active dashboard endpoint receives the result. Treat 200 as a finished check and 202 as queued work. Store requestId either way. You can add waitMs for a short inline wait, and you still need to handle 202.

type VerifyResponse = {
  success: boolean;
  message: string;
  requestId?: string;
  verification?: {
    processingStatus: "queued" | "running" | "completed" | "failed";
    status?: "success" | "failed" | "not_found" | "pending";
    verified?: boolean;
  };
  links?: {
    statusUrl: string;
    pollAfterMs?: number;
    webhookRegistered?: boolean;
  };
};

export async function verifyCbePayment() {
  const response = await fetch("https://verify.et/api/verify", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "x-api-key": process.env.VERIFY_ET_API_KEY!,
      "Idempotency-Key": crypto.randomUUID(),
    },
    body: JSON.stringify({
      bank: "cbe",
      reference: "fHCxyExampleRcpt1",
    }),
  });

  const body = (await response.json()) as VerifyResponse;

  if (!response.ok && response.status !== 202) {
    throw new Error(body.message);
  }

  return body;
}

Send a Receipt Image

Optional. The file part is image, and Idempotency-Key is required.

  1. 1

    Turn it on and add credits

    The billing owner enables Image Processing, then tops up image credits.Enable Image ProcessingTop up image credits
  2. 2

    Post the file

    JPEG, PNG, or WebP, up to 8 MB. Add bank or reference when the photo is missing the Receipt ID.
  3. 3

    Read the status

    200 is finished. 202 is queued. Keep requestId. 402 INSUFFICIENT_IMAGE_CREDITS means the image balance is empty. The same key and file spends no second image credit.
import { readFile } from "node:fs/promises";

export async function verifyReceiptImage(idempotencyKey: string) {
  const bytes = await readFile("receipt.jpg");
  const form = new FormData();
  form.append("image", new File([bytes], "receipt.jpg", { type: "image/jpeg" }));
  form.append("bank", "cbe");
  form.append("reference", "fHCxyExampleRcpt1");

  const response = await fetch("https://verify.et/api/verify", {
    method: "POST",
    headers: {
      "x-api-key": process.env.VERIFY_ET_API_KEY!,
      "Idempotency-Key": idempotencyKey,
    },
    body: form,
  });
  const body = await response.json();

  if (!response.ok && response.status !== 202) {
    throw new Error(body.message);
  }

  return {
    requestId: body.requestId as string,
    imageProcessing: body.imageProcessing as {
      attemptId: string;
      method: "qr" | "ocr";
      chargedCredits: 1;
    },
  };
}

Client timeout

OCR can take up to 30 seconds before the API responds. Set the HTTP client timeout to at least 45 seconds.

Poll While a Screen Is Open

Use this when a page should show progress after 202. Stop when processingStatus is completed or failed. Poll for up to 125 seconds. Keep the webhook so the result still arrives if the customer leaves. Do not submit the same receipt again while that request is in flight.

export async function pollVerification(requestId: string) {
  const statusUrl = "https://verify.et/api/verify/" + requestId;

  const deadline = Date.now() + 125_000;
  while (Date.now() < deadline) {
    const response = await fetch(statusUrl, {
      headers: { "x-api-key": process.env.VERIFY_ET_API_KEY! },
    });
    const body = await response.json();
    const status = body.data?.processingStatus;

    if (status === "completed" || status === "failed") {
      return body.data;
    }

    await new Promise((resolve) =>
      setTimeout(resolve, body.links?.pollAfterMs ?? 1500),
    );
  }

  throw new Error("Verification did not finish within 125 seconds.");
}

SDK Roadmap

JavaScript / TypeScript

Typed REST client, webhook verifier, polling helper.

Python

Requests/httpx client, retries, typed response models.

PHP

Composer package with webhook signature helper.

Dart

Flutter-friendly client for mobile merchant apps.

Next up

Full API Reference