{"openapi":"3.1.0","info":{"title":"KashLink API Platform","version":"1.0.0","description":"Escrow checkout, payouts and webhooks for KashLink merchants and partners.\n\n### Key Capabilities\n* **Payments by Paystack**: Buyers pay by M-Pesa (STK push) or card on the hosted checkout page. Payouts go to M-Pesa, Till, Paybill or Kenyan bank accounts via Paystack Transfers.\n* **Escrow**: The buyer's money is held until the order is released, either by your server (`POST /v1/escrow/{id}/release`) or by the buyer confirming delivery.\n* **Signed Webhooks**: Events signed with HMAC SHA-256 (`Kashlink-Signature`). If your endpoint doesn't answer 2xx within 10 s, the same event (same id) is retried after 1 min, 5 min, 30 min, 2 h, 6 h and 12 h. Every try is listed in `GET /v1/webhooks/deliveries`.\n* **Sandbox**: With a test key, simulate payment events on your own test orders. Test orders are never charged real money, released or paid out.\n* **Payees (pilot, by invitation)**: Marketplaces register their vendors as payees (`/v1/payees`) and pass `payee_id` (plus an optional `marketplace_fee`) at checkout. KashLink holds the vendor's share in escrow and pays the payee's verified account once the buyer gives the delivery code. Needs payees switched on for your platform (otherwise `403 payees_not_enabled`).\n\n### Scopes\n`escrow:create`, `escrow:read`, `escrow:release`, `payouts:create`, `payouts:read`, `refunds:create`, `webhooks:manage`, `payees:manage` (create, change and disable payees), `payees:read` (list payees, balances and payouts). The `payees:*` scopes are not given to new keys; KashLink adds them when it switches payees on.","contact":{"name":"KashLink Developer Support","email":"support@kash-link.com","url":"https://kash-link.com/help"},"license":{"name":"Proprietary","url":"https://kash-link.com/legal"}},"servers":[{"url":"https://api.kash-link.com","description":"Production API Gateway"}],"security":[{"ApiKeyAuth":[]}],"paths":{"/health":{"get":{"summary":"Gateway Health & Uptime Probe","description":"Returns server health and version. Useful for load balancer health probes.","operationId":"getHealth","security":[],"responses":{"200":{"description":"Gateway is healthy and operational","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","example":"healthy"},"service":{"type":"string","example":"kashlink-api-gateway"},"version":{"type":"string","example":"1.0.0"},"timestamp":{"type":"string","format":"date-time"}},"required":["status","service","version","timestamp"]}}}}}}},"/v1/fees":{"get":{"summary":"Get Fee Tiers","description":"Returns active collection markup tiers, payout markup tiers and `processor`: Paystack Kenya rates (M-Pesa 1.5%, Kenyan card 2.9%, international card 3.8%) and transfer fee bands per payout rail. Buyer total = ceil((amount + collection markup) / (1 - rate)). Payout fee = transfer fee for the rail + payout markup. Cached publicly for high performance.","operationId":"getFeeTiers","security":[],"responses":{"200":{"description":"Active fee tier configuration","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","example":"success"},"data":{"type":"object","properties":{"collectionMarkups":{"type":"array","items":{"$ref":"#/components/schemas/FeeTier"}},"payoutMarkups":{"type":"array","items":{"$ref":"#/components/schemas/FeeTier"}},"processor":{"$ref":"#/components/schemas/ProcessorFees"}}}}}}}}}}},"/v1/checkout/initialize":{"post":{"summary":"Initialize Hosted Escrow Checkout Session","description":"Creates a pending escrow transaction, resolves partner branding, calculates itemized fees, and returns a secure hosted checkout URL. Marketplace orders: pass payee_id (a verified payee of yours, same test/live mode as the key) and optionally marketplace_fee (your cut in KES). The vendor's share is the order amount minus marketplace_fee; it is held in escrow and paid to the payee once the buyer gives the delivery code. vendor_user_id and vendor_payout are not supported.","operationId":"initializeCheckout","parameters":[{"name":"Idempotency-Key","in":"header","required":false,"description":"Unique UUID to prevent duplicate financial charges on network retries. Cached for 24 hours.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckoutInitializeRequest"}}}},"responses":{"200":{"description":"Checkout session successfully initialized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckoutInitializeResponse"}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"409":{"description":"REFERENCE_ID_CONFLICT: reference_id is already used by an open checkout for a different payee."},"422":{"description":"Validation failed. Marketplace orders: PAYEES_NOT_ENABLED, PAYEE_NOT_FOUND (not one of your payees), PAYEE_NOT_VERIFIED, PAYEE_MODE_MISMATCH (test payee with a live key or the reverse), INVALID_MARKETPLACE_FEE, MARKETPLACE_FEE_TOO_LARGE, MARKETPLACE_FEE_WITHOUT_PAYEE, USE_MARKETPLACE_FEE (platform_fee sent on a marketplace order)."},"429":{"$ref":"#/components/responses/RateLimitError"}}}},"/v1/checkout/{id}":{"get":{"summary":"Get Escrow Transaction Status","description":"Retrieves current status, escrow balance, delivery state, and itemized fees for a transaction.","operationId":"getCheckoutStatus","parameters":[{"name":"id","in":"path","required":true,"description":"The unique UUID of the transaction.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Transaction details retrieved","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TransactionDetailResponse"}}}},"401":{"$ref":"#/components/responses/UnauthorizedError"},"404":{"$ref":"#/components/responses/NotFoundError"}}}},"/v1/checkout/{id}/refund":{"post":{"summary":"Refund Escrow Transaction","description":"Requires the refunds:create scope. Paid orders still held in escrow are refunded in full to the buyer through Paystack and move to refunded. Unpaid checkouts are cancelled instead (status cancelled; nothing to refund). Partial refunds are not supported.","operationId":"refundCheckout","parameters":[{"name":"id","in":"path","required":true,"description":"The unique UUID of the transaction to refund.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"reason":{"type":"string","example":"Buyer rejected goods upon inspection"}},"required":["reason"]}}}},"responses":{"200":{"description":"Refund initiated successfully","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","example":"success"},"message":{"type":"string","example":"Refund successfully initiated via provider."},"data":{"type":"object","properties":{"transaction_id":{"type":"string","format":"uuid"},"refund_reference":{"type":"string"},"status":{"type":"string","example":"refunded"}}}}}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"}}}},"/v1/checkout/{id}/release":{"post":{"summary":"Release Escrow Order (Checkout Alias)","deprecated":true,"description":"Deprecated: use POST /v1/escrow/{id}/release. Works until 1 February 2027, and responses carry Deprecation and Sunset headers. Fulfills and releases held escrow funds directly to the merchant available wallet balance using the checkout transaction ID. Only orders created through the API (POST /v1/checkout/initialize) can be released this way; any other order is refused with 422 NOT_API_ORDER and needs the delivery code of the buyer. Marketplace orders (created with payee_id) are released only by the delivery code of the buyer and are refused with 422 PAYEE_ORDER_BUYER_CODE_ONLY. Needs the escrow:release scope, which new keys do not get by default.","operationId":"releaseCheckoutEscrow","parameters":[{"name":"id","in":"path","required":true,"description":"The unique UUID of the transaction to fulfill and release.","schema":{"type":"string","format":"uuid"}},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EscrowReleaseRequest"}}}},"responses":{"200":{"description":"Escrow order fulfilled and funds released to wallet","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EscrowReleaseResponse"}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"422":{"$ref":"#/components/responses/UnprocessableEntityError"}}}},"/v1/escrow/{id}/release":{"post":{"summary":"Headless Escrow Order Fulfillment & Release","description":"Allows integrated e-commerce merchants to submit fulfillment proof (carrier, tracking number) and release held escrow directly to their available wallet balance without buyer OTP. Only orders created through the API (POST /v1/checkout/initialize) can be released this way; any other order is refused with 422 NOT_API_ORDER and needs the delivery code of the buyer. Marketplace orders (created with payee_id) are released only by the delivery code of the buyer and are refused with 422 PAYEE_ORDER_BUYER_CODE_ONLY. Needs the escrow:release scope, which new keys do not get by default.","operationId":"releaseEscrowOrder","parameters":[{"name":"id","in":"path","required":true,"description":"The transaction UUID held in escrow.","schema":{"type":"string","format":"uuid"}},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EscrowReleaseRequest"}}}},"responses":{"200":{"description":"Escrow order successfully released","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EscrowReleaseResponse"}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"422":{"$ref":"#/components/responses/UnprocessableEntityError"}}}},"/v1/checkout/{id}/dispatch":{"post":{"summary":"Dispatch an order and send the buyer their delivery code","description":"Call this when an API order leaves for delivery. KashLink creates the buyer's 6-character delivery code (valid 24 hours), shows it to the buyer in their KashLink order, and moves the order to awaiting_handshake (webhook transaction.awaiting_handshake). Works for marketplace (payee) orders too. Only orders created through the API, in escrow_held or out_for_delivery, and with a KashLink buyer account. Dispatching again while the order is awaiting_handshake or handoff_expired sends a new code; the old one stops working. Errors: 404 TRANSACTION_NOT_FOUND, 422 NOT_API_ORDER, INVALID_STATUS, NO_BUYER_ACCOUNT, 409 HANDSHAKE_IN_PROGRESS. Needs the escrow:release scope.","operationId":"dispatchOrder","parameters":[{"name":"id","in":"path","required":true,"description":"The transaction UUID.","schema":{"type":"string","format":"uuid"}},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"Code created and the order is awaiting the buyer's code","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","example":"success"},"data":{"type":"object","properties":{"transaction_id":{"type":"string","format":"uuid"},"status":{"type":"string","example":"awaiting_handshake"},"code_expires_at":{"type":"string","format":"date-time","nullable":true}}}}}}}},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"409":{"description":"Another code is being created; try again in a moment (HANDSHAKE_IN_PROGRESS)."},"422":{"$ref":"#/components/responses/UnprocessableEntityError"}}}},"/v1/checkout/{id}/verify-code":{"post":{"summary":"Confirm delivery with the buyer's delivery code","description":"Submit the code the buyer gave at delivery. A correct code releases escrow exactly as on the KashLink website: marketplace orders pay the payee and your marketplace_fee, other orders pay you. Each code allows 3 tries; the third wrong try locks the order for KashLink review. Errors: 400 INVALID_CODE_FORMAT, WRONG_CODE (with attempts_left), 404 TRANSACTION_NOT_FOUND, NO_ACTIVE_CODE, 409 CODE_ALREADY_USED, ORDER_CHANGED, ATTEMPT_IN_PROGRESS, 410 CODE_EXPIRED, 422 NOT_API_ORDER, INVALID_STATUS, 423 CODE_LOCKED. Needs the escrow:release scope.","operationId":"verifyDeliveryCode","parameters":[{"name":"id","in":"path","required":true,"description":"The transaction UUID.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["code"],"properties":{"code":{"type":"string","example":"ABC-123","description":"6 letters and numbers; spaces and dashes are ignored."}}}}}},"responses":{"200":{"description":"Code accepted and escrow released","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","example":"success"},"data":{"type":"object","properties":{"transaction_id":{"type":"string","format":"uuid"},"status":{"type":"string","example":"payout_pending"},"released":{"type":"boolean","example":true}}}}}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"409":{"description":"The code was already used or the order changed (e.g. a dispute)."},"410":{"description":"The code expired (CODE_EXPIRED)."},"422":{"$ref":"#/components/responses/UnprocessableEntityError"},"423":{"description":"Too many wrong codes; the order is locked for review (CODE_LOCKED)."}}}},"/v1/checkout/{id}/code-link":{"post":{"summary":"One-tap link to the buyer's delivery code","description":"For marketplaces whose buyers are signed in on the marketplace. Returns a one-time link to KashLink's code page that shows this order's delivery code without a KashLink sign-in. Send only the signed-in buyer of the order there, and never show or log the link anywhere else: anyone who opens it sees the code. The link works for 10 minutes and opens once; the page then stays usable in that browser tab for 30 minutes. A new link replaces any unopened one, and at most 10 links per order per hour can be made. The buyer gets a KashLink notice each time a link is opened. Only for orders awaiting the code (after POST /v1/checkout/{id}/dispatch). Errors: 404 TRANSACTION_NOT_FOUND, NO_ACTIVE_CODE, 410 CODE_EXPIRED (dispatch again to send a new code), 422 NOT_API_ORDER, INVALID_STATUS, 423 CODE_LOCKED, 429 TOO_MANY_LINKS. Needs the handshake:buyer_link scope, which KashLink turns on per marketplace.","operationId":"createDeliveryCodeLink","parameters":[{"name":"id","in":"path","required":true,"description":"The transaction UUID.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"One-time link created","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","example":"success"},"data":{"type":"object","properties":{"url":{"type":"string","format":"uri","example":"https://kash-link.com/code#t=…"},"expires_at":{"type":"string","format":"date-time"}}}}}}}},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"410":{"description":"The code expired (CODE_EXPIRED)."},"422":{"$ref":"#/components/responses/UnprocessableEntityError"},"423":{"description":"Too many wrong codes; the order is locked for review (CODE_LOCKED)."},"429":{"description":"Too many links for this order (TOO_MANY_LINKS)."}}}},"/v1/payouts":{"post":{"summary":"Initialize Wallet Payout Disbursement","description":"Pays out from your available wallet balance to one of your verified bank or M-PESA accounts. The amount plus fee is reserved before the transfer is sent, so the same balance can never be paid out twice.","operationId":"createPayout","parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PayoutInitializeRequest"}}}},"responses":{"202":{"description":"Payout accepted and queued for settlement execution","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PayoutInitializeResponse"}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"422":{"$ref":"#/components/responses/UnprocessableEntityError"}}}},"/v1/payouts/initialize":{"post":{"summary":"Initialize Wallet Payout Disbursement (Alias)","deprecated":true,"description":"Deprecated: use POST /v1/payouts. Works until 1 February 2027, and responses carry Deprecation and Sunset headers.","operationId":"initializePayoutAlias","parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PayoutInitializeRequest"}}}},"responses":{"202":{"description":"Payout accepted and queued for settlement execution","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PayoutInitializeResponse"}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"422":{"$ref":"#/components/responses/UnprocessableEntityError"}}}},"/v1/payouts/accounts":{"get":{"summary":"List Verified Settlement Accounts","description":"Returns all active settlement bank accounts and M-PESA paybill/till destinations registered for the authenticated merchant.","operationId":"listPayoutAccounts","responses":{"200":{"description":"Verified settlement accounts","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","example":"success"},"data":{"type":"array","items":{"$ref":"#/components/schemas/PayoutAccount"}}}}}}},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"}}}},"/v1/payout-accounts":{"get":{"summary":"List Verified Settlement Accounts (Alias)","deprecated":true,"description":"Deprecated: use GET /v1/payouts/accounts. Works until 1 February 2027, and responses carry Deprecation and Sunset headers.","operationId":"listPayoutAccountsRootAlias","responses":{"200":{"description":"Verified settlement accounts","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","example":"success"},"data":{"type":"array","items":{"$ref":"#/components/schemas/PayoutAccount"}}}}}}},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"}}}},"/v1/payouts/{id}":{"get":{"summary":"Query Payout Status","description":"Retrieves the status, provider reference, masked destination, and timestamp of a payout.","operationId":"getPayoutById","parameters":[{"name":"id","in":"path","required":true,"description":"Unique UUID of the payout.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Payout details retrieved","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","example":"success"},"data":{"$ref":"#/components/schemas/PayoutRecord"}}}}}},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"}}}},"/v1/webhooks/subscribe":{"post":{"summary":"Register Outbound Webhook Endpoint","description":"Registers a merchant URL to receive cryptographically signed HMAC SHA-256 event notifications.","operationId":"subscribeWebhook","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookSubscribeRequest"}}}},"responses":{"200":{"description":"Webhook successfully registered. Save the secret immediately.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","example":"success"},"data":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"url":{"type":"string","format":"uri"},"secret":{"type":"string","description":"Used to verify Kashlink-Signature header"},"events":{"type":"array","items":{"type":"string"}},"created_at":{"type":"string","format":"date-time"}}}}}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"}}}},"/v1/payees":{"post":{"summary":"Create a payee (marketplace vendor)","description":"Registers a vendor and its payout account and runs the account-holder name check. Scope payees:manage. Idempotent on reference per key mode: the same reference returns the existing payee with 200 and changes nothing. Status follows the name check: verified (names match), name_mismatch, or verifying (check unavailable). Fires payee.verified or payee.verification_failed. Test keys never call the name-check provider: an account number ending 0000 gives name_mismatch, ending 1111 gives verifying, anything else verified. A new account starts a 24-hour cooling-off (account.cooling_off_until) before any payout goes to it.","operationId":"createPayee","parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PayeeCreateRequest"}}}},"responses":{"200":{"description":"A payee with this reference already exists; returned unchanged","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PayeeResponse"}}}},"201":{"description":"Payee created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PayeeResponse"}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/PayeesForbiddenError"},"422":{"description":"Invalid input (INVALID_PAYEE_INPUT) or a bank that cannot be verified (BANK_NOT_VERIFIABLE)"},"429":{"$ref":"#/components/responses/RateLimitError"}}},"get":{"summary":"List payees","description":"Your payees for the key's mode (test keys see test payees only), newest first. Scope payees:read.","operationId":"listPayees","parameters":[{"name":"status","in":"query","schema":{"type":"string","enum":["verifying","verified","name_mismatch","disabled"]}},{"name":"reference","in":"query","schema":{"type":"string"}},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":50}},{"name":"starting_after","in":"query","description":"Payee id from the previous page (next_cursor)","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"A page of payees","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PayeeListResponse"}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/PayeesForbiddenError"},"429":{"$ref":"#/components/responses/RateLimitError"}}}},"/v1/payees/{id}":{"get":{"summary":"Get a payee","description":"Scope payees:read. 404 for another platform's payee or a payee in the other mode.","operationId":"getPayee","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Payee id"}],"responses":{"200":{"description":"The payee","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PayeeResponse"}}}},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/PayeesForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitError"}}},"patch":{"summary":"Update a payee","description":"Change name, metadata (replaces) and/or the payout account. Scope payees:manage. A new account replaces the old one, is name-checked again (status follows the result), starts a new 24-hour cooling-off (account.cooling_off_until, account.cooling_off_active) and fires payee.account_changed (with data.change) then payee.verified or payee.verification_failed. Changing only name or metadata does not touch the account.","operationId":"updatePayee","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Payee id"},{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PayeeUpdateRequest"}}}},"responses":{"200":{"description":"The updated payee","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PayeeResponse"}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/PayeesForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"409":{"description":"PAYEE_PAYOUT_IN_FLIGHT: a payout to this payee is being sent; retry the account change shortly"},"422":{"description":"Invalid input, or the payee is disabled (PAYEE_DISABLED)"},"429":{"$ref":"#/components/responses/RateLimitError"}}},"delete":{"summary":"Disable a payee","description":"Stops the payee taking new orders; nothing is deleted. Scope payees:manage. Refused with 422 PAYEE_HAS_PENDING_FUNDS while money is held for it in escrow, a payout is in progress, or an order is still open. To use a disabled payee again, contact KashLink.","operationId":"disablePayee","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Payee id"}],"responses":{"200":{"description":"The payee, status disabled","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PayeeResponse"}}}},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/PayeesForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"422":{"description":"PAYEE_HAS_PENDING_FUNDS"},"429":{"$ref":"#/components/responses/RateLimitError"}}}},"/v1/payees/{id}/recheck":{"post":{"summary":"Re-check a payee's account name","description":"Runs the account-holder name check again on the payee's current payout account, for a payee left `verifying` (the check could not run) or `name_mismatch`. Adds no new account and does not restart the 24-hour cooling-off. A `verified` payee is returned unchanged. Sends `payee.verified` or `payee.verification_failed` when the status changes. Scope payees:manage.","operationId":"recheckPayee","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Payee id"}],"responses":{"200":{"description":"The payee after the check","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","example":"success"},"data":{"$ref":"#/components/schemas/Payee"}}}}}},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/PayeesForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"422":{"description":"PAYEE_DISABLED or PAYEE_NO_ACCOUNT"},"429":{"$ref":"#/components/responses/RateLimitError"}}}},"/v1/payees/{id}/balance":{"get":{"summary":"Get a payee's balance","description":"Amounts in KES (not cents). Scope payees:read.","operationId":"getPayeeBalance","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Payee id"}],"responses":{"200":{"description":"Balance","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","example":"success"},"data":{"$ref":"#/components/schemas/PayeeBalance"}}}}}},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/PayeesForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitError"}}}},"/v1/payees/{id}/payouts":{"get":{"summary":"List a payee's payouts","description":"Newest first, amounts in KES, account numbers masked. Scope payees:read.","operationId":"listPayeePayouts","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"},"description":"Payee id"},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":50}},{"name":"starting_after","in":"query","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Payouts","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","example":"success"},"data":{"type":"array","items":{"$ref":"#/components/schemas/PayeePayout"}},"has_more":{"type":"boolean"},"next_cursor":{"type":"string","nullable":true}}}}}},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/PayeesForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitError"}}}},"/v1/sandbox/simulate-payment":{"post":{"summary":"Simulate Payment Lifecycle in Sandbox","description":"With a test API key, simulate authorization, failure, reversal or completion on your OWN test orders (orders created with a test key). Live orders and orders of other merchants are refused (403 SANDBOX_NOT_TEST_ORDER). Test orders can never be released, paid out or credited to a wallet. Dispatches your webhooks.","operationId":"simulatePayment","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SimulatePaymentRequest"}}}},"responses":{"200":{"description":"Simulated payment state transition applied","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","example":"success"},"message":{"type":"string"},"data":{"type":"object","properties":{"transaction_id":{"type":"string","format":"uuid"},"action":{"type":"string","enum":["authorize","fail","reverse","complete"]},"provider":{"type":"string","enum":["paystack"]},"previous_status":{"type":"string"},"new_status":{"type":"string"},"simulated_reference":{"type":"string"},"webhook_queued":{"type":"boolean","description":"A transaction.* webhook was queued for the status change"},"timestamp":{"type":"string","format":"date-time"}}}}}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"}}}}},"components":{"securitySchemes":{"ApiKeyAuth":{"type":"http","scheme":"bearer","bearerFormat":"sk_live_... or sk_test_...","description":"Provide your secret KashLink API Key (`sk_live_...` or `sk_test_...`) in the Authorization header."}},"schemas":{"FeeTier":{"type":"object","properties":{"min_kes":{"type":"number"},"max_kes":{"type":"number"},"markup_kes":{"type":"number"}}},"ProcessorFees":{"type":"object","properties":{"mpesa_rate":{"type":"number","example":0.015},"card_local_rate":{"type":"number","example":0.029},"card_international_rate":{"type":"number","example":0.038},"transfer":{"type":"object","description":"Paystack transfer fee bands per rail (mpesa_mobile, mpesa_till, mpesa_paybill, bank). A band applies up to and including max_kes; the last band has max_kes null.","additionalProperties":{"type":"array","items":{"type":"object","properties":{"max_kes":{"type":"number","nullable":true},"fee_kes":{"type":"number"}}}}}}},"FeeQuote":{"type":"object","description":"Priced as the hosted checkout charges it. The buyer chooses M-Pesa or card (Kenyan or international) on the checkout page.","properties":{"amount":{"type":"number","example":1000},"collection_fee":{"type":"number","example":30,"description":"KashLink collection markup"},"processing_fee":{"type":"number","example":16,"description":"Paystack's M-Pesa share"},"total_charged_buyer":{"type":"number","example":1046,"description":"M-Pesa total"},"totals":{"type":"object","properties":{"mpesa":{"type":"number","example":1046},"card_local":{"type":"number","example":1061},"card_international":{"type":"number","example":1071}}},"payout_rail":{"type":"string","enum":["mpesa_mobile","mpesa_till","mpesa_paybill","bank"],"description":"Seller's primary payout account rail"},"transfer_fee":{"type":"number","example":20},"payout_markup":{"type":"number","example":8},"payout_fee":{"type":"number","example":28,"description":"transfer_fee + payout_markup, deducted when the seller is paid out"},"net_amount":{"type":"number","example":972,"description":"amount - payout_fee"},"tenant_fee":{"type":"number","example":0,"description":"Always 0: partner fee settings are not charged"},"total_fee":{"type":"number","example":74,"description":"(total_charged_buyer - amount) + payout_fee"},"currency":{"type":"string","example":"KES"}}},"CheckoutInitializeRequest":{"type":"object","properties":{"amount":{"type":"number","minimum":10,"description":"Order gross amount in KES (minimum 10 KES)"},"product_id":{"type":"string","format":"uuid","description":"Existing product UUID in catalog"},"title":{"type":"string","description":"Item name if on-the-fly checkout"},"description":{"type":"string","description":"Item description"},"preferred_provider":{"type":"string","deprecated":true,"description":"Ignored. Every payment is collected by Paystack."},"buyer_phone":{"type":"string","example":"+254712345678","description":"Buyer phone for M-PESA STK prompt"},"buyer_email":{"type":"string","format":"email","description":"Buyer email address"},"buyer_name":{"type":"string","description":"Buyer full name"},"redirect_url":{"type":"string","format":"uri","description":"Where the buyer is sent after checkout. Must be an absolute https:// URL (http://localhost is also accepted with a test key).","maxLength":2048},"metadata":{"type":"object","description":"Arbitrary merchant metadata (e.g. order_id, sku, cart). Never put vendor account details here."},"payee_id":{"type":"string","format":"uuid","description":"Marketplace order: id of one of your verified payees (same test/live mode as the key). Needs payees switched on."},"marketplace_fee":{"type":"number","minimum":0,"example":150,"description":"Marketplace order: your cut in KES (0 or more, at most 2 decimals; default 0). Only with payee_id. Must be less than the vendor's share after KashLink's payout fee (fees.net_amount)."}},"required":["amount"]},"CheckoutInitializeResponse":{"type":"object","properties":{"status":{"type":"string","example":"success"},"data":{"type":"object","properties":{"transaction_id":{"type":"string","format":"uuid"},"checkout_url":{"type":"string","format":"uri"},"paystack_reference":{"type":"string"},"environment":{"type":"string","enum":["live","test"]},"fees":{"$ref":"#/components/schemas/FeeQuote"},"payee_id":{"type":"string","format":"uuid","nullable":true,"description":"The payee paid for this order; null for ordinary orders"},"marketplace_fee":{"type":"number","nullable":true,"description":"Your cut in KES on a marketplace order; null for ordinary orders"}},"required":["transaction_id","checkout_url","paystack_reference","environment"]}}},"TransactionDetailResponse":{"type":"object","properties":{"status":{"type":"string","example":"success"},"data":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"amount":{"type":"number"},"status":{"type":"string","example":"escrow_held"},"payment_provider":{"type":"string","example":"paystack","description":"paystack for all new orders; a few older orders show their original provider."},"buyer_markup_fee":{"type":"number"},"seller_payout_fee":{"type":"number"},"payee_id":{"type":"string","format":"uuid","nullable":true,"description":"Marketplace orders: the payee paid"},"marketplace_fee":{"type":"number","nullable":true,"description":"Marketplace orders: your cut in KES (marketplace_fee at checkout); null for ordinary orders."},"created_at":{"type":"string","format":"date-time"}}}}},"PayoutInitializeRequest":{"type":"object","properties":{"amount":{"type":"number","minimum":10,"description":"Amount to withdraw in KES (minimum 10 KES)"},"narration":{"type":"string","example":"Vendor sales withdrawal","description":"Transaction purpose/narration"},"payout_account_id":{"type":"string","format":"uuid","description":"UUID of verified destination settlement account from /v1/payouts/accounts"},"recipient":{"type":"object","description":"Pick one of your verified payout accounts by its number. If it matches none, the payout is refused (422); it never falls back to another account. Omit both recipient and payout_account_id to pay your primary account.","properties":{"type":{"type":"string","enum":["mpesa","bank"]},"identifier":{"type":"string","example":"0123456784821","description":"Account number or phone number"},"name":{"type":"string","example":"Merchant Enterprise"}}},"preferred_provider":{"type":"string","enum":["paystack"],"description":"Payouts are sent via Paystack only; any other value is rejected"}},"required":["amount","narration"]},"PayoutInitializeResponse":{"type":"object","properties":{"status":{"type":"string","example":"success"},"data":{"$ref":"#/components/schemas/PayoutRecord"}}},"PayoutRecord":{"type":"object","properties":{"payout_id":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["pending","processing","settled","failed"],"example":"pending"},"amount_kes":{"type":"number","example":1000},"fee_kes":{"type":"number","example":28,"description":"Paystack transfer fee for the rail + KashLink payout markup, charged to your wallet on top of the amount"},"net_amount_kes":{"type":"number","example":1000,"description":"What the recipient receives"},"currency":{"type":"string","example":"KES"},"provider":{"type":"string","enum":["paystack"],"example":"paystack"},"provider_reference":{"type":"string","nullable":true},"destination":{"type":"object","nullable":true,"properties":{"type":{"type":"string","example":"bank_account"},"bank_name":{"type":"string","example":"Equity Bank Kenya Ltd"},"account_number_masked":{"type":"string","example":"•••• •••• 4821"},"account_name":{"type":"string","example":"ACME DUKA LIMITED"}}},"narration":{"type":"string","example":"Vendor sales withdrawal"},"failure_reason":{"type":"string","nullable":true},"created_at":{"type":"string","format":"date-time"},"settled_at":{"type":"string","format":"date-time","nullable":true}},"required":["payout_id","status","amount_kes","fee_kes","net_amount_kes","currency","provider","narration","created_at"]},"PayoutAccount":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"account_type":{"type":"string","enum":["bank_account","mobile_money"],"example":"bank_account"},"bank_name":{"type":"string","example":"Equity Bank Kenya Ltd"},"bank_code":{"type":"string","example":"68"},"account_number_masked":{"type":"string","example":"•••• •••• 4821"},"account_name":{"type":"string","example":"ACME DUKA LIMITED"},"is_primary":{"type":"boolean","example":true},"is_verified":{"type":"boolean","example":true},"created_at":{"type":"string","format":"date-time"}},"required":["id","account_type","bank_name","account_number_masked","account_name","is_primary","is_verified"]},"EscrowReleaseRequest":{"type":"object","properties":{"fulfillment_type":{"type":"string","enum":["courier","digital","in_person"],"default":"courier"},"carrier":{"type":"string","example":"Fargo Courier","description":"Logistics provider name"},"tracking_number":{"type":"string","example":"FC-94819284","description":"Waybill or tracking ID"},"notes":{"type":"string","example":"Delivered to buyer at Westlands drop point"}}},"EscrowReleaseResponse":{"type":"object","properties":{"transaction_id":{"type":"string","format":"uuid"},"status":{"type":"string","example":"released"},"payout_mode":{"type":"string","enum":["pass_through","wallet_hold"],"example":"pass_through"},"net_payout":{"type":"number","example":194,"description":"KES released to the merchant"},"currency":{"type":"string","example":"KES"},"payout":{"type":"object","nullable":true,"description":"pass_through only: the payout sent for this release (same shape as POST /v1/payouts), or {status: \"failed\", message, funds} if it could not be sent. The release still happened: the money is in your KashLink wallet; withdraw it with POST /v1/payouts. null for wallet_hold."},"released_at":{"type":"string","format":"date-time"}},"required":["transaction_id","status","payout_mode","net_payout","currency","released_at"]},"WebhookSubscribeRequest":{"type":"object","properties":{"url":{"type":"string","format":"uri","example":"https://partner.com/api/kashlink-webhook"},"events":{"type":"array","description":"Events to receive; omit or [\"*\"] for all. Unknown names are refused (422 UNKNOWN_WEBHOOK_EVENT).","items":{"type":"string","enum":["*","transaction.initiated","transaction.awaiting_payment","transaction.payment_failed","transaction.escrow_held","transaction.out_for_delivery","transaction.awaiting_handshake","transaction.delivered_and_received","transaction.delivered_pending_confirmation","transaction.payout_pending","transaction.payout_retry","transaction.payout_failed","transaction.completed","transaction.cancelled","transaction.refunded","transaction.disputed","transaction.released","transaction.auto_released","payee.verified","payee.verification_failed","payee.account_changed","payout.sent","payout.paid","payout.failed"]},"example":["transaction.escrow_held","transaction.completed","payee.verified","payout.paid"]},"description":{"type":"string","example":"Main production webhook"}},"required":["url","events"]},"PayeeAccountInput":{"type":"object","properties":{"type":{"type":"string","enum":["mobile_money","till","paybill","bank_account"]},"number":{"type":"string","example":"0712345678","description":"M-Pesa phone, till number, paybill number or bank account number"},"bank_code":{"type":"string","example":"68","description":"bank_account only: the Paystack bank code (e.g. 68 Equity)"},"account_reference":{"type":"string","description":"paybill only (required): the account number at that paybill"}},"required":["type","number"]},"PayeeCreateRequest":{"type":"object","properties":{"reference":{"type":"string","example":"vendor-123","description":"Your own id for the vendor; unique per platform and mode"},"name":{"type":"string","example":"Jane W Doe","description":"Compared with the account holder name"},"account":{"$ref":"#/components/schemas/PayeeAccountInput"},"metadata":{"type":"object","description":"Up to 50 keys / 4 KB"}},"required":["reference","name","account"]},"PayeeUpdateRequest":{"type":"object","properties":{"name":{"type":"string"},"metadata":{"type":"object","description":"Replaces the stored metadata"},"account":{"$ref":"#/components/schemas/PayeeAccountInput"}}},"Payee":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"object":{"type":"string","example":"payee"},"reference":{"type":"string","example":"vendor-123"},"name":{"type":"string","example":"Jane's Shop"},"status":{"type":"string","enum":["verifying","verified","name_mismatch","disabled"]},"is_test":{"type":"boolean"},"verified_name":{"type":"string","nullable":true,"example":"JANE W DOE"},"name_match":{"type":"boolean","nullable":true},"account":{"type":"object","nullable":true,"properties":{"type":{"type":"string","enum":["mobile_money","till","paybill","bank_account"]},"bank_code":{"type":"string","example":"mpesa"},"bank_name":{"type":"string","example":"M-Pesa"},"number_masked":{"type":"string","example":"•••• 4821","description":"Full account numbers are never returned"},"cooling_off_until":{"type":"string","format":"date-time","description":"No payout goes to this account before this time"},"cooling_off_active":{"type":"boolean","description":"true while the 24-hour cooling-off runs"}}},"metadata":{"type":"object"},"disabled_at":{"type":"string","format":"date-time","nullable":true},"created_at":{"type":"string","format":"date-time"},"updated_at":{"type":"string","format":"date-time"}}},"PayeeResponse":{"type":"object","properties":{"status":{"type":"string","example":"success"},"data":{"$ref":"#/components/schemas/Payee"}}},"PayeeListResponse":{"type":"object","properties":{"status":{"type":"string","example":"success"},"data":{"type":"array","items":{"$ref":"#/components/schemas/Payee"}},"has_more":{"type":"boolean"},"next_cursor":{"type":"string","nullable":true,"description":"Pass as starting_after for the next page"}}},"PayeeBalance":{"type":"object","properties":{"payee_id":{"type":"string","format":"uuid"},"currency":{"type":"string","example":"KES"},"escrow":{"type":"number","example":4500,"description":"KES held on orders the buyer hasn't released yet"},"available":{"type":"number","example":2250,"description":"KES released and waiting for the next payout"},"pending_payout":{"type":"number","example":0,"description":"KES in a payout sent but not yet confirmed"}}},"PayeePayout":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"payee_id":{"type":"string","format":"uuid"},"amount":{"type":"number","example":2250,"description":"KES taken from the payee's balance"},"fee":{"type":"number","example":60},"net_amount":{"type":"number","example":2190},"currency":{"type":"string","example":"KES"},"status":{"type":"string","enum":["pending","processing","settled","failed","reversed"]},"failure_reason":{"type":"string","nullable":true},"account":{"type":"object","nullable":true,"properties":{"type":{"type":"string"},"bank_name":{"type":"string"},"number_masked":{"type":"string","example":"•••• 5678"}}},"created_at":{"type":"string","format":"date-time"},"settled_at":{"type":"string","format":"date-time","nullable":true}}},"SimulatePaymentRequest":{"type":"object","properties":{"transaction_id":{"type":"string","format":"uuid","description":"Transaction UUID to simulate"},"action":{"type":"string","enum":["authorize","fail","timeout","reverse","complete"],"description":"The simulated state transition to trigger."},"action_note":{"type":"string","description":"Allowed actions also include timeout (same as fail)."},"test_phone":{"type":"string","example":"+254712345678"},"test_receipt":{"type":"string","example":"SIM89123456"}},"required":["transaction_id","action"]}},"responses":{"PayeesForbiddenError":{"description":"The key lacks the payees scope, or payees are not switched on for your platform (code payees_not_enabled)","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","example":"error"},"error":{"type":"string","example":"Forbidden"},"code":{"type":"string","example":"payees_not_enabled"},"message":{"type":"string"}}}}}},"BadRequestError":{"description":"Invalid input parameters or validation failure","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","example":"error"},"error":{"type":"string","example":"Bad Request"},"message":{"type":"string"},"code":{"type":"string","example":"VALIDATION_ERROR"}}}}}},"UnauthorizedError":{"description":"Missing, invalid, or expired API Key","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","example":"error"},"error":{"type":"string","example":"Unauthorized"},"message":{"type":"string","example":"Invalid or missing KashLink API key."},"code":{"type":"string","example":"UNAUTHORIZED"}}}}}},"ForbiddenError":{"description":"Insufficient permissions or sandbox action blocked in live environment","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","example":"error"},"error":{"type":"string","example":"Forbidden"},"message":{"type":"string"},"code":{"type":"string","example":"SANDBOX_LIVE_GUARDRAIL_BLOCKED"}}}}}},"NotFoundError":{"description":"Requested resource does not exist","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","example":"error"},"error":{"type":"string","example":"Not Found"},"message":{"type":"string"},"code":{"type":"string","example":"NOT_FOUND"}}}}}},"RateLimitError":{"description":"Too many requests; rate limit exceeded","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","example":"error"},"error":{"type":"string","example":"Too Many Requests"},"message":{"type":"string"},"code":{"type":"string","example":"RATE_LIMIT_EXCEEDED"}}}}}},"UnprocessableEntityError":{"description":"Business rule violation (insufficient funds, unverified account, invalid status)","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","example":"error"},"error":{"type":"string","example":"Unprocessable Entity"},"code":{"type":"string","enum":["INSUFFICIENT_AVAILABLE_BALANCE","PAYOUT_DESTINATION_UNVERIFIED","INVALID_ESCROW_STATUS","NOT_API_ORDER","PAYEE_ORDER_BUYER_CODE_ONLY"],"example":"INSUFFICIENT_AVAILABLE_BALANCE"},"message":{"type":"string","example":"Insufficient available balance. Required: KES 115, Available: KES 45"}},"required":["status","error","code","message"]}}}}}}}