openapi: 3.1.0 info: title: ROKI Connect API version: "2.0.0" summary: ROKI payments - hosted links, embedded card components, and saved-card charges. description: | **ROKI Connect** lets a merchant's server accept card payments in three ways. In every mode the merchant never receives, transmits or stores the card number: card entry always happens on ROKI-controlled surfaces. ## The three modes | Mode | What it is | Card entry | Frontend credential | Backend credential | |---|---|---|---|---| | **1 - Hosted checkout** | Redirect the customer to a ROKI-hosted page | ROKI page | none | `sk_*` creates the payment | | **2 - Embedded components** | ROKI card fields inside a secure iframe on your own site | ROKI iframe | `pk_*` | `sk_*` confirms with the amount | | **3A - Tokenized payments** | Charge a card the customer already saved | none | none | `sk_*` charges an opaque `pm_*` | Mode 3B is not available yet. In modes 2 and 3A the customer's browser talks **only to your own backend**. Your backend is the only place that holds `sk_*` and calls ROKI directly. A "pay with saved card" button must call your server, never a ROKI secret-key endpoint. ## Status of this specification Verified empirically against the live API in sandbox and production (2026-08-12). See the contract test in `probes/` for the executable version of these claims. ## Critical integration warning **The API silently ignores unknown fields.** A body with a misspelled field name returns `201 Created` with no warning and the payment is created without that feature. Use exactly the names defined here, and verify the computed fields in the response (`sales_tax_amount`, `service_fee_amount`, `total`) against what you intended. One exception now works in your favour: `customer` and `lock_customer_fields` **are** echoed back in the response, so you can confirm the prefill was applied rather than having to open the checkout to check. ## Recent changes `GET /payments` (list, paginated and filterable) and the echoed `customer` object were added on 2026-08-13. Earlier integrations may still assume neither exists. contact: name: ROKI url: https://aura.roki.systems/merchant/connect license: name: Proprietary - ROKI url: https://aura.roki.systems x-verified-at: "2026-08-12" x-verification-method: "Empirical probing against the live API (sandbox and production)" servers: - url: https://aura.roki.systems/api/connect/v1 description: | Main API. The environment is determined solely by the API key prefix: `sk_test_` = sandbox, `sk_live_` = production. Routes are identical in both. security: - bearerAuth: [] tags: - name: Payments description: Creating and retrieving payments (all modes). - name: Transaction actions description: Void, refund and receipts. Indexed by transaction, not by payment. - name: Embedded components description: Mode 2 - card fields inside a ROKI iframe on the merchant's own site. - name: Saved cards description: Mode 3A - charging a card the customer already saved. paths: /payments: post: operationId: createPayment tags: [Payments] summary: Create a payment description: | Creates a payment and returns a `checkout_url` (mode 1) plus a payment `id`. Set `amount` to the exact total your platform needs to charge for this checkout - take it from the order or cart in your backend. ROKI does not read the amount from your site. Always send `Idempotency-Key` so a network retry cannot create a duplicate payment. parameters: - $ref: '#/components/parameters/AcceptLanguage' - $ref: '#/components/parameters/IdempotencyKey' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/PaymentCreateRequest' examples: minimal: summary: Minimal (required fields only) value: amount: 1500.00 external_reference: "order-1001" name: "Order #1001" full: summary: Full (customer prefill, tax, tip, service fee, expiry) value: amount: 1500.00 currency_code: "340" external_reference: "order-1001" name: "Order #1001" description: "Table for four" metadata: { order_id: "1001", table: "12" } success_url: "https://yoursite.com/success" cancel_url: "https://yoursite.com/cancel" expires_at: "2026-08-13 18:00:00" customer: name: "Ahmed Khan" email: "ahmed@example.com" phone: "+50499999999" identity_number: "0801199912345" lock_customer_fields: true reusable: false sales_tax_type: "percentage" sales_tax_value: 15 tip_enabled: true tip_customer_selectable: true tip_preset_percentages: [10, 15, 20] tip_allow_custom: true tip_min_amount: 0 tip_max_amount: 500 service_fee_enabled: true responses: '201': description: Payment created. content: application/json: schema: $ref: '#/components/schemas/Payment' examples: created: value: id: 42 status: "pending" name: "Order #1001" description: null amount: 1500.00 reusable: false subtotal: 1500.00 sales_tax_amount: 0 service_fee_amount: 0 total: 1500.00 currency: "340" currency_iso: "HNL" external_reference: "order-1001" metadata: { order_id: "1001" } checkout_url: "https://aura.roki.systems/pay/link/ksdjrovxqa60" transaction_id: null expires_at: "2026-08-13 18:00:00" created_at: "2026-08-12 12:00:00" paid_at: null '401': $ref: '#/components/responses/Unauthorized' '422': $ref: '#/components/responses/ValidationFailed' get: operationId: listPayments tags: [Payments] summary: List payments description: | Returns the merchant's payments, newest first, paginated. Scoped to the key's merchant and environment: a sandbox key never sees production payments. This is what makes reconciliation possible - before it existed, a payment whose `id` you had lost was unreachable. parameters: - $ref: '#/components/parameters/AcceptLanguage' - name: per_page in: query description: | Page size. Defaults to 20. **Note `limit` is ignored** - only `per_page` changes the page size. schema: { type: integer, minimum: 1 } example: 50 - name: page in: query description: 1-based page number. Use `meta.last_page` to know when to stop. schema: { type: integer, minimum: 1 } example: 2 - name: status in: query description: | Filter by payment status. An unrecognized value returns `422` rather than being ignored. schema: { $ref: '#/components/schemas/PaymentStatus' } - name: external_reference in: query description: | Filter by your own order id. Since `external_reference` is not unique, this can return more than one payment. schema: { type: string } - name: from in: query description: Start of a creation-date range, `YYYY-MM-DD`, Honduras time. schema: { type: string } example: "2026-08-01" - name: to in: query description: End of the creation-date range, inclusive. schema: { type: string } example: "2026-08-31" responses: '200': description: A page of payments, newest first. content: application/json: schema: type: object required: [data, meta] properties: data: type: array items: { $ref: '#/components/schemas/Payment' } meta: type: object description: | Pagination. `total` is the count matching the filters, not the page size. properties: current_page: { type: integer, example: 1 } per_page: { type: integer, example: 20 } total: { type: integer, example: 47 } last_page: { type: integer, example: 3 } '401': { $ref: '#/components/responses/Unauthorized' } '422': description: An invalid filter value, for example an unknown `status`. content: application/json: schema: { $ref: '#/components/schemas/ValidationError' } /payments/{id}: parameters: - name: id in: path required: true description: Numeric payment identifier returned at creation. schema: { type: integer, format: int64, minimum: 1 } example: 42 get: operationId: getPayment tags: [Payments] summary: Retrieve a payment description: | Returns the current state of the payment. Together with webhooks, this is the source of truth for confirming a charge. **A customer landing on `success_url` is NOT payment confirmation** - anyone can navigate to that URL. Confirm with this call or with the `payment.approved` webhook. Once paid, `transaction_id` holds the UUID you need for void, refund and receipts. parameters: - $ref: '#/components/parameters/AcceptLanguage' responses: '200': description: Payment found. content: application/json: schema: { $ref: '#/components/schemas/Payment' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/PaymentNotFound' } /payments/{transaction_id}/void: parameters: - $ref: '#/components/parameters/TransactionId' post: operationId: voidTransaction tags: [Transaction actions] summary: Void a transaction description: | Cancels an eligible transaction before settlement. **Full amount only.** Void acts on a **transaction**, identified by the `transaction_id` UUID from the payment response - not by the payment `id`. On a reusable payment link this voids only that customer's transaction; the link's other transactions are untouched. Eligibility is governed by the transaction's configured time window, not only by settlement state. Once a transaction is no longer voidable, use refund instead. Typical cases: the customer changed their mind right after paying, the order was placed by mistake, or fraud was detected before settlement. parameters: - $ref: '#/components/parameters/AcceptLanguage' requestBody: required: false content: application/json: schema: type: object additionalProperties: false properties: reason: type: string maxLength: 2000 description: Optional free-text reason, stored with the void. example: { reason: "Customer cancelled" } responses: '200': description: Transaction voided. content: application/json: schema: { $ref: '#/components/schemas/Payment' } example: id: 42 status: "voided" name: "Order #1001" amount: 1500.00 subtotal: 1500.00 total: 1500.00 currency: "340" currency_iso: "HNL" checkout_url: "https://aura.roki.systems/pay/link/ksdjrovxqa60" transaction_id: "9e2b6a34-6f1d-4e2a-8c9b-2f7a1d4e5c6b" external_reference: "order-1001" created_at: "2026-08-12 12:00:00" refunded_amount: 1500.00 refund_status: "full" '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/TransactionNotFound' } '422': description: | Not voidable. `errors.void` carries the reason: already voided, already refunded (use refund), not in a voidable state, or already settled (use refund). content: application/json: schema: { $ref: '#/components/schemas/ValidationError' } /payments/{transaction_id}/refund: parameters: - $ref: '#/components/parameters/TransactionId' post: operationId: refundTransaction tags: [Transaction actions] summary: Refund a transaction description: | Refunds part or all of an eligible settled transaction. Partial refunds leave the payment `partially_refunded`; a full refund sets `refunded`. Like void, this acts on a **transaction UUID**, not a payment id, so on a reusable link only that customer's transaction is refunded. Eligibility also depends on the transaction's configured time window. Attempting a refund before the transaction is eligible can return `422` with a processor rejection such as `"Invalid transaction"` - that text comes from the payment processor and is not a response guaranteed by ROKI. For eligible pre-settlement cancellations use void instead. Refund works identically for any Connect transaction regardless of the mode that created it, including an embedded checkout (mode 2) or a saved-card charge (mode 3A). There is no separate refund endpoint for token charges: use the `id` returned by the charge as the `transaction_id` here. parameters: - $ref: '#/components/parameters/AcceptLanguage' requestBody: required: true content: application/json: schema: type: object required: [amount] additionalProperties: false properties: amount: type: number format: double minimum: 0.01 description: Amount to refund, up to the remaining refundable amount. reason: type: string maxLength: 2000 example: { amount: 500.00, reason: "Partial return" } responses: '200': description: Refund applied. content: application/json: schema: { $ref: '#/components/schemas/Payment' } example: id: 42 status: "partially_refunded" name: "Order #1001" amount: 1500.00 subtotal: 1500.00 total: 1500.00 currency: "340" currency_iso: "HNL" checkout_url: "https://aura.roki.systems/pay/link/ksdjrovxqa60" transaction_id: "9e2b6a34-6f1d-4e2a-8c9b-2f7a1d4e5c6b" external_reference: "order-1001" created_at: "2026-08-12 12:00:00" refunded_amount: 500.00 refund_status: "partial" '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/TransactionNotFound' } '422': description: | Rejected. `errors.amount`: below 0.01, or above the remaining refundable amount. `errors.refund`: transaction is voided (a voided transaction cannot be refunded), not in a refundable state, or a processor rejection such as `"Invalid transaction"`. content: application/json: schema: { $ref: '#/components/schemas/ValidationError' } /payments/{transaction_id}/receipt: parameters: - $ref: '#/components/parameters/TransactionId' get: operationId: getReceipt tags: [Transaction actions] summary: Get receipt metadata description: | Receipt metadata for one transaction. Receipts remain available after a void or refund as long as the original charge still has a transaction. responses: '200': description: Receipt metadata. content: application/json: schema: { $ref: '#/components/schemas/Receipt' } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/TransactionNotFound' } /payments/{transaction_id}/receipt/download: parameters: - $ref: '#/components/parameters/TransactionId' get: operationId: downloadReceipt tags: [Transaction actions] summary: Download the receipt PDF description: Returns the receipt as a PDF for that exact transaction. responses: '200': description: PDF document. content: application/pdf: schema: { type: string, format: binary } '401': { $ref: '#/components/responses/Unauthorized' } '404': { $ref: '#/components/responses/TransactionNotFound' } /payment-methods: get: operationId: listPaymentMethods tags: [Saved cards] summary: List a customer's saved cards description: | Returns the saved payment methods for one customer, identified the same way as in Payments. Send `customer[identity_number]` (preferred) or `customer[email]`. A card is saved only when the customer explicitly ticks "save my card" during a normal Connect checkout. ROKI stores the processor's own token - never the card number - and emits the `payment_method.saved` webhook with the opaque `pm_*` reference. parameters: - name: customer[identity_number] in: query required: false description: National identity number of the customer. Preferred identifier. schema: { type: string } example: "0801199012345" - name: customer[email] in: query required: false description: Customer email, when the identity number is unknown. schema: { type: string, format: email } responses: '200': description: | The customer's saved methods. An empty list is a normal `200` with `data: []`, not a 404. content: application/json: schema: type: object required: [data] properties: data: type: array items: { $ref: '#/components/schemas/PaymentMethod' } example: data: - id: "pm_7k2n9xqf31ab" card_brand: "Visa" last_four: "4242" exp_month: 11 exp_year: 2027 is_default: true '401': { $ref: '#/components/responses/Unauthorized' } /payment-methods/{payment_method_id}: parameters: - $ref: '#/components/parameters/PaymentMethodId' delete: operationId: revokePaymentMethod tags: [Saved cards] summary: Revoke a saved card description: | Removes a saved payment method. Later charges against it fail; revocation is not silent. responses: '200': { description: Payment method revoked. } '401': { $ref: '#/components/responses/Unauthorized' } '404': description: Payment method not found. content: application/json: schema: { $ref: '#/components/schemas/ErrorMessage' } example: { message: "Payment method not found." } /payment-methods/{payment_method_id}/charge: parameters: - $ref: '#/components/parameters/PaymentMethodId' post: operationId: chargeSavedCard tags: [Saved cards] summary: Charge a saved card description: | Charges a card the customer already saved, with no new card entry. ROKI verifies that the payment method belongs to you, in the right environment, and is still usable (not revoked or expired), then charges the stored processor token. Neither your server nor the customer's browser ever sees that token. **Never call this from a browser.** It requires `sk_*`. On success, use the returned `id` as the `transaction_id` for void, refund and receipts. parameters: - $ref: '#/components/parameters/AcceptLanguage' - name: Idempotency-Key in: header required: true description: | **Mandatory on this endpoint** - unlike payment creation, where it is merely recommended. Use a unique key per charge attempt so a network retry can never charge twice. Omitting it returns `422`. schema: { type: string, maxLength: 191 } example: "order-2002-charge" requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/TokenChargeRequest' } example: amount: 500.00 currency_code: "HNL" external_reference: "order-2002" responses: '201': description: Charge processed. content: application/json: schema: { $ref: '#/components/schemas/ChargeResult' } example: id: "9e2b6a34-6f1d-4e2a-8c9b-2f7a1d4e5c6b" status: "approved" amount: 500.00 currency_code: "HNL" external_reference: "order-2002" created_at: "2026-08-12 12:10:00" '401': { $ref: '#/components/responses/Unauthorized' } '404': description: Payment method not found or not owned by this merchant. content: application/json: schema: { $ref: '#/components/schemas/ErrorMessage' } '422': description: | Validation failure (including a missing `Idempotency-Key`), or a processor decline carrying `IsoResponseCode` and `Errors`. content: application/json: schema: oneOf: - $ref: '#/components/schemas/ValidationError' - $ref: '#/components/schemas/ProcessorDecline' /payments/token-charge: post: operationId: tokenCharge tags: [Saved cards] summary: Charge a saved card (alias) description: | Equivalent to `POST /payment-methods/{payment_method_id}/charge`, with the saved `pm_*` passed in the body instead of the path. Suited to subscription renewals. Requires `sk_*` and `Idempotency-Key`. Never call it from a browser. parameters: - $ref: '#/components/parameters/AcceptLanguage' - name: Idempotency-Key in: header required: true description: Mandatory. Unique per charge attempt. schema: { type: string, maxLength: 191 } example: "netflix-renewal-1" requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/TokenChargeByBodyRequest' } example: payment_token: "pm_7k2n9xqf31ab" amount: 100.00 currency_code: "HNL" external_reference: "netflix-sub-aug-2026" responses: '200': description: Charge processed. content: application/json: schema: { $ref: '#/components/schemas/ChargeResult' } example: status: "approved" transaction_id: "9e2b6a34-6f1d-4e2a-8c9b-2f7a1d4e5c6b" amount: 100 currency: "HNL" '401': { $ref: '#/components/responses/Unauthorized' } '422': description: Validation failure or processor decline. content: application/json: schema: oneOf: - $ref: '#/components/schemas/ValidationError' - $ref: '#/components/schemas/ProcessorDecline' /confirm: servers: - url: https://aura.roki.systems/api/connect/embed description: Embedded components endpoint. Note the base is /api/connect/embed, not /v1. post: operationId: confirmEmbeddedPayment tags: [Embedded components] summary: Confirm an embedded-components payment description: | Completes a mode 2 payment. The browser tokenizes the card inside the ROKI iframe using `pk_*` and receives a `tok_*`; your **backend** then calls this endpoint with `sk_*`, the token and the amount. Full path: `https://aura.roki.systems/api/connect/embed/confirm` **Never put `sk_*` in the browser.** The frontend sends `tok_*` to your own server, and your server calls this endpoint. Three outcomes: `approved` (with full `transaction_details`), `declined` (with the processor's `IsoResponseCode` and `Errors`), or `pending` with an `authentication_url` that must be loaded so the customer can complete 3-D Secure. requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/EmbedConfirmRequest' } example: amount: 500.00 currency_code: "HNL" external_reference: "order-123" payment_token: "tok_xxxx" publishable_key: "pk_test_xxxxxxxx" success_redirect_url: "https://merchant.example/success" failed_redirect_url: "https://merchant.example/failed" responses: '200': description: | Outcome of the charge. Inspect `status`: `approved`, `declined` or `pending`. content: application/json: schema: oneOf: - $ref: '#/components/schemas/EmbedApproved' - $ref: '#/components/schemas/ProcessorDecline' - $ref: '#/components/schemas/EmbedPending' '401': { $ref: '#/components/responses/Unauthorized' } '422': description: Validation failure or processor decline. content: application/json: schema: oneOf: - $ref: '#/components/schemas/ValidationError' - $ref: '#/components/schemas/ProcessorDecline' webhooks: paymentEvent: post: operationId: receivePaymentWebhook summary: Payment event sent by ROKI description: | ROKI sends a signed `POST` to the HTTPS endpoint registered in the portal (`/merchant/connect/webhooks`), with **separate URLs and signing secrets per environment**. **Signature verification (mandatory):** header `ROKI-Signature: t={timestamp},v1={hex}`. Compute `HMAC-SHA256(timestamp + "." + raw_body, secret)` and compare in constant time. Invalid signature: respond `400`. Valid: respond `200` quickly and process asynchronously. Void and refund events are emitted whether the action came from the API or from the portal. parameters: - name: ROKI-Signature in: header required: true schema: { type: string } example: "t=1719234300,v1=8f3c2a1b..." requestBody: required: true content: application/json: schema: { $ref: '#/components/schemas/WebhookEvent' } responses: '200': { description: Event accepted. } '400': { description: Invalid signature - the merchant rejects the event. } components: securitySchemes: bearerAuth: type: http scheme: bearer description: | The merchant's secret key in `Authorization: Bearer {key}`. `sk_test_...` operates in sandbox and `sk_live_...` in production, over the same routes. Server-side only: never in a browser or mobile app. The publishable key (`pk_*`) is used only by the browser SDK in mode 2, and never authenticates a secret-key endpoint. parameters: AcceptLanguage: name: Accept-Language in: header required: false description: | Language of API messages and validation errors, `es` or `en`. Regional variants such as `en-US` or `es-HN` resolve to their base language. Spanish is the default: requests with no header, an empty header or an unsupported language get Spanish. Does not affect routing errors (404 route not found, 405), which are always English. In mode 2 the SDK `locale` option controls the iframe; send `Accept-Language` separately on confirm. schema: { type: string, enum: [es, en], default: es } IdempotencyKey: name: Idempotency-Key in: header required: false description: | Recommended on every payment creation. Replaying the same key returns the same payment. **Verified deviation from the industry standard:** the same key with a **different body** returns the original payment with no error. The key wins and the body is ignored on replay. Derive the key from the order's content, not only from its identifier. Without it, repeating the same `external_reference` creates distinct payments: `external_reference` is not unique. On the saved-card charge endpoints this header is **mandatory**, not optional. schema: { type: string, maxLength: 191 } example: "order-1001-create" TransactionId: name: transaction_id in: path required: true description: | The **transaction** UUID from the payment response - not the numeric payment `id`. Passing a numeric payment id produces a routing 404 (`"The route ... could not be found."`) because the route only matches the UUID format. That error means the wrong identifier, not a missing endpoint. schema: type: string format: uuid example: "9e2b6a34-6f1d-4e2a-8c9b-2f7a1d4e5c6b" PaymentMethodId: name: payment_method_id in: path required: true description: Opaque saved-card reference, prefixed `pm_`. schema: type: string pattern: '^pm_[A-Za-z0-9]+$' example: "pm_7k2n9xqf31ab" schemas: PaymentCreateRequest: type: object required: [amount, external_reference, name] additionalProperties: false description: | Body for creating a payment. **Unknown fields are silently ignored by the API** (it still returns `201`). This schema declares `additionalProperties: false` deliberately so validators on the integrator's side catch what the API does not. properties: amount: description: | The exact total to charge in this checkout, in decimal units (150.50 = L 150.50), **not** cents. Take it from the invoice or cart in your backend. The API enforces no maximum and accepts numeric strings; enforce a sane ceiling on your side. type: number format: double minimum: 0.01 example: 1500.00 external_reference: description: Your order id. **Not unique** - duplicate protection comes from `Idempotency-Key`. type: string minLength: 1 maxLength: 191 example: "order-1001" name: description: Payment label shown to the customer at checkout. type: string minLength: 1 maxLength: 191 example: "Order #1001" currency_code: description: | ISO 4217 **numeric** code (`"340"` = HNL). Defaults to the terminal's currency. Only currencies enabled on that terminal are accepted. type: string pattern: '^[0-9]{3}$' example: "340" description: type: string maxLength: 120 example: "Table for four" metadata: description: | Merchant-defined key/value pairs, returned untouched on retrieval and in every webhook. Not shown to the customer. type: object additionalProperties: true success_url: description: | Return URL after payment. **Landing there does not confirm payment.** The API accepts `http://` although the docs require HTTPS; always use HTTPS. type: string format: uri cancel_url: type: string format: uri expires_at: description: | Link expiry in Honduras time (UTC-6), must be in the future. Accepts `YYYY-MM-DD HH:MM:SS` and ISO 8601. type: string example: "2026-08-13 18:00:00" customer: $ref: '#/components/schemas/CustomerPrefill' lock_customer_fields: description: | When `true`, every prefilled field that carries a non-empty value becomes read-only at checkout. Customer fields you do not send stay blank and remain editable even when this is `true`. Verified caveat: accepted with `201` but **not echoed in the response**, so a success code alone does not prove the prefill was applied. Confirm by opening the checkout. type: boolean default: false reusable: description: | `false` (default) makes the payment single-use: it is consumed after an approved charge. `true` keeps the link payable until it expires or is disabled - recommended for mode 2 remounts. With `true`, creating again with the same `external_reference`, or the same customer email/identity plus amount and currency, may return the existing payment id while it is still payable. type: boolean default: false sales_tax_type: description: Tax mode. If not `none`, `sales_tax_value` is required. Computed on the subtotal only. type: string enum: [none, fixed, percentage] default: none sales_tax_value: description: Fixed amount or percentage. As a percentage it cannot exceed 100. type: number format: double minimum: 0 tip_enabled: description: | When `true`, define exactly one mode: a fixed tip (`tip_type` + `tip_value`) or customer selection (`tip_customer_selectable: true`). Omitting both returns `422`. type: boolean default: false tip_type: type: string enum: [fixed, percentage] tip_value: type: number format: double minimum: 0 tip_customer_selectable: description: The customer picks the tip at checkout, so the total at creation excludes it. type: boolean tip_preset_percentages: type: array items: { type: number, format: double, minimum: 0 } example: [10, 15, 20] tip_allow_custom: type: boolean tip_min_amount: description: Cannot exceed `amount`. type: number format: double minimum: 0 tip_max_amount: description: | Cannot exceed `amount` - a larger value returns `422` ("Tip limits cannot be greater than the plan amount."). type: number format: double minimum: 0 service_fee_enabled: description: | Per-request switch. `true` passes ROKI's processing costs to the customer: the checkout total is increased by reverse calculation so the merchant nets the original `amount`. Omitted or `false` means no service fee. **Do not replicate the calculation.** Rates, the fixed 3-D Secure charge and the threshold are per-merchant configuration. Read `service_fee_amount` and `total` from the response. Exact field name: `service_fee_enabled`. Variants such as `service_fee` or `pass_fees_to_customer` are silently ignored and produce a payment with no fee. type: boolean default: false CustomerPrefill: type: object additionalProperties: false description: | Prefills the hosted checkout with a known customer's details. Every field is optional and independent. By default prefilled fields remain editable; see `lock_customer_fields`. `identity_number` is also the preferred identifier when listing saved cards. properties: name: { type: string, example: "Ahmed Khan" } email: { type: string, format: email, example: "ahmed@example.com" } phone: { type: string, example: "+50499999999" } identity_number: type: string description: National identity number. example: "0801199912345" Payment: type: object description: A payment. The same object is returned by creation, retrieval, void and refund. required: [id, status, name, amount, subtotal, total, currency, currency_iso, external_reference, checkout_url, created_at] properties: id: description: Numeric payment identifier. Use it for `GET /payments/{id}`. type: integer format: int64 example: 42 status: { $ref: '#/components/schemas/PaymentStatus' } name: { type: string } description: { type: [string, "null"] } amount: description: Base amount requested, before tax, tip and service fee. type: number format: double reusable: { type: boolean } subtotal: { type: number, format: double } sales_tax_amount: { type: number, format: double } service_fee_amount: description: | Service fee passed to the customer when `service_fee_enabled` was `true`, `0` otherwise. **Authoritative** - do not recompute it. type: number format: double total: description: | Amount actually charged (`subtotal` + tax + service fee; a customer-selected tip is added at checkout, so the charged amount can exceed this). **Authoritative.** type: number format: double currency: { type: string, example: "340" } currency_iso: { type: string, example: "HNL" } external_reference: { type: string } metadata: description: | When none was sent, the API may return an empty array `[]` instead of `{}` - treat both as "no metadata". type: [object, array] customer: description: | The customer prefill, echoed back exactly as sent. Use it to confirm the prefill was applied: since the API silently ignores unknown fields, this is the only way to be sure from the response alone. allOf: - $ref: '#/components/schemas/CustomerPrefill' lock_customer_fields: description: Whether the prefilled fields were locked at checkout. type: boolean checkout_url: description: | ROKI-hosted payment page. Redirect the customer here. **Verified:** the slug is a 12-character lowercase alphanumeric string with **no `plink_` prefix**, despite what the official documentation shows. Never hardcode or pattern-match a slug format - always redirect to the exact `checkout_url` returned. type: string format: uri example: "https://aura.roki.systems/pay/link/ksdjrovxqa60" transaction_id: description: | **UUID string**, `null` until the payment is charged. This is the identifier required by void, refund and receipts - not the numeric `id`. It changed type: earlier versions of this API returned an integer here. type: [string, "null"] format: uuid example: "9e2b6a34-6f1d-4e2a-8c9b-2f7a1d4e5c6b" expires_at: { type: [string, "null"] } created_at: { type: string } paid_at: { type: [string, "null"] } refunded_amount: description: Cumulative refunded amount. Present on payments with void or refund history. type: number format: double refund_status: type: string enum: [none, partial, full] PaymentStatus: type: string description: | - `pending`: created, not yet paid. - `paid`: successfully charged. - `partially_refunded` / `refunded`: partially or fully refunded. - `voided`: voided before settlement; funds released. - `expired`: the link expired unpaid. - `disabled`: disabled. enum: [pending, paid, partially_refunded, refunded, voided, expired, disabled] Receipt: type: object required: [payment_id, transaction_id, receipt_url] properties: payment_id: { type: integer, format: int64, example: 42 } transaction_id: { type: string, format: uuid } transaction_identifier: { type: string, format: uuid } receipt_url: type: string format: uri description: Public, human-readable receipt page for this transaction. PaymentMethod: type: object description: | An opaque reference to a card the customer saved. Never contains the card number: ROKI stores the processor's own token. required: [id, card_brand, last_four] properties: id: type: string pattern: '^pm_[A-Za-z0-9]+$' example: "pm_7k2n9xqf31ab" card_brand: { type: string, example: "Visa" } last_four: { type: string, example: "4242" } exp_month: { type: integer, minimum: 1, maximum: 12, example: 11 } exp_year: { type: integer, example: 2027 } is_default: { type: boolean } TokenChargeRequest: type: object required: [amount] additionalProperties: false properties: amount: { type: number, format: double, minimum: 0.01, example: 500.00 } currency_code: description: | Accepts the alphabetic code here (`"HNL"`), unlike payment creation which uses the numeric code (`"340"`). type: string example: "HNL" external_reference: { type: string, maxLength: 191, example: "order-2002" } metadata: { type: object, additionalProperties: true } TokenChargeByBodyRequest: type: object required: [payment_token, amount] additionalProperties: false description: Same as TokenChargeRequest, with the saved card passed in the body. properties: payment_token: type: string pattern: '^pm_[A-Za-z0-9]+$' description: The saved payment method reference. example: "pm_7k2n9xqf31ab" amount: { type: number, format: double, minimum: 0.01, example: 100.00 } currency_code: { type: string, example: "HNL" } external_reference: { type: string, maxLength: 191 } metadata: { type: object, additionalProperties: true } ChargeResult: type: object description: | Outcome of a saved-card charge. The returned identifier is the **transaction UUID** to use for void, refund and receipts. properties: id: { type: string, format: uuid } transaction_id: { type: string, format: uuid } status: { type: string, enum: [approved, declined, pending] } amount: { type: number, format: double } currency: { type: string } currency_code: { type: string } external_reference: { type: string } created_at: { type: string } EmbedConfirmRequest: type: object required: [amount, currency_code, payment_token, publishable_key] additionalProperties: false description: Sent by the merchant BACKEND with `sk_*`. Never from the browser. properties: amount: { type: number, format: double, minimum: 0.01 } currency_code: type: string description: Accepts `"HNL"` or the numeric `"340"`. example: "HNL" payment_token: type: string pattern: '^tok_' description: The `tok_*` produced by the browser SDK after the customer submits the card. publishable_key: type: string pattern: '^pk_' description: The same publishable key the SDK was mounted with. external_reference: { type: string, maxLength: 191 } success_redirect_url: { type: string, format: uri } failed_redirect_url: { type: string, format: uri } description: { type: string, maxLength: 120 } metadata: { type: object, additionalProperties: true } EmbedApproved: type: object description: Approved embedded payment, with the same fields the merchant transaction detail page shows. properties: status: { type: string, const: approved } transaction_id: { type: string, format: uuid } amount: { type: number, format: double } currency: { type: string } description: { type: [string, "null"] } metadata: { type: [object, array] } transaction_details: type: object description: | Financial breakdown and card detail. `roki_commission`, `isv` and `expected_settlement` are commercially sensitive - do not surface them to the cardholder. additionalProperties: true EmbedPending: type: object description: | 3-D Secure is required. Load `authentication_url` so the customer can complete the issuer challenge; the final outcome arrives through the normal webhooks. properties: status: { type: string, const: pending } authentication_url: { type: string, format: uri } transaction_id: { type: string, format: uuid } ProcessorDecline: type: object description: | A decline coming from the payment processor rather than from validation. `Errors[].Code` is the processor code (for example `201`, `203`); `IsoResponseCode` is the ISO response. Note the capitalized field names - they are passed through from the processor. properties: status: { type: string, enum: [declined, failed] } IsoResponseCode: { type: string, example: "05" } Errors: type: array items: type: object properties: Code: { type: string, example: "201" } Message: { type: string } WebhookEvent: type: object required: [id, type, created_at, data] properties: id: description: Unique event id. Deduplicate on it - events can be redelivered. type: string example: "9a7a9698-e270-422a-ba8f-365e944248aa" # UUID plano, sin prefijo evt_ type: { $ref: '#/components/schemas/WebhookEventType' } created_at: { type: string } data: description: | For payment events, the affected payment (refund events add `refund_amount`, `refunded_at` and `refund_reason`). For `payment_method.saved`, a `PaymentMethod` object. type: object additionalProperties: true WebhookEventType: type: string description: | - `payment.approved`: the customer paid successfully. - `payment.failed`: card declined or payment error. - `payment.expired`: the link expired before checkout completed. - `payment.voided`: a transaction was voided. - `payment.refunded` / `payment.partially_refunded`: refunds. - `payment_method.saved`: the customer saved a card; carries the opaque `pm_*`. Void and refund events also fire when the action is performed from the merchant portal. enum: - payment.approved - payment.failed - payment.expired - payment.voided - payment.refunded - payment.partially_refunded - payment_method.saved ErrorMessage: type: object required: [message] properties: message: { type: string } ValidationError: type: object description: | Validation or business-rule error. `message` holds the first error, suffixed `(and N more errors)` when there are several; `errors` groups them by field. Message text is localized by `Accept-Language`; the **keys** of `errors` are stable and are what integration logic should branch on. required: [message, errors] properties: message: { type: string } errors: type: object additionalProperties: type: array items: { type: string } responses: Unauthorized: description: The `Authorization` header is missing or the key is invalid. content: application/json: schema: { $ref: '#/components/schemas/ErrorMessage' } examples: missing_header: value: { message: "Encabezado Authorization ausente o invalido. Use: Bearer sk_test_... o Bearer sk_live_..." } invalid_key: value: { message: "Clave API invalida." } PaymentNotFound: description: | No payment with that id exists for the key in use - often an id from the other environment. Distinguish from the routing 404 (`"The route ... could not be found."`). content: application/json: schema: { $ref: '#/components/schemas/ErrorMessage' } example: { message: "Pago no encontrado." } TransactionNotFound: description: | Transaction not found, or not owned by this merchant. If you instead get `"The route ... could not be found."`, you passed the numeric payment id where a transaction UUID is required. content: application/json: schema: { $ref: '#/components/schemas/ErrorMessage' } example: { message: "Pago no encontrado." } ValidationFailed: description: | Validation failed or a business rule was violated: required field, length, type, currency not enabled on the terminal, tip above the amount, tax percentage above 100, expiry in the past, or a missing mandatory `Idempotency-Key` on the charge endpoints. content: application/json: schema: { $ref: '#/components/schemas/ValidationError' }