Documentation

Create it, host it, prove it.

The essentials on one page. The full contract reference is generated from the contract package and published with every release.

Before you start

People reach pay through three doors, and what you need depends on the door:

  • Merchants call the API with a secret key minted on their dashboard's Developers page. Keys exist in two modes, test and live; a test key only ever moves test money.
  • Operators reach the back office with the operator access granted to them, and run the host itself with the commands below.
  • Payers need nothing at all: no account with pay, and no install.

Every POST that creates something should carry an Idempotency-Key header with a value you make up once per intent. Re-sending the same intent with the same key cannot create the thing twice.

Quick start: one checkout

Create a checkout with the amount in minor units, a currency, and a reference that is yours to define. The same reference and amount while the checkout is open returns the checkout unchanged; the same reference with anything else is refused:

curl https://pay.ifinsta.online/v1/checkouts \
  -u sk_test_yourkey: \
  -H "Idempotency-Key: order-1042-attempt-1" \
  -H "Content-Type: application/json" \
  -d '{
    "amount_minor": 480000,
    "currency": "KES",
    "reference": "ORDER-1042",
    "description": "Two canvas bags",
    "return_url": "https://merchant.example/order/1042/thanks",
    "cancel_url":  "https://merchant.example/order/1042"
  }'

The answer carries the checkout as the contract defines it, including the two things you act on straight away:

{
  "id": "ch_...",
  "object": "checkout",
  "status": "open",
  "amount_minor": 480000,
  "currency": "KES",
  "reference": "ORDER-1042",
  "url": "https://pay.ifinsta.online/c/a-…",
  "client_secret": "ch…",
  "expires_at": "2026-…"
}

Send the payer to url. The hosted page asks for nothing more: it shows who is being paid, the amount the server fixed, and the methods the merchant's connections offer. It refreshes itself, and every outcome has a screen that says what happened and what to do next.

Or listen instead of polling. Subscribe a webhook endpoint (below) and react to the event the checkout fires when it succeeds. The status answer is the same object the API returns, so nothing needs translating.

Webhooks

Register an endpoint once, over the API, for the events you want:

curl https://pay.ifinsta.online/v1/webhook-endpoints \
  -u sk_test_yourkey: \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://merchant.example/hooks/pay", "events": ["checkout.succeeded"] }'

pay signs every delivery. Verify it, answer 2xx within ten seconds, and the delivery is done:

  • The header Pay-Signature reads t=…,v1=…: a unix time and one or more HMAC-SHA256 hex digests, keyed by the endpoint's secret, of {t}.{raw body}.
  • Verify against the raw request body, not a parsed copy, and compare in constant time. During a secret rotation two v1 values arrive; accept the delivery if either matches.
  • Refuse a t more than 300 seconds from your clock, either way. That is the replay bound.
  • Deduplicate on the event id. Delivery is at least once, so the same event can arrive more than once, and each object carries its own subject.sequence to order what you apply.

A link is a fixed-amount checkout you can send in chat or on an invoice. Create it with the amount and a description; its url is permanent until you disable it:

curl https://pay.ifinsta.online/v1/links \
  -u sk_test_yourkey: \
  -H "Content-Type: application/json" \
  -d '{ "amount_minor": 150000, "currency": "KES", "description": "February dues" }'

Every payment a link produced is listed against it on the dashboard and exported as one CSV. A link can be capped so it refuses to collect past what it says, and it can be switched off the moment it should stop.

Refunds

Refund against the checkout that was paid, with an amount in the checkout's currency:

curl https://pay.ifinsta.online/v1/refunds \
  -u sk_test_yourkey: \
  -H "Content-Type: application/json" \
  -d '{ "checkout": "ch_...", "amount_minor": 300000 }'

A refund is bounded by what the provider confirmed was collected on the payment, and a provider refund that a provider refuses stays visible as refused. A refund handled outside the engine, such as cash handed back, is recorded the same way with the name of whoever handled it, so the one record still tells the whole story.

Errors and idempotency

Every refusal from the API is the contract envelope: HTTP status, code, a human message, and the param at fault when there is one. The error catalogue and its statuses are generated from the contract, so your error handling can be written against the list once:

HTTP/1.1 409 Conflict
{ "error": { "code": "reference_taken", "message": "…", "param": "reference" } }
  • Sending the same Idempotency-Key twice returns the first result unchanged, even on a retry after a network failure, instead of creating a second thing.
  • 429 Too Many Requests carries Retry-After; wait, then resume. The rate your key may sustain is generous, and it is the same for every key.

Running the engine

One command per role, from a single configuration file:

pay-server migrate        # apply migrations, then bootstrap partners
pay-server serve          # run the configured roles: web, worker, scheduler
pay-server check-plugins  # load and lint every connector document, then exit
pay-server egress-allowlist  # print the origins the host may call
  • The configuration file names the database, the plugin directories, and every secret by the environment variable it comes from; development values are refused outside development.
  • The egress allowlist is rendered into the host's proxy from the connector documents, and a provisioning script proves direct egress is refused.
  • Backups, restore, rollback and certificate checks are lifecycle scripts next to the deployment files; the health gate refuses a deploy whose process never comes up.

Reference

  • The contract: vocabularies, state machines, the error catalogue and event envelopes are generated from the contract package and published with every release; the signed-webhook rules are part of it.
  • The repository: the engine, the connector documents it ships with, and the provisioning that runs it. The source links live in this page's footer.
  • The changelog: what changed, release by release, as written in the repository.