# HarePay > HarePay is a one-stop payment gateway for Nigeria and Africa (by LodumAI): one REST API to collect via cards, bank transfer, USSD and stablecoins, convert at a locked FX rate, and settle to merchants in NGN or USD (stablecoins consolidate 1:1 into USD) over a correct double-entry ledger. Base URL: https://api.harepay.click Authentication: send your secret API key as a Bearer token — `Authorization: Bearer sk_live_...` (test keys are `sk_test_...`). Mode: the key decides the mode — `sk_test_` is test, `sk_live_` is live. There is ONE base URL for both; you do not change hosts. Every response includes `"livemode": true|false`. Money: amounts are JSON objects `{ "amount": , "currency": "NGN" }` where `amount` is in the currency's SMALLEST unit — kobo/cents for fiat, base units for crypto (e.g. ₦10,000.00 = `1000000`; USDC has 6 decimals). Crypto adds `"network"`. Never floats or decimal strings. Response amounts also carry a preformatted `"display"` string for UIs. The OpenAPI `AmountInput` schema is authoritative. Idempotency: on money-moving POSTs, send an `Idempotency-Key` header so retries are safe. Errors: non-2xx responses return `{ "error": { "type": "...", "message": "..." } }`. Gating: two error types deserve handling — 403 `capability_disabled` (an admin restricted that endpoint group for your account in this mode; request re-activation from the dashboard) and 503 `method_paused` (the platform paused that payment method for maintenance/compliance; retry later or offer another method — in-flight payments still settle). ## Listing & pagination - List endpoints return `{ "object": "list", "data": [...], "has_more": bool }`, plus `next_cursor` when there is a next page (and `total` on payments). Records are NEWEST FIRST, ordered by `created_at` descending with the record id as tiebreak — a total, stable order, so the same record never lands on two pages. - `limit` is 1–200, default 50. - TO SWEEP (reconciliation, nightly export, backfill) USE THE CURSOR, NOT `offset`: read a page, then re-request with `starting_after=`, and stop when `has_more` is false. A cursor resumes from a fixed record, so it is unaffected by anything written while you page. An `offset` is a moving window — every payment that arrives mid-sweep shifts it, and your job re-reports records it already processed (and, with an unstable order, can skip some). `offset` still works and is fine for a UI page-through; it is ignored when `starting_after` is set. - An unknown `starting_after` is a 400 `invalid_cursor`, NOT a silent restart from page one — a sweep that quietly looped would re-process everything forever. Keep the filters identical across the pages of one sweep; a cursor is only meaningful within the list that produced it. - Date windows: `created_after` (inclusive) and `created_before` (EXCLUSIVE), each `YYYY-MM-DD` (UTC midnight) or RFC3339. The exclusive upper bound means consecutive days tile exactly — `created_after=2026-03-01&created_before=2026-03-02` then `…=2026-03-02&…=2026-03-03` covers everything once, with no boundary record counted twice or dropped. - Available on GET /v1/payment_intents and GET /v1/payouts. Passing nothing behaves exactly as before cursors existed, so no existing integration needs to change. ## Test mode & testing against production - Use the SAME base URL (https://api.harepay.click) with a `sk_test_` key to run your full suite against the live deployment safely. Test-mode calls route to an in-memory sandbox rail: no real money moves and no real rail credentials are used. - Test and live data are isolated: a `sk_test_` key only ever sees test objects; a `sk_live_` key only sees live objects. Cross-mode reads return 404. - Live money movement (payments, payouts) and issuing `sk_live_` keys require the account to be verified (KYB/KYC). Test mode always works, unverified. - Naming the `sandbox` rail explicitly is only valid in test mode; live requests may not use it. - POST https://api.harepay.click/v1/test_data/reset with body `{ "confirm": "RESET" }` wipes YOUR test-mode footprint — payments, refunds, payouts, invoices/subscriptions, customers, checkout sessions, PayMe ledgers, disputes, webhook deliveries, notifications and test balances. Test-mode requests only; live data is never touched. ## Reference - [OpenAPI 3.0 spec](https://api.harepay.click/openapi.json): Full machine-readable API description — generate a typed client/SDK from this. - [Developer docs](https://www.harepay.click/docs): Quickstart, guides and multi-language SDK examples. - [System status](https://api.harepay.click/v1/status): Live component health as JSON. ## Collect (pay-ins) - POST https://api.harepay.click/v1/payment_intents: Create a payment — accept card / transfer / USSD / stablecoin, optionally settling into a different currency. `method` is bank_transfer|card|mobile_money|crypto (crypto uses `currency` USDC/USDT + `network`; mobile_money needs `payer_phone`, the wallet MSISDN that receives the approval prompt — live coverage is GHS). HarePay selects the provider automatically from the method — you don't pass a rail. Call GET https://api.harepay.click/v1/payment_options for the supported currencies/assets — its top-level `currencies` array is the platform's full fiat list (`{code, name, symbol, decimals}`), so drive currency pickers from it instead of hard-coding codes. Any `metadata` object is echoed back on the resource and on webhooks. `fee_bearer` (merchant|customer) overrides your account default for this charge: customer means the platform fee is added on top (the customer pays it, you receive the full amount); merchant (default) deducts it from your settlement. - Response carries `id` (pi_…), `status`, `livemode`, and `next_action` (type redirect|bank_transfer|mobile_money|crypto_deposit|none; account_no+bank_name for transfers, url for card, mobile_money = the payer approves on their phone). Treat the charge as pending until a webhook confirms it. - Crypto deposits go to a STANDING (shared) address, so the AMOUNT is what identifies the payment: the created intent's amount may include a tiny unique adjustment (at most 999 base units — e.g. under $0.001 of USDC) over what you requested, and the payer must send EXACTLY the intent's returned amount. A deposit for any other amount cannot be matched automatically. Always display the response's `amount`/`display`, never the amount you originally posted. - GET https://api.harepay.click/v1/payment_intents: List payments — newest first; cursor/date-window parameters under "Listing & pagination". - GET https://api.harepay.click/v1/payment_intents/{id}: Retrieve a payment and its status. A failed payment carries `failure_reason` — a clean, human-readable reason why (the provider's decline reason, or what went wrong at initiation; absent when the customer simply abandoned it) — also present on the `payment_intent.payment_failed` webhook's `data.object`. Once `status` is succeeded, `settled` carries the exact amount credited to your available balance in `settle_as` — for a same-asset charge this equals `net` (amount minus fee); for a cross-asset (FX) charge, such as a non-pegged crypto payment that auto-converted to fiat, this is the converted amount net of the FX spread, so you never have to re-derive it from a rate. - POST https://api.harepay.click/v1/payment_intents/{id}/refund: REQUEST a refund (full or partial). Returns 202 with a `refund` object in status `pending` — the amount leaves the merchant's available balance at once into a HarePay clearing account, and HarePay disburses it to the customer once their receiving details are confirmed (manual fulfilment per the refund policy), moving to `completed` or `rejected` (funds returned). Track via the intent's `refunds[]` and `refund.pending/completed/rejected` webhooks. Cumulative refunds are CAPPED at the settled net-of-fee amount (400 beyond it); the strict policy also rejects a refund beyond current available balance. ## Identity checks (KYC / KYB) - POST https://api.harepay.click/v1/verifications: Verify a customer's identity against official registries — `type` bvn|nin|cac|drivers_license. Answers INSTANTLY with `verified` (the registry verdict), `field_matches` (per-field name agreement) and `data` — the FULL registry record (bio-data, photo where available): handle it as personal data, and only look up people who have consented. - REQUIRED FIELDS: `type`, `id_number`, and — for the person types (bvn, nin, drivers_license) — `firstname` and `lastname`. The registry matches the ID against the name you submit, so the question it answers is "does this person own this ID?". `middlename`/`dob`/`phone`/`email`/`gender` are optional and narrow the match. CAC takes only the RC/BN/IT number (a business has no person to match). - CONSENT: send `consent` — either `true` or `{"obtained":true,"reference":""}` — your attestation that the subject agreed. It is ADVISORY (a check without it still runs) and is never sent to the registry: it is stored on the record and echoed back, so "who authorised looking this person up?" is answerable per check. - THE RECORD COMES BACK ON A PARTIAL MISMATCH, NOT A TOTAL ONE. A name that partly disagrees (a typo, a married name, an initial) still returns the registry record — `verified:false` with the detail in `field_matches` — because that is the branch a human reviews. But when NOTHING you submitted matches, the personal data is WITHHELD: the response carries `withheld:true` and `data_withheld` instead of `data`. The check still ran and is still billed (the registry charges per query); you simply do not get a stranger's bio-data for a name you guessed. Get the ID number and name right and retry. {"type":"nin","id_number":"70123456789","firstname":"Amina","lastname":"Bello"} -> {"verified":true,"field_matches":{"firstname":true,"lastname":true},"data":{...full record...}} - Each live check is charged from your available NGN balance at the fee in GET https://api.harepay.click/v1/verification_options — billed whether the identity matches or not (registries charge per query); a check that cannot complete is never charged. Live checks need a verified account. Send an `Idempotency-Key`. - Test mode is free and returns realistic fixtures: any id number verifies; an id ending in `00` completes as NOT verified (no record found); an id ending in `01` is a PARTIAL match (record found, firstname matches, lastname does not — `verified:false`, record returned, the human-review branch); an id ending in `02` is a TOTAL mismatch (record found, nothing matches — `verified:false` and the payload WITHHELD, the anti-fishing branch). Build your failure paths against both. - GET https://api.harepay.click/v1/verifications, GET https://api.harepay.click/v1/verifications/{id}: History / one check (the stored record keeps the full payload). The list carries a `summary` — full-history `count`, `verified`, `total_fees` and a per-type breakdown for the current mode — reconcile it against your balance: checks x fee = charged, exactly. ## Hosted checkout (payment links) - Use this when you'd rather not build the payer UI: create a session, share its `url`, and the payer picks a method and pays on a HarePay-hosted page. - POST https://api.harepay.click/v1/checkout_sessions: Create a checkout session with `amount` {amount,currency}, optional `methods` (subset of bank_transfer|card|mobile_money|crypto; default = the methods enabled in your account settings, PATCH /v1/me `checkout_methods`), `description`, `success_url` / `cancel_url` (absolute http(s) URLs the payer is redirected to after paying or on cancel — HarePay appends `?checkout_id=` so you can reconcile), `metadata`. Returns `id` and a hosted `url`. - GET https://api.harepay.click/v1/checkout_sessions: List your sessions — a log of every link and its outcome. `status` moves open (created) → processing (payer started) → paid | failed, or expired (unpaid past its 24h expiry = abandoned), or canceled (deliberately abandoned — distinct from a silent expiry). Each carries the attempted `method`, `created_at`/`updated_at`, and `expires_at`. - POST https://api.harepay.click/v1/checkout_sessions/{id}/cancel: Cancel one of your OWN sessions server-to-server (authenticated, like POST /v1/subscriptions/{id}/cancel) — the link can no longer be paid. Idempotent; refuses only an already-paid session (409); a session you don't own or one in the other mode is a 404. Prefer this over the public cancel for backend calls, so the action is attributable/audited to your account. (The public POST /public/checkout/{id}/cancel exists only for the payer's browser, which can't hold a secret key.) If money nonetheless lands after a cancel, the settlement webhook still captures it and flips the session to paid. - Public (no API key; the session id is the capability): GET https://api.harepay.click/public/checkout/{id} returns the payer view + live status; POST https://api.harepay.click/public/checkout/{id}/pay {method, email, phone?} starts the intent (`phone` required for mobile_money) and returns `next_action` plus the payment `id` and a `verify_pin` (so the payer can self-check it later at /public/payment_status) — `email` is required (for the receipt and to identify the payer). HarePay routes the provider by method server-side. A lapsed link returns 410. POST https://api.harepay.click/public/checkout/{id}/cancel marks the session `canceled` (a deliberate abandon by the payer, distinct from expiry) so the link can no longer be paid — idempotent, refused with 409 only once paid, and it returns the `cancel_url` for the hosted page to redirect to. The session records the payer's `payer_email` and `payer_ip` (visible to the merchant), and the payer is auto-enrolled as a customer (matched by email per mode, never duplicated) with the payment linked via customer_id — the merchant's customer pool builds from checkout traffic. - The session flips to `paid` when the underlying payment settles; the linked payment carries the `checkout_id` in metadata, so payment_intent.succeeded lets you reconcile it. - Sandbox experience: the public session view carries `livemode`. On a TEST session, POST https://api.harepay.click/public/checkout/{id}/simulate (after a pay attempt) completes the payment as if the customer paid — same settlement pipeline as a real webhook. Refused (403) on live sessions. - Subscription checkout: create the session with `price_id` (a RECURRING price) instead of `amount` — the payer pays the first period on the hosted page, and on settlement a subscription activates automatically (opening period prepaid; the payer becomes a customer keyed by email). The session echoes `subscription_id` once active. - Inline (popup) checkout: instead of redirecting, pop the same session as an overlay on your own page. Include `` and call `HarePay.checkout({ sessionUrl, onSuccess, onClose, autoClose? })` with the session `url` your SERVER created (the secret key never belongs in a web page). The checkout renders in an iframe on the HarePay origin (no payment details touch your DOM); `onSuccess({checkout_id, payment_id})` fires once on completion (overlay closes itself unless `autoClose:false`), `onClose({reason})` fires only when the payer dismisses without paying — mutually exclusive. Redirect methods (card/USSD) open the provider page in a new tab from the popup, which keeps polling and completes when the payment lands. Fulfillment truth stays with the webhook, not the browser callback. - Crypto on hosted checkout: `crypto` works on any session. The payer picks an asset+network from the session's `crypto_assets` (the live list HarePay's crypto provider actually supports — stablecoins AND majors like BTC/ETH where available) and POSTs the pay call with `{ method:"crypto", currency, network }`. The charge is the session's value in that asset at 1:1 par for USD-priced stablecoin sessions, or a live forward FX quote otherwise, plus the platform FX margin (so the payer bears the spread). Stablecoins (USDC/USDT) settle into the merchant's USD balance by default, or NGN if they've opted to auto-convert (`crypto_settle`). Non-pegged assets (BTC, ETH, ...) always auto-convert into the merchant's fiat settlement currency at collection time — HarePay never holds a volatile-asset balance on your behalf, regardless of `crypto_settle` or any `settle_as` you pass. The hosted page shows the payer a standing deposit address and an exact amount to send (which can carry a sub-cent unique adjustment — that amount is how the deposit is matched back to the session), so the payer must transfer precisely what the page displays. ## PayMe (invoice-less pay link + customer ledgers) - ACCESS IS APPLICATION-GATED (like Refer & earn): POST https://api.harepay.click/v1/payme/request {note describing the use case, min 20 chars} — an admin reviews and approves before the link and ledger endpoints work (403 `payme_access_required` until then; GET /v1/payme reports `approved` + `payme_status`). - PayMe gives every merchant ONE durable, shareable payment address — https://www.harepay.click/payme/{handle} — where a customer pays ANY amount with no prior invoice (settling a debt, paying a bill, ad-hoc collections). Each customer also gets a personal ledger link (https://www.harepay.click/payme/{handle}/c/{slug}) showing their full payment history with the merchant, optionally against an "owed" target with a running remaining balance. Payments execute through the hosted checkout, so every method/mode/receipt rule above applies unchanged. - GET https://api.harepay.click/v1/payme: Your PayMe address (auto-provisioned from your business name on first call) — `handle`, public `url`, `enabled`, `title`, `message`. PUT updates `title`/`message`/`enabled` anytime; `handle` may only change while you have NO ledgers (after that a rename would break links in circulation — 409 `handle_locked`). - GET https://api.harepay.click/v1/payme/ledgers: List your customer ledgers for the current mode (name, contact, `paid`, optional `owed`/`remaining`). POST creates one for a known debtor ({name, email and/or phone, owed?, note?}) and returns the personal `url` to share. - GET https://api.harepay.click/v1/payme/ledgers/{id}: One ledger with its full entry log — each settled payment as {amount, method, paid_at, status paid|partially_refunded|refunded}; `paid`/`remaining` are refund-aware. PATCH adjusts {name?, owed?, note?, archived?} (owed of 0 clears the target). - Public (no API key): GET https://api.harepay.click/public/payme/{handle} returns the page data (`accepting` false when paused); POST https://api.harepay.click/public/payme/{handle}/pay {name, email and/or phone, amount} finds-or-creates the customer's ledger (matched by exact email, then phone) and returns a hosted `checkout_url` — plus `ledger_url` ONLY when the ledger was newly created (`returning: true` otherwise; an existing customer's history link is never re-disclosed to someone merely claiming their contact). GET https://api.harepay.click/public/payme/l/{slug} is the customer's history view (first name only — contact details never appear); POST https://api.harepay.click/public/payme/l/{slug}/pay {amount} adds a payment without re-identifying. - Test mode: the PUBLIC generic link has NO test variant — once the merchant is verified it always takes real payments (a customer must never be able to "pay" in sandbox money). To test PayMe end-to-end, create a ledger with a `sk_test_` key and use THAT ledger's link: its checkout runs on the sandbox rail (simulate works) and its entries land on the test ledger only. Ledgers are mode-scoped everywhere (cross-mode reads 404), and the public ledger page carries `livemode` so test pages announce themselves. - Denominations: NGN, USD, GHS or KES (the page's `currencies` field lists them with per-currency minimums; e.g. ₦100–₦10,000,000 for NGN, $1–$10,000 for USD). Each LEDGER is kept in ONE currency — the same contact paying in a second currency gets a second ledger, so totals stay exact with no FX guesswork. The payment METHOD is independent of the denomination (a USD-denominated payment can be settled with stablecoins; an NGN one by transfer/card/USSD), and the merchant is remitted per their settlement preferences. Ledger entries are appended when the payment SETTLES (webhook `payment_intent.succeeded`; the intent's metadata carries `payme_ledger`). ## Pay out (disbursements) - GET https://api.harepay.click/v1/banks: List supported banks ({code, name}) for a bank picker. - POST https://api.harepay.click/v1/bank/resolve: Resolve {bank_code, account_no} -> {account_name} via live NIP name-enquiry. Confirm the name before paying out. - POST https://api.harepay.click/v1/payouts: Send money to a bank account or crypto wallet. Bank: rail:"bank", destination {kind:"bank", bank_code, account_no, account_name}. Crypto: rail:"crypto", amount {currency:"USDC"|"USDT", network:"TRON"|"ETHEREUM"|"SOLANA"} — stablecoins only; the platform never holds non-pegged crypto (BTC, ETH, ...) to pay out from, since those pay-ins always auto-convert to fiat on collection. destination {kind:"crypto", crypto_chain, crypto_addr}. HarePay picks the provider server-side either way. Status: scheduled->processing->paid, or failed/returned; large amounts are held pending_approval (maker-checker). Track via payout.paid/payout.failed webhooks or poll. Optional `reference` is your own free-form label, echoed back on the payout — it need NOT be unique, because HarePay sends its own unique reference to the bank; use `Idempotency-Key` to make the request itself safe to retry. - GET https://api.harepay.click/v1/payouts, GET https://api.harepay.click/v1/payouts/{id}: List / retrieve payouts. The list pages by cursor and date window like payments — see "Listing & pagination"; use it to reconcile a payroll run without missing or double-reporting a transfer. - Bank payouts settle ASYNCHRONOUSLY: the bank accepts the transfer before the money lands, so a payout sits in `processing` briefly. HarePay resolves it from the bank's own status callback and from a background sweep that re-asks the bank, then fires payout.paid/payout.failed and emails the merchant the outcome. A failed payout returns the amount AND the fee to the available balance; `failure_reason` says why. Don't treat `processing` as final and don't re-send a payout that is still processing. - FAILURE CODES: a failed payout carries `failure_code` (stable, machine-readable) and `failure_retryable` (bool) on the REST resource AND the payout.failed webhook, plus a merchant-safe `failure_reason` sentence. TERMINAL — do not re-send, fix the beneficiary's details first: `invalid_account`, `name_mismatch`, `account_closed`, `compliance_block`. RETRYABLE — the identical request may succeed later: `rail_timeout`, `bank_unavailable`, `insufficient_rail_liquidity`. `unspecified` means we could not characterise the rail's answer; it is reported NOT retryable on purpose, because a payout we cannot characterise might have reached the beneficiary despite reporting failure — a person decides those. Branch on `failure_retryable` rather than re-implementing the split. A failed payout always returns the amount AND the fee to your balance, so re-issuing never costs twice. - Test-mode payout behaviour: test payouts run the full real path (fee, balance check, maker-checker threshold, ledger debit) but execute on the sandbox rail, which pays instantly — any account number succeeds. To rehearse the FAILURE path, send a test payout to a magic declining account number — each one declines with a different cause so you can exercise BOTH branches of your retry logic: `0000000000` invalid_account, `0000000001` name_mismatch, `0000000002` account_closed, `0000000003` compliance_block (all terminal); `0000000004` rail_timeout, `0000000005` bank_unavailable, `0000000006` insufficient_rail_liquidity (all retryable). Crypto: `crypto_addr: "0xFAIL"`. In every case the rail declines after the debit, the amount AND fee bounce back to your test balance, and `payout.failed` fires with the matching `failure_code`, exactly like a real bank decline. - AUTO-SETTLEMENT: PUT https://api.harepay.click/v1/me/settlement `{"schedule":"manual"|"daily"|"weekly"}` sweeps your LIVE available balance to your DEFAULT saved payout destination in a FIXED settlement window (daily from 09:00 WAT; weekly on Monday mornings) — no manual payout needed. One payout per currency that has a default destination; balances under a small dust floor (₦1,000 / $10) are left to grow; the sweep uses the same fee, approval and webhook path as a manual payout (you'll see payout.created/paid/failed). GET /v1/me echoes `settlement_schedule` and `payout_trusted`. Trust is ADMIN-set: a trusted account's payouts — manual and scheduled alike — skip the maker-checker approval hold and execute instantly; everyone else's large payouts (and sweeps) park in pending_approval until an admin releases them, and a sweep never stacks a second pending payout while one is waiting. - Saved destinations (reusable recipients, scoped to the current mode): GET https://api.harepay.click/v1/payout_destinations lists them (`default` first); POST creates one ({kind:"bank", bank_code, account_no, account_name} — confirm the name via /v1/bank/resolve first — or {kind:"crypto", crypto_chain, crypto_addr}, optional `label`); DELETE https://api.harepay.click/v1/payout_destinations/{id} removes one; PUT https://api.harepay.click/v1/payout_destinations/{id}/default makes it the default for its currency. The first destination saved for a currency becomes its default automatically. SECURITY NOTICE: adding, deleting or re-defaulting a LIVE destination emails the account owner (masked details, naming the credential that acted) — expected behaviour, not an error in your integration; test-mode changes are in-app only. ## Refer & earn (partnership program) - The program is APPLICATION-GATED: you must be an approved partner before you can refer. POST https://api.harepay.click/v1/referrals/apply {pitch} applies (the `pitch` describes the audience/pool you can bring; min 20 chars) — an admin reviews and approves it. - GET https://api.harepay.click/v1/referrals/me: your `partner_status` (""/pending/approved/rejected). Once approved it also returns your shareable `referral_code` + `referral_link`, the merchants you've `referred`, and your `earnings` per asset (accrued vs paid). When the program is enabled, an approved partner earns `default_rate_bps` of the PLATFORM fee on every payment their referred merchants process, for the life of that account. Earnings accrue automatically and are paid out-of-band by the HarePay team. - Signing up takes two steps in production: POST https://api.harepay.click/v1/signup/code with `{email}` emails a 6-digit code (proving the inbox before any account exists), then POST https://api.harepay.click/v1/signup with the same details plus `code`. A missing code returns 400 `code_required`; the account is created with its email already verified. - POST https://api.harepay.click/v1/signup declines a registration that looks automated (422 `signup_declined`) without creating an account. If that happens to a real business, POST https://api.harepay.click/v1/support (public) with `{name, email, phone, category:"Account review", message}` — a human reviews it and sets the account up. - To attribute yourself at signup, pass `referral_code` in the POST https://api.harepay.click/v1/signup body. The code only attributes if it belongs to an APPROVED partner; an unknown or unapproved code is ignored and never blocks signup. ## Balances & customers - FUNDING YOUR BALANCE. There is no separate top-up or deposit endpoint, and none is needed: your NGN balance is funded by the payments you COLLECT. To put money in — e.g. to cover identity-check fees before you have collection volume — create an ordinary payment with POST https://api.harepay.click/v1/payment_intents for the amount you want to add and pay it through any method you offer (bank transfer, card, …). It settles into `available` like any other collection, net of the standard collection fee, and shows up in your normal payment history and reporting. Watch for `payment_intent.succeeded` (or `balance.updated`) to know it landed; a bank transfer typically reflects within seconds of the payer completing it. - GET https://api.harepay.click/v1/balances/{asset}: Available and pending balance for an asset (e.g. NGN, USD). Stablecoin pay-ins (USDC/USDT, any network) settle 1:1 into a SINGLE `USD` balance by default — not per-coin/per-network — and crypto payouts draw from that USD balance. A merchant can set `crypto_settle` to `auto_convert_ngn` to instead auto-convert crypto pay-ins to NGN at the live rate (a platform FX margin applies); `hold_stablecoin` (default) keeps them as USD. Passing `settle_as` {currency:NGN} on a single payment overrides the preference. Non-pegged crypto (BTC, ETH, ...) ignores this preference entirely — it always auto-converts to your account's `settle_fiat` currency, since HarePay never holds a volatile-asset balance; only stablecoins (USDC/USDT) can ever sit in your balance unconverted. - GET https://api.harepay.click/v1/stats/overview?range=today|30d|3m|12m|all¤cy=: Dashboard analytics — bucketed revenue/orders/subscriptions/MRR/conversion series + totals in one call (minor units; `currencies` lists every currency you've collected in). - POST https://api.harepay.click/v1/customers, GET, PATCH: Create, list and update customers. - POST https://api.harepay.click/v1/customers/{id}/prepaid/topup: Top up a customer's prepaid balance. ## Invoicing & subscriptions - POST https://api.harepay.click/v1/invoices, GET, GET /v1/invoices/{id}: Create, list and retrieve invoices. - POST https://api.harepay.click/v1/invoices/{id}/send, /v1/invoices/{id}/pay: Send an invoice or mark it paid. - POST https://api.harepay.click/v1/prices: Create a price — `model` recurring|usage. Recurring takes `amount` + `interval` + optional `trial_days` (free trial; the sub starts `trialing`). Usage takes `unit_amount` + optional `included_units` allowance and `auto_charge` (debit the customer's prepaid balance on overage). - POST https://api.harepay.click/v1/subscriptions {customer_id, price_id}: Start a subscription directly (first invoice bills in arrears when the period ends). For PAID signup, prefer subscription checkout (price_id on a checkout session — see Hosted checkout). - GET https://api.harepay.click/v1/subscriptions and GET https://api.harepay.click/v1/subscriptions/{id}: List or retrieve subscriptions — use them for entitlement/feature-gating. The list filters by `?customer=cus_…` and/or `?status=active` so "is this customer entitled right now?" is one call; retrieve returns one by id (owner/mode-scoped; a foreign or wrong-mode id is 404). Gate on `status` (trialing/active = entitled, past_due = grace, canceled = revoke); `current_period_end` is when access lapses and `cancel_at_period_end` flags a scheduled stop. HarePay is the source of truth: read it here and mirror it locally, kept in sync by the subscription.* webhooks (below) for instant updates when a sub renews, lapses or is canceled. - Renewals are collected automatically: prepaid balance auto-debit -> saved card (when the payer previously paid by tokenized card) -> emailed invoice with a hosted pay link. Unpaid renewals retry at +3/+7/+14 days, then the subscription goes past_due (a later payment recovers it to active). Statuses: trialing -> active -> past_due -> canceled. - POST https://api.harepay.click/v1/subscriptions/{id}/cancel: Cancels at period end by default (reversible); body {"at_period_end": false} revokes immediately. POST https://api.harepay.click/v1/subscriptions/{id}/resume reverses a scheduled cancellation. - POST https://api.harepay.click/v1/subscriptions/{id}/usage: Report metered usage (PAYG); crossing the allowance auto-raises an overage invoice. ## Multiple businesses, one login - One HarePay sign-in can hold up to 10 SEPARATE businesses ("add a business" in the dashboard's account menu, or POST https://api.harepay.click/v1/me/accounts — session token, not API key). Each business is fully isolated: its own balances, API keys, webhook endpoints, KYB and pricing. The sign-in (email/password/2FA) lives on the PRIMARY business only; siblings have no login of their own. GET https://api.harepay.click/v1/me/accounts lists the group; POST https://api.harepay.click/v1/me/accounts/{id}/switch issues a session for another business in the group. For server-to-server calls nothing changes — each business's own sk_ keys scope every request to that business. ## Statements and exports - GET https://api.harepay.click/v1/statement?from=YYYY-MM-DD&to=YYYY-MM-DD¤cy=NGN&format=csv|pdf returns a STATEMENT OF ACCOUNT as a download (`format=pdf` = branded PDF document; default CSV). `to` is INCLUSIVE of that whole day; omit both for all time; `currency` defaults to your settlement currency. Lines are the settled payments (in), completed payouts (out, amount + fee) and completed refunds (out) in that currency, oldest first, each with gross/fee/net and a cumulative RUNNING NET, followed by period totals and your current available balance. - Deliberate limit: the running column is the cumulative net of those lines, NOT a bank-style opening/closing balance — adjustments such as dispute reserves and verification fees are not shown as lines, so don't reconcile it as one. A refunded payment still appears as its original inflow (the credit really happened) with the refund as a separate outflow. - GET https://api.harepay.click/v1/exports/payments.csv and GET https://api.harepay.click/v1/exports/payouts.csv are flat dumps for reconciliation, honouring the same `from`/`to`. Payments include EVERY status (a finance team chasing a missing order needs the failures too), with amount/fee/settled/refunded, payer name and bank, provider ref and failure reason. - All three are scoped to the caller and the request's MODE — a test-mode session can never export live money, and test files are named with a `-test` suffix. CSV amounts are plain decimals (no currency symbols or separators) so a spreadsheet can sum them; the statement's signed `net` column sums to the period's net movement. ## Team (staff logins with roles) - The account OWNER can invite staff into their dashboard, each with their own email + password and a ROLE that bounds what they may do across every business in the group: `admin` (everything except the team itself), `finance` (payouts, saved destinations, refunds, settlement), `developer` (API keys, webhook endpoints, test data), `viewer` (read-only). Every role can read; the owner's credentials (password, login email, 2FA), the team roster, and adding businesses are the owner's alone — no role reaches them. - Management (owner session only): GET https://api.harepay.click/v1/team lists members; POST https://api.harepay.click/v1/team/invites {email, name?, role} emails an accept link (7-day token; the address must not already sign into any HarePay account); PUT https://api.harepay.click/v1/team/{id} {role} re-roles; POST https://api.harepay.click/v1/team/{id}/disable | /enable suspend/restore (a disabled member's LIVE sessions die on their next request, not at token expiry); POST https://api.harepay.click/v1/team/{id}/resend re-sends a pending invite; DELETE https://api.harepay.click/v1/team/{id} removes the member. - Members sign in on the normal POST https://api.harepay.click/v1/login with their own email + password (the response carries an `actor` object naming them and their role, echoed on GET /v1/me), act on the group's businesses, and may switch between them — the role travels with the session. Accepting an invite is public: POST https://api.harepay.click/v1/team/accept {token, name?, password}. Password reset works through the normal forgot-password flow. API keys are unaffected by roles — a key belongs to the business, not a person. ## Webhooks - POST https://api.harepay.click/v1/webhook_endpoints: Add a signed webhook endpoint — up to 5 per account (one per product/site); every event is delivered to ALL of them, each signed with its own secret. Posting a NEW url adds an endpoint; re-posting an EXISTING url rotates that endpoint's secret. GET lists them (`data` array; top-level url/secret_hint mirror the first endpoint for older single-endpoint clients). DELETE https://api.harepay.click/v1/webhook_endpoints/{id} removes one; DELETE without an id removes all. - SUBSCRIPTION FILTERING (per endpoint): POST https://api.harepay.click/v1/webhook_endpoints accepts an optional `events` array naming exactly what THAT endpoint receives — an event type (`"payout.paid"`), a family wildcard (`"payout.*"`), or `"*"`. OMITTING `events` MEANS EVERYTHING, so existing integrations are unchanged and you only opt in if you want less. Semantics: omitting the key on a re-post leaves the endpoint's current filter alone (rotating a secret never silently widens it); sending `[]` or `["*"]` resets it to every event. An unknown type or family is REJECTED with 400 rather than silently delivering nothing — so a typo can't black-hole your webhooks. Endpoints may differ: point `payout.*` at your finance service and `payment_intent.*` at your order service. GET https://api.harepay.click/v1/webhook_endpoints returns `events` (array) and `all_events` (bool) per endpoint, plus a top-level `available_events` catalogue you can drive a UI from. When editing only the filter of an EXISTING url you may omit `secret` — the endpoint keeps the secret it already has. - GET https://api.harepay.click/v1/webhook_deliveries: Inspect the per-attempt delivery log. GET https://api.harepay.click/v1/webhook_deliveries/{id} returns a single attempt including the `body` we signed and POSTed. - RETRIES: a delivery that doesn't return 2xx is retried on a BACKING-OFF schedule — 30s, 2m, 10m, 1h, 4h, 12h, 24h, 24h after the first attempt, so 9 attempts spanning about 2.7 days. Your endpoint is not hammered in between, and a deploy, a restart or a certificate renewal recovers by itself with nothing lost. Each attempt carries `X-Lodum-Attempt` (1-based); the `X-Lodum-Event-Id` is IDENTICAL across retries, so de-duplicate on it — a retry is the same event, not a new one. Anything other than 2xx (and any timeout — we wait 15s) counts as a failure, so return 2xx as soon as you have DURABLY accepted the event and do your work afterwards. - DEAD LETTERS: when the schedule runs out the event stops being retried, but it is NOT lost. The final attempt is kept as a dead letter with its body. GET https://api.harepay.click/v1/webhook_deliveries?status=dead lists what never reached you — this is the list to work through after an outage. POST https://api.harepay.click/v1/webhook_deliveries/{id}/retry replays one; POST https://api.harepay.click/v1/webhook_deliveries/replay_dead replays the outstanding ones OLDEST FIRST, across calls (up to 50 per call, and it stops early if the endpoint is slow — read `attempted`/`replayed`/`remaining` and call again until `remaining` is 0). A dead letter is cleared only when a replay of it actually SUCCEEDS, so a failed replay leaves it on the list. Both are mode-scoped, and a merchant's dead letters are never evicted by newer traffic. - Signing: each delivery carries `X-Lodum-Timestamp` and `X-Lodum-Signature: sha256=`, where the signature is HMAC-SHA256 of `.` keyed with your endpoint secret. Verify against the raw bytes before trusting the payload; reject stale timestamps. - Body shape: `{ id, object:"event", type, livemode, created, data:{ object } }` — the event `type` is in the body (and mirrored in the `X-Lodum-Event` header), and the domain resource is nested under `data.object`. - Mode: every endpoint receives BOTH test and live deliveries. Read the top-level `livemode` boolean (also on `data.object.livemode`) to tell them apart — test-mode events come from the sandbox rail and move no real money. Do not act on test deliveries in production. - MONEY SPLIT ON PAYMENT EVENTS: payment_intent.* events carry `amount` (the GROSS charge — what the payer was asked for, in minor units), and once settled also `fee` (what HarePay kept), `net` and `settled` (what actually reached your balance, in `settle_as`). These are additive; a consumer reading only `amount` is unaffected. They matter when your account is set to CUSTOMER-pays-the-fee, where `amount` EXCEEDS the order total by the fee — without `settled` you cannot tell that apart from a merchant-pays charge. - Events: `payment_intent.created`, `payment_intent.succeeded`, `payment_intent.payment_failed`, `payout.created`, `payout.paid`, `payout.failed`, subscription.created/renewed/past_due/recovered/cancel_scheduled/resumed/canceled, invoice.created, invoice.paid, refund.pending, refund.completed, refund.rejected, `balance.updated`, `balance.low`, `compliance.alert` — gate subscription access on these, and use them verbatim in an endpoint's `events` filter. The `data.object` mirrors the REST resource and echoes the `metadata` you set at creation. - BALANCE EVENTS (wallet monitoring): `balance.updated` fires on EVERY movement of your balance — a settlement, a payout, a refund hold or release, a verification fee, a dispute reserve — carrying the NEW balance (`available`/`pending`/`reserved`, each `{amount,currency,decimal,display}` with `amount` in minor units), `livemode`, and a `reason` naming what moved it. No follow-up GET needed. `balance.low` fires when available drops BELOW the threshold you set with PUT https://api.harepay.click/v1/me/low_balance_threshold `{"threshold": }` (0 disables). It is edge-triggered: one event per crossing, re-armed once the balance recovers, so a low balance doesn't page you on every fee. ## Public (no API key) - POST https://api.harepay.click/public/payment_status: Let a payer check a payment by reference + PIN. - POST https://api.harepay.click/public/invoices/verify: Verify an invoice by id + PIN (returns a public view + `payable`). - POST https://api.harepay.click/public/invoices/{id}/pay {pin}: For an unpaid invoice, returns a hosted checkout `url` (reused while live) the customer pays through — it carries invoice_id, so the invoice is marked paid on settlement.