# Bayar Partner API — complete reference Version 1.0.0. Generated from api/openapi.yaml. Canonical: https://docs.beever.xyz Partner-facing REST API for the Bayar payment orchestrator. These are the endpoints the Partner Dashboard consumes, authenticated with a partner API key. This spec is hand-authored to describe the existing handlers in `internal/api` and `cmd/bayar/serve.go`; keep it in sync when those change. --- # Account The authenticated partner: who the calling key belongs to, and the account-level settings that shape every other response. Part of the **Bayar Partner API** v1.0.0. Full reference: https://docs.beever.xyz ### GET /me **The authenticated partner's own profile** `operationId: getMe` #### Responses | Status | Description | |---|---| | `200` | Partner profile | | `401` | Missing or invalid credentials | `401` example: ```json { "error": { "code": "validation_error", "message": "amount has more decimal places than MYR allows" } } ``` --- # Payments Create, list, and inspect collections and payouts. Part of the **Bayar Partner API** v1.0.0. Full reference: https://docs.beever.xyz ### GET /payments **List payments (cursor-paginated)** `operationId: listPayments` #### Parameters | Name | In | Required | Type | Description | |---|---|---|---|---| | `limit` | query | no | integer | — | | `cursor` | query | no | string | Opaque cursor from a prior response's next_cursor | | `state` | query | no | string | — | | `currency` | query | no | string | — | | `method` | query | no | string | — | | `direction` | query | no | Direction | — | | `from` | query | no | string | Inclusive lower bound on `created_at`. Two forms are accepted: an RFC3339 timestamp (`2026-07-01T09:30:00Z`), or a UTC calendar date (`2026-07-01`), read as the START of that UTC day. Any other value is a 400 — a malformed bound is never ignored, because a silently dropped filter would return the partner's entire history under the guise of a narrow window. | | `to` | query | no | string | Inclusive upper bound on `created_at`. An RFC3339 timestamp, or a UTC calendar date read as the END of that UTC day (`2026-07-31` -> `23:59:59.999999Z`). Any other value is a 400. | #### Responses | Status | Description | |---|---| | `200` | A page of payments | | `400` | Invalid request | | `401` | Missing or invalid credentials | `200` example: ```json { "data": [ { "id": "pay_01H8ABCDEFGHJKMNPQRSTVWXYZ", "merchant_reference": "order-4821", "state": "succeeded", "direction": "collection", "payment_method": "online_banking", "presentment": { "amount": "150.00", "currency": "MYR" }, "settlement": { "amount": "150.00", "currency": "MYR" }, "settled_total": "150.00", "reversed_total": "0.00", "created_at": "2026-07-14T09:31:22Z", "updated_at": "2026-07-14T09:33:05Z" }, { "id": "pay_01H8BCDEFGHJKMNPQRSTVWXYZA", "merchant_reference": "payout-1190", "state": "processing", "direction": "payout", "payment_method": "bank_transfer", "presentment": { "amount": "2400.00", "currency": "MYR" }, "settlement": { "amount": "2400.00", "currency": "MYR" }, "settled_total": "0.00", "reversed_total": "0.00", "created_at": "2026-07-14T08:02:10Z", "updated_at": "2026-07-14T08:02:44Z" } ], "next_cursor": "eyJpZCI6InBheV8wMUg4QUJDIn0" } ``` `400` example: ```json { "error": { "code": "validation_error", "message": "amount has more decimal places than MYR allows" } } ``` `401` example: ```json { "error": { "code": "validation_error", "message": "amount has more decimal places than MYR allows" } } ``` ### POST /payments **Create a payment (collection or payout)** `operationId: createPayment` #### Parameters | Name | In | Required | Type | Description | |---|---|---|---|---| | `Idempotency-Key` | header | no | string | Caller-generated key that makes a mutation safely retryable. Every mutating operation honours it. Replaying the same key with the same body returns the first response verbatim; the same key with a DIFFERENT body is a 409 (`idempotency_conflict`). Keys expire after 24h. See ADR 0006. | #### Request body (required) | Field | Type | Required | Description | |---|---|---|---| | `merchant_reference` | string | no | Your own reference for this payment. Echoed back on every read. | | `amount` | string | yes | Decimal string in major units. Must have no more decimal places than the currency allows (2 for MYR, 0 for JPY) — an over-precise amount is a 400, never a silent round. | | `currency` | CurrencyCode | yes | — | | `method` | string | yes | The rail to use. Which values are valid depends on the routed PSP and the country. | | `direction` | Direction | yes | — | | `country` | string | no | ISO-3166-1 alpha-2 country of the payer or beneficiary. Feeds routing. | | `notification_url` | string | no | Per-payment webhook override. Leave unset to use the account's configured webhook endpoints, which is the normal case. | | `return_url` | string | no | Where to send the payer's browser after a redirect-based rail completes. | | `instrument` | object | no | Per-attempt payer details, shaped by `method` — a bank code for online banking, a beneficiary id for a payout. Consult the method's own docs. | | `metadata` | object | no | Free-form key/value bag stored with the payment and returned on read. Never interpreted by Bayar. Do not put anything here that the payment engine needs to behave correctly. | Example: ```json { "merchant_reference": "order-4821", "amount": "150.00", "currency": "MYR", "method": "online_banking", "direction": "collection", "country": "MY", "return_url": "https://merchant.example/checkout/return" } ``` #### Responses | Status | Description | |---|---| | `201` | Created | | `400` | Invalid request | | `401` | Missing or invalid credentials | | `409` | Conflict | `201` example: ```json { "id": "pay_01H8ABCDEFGHJKMNPQRSTVWXYZ", "merchant_reference": "order-4821", "state": "succeeded", "direction": "collection", "payment_method": "online_banking", "presentment": { "amount": "150.00", "currency": "MYR" }, "settlement": { "amount": "150.00", "currency": "MYR" }, "settled_total": "150.00", "reversed_total": "0.00", "created_at": "2026-07-14T09:31:22Z", "updated_at": "2026-07-14T09:33:05Z" } ``` `400` example: ```json { "error": { "code": "validation_error", "message": "amount has more decimal places than MYR allows" } } ``` `401` example: ```json { "error": { "code": "validation_error", "message": "amount has more decimal places than MYR allows" } } ``` `409` example: ```json { "error": { "code": "validation_error", "message": "amount has more decimal places than MYR allows" } } ``` ### GET /payments/{id} **Get a payment by id (or ?merchant_reference=)** `operationId: getPayment` #### Parameters | Name | In | Required | Type | Description | |---|---|---|---|---| | `id` | path | yes | string | — | | `merchant_reference` | query | no | string | — | #### Responses | Status | Description | |---|---| | `200` | The payment | | `401` | Missing or invalid credentials | | `404` | Not found | `200` example: ```json { "id": "pay_01H8ABCDEFGHJKMNPQRSTVWXYZ", "merchant_reference": "order-4821", "state": "succeeded", "direction": "collection", "payment_method": "online_banking", "presentment": { "amount": "150.00", "currency": "MYR" }, "settlement": { "amount": "150.00", "currency": "MYR" }, "settled_total": "150.00", "reversed_total": "0.00", "created_at": "2026-07-14T09:31:22Z", "updated_at": "2026-07-14T09:33:05Z" } ``` `401` example: ```json { "error": { "code": "validation_error", "message": "amount has more decimal places than MYR allows" } } ``` `404` example: ```json { "error": { "code": "validation_error", "message": "amount has more decimal places than MYR allows" } } ``` ### GET /payments/{id}/timeline **A payment's ordered state-transition history** `operationId: getPaymentTimeline` #### Parameters | Name | In | Required | Type | Description | |---|---|---|---|---| | `id` | path | yes | string | — | #### Responses | Status | Description | |---|---| | `200` | The payment's transitions, chronological | | `401` | Missing or invalid credentials | | `404` | Not found | `200` example: ```json { "data": [ { "from_state": null, "to_state": "created", "reason": "api_create", "actor_kind": "api_key", "actor_id": "apk_01H8ABCDEFGHJKMNPQRSTVWXYZ", "at": "2026-07-14T09:31:22Z" }, { "from_state": "created", "to_state": "authorizing", "reason": "route_selected", "actor_kind": "system", "actor_id": null, "at": "2026-07-14T09:31:23Z" }, { "from_state": "authorizing", "to_state": "succeeded", "reason": "psp_callback", "actor_kind": "psp", "actor_id": "psp_01H8MAXPAY0000000000000000", "at": "2026-07-14T09:33:05Z" } ] } ``` `401` example: ```json { "error": { "code": "validation_error", "message": "amount has more decimal places than MYR allows" } } ``` `404` example: ```json { "error": { "code": "validation_error", "message": "amount has more decimal places than MYR allows" } } ``` ### GET /payments/export **Export payments as CSV** `operationId: exportPayments` #### Parameters | Name | In | Required | Type | Description | |---|---|---|---|---| | `format` | query | yes | `csv` | — | | `from` | query | no | string | Inclusive lower bound on `created_at`. Two forms are accepted: an RFC3339 timestamp (`2026-07-01T09:30:00Z`), or a UTC calendar date (`2026-07-01`), read as the START of that UTC day. Any other value is a 400 — a malformed bound is never ignored, because a silently dropped filter would return the partner's entire history under the guise of a narrow window. | | `to` | query | no | string | Inclusive upper bound on `created_at`. An RFC3339 timestamp, or a UTC calendar date read as the END of that UTC day (`2026-07-31` -> `23:59:59.999999Z`). Any other value is a 400. | | `state` | query | no | string | — | | `direction` | query | no | Direction | — | #### Responses | Status | Description | |---|---| | `200` | CSV stream | | `400` | Invalid request | | `401` | Missing or invalid credentials | | `422` | The window would export more than 10,000 rows. Narrow it via from/to. Code `export_too_large`. | `400` example: ```json { "error": { "code": "validation_error", "message": "amount has more decimal places than MYR allows" } } ``` `401` example: ```json { "error": { "code": "validation_error", "message": "amount has more decimal places than MYR allows" } } ``` `422` example: ```json { "error": { "code": "validation_error", "message": "amount has more decimal places than MYR allows" } } ``` ### GET /payments/{id}/next_action **The current next_action for an in-flight payment (pull-on-demand)** `operationId: getPaymentNextAction` Returns the payment's live next action, re-fetching a fresh one from the PSP when the stored action is missing or close to expiry (e.g. a DuitNow QR, which rotates about every minute). Poll this near expiry rather than caching the action. #### Parameters | Name | In | Required | Type | Description | |---|---|---|---|---| | `id` | path | yes | string | — | #### Responses | Status | Description | |---|---| | `200` | The current next action | | `401` | Missing or invalid credentials | | `404` | Payment not found (`not_found`), or the payment has no next action — e.g. it is terminal (`no_next_action`). | | `502` | The PSP could not be reached to refresh the action. Code `psp_error`. | `401` example: ```json { "error": { "code": "validation_error", "message": "amount has more decimal places than MYR allows" } } ``` `404` example: ```json { "error": { "code": "validation_error", "message": "amount has more decimal places than MYR allows" } } ``` `502` example: ```json { "error": { "code": "validation_error", "message": "amount has more decimal places than MYR allows" } } ``` --- # Balances Per-currency held/reserved/available balances. Part of the **Bayar Partner API** v1.0.0. Full reference: https://docs.beever.xyz ### GET /balances **Partner balances per currency (held / reserved / available)** `operationId: getBalances` #### Responses | Status | Description | |---|---| | `200` | Balances | | `401` | Missing or invalid credentials | `401` example: ```json { "error": { "code": "validation_error", "message": "amount has more decimal places than MYR allows" } } ``` --- # Settlements Payout settlements (a filtered view of payments). Part of the **Bayar Partner API** v1.0.0. Full reference: https://docs.beever.xyz ### GET /settlements **List settlements (alias for payments with direction=payout)** `operationId: listSettlements` Delegates to listPayments with direction forced to `payout`; it therefore accepts the same filters. A caller-supplied `direction` is ignored, so it is not declared. #### Parameters | Name | In | Required | Type | Description | |---|---|---|---|---| | `limit` | query | no | integer | — | | `cursor` | query | no | string | — | | `state` | query | no | string | — | | `currency` | query | no | string | — | | `method` | query | no | string | — | | `from` | query | no | string | Inclusive lower bound on `created_at`. Two forms are accepted: an RFC3339 timestamp (`2026-07-01T09:30:00Z`), or a UTC calendar date (`2026-07-01`), read as the START of that UTC day. Any other value is a 400 — a malformed bound is never ignored, because a silently dropped filter would return the partner's entire history under the guise of a narrow window. | | `to` | query | no | string | Inclusive upper bound on `created_at`. An RFC3339 timestamp, or a UTC calendar date read as the END of that UTC day (`2026-07-31` -> `23:59:59.999999Z`). Any other value is a 400. | #### Responses | Status | Description | |---|---| | `200` | A page of settlement payments | | `400` | Invalid request | | `401` | Missing or invalid credentials | `200` example: ```json { "data": [ { "id": "pay_01H8ABCDEFGHJKMNPQRSTVWXYZ", "merchant_reference": "order-4821", "state": "succeeded", "direction": "collection", "payment_method": "online_banking", "presentment": { "amount": "150.00", "currency": "MYR" }, "settlement": { "amount": "150.00", "currency": "MYR" }, "settled_total": "150.00", "reversed_total": "0.00", "created_at": "2026-07-14T09:31:22Z", "updated_at": "2026-07-14T09:33:05Z" }, { "id": "pay_01H8BCDEFGHJKMNPQRSTVWXYZA", "merchant_reference": "payout-1190", "state": "processing", "direction": "payout", "payment_method": "bank_transfer", "presentment": { "amount": "2400.00", "currency": "MYR" }, "settlement": { "amount": "2400.00", "currency": "MYR" }, "settled_total": "0.00", "reversed_total": "0.00", "created_at": "2026-07-14T08:02:10Z", "updated_at": "2026-07-14T08:02:44Z" } ], "next_cursor": "eyJpZCI6InBheV8wMUg4QUJDIn0" } ``` `400` example: ```json { "error": { "code": "validation_error", "message": "amount has more decimal places than MYR allows" } } ``` `401` example: ```json { "error": { "code": "validation_error", "message": "amount has more decimal places than MYR allows" } } ``` --- # Beneficiaries Payout destinations (bank accounts, wallets, cards, and similar). Part of the **Bayar Partner API** v1.0.0. Full reference: https://docs.beever.xyz ### GET /beneficiaries **List beneficiaries** `operationId: listBeneficiaries` #### Responses | Status | Description | |---|---| | `200` | Beneficiaries | | `401` | Missing or invalid credentials | `401` example: ```json { "error": { "code": "validation_error", "message": "amount has more decimal places than MYR allows" } } ``` ### POST /beneficiaries **Create a beneficiary** `operationId: createBeneficiary` #### Parameters | Name | In | Required | Type | Description | |---|---|---|---|---| | `Idempotency-Key` | header | no | string | Caller-generated key that makes a mutation safely retryable. Every mutating operation honours it. Replaying the same key with the same body returns the first response verbatim; the same key with a DIFFERENT body is a 409 (`idempotency_conflict`). Keys expire after 24h. See ADR 0006. | #### Request body (required) | Field | Type | Required | Description | |---|---|---|---| | `type` | `bank_account` \| `iban` \| `upi_vpa` \| `pix_key` \| `wallet` \| `card` | yes | The destination kind. Enforced by a CHECK on the column, not by the handler — so an unlisted value fails as a 500, not a 400. Send one of these. | | `account_ref` | string | yes | — | | `details` | object | no | — | | `currency` | string | no | — | Example: ```json { "type": "bank_account", "account_ref": "157203004488", "details": { "bank_code": "MBBEMYKL", "account_holder": "Acme Payments Sdn Bhd" }, "currency": "MYR" } ``` #### Responses | Status | Description | |---|---| | `201` | Created | | `401` | Missing or invalid credentials | | `409` | Conflict | `201` example: ```json { "id": "ben_01H8ABCDEFGHJKMNPQRSTVWXYZ", "type": "bank_account", "account_ref": "157203004488", "details": { "bank_code": "MBBEMYKL", "account_holder": "Acme Payments Sdn Bhd" }, "currency": "MYR", "status": "active" } ``` `401` example: ```json { "error": { "code": "validation_error", "message": "amount has more decimal places than MYR allows" } } ``` `409` example: ```json { "error": { "code": "validation_error", "message": "amount has more decimal places than MYR allows" } } ``` ### PUT /beneficiaries/{id} **Update a beneficiary** `operationId: updateBeneficiary` #### Parameters | Name | In | Required | Type | Description | |---|---|---|---|---| | `id` | path | yes | string | — | | `Idempotency-Key` | header | no | string | Caller-generated key that makes a mutation safely retryable. Every mutating operation honours it. Replaying the same key with the same body returns the first response verbatim; the same key with a DIFFERENT body is a 409 (`idempotency_conflict`). Keys expire after 24h. See ADR 0006. | #### Request body (required) | Field | Type | Required | Description | |---|---|---|---| | `account_ref` | string | no | — | | `details` | object | no | — | | `currency` | string | no | — | | `status` | `active` \| `disabled` | no | Enforced by the column's CHECK, not the handler — an unlisted value fails as a 500, not a 400. | Example: ```json { "status": "disabled" } ``` #### Responses | Status | Description | |---|---| | `204` | Updated | | `401` | Missing or invalid credentials | | `409` | Conflict | `401` example: ```json { "error": { "code": "validation_error", "message": "amount has more decimal places than MYR allows" } } ``` `409` example: ```json { "error": { "code": "validation_error", "message": "amount has more decimal places than MYR allows" } } ``` ### DELETE /beneficiaries/{id} **Delete a beneficiary** `operationId: deleteBeneficiary` #### Parameters | Name | In | Required | Type | Description | |---|---|---|---|---| | `id` | path | yes | string | — | | `Idempotency-Key` | header | no | string | Caller-generated key that makes a mutation safely retryable. Every mutating operation honours it. Replaying the same key with the same body returns the first response verbatim; the same key with a DIFFERENT body is a 409 (`idempotency_conflict`). Keys expire after 24h. See ADR 0006. | #### Responses | Status | Description | |---|---| | `204` | Deleted | | `401` | Missing or invalid credentials | | `409` | Conflict | `401` example: ```json { "error": { "code": "validation_error", "message": "amount has more decimal places than MYR allows" } } ``` `409` example: ```json { "error": { "code": "validation_error", "message": "amount has more decimal places than MYR allows" } } ``` --- # Disputes Chargebacks, returns, inquiries, and retrievals against payments. Part of the **Bayar Partner API** v1.0.0. Full reference: https://docs.beever.xyz ### GET /disputes **List disputes (chargebacks, returns, inquiries, retrievals)** `operationId: listDisputes` Partner-scoped, read-only. **Bayar has no dispute ingestion path yet**: no PSP adapter reports chargebacks and no engine transition produces a dispute, so this endpoint returns an empty list for every partner today. An empty `data` array means "Bayar has ingested no disputes" — it does NOT mean "no chargebacks occurred against your payments". Do not present it to an end user as an authoritative chargeback record until ingestion ships. #### Parameters | Name | In | Required | Type | Description | |---|---|---|---|---| | `limit` | query | no | integer | — | | `cursor` | query | no | string | — | | `state` | query | no | `received` \| `evidence_required` \| `under_review` \| `won` \| `lost` \| `accepted` \| `expired` | — | | `kind` | query | no | `chargeback` \| `return` \| `inquiry` \| `retrieval` | — | | `payment_id` | query | no | string | — | | `from` | query | no | string | Inclusive lower bound on `created_at`. Two forms are accepted: an RFC3339 timestamp (`2026-07-01T09:30:00Z`), or a UTC calendar date (`2026-07-01`), read as the START of that UTC day. Any other value is a 400 — a malformed bound is never ignored, because a silently dropped filter would return the partner's entire history under the guise of a narrow window. | | `to` | query | no | string | Inclusive upper bound on `created_at`. An RFC3339 timestamp, or a UTC calendar date read as the END of that UTC day (`2026-07-31` -> `23:59:59.999999Z`). Any other value is a 400. | #### Responses | Status | Description | |---|---| | `200` | A page of disputes | | `400` | Invalid request | | `401` | Missing or invalid credentials | `400` example: ```json { "error": { "code": "validation_error", "message": "amount has more decimal places than MYR allows" } } ``` `401` example: ```json { "error": { "code": "validation_error", "message": "amount has more decimal places than MYR allows" } } ``` ### GET /disputes/{id} **Get a dispute, including its evidence blob** `operationId: getDispute` #### Parameters | Name | In | Required | Type | Description | |---|---|---|---|---| | `id` | path | yes | string | — | #### Responses | Status | Description | |---|---| | `200` | The dispute | | `401` | Missing or invalid credentials | | `404` | Not found | `401` example: ```json { "error": { "code": "validation_error", "message": "amount has more decimal places than MYR allows" } } ``` `404` example: ```json { "error": { "code": "validation_error", "message": "amount has more decimal places than MYR allows" } } ``` ---