{"openapi_spec":"GET /api/v1/openapi.json — machine-readable OpenAPI 3.0 spec of this whole API, for Postman import or client SDK generation.","payments":{"statuses":{"pending":"Payment record created, checkout not yet completed.","processing":"Charge submitted to the processor, awaiting confirmation.","succeeded":"Charge confirmed — full amount collected, nothing refunded.","partially_refunded":"Charge succeeded and part of it has since been refunded via POST /api/merchant/payments/{id}/refund. refunded_amount on the payment (in GET /api/v1/payments) shows how much.","refunded":"Charge succeeded and the full amount has since been refunded.","failed":"The charge attempt was declined or otherwise did not complete — no money was collected.","cancelled":"Voided via POST /api/v1/payments/{id}/cancel while still pending — never reached the processor."},"refund_amount_semantics":"refunded_amount tracks cumulative refunds. A payment can be refunded more than once as long as the total refunded never exceeds the original amount (validated server-side on every refund call).","refunds":"POST /api/v1/payments/{id}/refund — omit amount for a full refund (refunds:create_full), pass amount for a partial refund (refunds:create_partial). Refunds above a per-currency threshold on live keys return 202 with requiresApproval: true and go through the same maker-checker flow as dashboard refunds; sandbox keys skip approval entirely (no real money moves).","cancel":"POST /api/v1/payments/{id}/cancel (payments:cancel) — only works while the payment is still pending. A payment already submitted to the processor can't be voided from here; refund it once it succeeds instead."},"payouts":{"statuses":{"pending":"Payout request created, not yet submitted to Flutterwave.","processing":"Submitted to Flutterwave, awaiting transfer.completed/transfer.failed.","completed":"Flutterwave confirmed the transfer landed — ledger debited.","failed":"Flutterwave reported the transfer failed. No ledger entry was posted.","cancelled":"Cancelled via POST /api/v1/payouts/{id}/cancel while still pending — no money ever moved."},"overview":"POST /api/v1/payouts (payouts:create) and GET /api/v1/payouts (payouts:read). Live keys only — payouts move real settlement funds and are rejected outright on sandbox keys. Supports olympus_account and flutterwave_bank destinations. Payouts above a per-currency threshold return 202 with requiresApproval: true (same maker-checker flow as dashboard payouts)."},"webhooks":{"delivery":"POST to your registered URL (see POST /api/merchant/webhooks) with header X-Olympus-Event naming the event type, X-Olympus-Signature = hex HMAC-SHA256 of \"{X-Olympus-Timestamp}.{raw body}\" using your webhook secret, and X-Olympus-Timestamp = unix seconds at send time. Verify the signature over the exact timestamp+body before trusting a delivery, and reject anything outside a ~5 minute tolerance window to prevent replay. Body shape: {\"type\": \"<event>\", \"created_at\": \"<ISO8601>\", \"data\": {...}}.","event_types":{"payment.succeeded":"A QR code or payment link charge succeeded. data includes id, amount, currency, fee_amount, net_amount, status, qr_code_id, payment_link_id (subscription charges instead include subscription_id).","payment.failed":"A checkout attempt was declined or otherwise failed to complete — fires for QR/payment-link charges and for a failed subscription renewal charge (subscription renewal instead sets the subscription to past_due). data includes amount, currency, status, failure_reason, and either the payment identifiers or subscription_id.","payment.refunded":"A refund (full or partial) was executed. data includes id, amount (original), refunded_amount (cumulative), refund_amount (this refund), currency, status (partially_refunded or refunded), fully_refunded (boolean).","payout.completed":"A payout to your settlement bank account was confirmed by Flutterwave. data includes id, amount, currency, status.","payout.failed":"A payout to your settlement bank account failed. data includes id, amount, currency, status, failure_reason."},"retries":"Each delivery attempt (success or failure) is logged — see GET /api/v1/webhooks/{id}/deliveries (webhooks:read_logs), which includes attempt_number, dead_lettered, and next_retry_at. A failed delivery is automatically retried with exponential backoff (1 min, 5 min, 30 min, 2 hr after the initial attempt) up to 5 attempts total, after which it is marked dead_lettered and not retried again. Call POST /api/v1/webhooks/deliveries/{id}/retry (webhooks:retry) at any point to force an immediate retry instead of waiting for the schedule. Poll GET /api/v1/payments or GET /api/v1/settlements as a fallback source of truth if a delivery is ultimately dead-lettered.","manage":"POST /api/v1/webhooks and DELETE /api/v1/webhooks/{id} (webhooks:manage) let an API key register/remove its own webhook endpoints, in addition to the dashboard."},"payment_links":{"idempotency":"POST /api/v1/payment-links accepts either an Idempotency-Key header or an idempotencyKey body field. Retrying the same key returns the original link instead of creating a duplicate.","return_url":"POST /api/v1/payment-links accepts an optional returnUrl. When set, the payer is redirected there after checkout with ?payment_id={id}&status=success or &status=cancelled appended, instead of Olympus's own generic confirmation page."},"split_payments":{"overview":"POST /api/v1/split-recipients registers a real Flutterwave Subaccount for a landlord/contractor (idempotent per externalReference — retrying the same externalReference returns the existing recipient rather than creating a duplicate Subaccount). Pass the returned id as splitRecipientId on POST /api/v1/payment-links or POST /api/v1/qr-codes to settle that specific charge directly to the recipient's own Subaccount — Flutterwave splits the platform fee off automatically at charge time, in the same transaction, rather than Olympus collecting the full amount and paying the recipient out separately.","verify_and_disable":"PATCH /api/v1/split-recipients/{id} with { verified: true } (recipients:verify) or { disabled: true } (recipients:disable). Disabling is enforced at checkout, not cosmetic — a payment link/QR code assigned to a disabled recipient falls back to settling into your own default account instead of the recipient's."},"settlements":{"overview":"GET /api/v1/settlements?from=<ISO date>&to=<ISO date>&splitRecipientId=<optional> returns every payment that settled in the range (gross, fee, net, refunded, net_settled per payment) plus a per-currency summary — pass splitRecipientId to scope it to one recipient's settlements only.","export":"Add &format=csv (reports:export, in addition to settlements:read) to get the same data as a downloadable CSV instead of JSON."},"account":{"overview":"GET /api/v1/account (account:read or kyc:read) returns your business profile plus verification status — kyc_status, submitted document count, verified UBO count, and whether a settlement destination is on file.","verification":"POST /api/v1/account/verification (kyc:write) starts a real Didit hosted identity/liveness session and returns { verificationUrl, sessionId } — send the business owner to verificationUrl to complete it. KYC on this platform is a hosted verification flow, not raw document upload. Poll GET /api/v1/account afterward to see kyc_status move past in_review."}}