# Verify Checkout Developer Docs > Public API documentation for Verify Checkout merchant integrations. ## Authentication URL: https://checkout.verify.et/docs/authentication Create, scope, store, rotate, and revoke Verify Checkout API keys safely. # Authenticate server requests Create API keys in [Developer settings](/dashboard/developers). A secret begins with `vchk_` and is displayed once. Store it immediately in your server's secret manager; only a non-secret fingerprint remains visible in the dashboard. ```bash Authorization: Bearer vchk_replace_with_your_secret VerifyCheckout-Version: 2026-06-01 ``` Do not place a key in browser JavaScript, a mobile bundle, a public environment variable, source control, analytics, logs, or a hosted checkout URL. ## Key scopes Grant only the actions a service performs. A deposit integration typically needs permission to create deposits and to read deposits or events for reconciliation. A key without the required scope receives `403` even when the secret is valid. ## Version and idempotency headers Send `VerifyCheckout-Version: 2026-06-01` so your integration keeps an explicit contract. The header is currently optional, but pinning it is recommended. Every `POST /v1/deposits` request requires `Idempotency-Key`. Reuse the same value only when retrying the same logical deposit; use a new value for a new deposit. ## Rotation and revocation 1. Create a replacement key with the same minimum scopes. 2. Deploy the replacement secret to every server that needs it. 3. Confirm requests succeed with the new key. 4. Revoke the old key in the dashboard. Revocation is immediate. Keep key ownership and last-used information in your operational review so unused credentials do not linger. ## Errors and request correlation Authentication failures use the same stable envelope as other API errors: ```json { "data": null, "error": { "code": "unauthorized", "message": "Authentication is required" }, "meta": { "requestId": "req_example" } } ``` Record `meta.requestId` with the failed operation. It lets support correlate your report without sharing secrets or authorization headers. --- ## Dashboard Setup URL: https://checkout.verify.et/docs/dashboard-setup Configure branding, receiving accounts, API keys, return domains, and readiness. # Prepare your workspace The dashboard setup checklist is complete when your workspace has a business name and checkout logo, at least one active receiving account, and an API key. ## Business Details Business name and logo are required for setup readiness. Upload PNG, JPEG, or WebP up to 7 MB. Phone, website, and support URL are recommended customer-trust fields, but they are not readiness gates. ## Receiving accounts New accounts are active as soon as the merchant creates them. The supported providers are: | Provider | Dashboard value | | --- | --- | | Telebirr | `telebirr` | | M-PESA | `mpesa` | | Commercial Bank of Ethiopia | `cbe` | | Bank of Abyssinia | `boa` | | CBE Birr | `cbebirr` | | Dashen Bank | `dashen` | | Awash Bank | `awash` | | Siinqee Bank | `siinqee` | | Kaafi eBirr | `kaafiebirr` | Use the dashboard's displayed workspace and provider limits as the current limit for your account. Disable or replace an account you no longer control; deposits can only route to active accounts. ## API keys and scopes Name keys by service or environment boundary, grant only required scopes, and copy each `vchk_…` secret when it is created. Use separate keys for separate deployments so one can be rotated without interrupting the others. ## Return URL domains Register the origin, not a path: `https://shop.example.com` authorizes `https://shop.example.com/payments/return`. Scheme, hostname, and port must match. Subdomains are separate origins. HTTP is restricted to loopback development. A saved but inactive return origin does not authorize deposit creation. Activate it before sending a matching `return_url`. --- ## Hosted Checkout URL: https://checkout.verify.et/docs/hosted-checkout Know what customers see, submit, and receive during payment verification. # A checkout built for verified transfers The hosted page displays your business name and logo, the available merchant-owned receiving accounts, payment instructions, and the form for a transaction reference. ## Customer journey 1. Choose an available provider when more than one payment option is returned. 2. Enter an amount for a variable deposit, or review the fixed amount. 3. Send money directly to the displayed merchant receiving account. 4. Submit the payment reference for verification. 5. Wait for verification, then continue to the merchant's registered return URL. Supported providers are Telebirr, M-PESA, Commercial Bank of Ethiopia, Bank of Abyssinia, CBE Birr, Dashen Bank, Awash Bank, Siinqee Bank, and Kaafi eBirr. Only the active accounts selected for a deposit appear on that checkout. ## Amounts and expiry A fixed deposit locks the requested amount. A variable deposit asks the customer for an amount within the checkout's enforced limits. Checkout links expire 60 minutes after creation; create a new deposit rather than reusing an expired link. ## What you should not build Do not reproduce receiving-account routing, collect references in your own form, expose API keys, infer success from the browser redirect, or attempt to verify a provider response yourself. Redirect to the returned URL and trust the deposit API state. Treat the checkout token as customer-facing but sensitive: do not publish it or reuse it for another order. Your backend should store the deposit ID; it never needs to parse the token from `checkout_url`. --- ## How It Works URL: https://checkout.verify.et/docs/how-it-works Understand the non-custodial money path, verification lifecycle, and ownership boundaries. # Verification, not custody Verify Checkout connects a merchant-owned receiving account, a branded checkout, and Verify.et transaction verification. It does not collect, hold, transfer, or settle customer money. ## Who owns what | Responsibility | Owner | | --- | --- | | Receiving account and funds | Merchant | | Hosted payment instructions and reference collection | Verify Checkout | | Transaction-reference verification | Verify.et through Verify Checkout | | Customer wallet, order, or entitlement | Merchant | | Refunds, reversals, and settlement operations | Merchant and payment provider | ## Receiving-account routing At deposit creation, Verify Checkout selects from the workspace's active receiving accounts. If you pass `payment_method`, checkout is limited to that provider. Otherwise the response can contain several `payment_options` and the customer chooses on the hosted page. ## Checkout lifecycle A checkout link expires 60 minutes after creation. A verification credit is consumed when a valid reference starts a verification attempt—not when the link is created, refreshed, polled, or delivered by webhook. Treat the API deposit status as authoritative. The customer-facing return page is a navigation convenience and must not directly credit a wallet or fulfill an order. --- ## Create a Deposit URL: https://checkout.verify.et/docs/integration Create variable or fixed deposits, redirect to hosted checkout, and retrieve outcomes. # Create a hosted checkout deposit Before calling the API, your workspace needs an active receiving account, an API key with the required scope, and an active return origin matching `return_url`. ## Request `POST https://checkoutapi.verify.et/v1/deposits` Omit `amount`. The customer enters an allowed ETB amount on checkout. ```json { "merchant_customer_id": "customer_42", "currency": "ETB", "return_url": "https://shop.example.com/payments/return" } ``` Send `amount` and optionally `payment_method` to limit checkout to one provider. ```json { "merchant_customer_id": "customer_42", "amount": "250.00", "currency": "ETB", "payment_method": "telebirr", "return_url": "https://shop.example.com/payments/return" } ``` Amounts may be sent as JSON strings or numbers and are returned as normalized strings. ETB is the current currency. Provider and amount limits are enforced by the response options and hosted checkout. ## Response A successful creation returns `201` with the deposit, a public checkout URL, its expiry, and the accounts available for this deposit. ```json { "data": { "id": "dep_example", "merchant_order_id": "order_1042", "merchant_customer_id": "customer_42", "status": "awaiting_transfer", "verification_status": "not_started", "notification_status": "not_ready", "amount": "250.00", "currency": "ETB", "checkout_url": "https://checkout.verify.et/c/example_token", "support_reference": "VC-1042", "payment_options": [ { "payment_method": "telebirr", "institution_code": "telebirr", "institution_name": "Telebirr", "masked_identifier": "09••••1234", "min_amount": "1.00", "max_amount": "30000.00" } ], "metadata": {}, "created_at": "2026-08-21T10:00:00.000Z", "expires_at": "2026-08-21T11:00:00.000Z" }, "meta": { "requestId": "req_example", "apiVersion": "2026-06-01" } } ``` Persist `data.id`, then redirect the customer's browser to `data.checkout_url`. The link expires 60 minutes after creation. ## Recommended production flow Use signed webhooks for prompt change notifications and the deposits API for the authoritative current state: ```text Create checkout ↓ Save deposit ID ↓ Customer completes payment ↓ Signed webhook informs merchant backend ↓ Merchant fetches deposit by ID when confirmation is needed ↓ Merchant fulfills only when current status is succeeded ``` The webhook tells your backend that something changed; it is not a replacement for storing the deposit ID or retrieving the current deposit. Webhooks are delivered at least once and may be delayed, retried, duplicated, or arrive out of order. Verify the webhook signature over the raw body, deduplicate by event ID, then use `GET /v1/deposits/{depositId}` whenever you need to confirm the latest state. Only `succeeded` authorizes fulfillment. Treat `review_required` as pending an operational decision, and never fulfill `failed`, `expired`, or `cancelled`. Apply fulfillment exactly once using the Verify Checkout deposit ID as the idempotency key in your own system. ## Retrieve and reconcile - `GET /v1/deposits/{depositId}` — retrieve the current authoritative state. - `GET /v1/deposits` — list deposits using the documented filters and pagination. - `GET /v1/deposits/{depositId}/events` — inspect the deposit event timeline. Use [signed webhooks](/docs/webhooks) for prompt automation and polling as a recovery path. The public API does not require you to build a manual checkout or accept payment references in your own frontend. ## Error envelope ```json { "data": null, "error": { "code": "return_url_not_allowed", "message": "The return URL is not allowed" }, "meta": { "requestId": "req_example" } } ``` Branch on the stable `error.code`, show your own customer-safe message, and log `meta.requestId`. See the generated [API reference](/docs/api) for every field, filter, status, and error. --- ## Verify Checkout Developer Docs URL: https://checkout.verify.et/docs Accept and verify Ethiopian account-to-account payments without holding customer funds. # From sign-in to a verified payment Verify Checkout gives your customers a branded hosted checkout, verifies their payment reference through Verify.et, and tells your backend when it is safe to credit your own wallet or order. ## The merchant journey Customers pay a receiving account that your business owns. Verify Checkout coordinates checkout and verification; it does not custody or settle funds. ## Choose your path API keys belong on your server. Never expose them in browser code, mobile apps, logs, or checkout URLs. --- ## Quick Start URL: https://checkout.verify.et/docs/quick-start Go from Telegram sign-in to your first hosted checkout and verified deposit. # Make your first deposit This guide follows the same order as the dashboard. You will need a receiving account that your business controls and a backend where secrets can be stored. ## Sign in with Telegram Continue with Telegram. A new workspace receives monthly Starter verification credits; creating a checkout does not consume a credit. ## Complete Business Details Add your business name and upload the logo shown on checkout. PNG, JPEG, and WebP images up to 7 MB are accepted. Phone, website, and support URL are useful to customers, but do not block setup. ## Add a receiving account Choose a supported provider and enter an account owned by your business. New receiving accounts are active as soon as they are created. ## Create an API key Create a key with the deposit permissions you need. Copy the `vchk_…` secret immediately—the dashboard cannot reveal it again. ```bash VERIFY_CHECKOUT_API_KEY=vchk_replace_with_your_secret ``` ## Register the return domain Add and activate the exact origin that will receive the customer after checkout, such as `https://shop.example.com`. The scheme, host, and port of every `return_url` must match. HTTP is accepted only for loopback development. ## Create a deposit Use a fresh idempotency key for this logical deposit. Omit `amount` to let the customer enter it, or send an ETB amount for a fixed checkout. ```bash title="cURL" curl --request POST 'https://checkoutapi.verify.et/v1/deposits' \ --header "Authorization: Bearer $VERIFY_CHECKOUT_API_KEY" \ --header 'Content-Type: application/json' \ --header 'VerifyCheckout-Version: 2026-06-01' \ --header 'Idempotency-Key: order_1042_deposit' \ --data '{ "merchant_customer_id": "customer_42", "amount": "250.00", "currency": "ETB", "return_url": "https://shop.example.com/payments/return" }' ``` ```ts title="TypeScript" const response = await fetch("https://checkoutapi.verify.et/v1/deposits", { method: "POST", headers: { Authorization: `Bearer ${process.env.VERIFY_CHECKOUT_API_KEY}`, "Content-Type": "application/json", "VerifyCheckout-Version": "2026-06-01", "Idempotency-Key": "order_1042_deposit", }, body: JSON.stringify({ merchant_customer_id: "customer_42", amount: "250.00", currency: "ETB", return_url: "https://shop.example.com/payments/return", }), }); if (!response.ok) throw new Error(`Deposit creation failed: ${response.status}`); const result = await response.json(); ``` ## Send the customer to checkout Read `data.checkout_url` from the `201` response and redirect the customer's browser to it. Store `data.id` in your system before redirecting. ```ts window.location.assign(result.data.checkout_url); ``` ## Reconcile the result Poll `GET /v1/deposits/{depositId}` until the deposit reaches a terminal state. For production automation, add a signed webhook and keep polling as a recovery path. A webhook tells your backend that the deposit changed; the deposits API returns its authoritative current state. Credit your customer exactly once, keyed by the Verify Checkout deposit ID, and only when that state is `succeeded`. Browser and mobile code must call your backend, not the Verify Checkout API directly. Never paste a real API key into documentation, source control, or chat. Next, read [Create a deposit](/docs/integration) for every request shape and [Reconciliation](/docs/reconciliation) before crediting customer balances. --- ## Reconciliation URL: https://checkout.verify.et/docs/reconciliation Combine webhooks and polling to credit deposits exactly once and recover safely. # Turn outcomes into one ledger entry Use webhooks for low-latency notification and the deposit API as the current source of truth. A scheduled poller closes gaps caused by downtime or exhausted retries. | Path | Best use | Required protection | | --- | --- | --- | | Webhook | Prompt processing | Signature verification and event deduplication | | `GET /v1/deposits/{id}` | Current state lookup | API authentication and rate-aware retry | | `GET /v1/deposits` | Periodic sweep | Cursor/checkpoint and bounded time windows | | `GET /v1/deposits/{id}/events` | Investigation | Treat as history, not a second credit trigger | ## Credit exactly once Use the Verify Checkout deposit ID as a unique ledger key inside the same database transaction that credits your customer. ```ts await database.transaction(async (transaction) => { const inserted = await transaction.creditEvents.insertIfAbsent({ externalId: deposit.id, amount: deposit.amount, currency: deposit.currency, }); if (inserted) await transaction.wallets.credit(deposit.merchant_customer_id, deposit.amount); }); ``` `succeeded` is the crediting outcome. Failed and expired deposits must not be credited. Hold review outcomes for operational resolution. For any unfamiliar or non-terminal state, store it and wait rather than assuming success. ## Recovery loop 1. Verify and durably enqueue each webhook before returning `2xx`. 2. Deduplicate by event ID and apply ledger changes by unique deposit ID. 3. Retrieve the deposit before acting when events arrive out of order. 4. Periodically list unresolved deposits and poll their current state. 5. Use the event timeline and `meta.requestId` to investigate discrepancies. At-least-once delivery means repeated events are expected, not exceptional. Correctness comes from idempotent storage, not from assuming one delivery. --- ## Troubleshooting URL: https://checkout.verify.et/docs/troubleshooting Diagnose setup, API, checkout, credit, and webhook failures quickly. # Fix common integration failures | Symptom | Likely cause | Fix | | --- | --- | --- | | `401` or invalid API key | Missing, malformed, revoked, or incorrect `vchk_…` secret | Replace the server secret from Developer settings; never send the fingerprint. | | `403` or insufficient scope | The key is valid but lacks the operation's scope | Create or rotate to a least-privilege key with the required deposit permission. | | Return URL rejected | Its origin is missing, inactive, or does not match scheme/host/port | Register and activate the exact origin; use HTTP only on loopback. | | No payment options | No active receiving account matches the request | Add or enable an account, or remove an overly restrictive `payment_method`. | | `402` or insufficient credits | The workspace has no verification credits | Add credits in the dashboard, then retry the same logical operation safely. | | Checkout says expired | More than 60 minutes elapsed | Create a new deposit and redirect to its new `checkout_url`. | | Webhook signature fails | Parsed body, wrong secret, stale timestamp, or incorrect `v1` format | Verify the raw bytes against `.` using the endpoint's `whsec_…` secret. | | Repeated deliveries | The receiver returned non-`2xx`, timed out, or received at-least-once duplicates | Acknowledge after durable storage and deduplicate by event ID. | | Event appears out of order | Retries and independent deliveries crossed | Retrieve the current deposit and apply only valid state transitions. | ## Before contacting support Capture the endpoint, HTTP method, status, UTC time, deposit ID when available, and `meta.requestId` from the response. For webhooks, include event ID and delivery ID. Redact API keys, signing secrets, authorization headers, customer data, and full payment references. Support never needs the full `vchk_…` API key or `whsec_…` signing secret. Rotate a credential immediately if it was pasted into logs, tickets, source control, or chat. For exact request and response schemas, use the generated [API reference](/docs/api). --- ## Webhooks URL: https://checkout.verify.et/docs/webhooks Receive, verify, retry, and rotate signed deposit event deliveries. # Receive signed events Webhooks are recommended automation, not a requirement. Polling remains a supported fallback and recovery path. Save the deposit ID when you create the checkout. When a signed webhook arrives, verify and deduplicate it, then retrieve `GET /v1/deposits/{depositId}` whenever you need the authoritative current state. Fulfill exactly once only when that state is `succeeded`; never fulfill from the browser return or delivery order alone. ## Create and activate an endpoint 1. Open [Webhook endpoints](/dashboard/developers/webhooks) and add an HTTPS URL. 2. Select events. Outcome events and `webhook.test` are selected by default; lifecycle events are opt-in. 3. Copy the one-time `whsec_…` signing secret. 4. Send a test delivery. The first successful `2xx` response activates the endpoint. ## Verify every delivery Read the raw request bytes before JSON parsing. The signature is HMAC-SHA256 over `.` and uses the current signature version `v1`. ```ts import { createHmac, timingSafeEqual } from "node:crypto"; function verifyWebhook(rawBody: Buffer, timestamp: string, signature: string, secret: string) { const ageSeconds = Math.abs(Date.now() / 1000 - Number(timestamp)); if (!Number.isFinite(ageSeconds) || ageSeconds > 300) return false; const expected = createHmac("sha256", secret) .update(`${timestamp}.`) .update(rawBody) .digest("hex"); const received = signature.startsWith("v1=") ? signature.slice(3) : ""; if (received.length !== expected.length) return false; return timingSafeEqual(Buffer.from(received, "hex"), Buffer.from(expected, "hex")); } ``` Required headers: | Header | Purpose | | --- | --- | | `VerifyCheckout-Event` | Event type | | `VerifyCheckout-Event-Id` | Stable event ID for deduplication | | `VerifyCheckout-Delivery-Id` | This delivery attempt | | `VerifyCheckout-Timestamp` | Unix timestamp included in the signature | | `VerifyCheckout-Signature` | `v1=` signature | | `VerifyCheckout-Api-Version` | Webhook payload contract version | | `VerifyCheckout-Test` | Present on test deliveries | The current webhook payload API version is `2026-06-01`. Reject stale timestamps before processing, compare signatures in constant time, and return `2xx` only after the event is durably accepted. ## Events and delivery semantics Outcome events cover completed, failed, expired, and review results. Optional lifecycle events describe earlier progress; `webhook.test` verifies connectivity. The dashboard event selector is the authoritative catalog for your account. Deliveries are at least once. They can be duplicated, retried after non-`2xx` responses, and arrive out of order. Deduplicate by event ID and make wallet crediting idempotent by deposit ID. Do not assume the next event is newer—retrieve the current deposit when ordering matters. The recommended end-to-end sequence is: create checkout → save deposit ID → send the customer to checkout → accept the signed webhook → retrieve the deposit when confirmation is needed → fulfill only when its current status is `succeeded`. See [Create a Deposit](/docs/integration) for the complete flow. Webhook payloads include `environment: "live"` as a compatibility field. The current platform has one active flow; do not branch business logic on this value. ## Rotate a signing secret Create or rotate the endpoint secret in the dashboard, deploy the new `whsec_…` value to your receiver, send a test delivery, and remove the old value after the transition window. Never use an API key as a webhook secret. ---