---
title: "Verify Checkout"
canonical_url: "https://scholarxiv.com/developers/docs/verify-checkout"
markdown_url: "https://scholarxiv.com/developers/docs/verify-checkout.md"
---

> For the complete documentation index, see [llms.txt](/llms.txt).

# Verify Checkout
URL: /developers/docs/verify-checkout
LLM index: /llms.txt

# Verify Checkout

Verify Checkout is a separate ETB payment option. The existing bank-transfer page
at `/verify_payment`, its verification API, and the USD/Polar integration remain
unchanged. On each pricing page, select **ETB**, then a plan. Verify Checkout is
the default ETB method; select **Bank transfer** to use the existing checkout.

The new review page is `/verify_checkout?plan=plus&interval=monthly`. It sends the
customer to Verify's hosted checkout, where they select a receiving account and
submit their transfer reference. `/verify_checkout/return?order=…` shows the stored
order and reconciles its current payment state. No bank-reference input is added
to the new UI.

## Server configuration

Set these independently of `VERIFY_ET_API_KEY`:

```dotenv
VERIFY_CHECKOUT_API_KEY=vchk_...
VERIFY_CHECKOUT_WEBHOOK_SECRET=whsec_...
VERIFY_CHECKOUT_RETURN_ORIGIN=https://scholarxiv.com
VERIFY_CHECKOUT_RECONCILE_SECRET=...
```

Use the actual canonical deployment origin, including `www` if applicable. Never
include a path. `http://localhost:5173` is accepted for local development. This
origin is server configuration, never taken from a client or forwarded host header.

In the [Verify Checkout dashboard](https://checkout.verify.et/dashboard):

1. Complete the business name/logo and add an active merchant-owned receiving account.
2. Create a key with `deposits:create` and `deposits:read` scopes.
3. Register and activate the exact return origin (scheme, host and port).
4. Add `https://YOUR_ORIGIN/api/verify_checkout/webhook` as the webhook endpoint.
   Subscribe to the default outcome events and `webhook.test`; all `deposit.*`
   events can also be handled. Set its signing secret on the server.
5. Send a dashboard test delivery. The receiver durably records it and returns
   `200`, without activating a subscription. The successful test activates the endpoint.

The database must support **MongoDB transactions** (Atlas or a replica set).
Fulfillment intentionally fails and remains retryable on a standalone MongoDB
server, rather than granting a subscription without an atomic payment receipt.
No new package or dependency upgrade is required for the application.

## Reconciliation and recovery

- Prices and active plans come from `plan_pricing` and `plans`, with currency
  `ETB` and the selected monthly/yearly interval. The browser never supplies a price.
- An order snapshots its price, user, interval and return URL before contacting
  Verify. A per-customer attempt ID produces a stable idempotency key; retries
  replay the stored request even after a timeout or price change. The browser
  keeps its attempt in session storage across reloads when storage is available.
- The API contract is pinned to `2026-06-01`. Deposit IDs are persisted before
  returning a checkout URL. Only HTTPS links on `checkout.verify.et/c/` are used.
- Status and return routes require the signed-in owner. Anonymous accounts cannot
  create or inspect checkout orders. Return query parameters never authorize access.
- Webhooks verify HMAC-SHA256 over the exact raw body with a 300-second timestamp
  tolerance, validate event headers, and deduplicate by event ID. They fetch the
  current deposit from Verify before fulfillment. Unlinked deposits return `503`
  so a delivery racing deposit creation will be retried.
- Only authoritative `succeeded` deposits with matching ID, customer, ETB amount
  and currency activate a subscription. `review_required` and unknown states
  stay pending; failed, expired and cancelled deposits never activate a plan.
- The deposit receipt, subscription, subscription event and fulfilled order marker
  commit in one transaction. A provider-specific per-user lock serializes parallel
  purchases. A repeated webhook or concurrent browser check cannot extend the
  same payment twice. API-key entitlement synchronization can retry after commit.
- A still-active matching plan extends from its current period end. A different
  or expired plan starts now. Payments grant one month/year and do not auto-renew.
  The shared lifecycle used by Polar is not changed.

For unattended recovery, configure an external scheduler to POST to
`/api/verify_checkout/reconcile` every few minutes with
`Authorization: Bearer <VERIFY_CHECKOUT_RECONCILE_SECRET>`. The route is disabled
unless that secret is set. Each request checks at most 20 unresolved orders,
oldest check first, including interrupted creations and review cases. It also
retries incomplete entitlement synchronization. No scheduler is enabled by this
change. Failed checks return `503`; provider throttling stops the batch.

The return page polls with increasing intervals, honors `Retry-After`, and pauses
after 24 checks. Review and terminal states stop automatic polling. Customers can
manually check again, resume an unexpired checkout, or create a fresh attempt
after a failed/expired/cancelled checkout. The support reference is displayed.

Collections: `verify_checkout_orders`, `verify_checkout_receipts`,
`verify_checkout_customers`, and `verify_checkout_webhooks`. Order indexes are
awaited before use. Receipts and webhook events use the provider ID as MongoDB
`_id`, providing an intrinsic unique key. Existing subscription collections are
updated in their established shape.

## Validation

```sh
pnpm exec vitest run src/lib/server/verify_checkout/client.test.ts
```

For the transactional suite, point `VERIFY_CHECKOUT_TEST_MONGO_URI` at a disposable
replica set and run:

```sh
pnpm exec vitest run src/lib/server/verify_checkout
```

The suite creates and removes its own randomly named `verify_checkout_test_*`
database. It mocks Verify's network responses, never calls the live payment API,
and covers concurrent webhook/poll fulfillment, rollback after a failed event
write, renewals, fixed yearly pricing, replay after lost responses, ownership,
signature/header checks, and non-success outcomes. Without the test URI the
transaction suite is skipped. A live merchant smoke test still needs configured
credentials, return-origin registration and webhook activation.

## Provider references

- [Quick Start](https://checkout.verify.et/docs/quick-start)
- [Create a Deposit](https://checkout.verify.et/docs/integration)
- [Hosted Checkout](https://checkout.verify.et/docs/hosted-checkout)
- [Webhooks](https://checkout.verify.et/docs/webhooks)
- [Reconciliation](https://checkout.verify.et/docs/reconciliation)
- [OpenAPI schema](https://checkoutapi.verify.et/openapi/public-api.json)

The docs site's generated API reference returned an unavailable-schema placeholder
during implementation; the direct OpenAPI URL above supplied the complete contract.

## Sitemap

See the full [sitemap](/sitemap.md) for all pages.
Docs-scoped sitemap: [/docs/sitemap.md](/docs/sitemap.md).
Well-known sitemap: [/.well-known/sitemap.md](/.well-known/sitemap.md).
