{"components":{"parameters":{"IdempotencyKey":{"description":"Optional, at most 255 characters of valid UTF-8. The only thing that deduplicates a create: a replay with the identical body returns the original object (status 200), a replay with a different body is refused 409. No key means every attempt is its own object.\n","in":"header","name":"Idempotency-Key","required":false,"schema":{"maxLength":255,"type":"string"}}},"responses":{"Internal":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"`internal` — something went wrong on our side."},"NotFound":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"`not_found` — no such object in this key's mode. A live key cannot see test objects nor a test key live ones.\n"},"RateLimited":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"`rate_limited` — over 300 requests/minute on this key."},"Unauthorized":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"`unauthorized` — missing/malformed Authorization header or an invalid/revoked API key.\n"}},"schemas":{"CreateRefundRequest":{"properties":{"amount":{"description":"Whole rupiah. Omitted means the whole remaining gross.","format":"int64","maximum":1000000000,"minimum":1,"type":"integer"},"reason":{"maxLength":255,"type":"string"},"transaction_id":{"description":"`payreq_\u003cuuid\u003e` of a succeeded card payment.","maxLength":64,"type":"string"}},"required":["transaction_id"],"type":"object"},"CreateTransactionRequest":{"properties":{"amount":{"description":"Whole rupiah. Authoritative even when order_items is sent.","format":"int64","maximum":1000000000,"minimum":1,"type":"integer"},"checkout":{"description":"Everything that shapes the Kasera Pay Checkout page. Sending it, even as `{}`, is what makes this a Checkout create: the buyer walks `checkout_url` and the page collects whatever a chosen method still needs. Omitting it makes the create Direct API: `checkout_url` is still returned, but no page is assumed, so a method that needs a buyer field you did not send is refused 422 `customer_required`, and the three `is_*_required` asks are inherited from the account's payment-method settings. Sending the object replaces all three at once, so an object carrying only `steps` turns those account defaults off for this payment.\n","properties":{"is_email_required":{"type":"boolean"},"is_name_required":{"type":"boolean"},"is_phone_required":{"type":"boolean"},"steps":{"items":{"maxLength":32,"type":"string"},"maxItems":8,"type":"array"}},"type":"object"},"customer":{"allOf":[{"$ref":"#/components/schemas/Person"}],"description":"Who is paying — what payment rails are fulfilled with (not `payer`, which is display data). Methods may require `customer.name` etc.; read `required_customer_fields` on GET /v1/payment_methods.\n"},"description":{"maxLength":255,"type":"string"},"expires_in_minutes":{"maximum":10080,"minimum":1,"type":"integer"},"external_id":{"description":"Your order id. A label — stored, echoed, filterable, never deduplicating.","maxLength":64,"type":"string"},"merchant_ref":{"description":"Your own reference. A label, like external_id.","maxLength":64,"type":"string"},"order_items":{"description":"Display-only detail. Σ price×quantity must equal amount or the create is refused 422 `order_items_mismatch`.\n","items":{"$ref":"#/components/schemas/OrderItem"},"maxItems":50,"type":"array"},"payer":{"$ref":"#/components/schemas/Person"},"payment_methods":{"description":"Method codes this payment may be paid with, in the order a buyer should be offered them. Omitted or empty means every method enabled on the account. An unknown or disabled code is refused 422 `payment_method_unavailable`, never dropped.\n","items":{"maxLength":32,"type":"string"},"maxItems":32,"type":"array"},"return_url":{"description":"Where the hosted checkout sends the buyer afterwards. https only on live creates; a test key also accepts http, so a store on localhost can develop the full redirect flow.\n","maxLength":2048,"type":"string"}},"required":["amount"],"type":"object"},"ErrorResponse":{"properties":{"error":{"properties":{"code":{"description":"Stable machine-readable code, e.g. `validation_failed`.","type":"string"},"fields":{"additionalProperties":{"type":"string"},"description":"Offending fields as dotted JSON paths of the body you sent (`customer.email`, `order_items.3.price`) mapped to the rule that failed or the offending value.\n","type":"object"},"message":{"description":"English, developer-facing.","type":"string"},"request_id":{"description":"Echoes the X-Request-Id response header so you can quote the exact failed request to support.\n","type":"string"}},"required":["code","message"],"type":"object"}},"required":["error"],"type":"object"},"Instructions":{"description":"How-to for the buyer, in Indonesian. Print it verbatim.","properties":{"steps":{"items":{"type":"string"},"type":"array"},"title":{"type":"string"}},"required":["title","steps"],"type":"object"},"OrderItem":{"properties":{"name":{"maxLength":120,"type":"string"},"price":{"format":"int64","minimum":1,"type":"integer"},"quantity":{"minimum":1,"type":"integer"}},"required":["name","price","quantity"],"type":"object"},"PaymentMethod":{"properties":{"code":{"example":"qris","type":"string"},"display_name":{"type":"string"},"fee":{"properties":{"flat":{"description":"Flat rupiah amount.","format":"int64","type":"integer"},"label":{"example":"0,7% + Rp250","type":"string"},"percent_bps":{"description":"Percent of gross in basis points.","format":"int64","type":"integer"}},"required":["percent_bps","flat","label"],"type":"object"},"max_amount":{"format":"int64","type":"integer"},"min_amount":{"format":"int64","type":"integer"},"required_customer_fields":{"description":"`customer.*` paths a create naming this method must send. Always an array, `[]` when the method needs nothing.\n","items":{"type":"string"},"type":"array"},"type":{"description":"The `payment.type` this method's payload will carry.","enum":["qr","payment_code","redirect"],"type":"string"}},"required":["code","display_name","type","min_amount","max_amount","required_customer_fields","fee"],"type":"object"},"PaymentMethodList":{"properties":{"data":{"items":{"$ref":"#/components/schemas/PaymentMethod"},"type":"array"}},"required":["data"],"type":"object"},"PaymentPayload":{"description":"The method-shaped payload, a discriminated union keyed on `type`. Omitted entirely until the rail has issued something for the buyer to use — never a placeholder. Only the fields belonging to the live `type` are populated.\n","properties":{"bank":{"description":"Short bank label, e.g. `Mandiri`.","type":"string"},"display_name":{"description":"Long label, e.g. `Mandiri Virtual Account` or `Credit Card`.","type":"string"},"payment_code":{"description":"The Virtual Account number the buyer types into their banking app (`type: payment_code`).","type":"string"},"qr_string":{"description":"Raw EMV QRIS payload (`type: qr`) — encode it into a QR client-side.","type":"string"},"redirect_url":{"description":"Where to send the buyer (`type: redirect`). Present only while pending.","type":"string"},"type":{"enum":["qr","payment_code","redirect"],"type":"string"}},"required":["type"],"type":"object"},"Person":{"properties":{"email":{"format":"email","maxLength":254,"type":"string"},"name":{"maxLength":120,"type":"string"},"phone":{"description":"payer.phone must be E.164; customer.phone also accepts the local Indonesian format (e.g. `08123456789`).\n","maxLength":32,"type":"string"}},"type":"object"},"Refund":{"properties":{"amount":{"format":"int64","type":"integer"},"created_at":{"description":"ISO-8601 at +07:00.","type":"string"},"currency":{"example":"IDR","type":"string"},"failure_reason":{"description":"The processor's own words when status is failed.","type":"string"},"id":{"description":"`rfd_\u003cuuid\u003e`","type":"string"},"livemode":{"type":"boolean"},"reason":{"type":"string"},"status":{"description":"`pending` is a refund the processor never answered; it holds its amount until a human resolves it.\n","enum":["succeeded","failed","pending"],"type":"string"},"transaction_id":{"description":"`payreq_\u003cuuid\u003e`","type":"string"}},"required":["id","transaction_id","status","currency","amount","created_at","livemode"],"type":"object"},"Transaction":{"properties":{"amount":{"description":"Gross, whole rupiah. `amount = fee + net`, always.","format":"int64","type":"integer"},"checkout_url":{"description":"The hosted Kasera Pay Checkout page for this payment.","type":"string"},"created_at":{"description":"ISO-8601 at +07:00.","type":"string"},"currency":{"example":"IDR","type":"string"},"customer":{"$ref":"#/components/schemas/Person"},"description":{"type":"string"},"expires_at":{"description":"ISO-8601 at +07:00.","type":"string"},"external_id":{"type":"string"},"fee":{"format":"int64","type":"integer"},"id":{"description":"`payreq_\u003cuuid\u003e`","type":"string"},"instructions":{"$ref":"#/components/schemas/Instructions"},"livemode":{"description":"false on objects a test key created — no real money moves.","type":"boolean"},"merchant_ref":{"type":"string"},"net":{"format":"int64","type":"integer"},"order_items":{"items":{"$ref":"#/components/schemas/OrderItem"},"type":"array"},"paid_at":{"description":"ISO-8601 at +07:00, null until paid.","nullable":true,"type":"string"},"payer":{"$ref":"#/components/schemas/Person"},"payment":{"$ref":"#/components/schemas/PaymentPayload"},"payment_method":{"description":"Lowercase code of the method this payment was created for — read off the row, not today's catalogue.\n","type":"string"},"return_url":{"type":"string"},"source":{"description":"Which channel created this request. Always `api` here; history read back may also say `merchant`, `payment_page` or `unknown`.\n","type":"string"},"status":{"enum":["pending","succeeded","failed","expired","canceled"],"type":"string"}},"required":["id","status","currency","amount","fee","net","description","checkout_url","source","payment_method","instructions","expires_at","paid_at","created_at","livemode"],"type":"object"},"TransactionList":{"properties":{"data":{"items":{"$ref":"#/components/schemas/Transaction"},"type":"array"},"has_more":{"description":"More rows behind this page — pass the last id as starting_after.\n","type":"boolean"}},"required":["data","has_more"],"type":"object"},"WebhookEvent":{"description":"The `payment.paid` delivery POSTed to your webhook endpoint. Verify `Kasera-Signature-V1` (`t=\u003cunix\u003e,v1=\u003chex\u003e`; each `v1` is lowercase hex HMAC-SHA256 over `\u003cunix\u003e.\u003craw body\u003e` with your endpoint's signing secret; reject if `t` is older than five minutes; accept if any `v1` matches, since two are sent during a secret-rotation grace). The legacy body-only `Kasera-Signature` header is still sent but deprecated. Dedupe on `id` (also sent as the `Kasera-Event-Id` header). Delivery is at-least-once.\n","properties":{"created_at":{"description":"ISO-8601 at +07:00.","type":"string"},"data":{"properties":{"amount":{"format":"int64","type":"integer"},"currency":{"example":"IDR","type":"string"},"customer":{"$ref":"#/components/schemas/Person"},"external_id":{"type":"string"},"merchant_ref":{"type":"string"},"paid_at":{"description":"ISO-8601 at +07:00.","nullable":true,"type":"string"},"payment_request_id":{"description":"`payreq_\u003cuuid\u003e`","type":"string"}},"required":["payment_request_id","amount","currency","paid_at"],"type":"object"},"id":{"description":"`evt_\u003cuuid\u003e` — dedupe on this.","type":"string"},"livemode":{"type":"boolean"},"type":{"enum":["payment.paid"],"type":"string"}},"required":["id","type","livemode","created_at","data"],"type":"object"},"WebhookTestPing":{"description":"The dashboard's test delivery — proof that signing and connectivity work, carrying nothing that looks like money. Signed and headed exactly like a real event.\n","properties":{"created_at":{"description":"ISO-8601 at +07:00.","type":"string"},"id":{"description":"`evt_\u003cuuid\u003e`","type":"string"},"livemode":{"type":"boolean"},"test":{"description":"Always true.","type":"boolean"},"type":{"enum":["test.ping"],"type":"string"}},"required":["id","type","livemode","test","created_at"],"type":"object"}},"securitySchemes":{"apiKey":{"description":"An API key from the dashboard's Developer page: `kp_live_...` or `kp_test_...`.\n","scheme":"bearer","type":"http"}}},"info":{"description":"The public developer API. Create payment requests, read them back, list\nthe payment methods your account may offer, and refund card payments.\n\nAll amounts are whole rupiah (no decimals): `amount = fee + net`, always.\nTimestamps are ISO-8601 with a `+07:00` offset. Ids are self-describing:\n`payreq_\u003cuuid\u003e` for transactions, `rfd_\u003cuuid\u003e` for refunds.\n\n## Authentication\n\nEvery endpoint (except this document) requires a bearer API key in the\n`Authorization` header: `Authorization: Bearer kp_live_...` or\n`Bearer kp_test_...`. A test key sees and creates only test objects, a\nlive key only live ones — the other mode's objects answer 404. Requests\nare limited to 300 per minute per key; failed authentication is throttled\nper IP.\n\n## Idempotency\n\n`POST /v1/transactions` and `POST /v1/refunds` accept an optional\n`Idempotency-Key` header — the ONLY thing that deduplicates a create.\nReplaying a key with the identical body returns the original object with\nstatus 200 (not 201); replaying it with a different body is refused\n409 `idempotency_conflict`. `merchant_ref` and `external_id` are labels:\nstored, echoed, filterable, never deduplicating.\n\n## Pagination\n\nLists return `{data, has_more}` pages ordered newest first. Pass\n`starting_after` with the last `id` you saw to fetch the next page;\n`limit` is 1–100, default 20.\n\n## Webhooks\n\nKasera Pay POSTs a `payment.paid` event to your configured endpoint when\na payment settles, carrying `Kasera-Event-Id` for deduplication and two\nsignature headers:\n\n- `Kasera-Signature-V1` — verify this one. Format `t=\u003cunix\u003e,v1=\u003chex\u003e`,\n  where each `v1` is lowercase hex HMAC-SHA256 over `\u003cunix\u003e.\u003craw body\u003e`\n  keyed with your endpoint's signing secret. Reject deliveries whose `t`\n  is older than five minutes. During the 24 hours after a secret\n  rotation the header carries two `v1` entries, one per secret; accept\n  the delivery if any entry matches.\n- `Kasera-Signature` — deprecated. Lowercase hex HMAC-SHA256 of the raw\n  body alone, with no timestamp, so a captured delivery replays forever.\n  Still sent for existing verifiers; it will be removed after a\n  deprecation period announced on the webhook docs page. New\n  integrations must not rely on it.\n\nDelivery is at-least-once with backoff over ~34 hours. The payload\nshapes are `WebhookEvent` and `WebhookTestPing` under components/schemas.\n","title":"Kasera Pay API","version":"1.0"},"openapi":"3.0.3","paths":{"/v1/openapi.json":{"get":{"operationId":"getOpenAPISpec","responses":{"200":{"content":{"application/json":{"schema":{"type":"object"}}},"description":"The OpenAPI 3.0 document describing this API."}},"security":[],"summary":"This document, as JSON"}},"/v1/payment_methods":{"get":{"description":"The method codes a create may name in `payment_methods`, with each method's fee, amount bounds and required `customer.*` fields. Read this instead of hard-coding codes — methods can be enabled or suspended without a release on your side.\n","operationId":"listPaymentMethods","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaymentMethodList"}}},"description":"The catalogue. Not paged."},"401":{"$ref":"#/components/responses/Unauthorized"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/Internal"}},"summary":"List this account's payment methods"}},"/v1/refunds":{"post":{"description":"Returns money on a succeeded card payment, in full (omit `amount`) or in part. QRIS and Virtual Account refunds are manual. The same optional `Idempotency-Key` contract as creating a transaction — a replayed key returns the original refund and never reaches the card processor again.\n","operationId":"createRefund","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateRefundRequest"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Refund"}}},"description":"The refund as it stands once the processor answered."},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"`invalid_idempotency_key`."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"`not_refundable` or `idempotency_conflict`."},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"`validation_failed`, `refund_not_supported` or `refund_exceeds_amount`.\n"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/Internal"},"502":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"`refund_refused` — nothing was refunded, safe to retry — or `refund_unresolved` — the amount is held pending and must NOT be retried; contact support.\n"}},"summary":"Refund a card payment"}},"/v1/refunds/{id}":{"get":{"operationId":"getRefund","parameters":[{"description":"`rfd_\u003cuuid\u003e`","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Refund"}}},"description":"The refund."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/Internal"}},"summary":"Retrieve a refund"}},"/v1/transactions":{"get":{"description":"One page of this key's payment requests, newest first, scoped to the key's mode. Malformed filters are refused 422, never silently dropped.\n","operationId":"listTransactions","parameters":[{"description":"Page size, 1–100. Out of range falls back to 20.","in":"query","name":"limit","schema":{"default":20,"maximum":100,"minimum":1,"type":"integer"}},{"description":"The last `id` of the previous page (`payreq_\u003cuuid\u003e`). A cursor that does not name one of your payment requests is refused 422 `invalid_cursor`.\n","in":"query","name":"starting_after","schema":{"type":"string"}},{"in":"query","name":"status","schema":{"enum":["pending","succeeded","failed","expired","canceled"],"type":"string"}},{"in":"query","name":"external_id","schema":{"type":"string"}},{"in":"query","name":"merchant_ref","schema":{"type":"string"}},{"description":"RFC3339 timestamp, e.g. `2026-08-18T09:00:00+07:00`.","in":"query","name":"created_after","schema":{"format":"date-time","type":"string"}},{"description":"RFC3339 timestamp.","in":"query","name":"created_before","schema":{"format":"date-time","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TransactionList"}}},"description":"One page."},"401":{"$ref":"#/components/responses/Unauthorized"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"`invalid_filter` or `invalid_cursor`."},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/Internal"}},"summary":"List payment requests"},"post":{"description":"Creates a payment request and, for headless integrations, attaches the\nrail's payload (`payment`) when it can be generated immediately. Send\nthe buyer to `checkout_url`, or render `payment` in your own screens.\nA replayed `Idempotency-Key` with the identical body returns the\noriginal with status 200.\n","operationId":"createTransaction","parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateTransactionRequest"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Transaction"}}},"description":"Idempotent replay — the original payment request."},"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Transaction"}}},"description":"Created."},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"`invalid_idempotency_key` — the header is over 255 characters or not valid UTF-8.\n"},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"`merchant_suspended` — this account is suspended."},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"`idempotency_conflict` — this Idempotency-Key was already used with a different request body.\n"},"413":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"`body_too_large` — request body over 1 MB."},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"`validation_failed`, `invalid_body`, `order_items_mismatch`, `payment_method_unavailable`, `customer_required`, `amount_too_small`, `amount_too_large`, `expiry_too_long` or `expiry_too_short` — `error.fields` names the offending fields as dotted JSON paths.\n"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"`rate_limited`, `daily_count_cap` or `daily_amount_cap`.\n"},"500":{"$ref":"#/components/responses/Internal"}},"summary":"Create a payment request"}},"/v1/transactions/{id}":{"get":{"operationId":"getTransaction","parameters":[{"description":"`payreq_\u003cuuid\u003e`","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Transaction"}}},"description":"The payment request."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/Internal"}},"summary":"Retrieve a payment request"}}},"security":[{"apiKey":[]}],"servers":[{"url":"https://pay.kasera.id"}]}