{"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, or a second read of one transaction within a second."},"SubscriptionConflict":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"`already_canceled` or `invalid_state` — the request was well formed and would have been legal a moment ago. A retried cancel lands here, which is why it is not a 422.\n"},"Unauthorized":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"`unauthorized` — missing/malformed Authorization header or an invalid/revoked API key.\n"},"Unprocessable":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"`invalid_request` — the body is malformed or a field is unusable. `error.message` names what to fix.\n"}},"schemas":{"Address":{"description":"The buyer's address. Only the PayLater lenders `kredivo` and `indodana` require it; all three parts or it counts as absent.\n","properties":{"city":{"maxLength":100,"type":"string"},"line":{"maxLength":200,"type":"string"},"postal_code":{"maxLength":10,"type":"string"}},"type":"object"},"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 Fintar 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/Customer"}],"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"},"delivery_date":{"description":"The promised delivery day, YYYY-MM-DD in WIB, today or later. Required above Rp1.000.000 (with a description of at least 10 characters, customer.name, and customer.phone or customer.email), even on a Checkout create. Links to the same buyer contact within 30 minutes are summed.\n","format":"date","type":"string"},"delivery_terms_shown":{"description":"Your attestation that your checkout showed the buyer delivery_date before they paid. Must be true above Rp1.000.000.\n","type":"boolean"},"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"},"send_receipt":{"description":"Whether Fintar emails the buyer a receipt when this payment succeeds. `true` sends it and `false` does not, whatever the account setting says. Omitted leaves it to the account's receipt setting, which is off by default. A receipt is only ever sent for a live payment that carries a buyer email.\n","type":"boolean"}},"required":["amount"],"type":"object"},"Customer":{"allOf":[{"$ref":"#/components/schemas/Person"},{"properties":{"address":{"$ref":"#/components/schemas/Address"}},"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":{"description":"Smallest amount a create on this method accepts, in whole rupiah, for this account. Each method has its own range.\n","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"},"Subscription":{"description":"`status` and `paid_through` answer different questions and both matter. `status` is where the subscription sits in its lifecycle; `paid_through` is whether the customer may use the service right now. Grant access on `paid_through`: an integration that reads `status` alone cuts off every `past_due` subscriber inside the grace period they have already paid for.\n","properties":{"canceled_at":{"format":"date-time","type":"string"},"created_at":{"format":"date-time","type":"string"},"current_period_end":{"description":"When the next invoice is raised.","format":"date-time","type":"string"},"current_period_start":{"format":"date-time","type":"string"},"customer_id":{"description":"`cust_\u003cuuid\u003e`","type":"string"},"ends_at":{"description":"A cancellation already scheduled for the period end.","format":"date-time","type":"string"},"external_id":{"type":"string"},"id":{"description":"`sub_\u003cuuid\u003e`","type":"string"},"is_test":{"description":"Which mode this subscription belongs to. Always present, `false` included — a missing field would have to be read as live.\n","type":"boolean"},"paid_through":{"description":"Service access lapses at this instant. Absent until something has been paid for.","format":"date-time","type":"string"},"pending_plan_id":{"description":"A change waiting for the next renewal. This is what the next invoice will use.","type":"string"},"pending_quantity":{"type":"integer"},"plan_id":{"description":"`plan_\u003cuuid\u003e`","type":"string"},"quantity":{"type":"integer"},"status":{"enum":["trialing","active","past_due","paused","canceled"],"type":"string"},"test_clock":{"description":"Where this sandbox subscription's clock stands, when it has been advanced. Absent on live subscriptions, which have no clock to move.\n","format":"date-time","type":"string"},"trial_ends_at":{"format":"date-time","type":"string"}},"type":"object"},"SubscriptionCustomer":{"properties":{"created_at":{"format":"date-time","type":"string"},"email":{"type":"string"},"external_id":{"type":"string"},"id":{"description":"`cust_\u003cuuid\u003e`","type":"string"},"name":{"type":"string"},"phone":{"type":"string"}},"type":"object"},"SubscriptionInvoice":{"description":"One billing cycle's bill. It carries what the customer owes and nothing about Fintar's fee — that is on the transaction named by `payment_request_id`, which you can fetch from `GET /v1/transactions/{id}`.\n","properties":{"amount_due":{"description":"Still owed. This is the figure to show a payer.","format":"int64","type":"integer"},"created_at":{"format":"date-time","type":"string"},"currency":{"example":"IDR","type":"string"},"customer_id":{"description":"`cust_\u003cuuid\u003e`","type":"string"},"discount":{"format":"int64","type":"integer"},"due_at":{"description":"When payment is expected. Not the payment attempt's expiry — a QRIS code lives an hour and the invoice link outlives it.\n","format":"date-time","type":"string"},"id":{"description":"`inv_\u003cuuid\u003e`","type":"string"},"number":{"description":"Human-facing","type":"string","unique per account.":null},"paid_at":{"format":"date-time","type":"string"},"paid_externally":{"description":"Recorded by the merchant rather than collected by Fintar. Settled, but not Fintar-processed volume — keep the two apart in reporting.\n","type":"boolean"},"payment_request_id":{"description":"`payreq_\u003cuuid\u003e` — the live collection attempt, absent when there is none.","type":"string"},"period_end":{"format":"date-time","type":"string"},"period_start":{"format":"date-time","type":"string"},"setup":{"format":"int64","type":"integer"},"status":{"description":"`void` withdraws the debt; `uncollectible` admits it is still owed and no longer being chased.\n","enum":["open","paid","void","uncollectible"],"type":"string"},"subscription_id":{"description":"`sub_\u003cuuid\u003e`","type":"string"},"subtotal":{"format":"int64","type":"integer"},"tax":{"format":"int64","type":"integer"},"total":{"format":"int64","type":"integer"}},"type":"object"},"SubscriptionPlan":{"properties":{"amount":{"format":"int64","type":"integer"},"archived":{"description":"A superseded version. Still readable; no new subscription may start on it.","type":"boolean"},"code":{"type":"string"},"created_at":{"format":"date-time","type":"string"},"currency":{"example":"IDR","type":"string"},"discount_amount":{"format":"int64","type":"integer"},"id":{"description":"`plan_\u003cuuid\u003e`","type":"string"},"interval_count":{"type":"integer"},"interval_unit":{"enum":["day","week","month","year"],"type":"string"},"name":{"type":"string"},"setup_amount":{"format":"int64","type":"integer"},"trial_days":{"type":"integer"},"version":{"description":"Which version of `code` this is. An invoice records the version it was billed on, so a repriced plan never restates an old bill.\n","type":"integer"}},"type":"object"},"Transaction":{"properties":{"amount":{"description":"Gross, whole rupiah. `amount = fee + net`, always.","format":"int64","type":"integer"},"checkout_url":{"description":"The hosted Fintar 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/Customer"},"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"},"send_receipt":{"description":"The create's own receipt choice. Absent when the create left it to the account's receipt setting.\n","type":"boolean"},"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 `Fintar-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 `Fintar-Signature` header is still sent but deprecated. Dedupe on `id` (also sent as the `Fintar-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: `ft_live_...` or `ft_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 ft_live_...` or\n`Bearer ft_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. `GET /v1/transactions/{id}` is also paced at one read per second\nper transaction (per key): its answers carry `RateLimit-Limit`,\n`RateLimit-Remaining` and `RateLimit-Reset`, and a refusal adds\n`Retry-After`. Listen for the `payment.paid` webhook for status changes\ninstead of polling.\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\nFintar POSTs a `payment.paid` event to your configured endpoint when\na payment settles, carrying `Fintar-Event-Id` for deduplication and two\nsignature headers:\n\n- `Fintar-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- `Fintar-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":"Fintar 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/subscription_customers":{"get":{"description":"Closed beta. Every subscription endpoint answers 404 for accounts we have not enabled by hand, and the shape may change before general availability.\n","operationId":"listSubscriptionCustomers","parameters":[{"in":"query","name":"limit","schema":{"type":"integer"}},{"in":"query","name":"offset","schema":{"type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"data":{"items":{"$ref":"#/components/schemas/SubscriptionCustomer"},"type":"array"}},"type":"object"}}},"description":"This account's customers."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/Internal"}},"summary":"[Beta] List customers"},"post":{"description":"Sending an `external_id` you have used before updates that customer instead of creating a second one, which is what makes re-running an import safe. One of `email` or `phone` is required: a customer nobody can be reached at cannot be sent a renewal reminder.\n","operationId":"createSubscriptionCustomer","requestBody":{"content":{"application/json":{"schema":{"properties":{"email":{"type":"string"},"email_opted_out":{"description":"They asked not to be emailed. Email is opt-out where WhatsApp is opt-in.","type":"boolean"},"external_id":{"description":"Your own identifier. Matching on it is what makes a re-import an update.","type":"string"},"name":{"type":"string"},"phone":{"type":"string"},"whatsapp_opted_in":{"description":"They agreed to be messaged on WhatsApp. Without it no WhatsApp reminder is ever scheduled for them.\n","type":"boolean"}},"required":["name"],"type":"object"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubscriptionCustomer"}}},"description":"The customer."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/Unprocessable"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/Internal"}},"summary":"[Beta] Create or update a customer"}},"/v1/subscription_customers/{id}":{"get":{"operationId":"getSubscriptionCustomer","parameters":[{"description":"`cust_\u003cuuid\u003e`","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubscriptionCustomer"}}},"description":"The customer."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/Internal"}},"summary":"[Beta] Retrieve a customer"},"patch":{"description":"Edits the customer at this id and never creates one. Use this rather than the create endpoint when correcting an `external_id`: that one matches on `external_id`, so changing it there mints a second customer and leaves the subscriptions pointing at the first. Every field is replaced by what you send, and the same rule as creating applies — one of `email` or `phone` must survive the edit.\n","operationId":"updateSubscriptionCustomer","parameters":[{"description":"`cust_\u003cuuid\u003e`","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"email":{"type":"string"},"email_opted_out":{"type":"boolean"},"external_id":{"description":"Unique among your customers. 422 if another of yours already holds it.","type":"string"},"name":{"type":"string"},"phone":{"type":"string"},"whatsapp_opted_in":{"type":"boolean"}},"required":["name"],"type":"object"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubscriptionCustomer"}}},"description":"The customer as it now stands."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/Unprocessable"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/Internal"}},"summary":"[Beta] Edit a customer"}},"/v1/subscription_invoices":{"get":{"operationId":"listSubscriptionInvoices","parameters":[{"in":"query","name":"limit","schema":{"type":"integer"}},{"in":"query","name":"offset","schema":{"type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"data":{"items":{"$ref":"#/components/schemas/SubscriptionInvoice"},"type":"array"}},"type":"object"}}},"description":"This account's invoices."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/Internal"}},"summary":"[Beta] List invoices"}},"/v1/subscription_invoices/{id}":{"get":{"operationId":"getSubscriptionInvoice","parameters":[{"description":"`inv_\u003cuuid\u003e`","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubscriptionInvoice"}}},"description":"The invoice."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/Internal"}},"summary":"[Beta] Retrieve an invoice"}},"/v1/subscription_plans":{"get":{"description":"Only the current version of each plan. Archived versions stay readable through the subscriptions that reference them.\n","operationId":"listSubscriptionPlans","responses":{"200":{"content":{"application/json":{"schema":{"properties":{"data":{"items":{"$ref":"#/components/schemas/SubscriptionPlan"},"type":"array"}},"type":"object"}}},"description":"The plans a new subscription may start on."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/Internal"}},"summary":"[Beta] List live plans"},"post":{"description":"Plans are immutable. Posting a `code` you have used before writes the next version and archives the one it replaces — subscribers stay on the version they signed up to until you move them, and their existing invoices keep the price they were issued at.\n\n`amount` is required even when it is 0: a free plan is a real plan, so an absent field cannot be read as a free one.\n\nDaily, weekly, monthly and annual. Anything longer than a year is refused.\n","operationId":"createSubscriptionPlan","requestBody":{"content":{"application/json":{"schema":{"properties":{"amount":{"description":"Whole rupiah per cycle. 0 is a free plan.","format":"int64","type":"integer"},"code":{"description":"Stable across versions; what ties a plan's history together.","type":"string"},"discount_amount":{"description":"A flat reduction per cycle. Cannot exceed `amount`.","format":"int64","type":"integer"},"interval_count":{"default":1,"type":"integer"},"interval_unit":{"enum":["day","week","month","year"],"type":"string"},"name":{"type":"string"},"setup_amount":{"description":"Charged once, on the first invoice only.","format":"int64","type":"integer"},"trial_days":{"description":"A trial raises no invoice at all. Access runs to the trial's end and the first bill is issued when it expires.\n","type":"integer"}},"required":["code","name","amount","interval_unit"],"type":"object"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubscriptionPlan"}}},"description":"The new plan version."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/Unprocessable"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/Internal"}},"summary":"[Beta] Create a plan, or reprice one"}},"/v1/subscriptions":{"get":{"operationId":"listSubscriptions","parameters":[{"in":"query","name":"limit","schema":{"type":"integer"}},{"in":"query","name":"offset","schema":{"type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"properties":{"data":{"items":{"$ref":"#/components/schemas/Subscription"},"type":"array"}},"type":"object"}}},"description":"This account's subscriptions."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/Internal"}},"summary":"[Beta] List subscriptions"},"post":{"description":"Bills the opening cycle immediately and returns with the first invoice already issued — unless the plan has a trial, which raises nothing until it ends.\n\nThe mode follows your key: a `ft_test_` key creates a sandbox subscription, which raises sandbox payment requests and sends no reminders to anybody.\n\nLive subscriptions are for verified accounts only: until verification is through, a live key is answered `403 verification_required` and nothing is written. A sandbox key works throughout.\n","operationId":"createSubscription","requestBody":{"content":{"application/json":{"schema":{"properties":{"customer_id":{"description":"`cust_\u003cuuid\u003e`","type":"string"},"external_id":{"type":"string"},"plan_id":{"description":"`plan_\u003cuuid\u003e`","type":"string"},"quantity":{"default":1,"type":"integer"},"start_at":{"description":"RFC3339. The day the first cycle opens, and the calendar day every later cycle anchors to. Defaults to today.\n","format":"date-time","type":"string"}},"required":["customer_id","plan_id"],"type":"object"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Subscription"}}},"description":"The subscription."},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"`verification_required` — live subscriptions need a verified account; use a sandbox key meanwhile."},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/Unprocessable"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/Internal"}},"summary":"[Beta] Start a subscription"}},"/v1/subscriptions/{id}":{"get":{"operationId":"getSubscription","parameters":[{"description":"`sub_\u003cuuid\u003e`","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Subscription"}}},"description":"The subscription."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/Internal"}},"summary":"[Beta] Retrieve a subscription"}},"/v1/subscriptions/{id}/advance":{"post":{"description":"Simulate the passage of time so a renewal can be watched without waiting for one.\n\nThis bills nothing itself. It moves the subscription's own clock; the renewal sweep then does on its next pass exactly what it does in production — issues the invoice for each cycle passed, creates the QRIS request, and extends access when it is paid. A simulation running its own billing path would be testing code nobody runs.\n\nAdvances accumulate, so a year of renewals can be walked one call at a time. It only moves forward: rewinding would put a subscription behind cycles it has already billed.\n\nSandbox only. A live subscription is refused `422`.\n","operationId":"advanceSubscriptionClock","parameters":[{"description":"`sub_\u003cuuid\u003e`","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"to":{"description":"RFC3339. Where the subscription's clock should stand after this call — later than where it stands now, which the `test_clock` field reports.\n","format":"date-time","type":"string"}},"required":["to"],"type":"object"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Subscription"}}},"description":"The subscription, with its clock moved."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/Unprocessable"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/Internal"}},"summary":"[Beta, test mode] Move a sandbox subscription's clock forward"}},"/v1/subscriptions/{id}/cancel":{"post":{"description":"`at_period_end: false` (the default) stops access immediately. `at_period_end: true` honours the service already paid for and stops when the current period runs out. Neither refunds anything.\n\nCancellation is final. A payment arriving afterwards settles its invoice — the debt was real — and grants no access and restarts no billing. Cancelling an already-cancelled subscription is `409`.\n","operationId":"cancelSubscription","parameters":[{"description":"`sub_\u003cuuid\u003e`","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"at_period_end":{"default":false,"type":"boolean"}},"type":"object"}}},"required":false},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Subscription"}}},"description":"The subscription."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/SubscriptionConflict"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/Internal"}},"summary":"[Beta] Cancel a subscription"}},"/v1/subscriptions/{id}/change":{"post":{"description":"Takes effect at the next renewal, and only then. There is no immediate option: proration is not available in this release, and applying a paid upgrade before it is paid for would grant service nobody authorised.\n\nThe scheduled values appear as `pending_plan_id` and `pending_quantity` until the renewal applies them.\n","operationId":"changeSubscription","parameters":[{"description":"`sub_\u003cuuid\u003e`","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"properties":{"plan_id":{"description":"`plan_\u003cuuid\u003e`","type":"string"},"quantity":{"minimum":1,"type":"integer"}},"type":"object"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Subscription"}}},"description":"The subscription, with the change pending."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"422":{"$ref":"#/components/responses/Unprocessable"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/Internal"}},"summary":"[Beta] Schedule a plan or quantity change"}},"/v1/subscriptions/{id}/pause":{"post":{"description":"Stops billing, and stops access with it. A pause is not a free month.\n","operationId":"pauseSubscription","parameters":[{"description":"`sub_\u003cuuid\u003e`","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Subscription"}}},"description":"The subscription."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/SubscriptionConflict"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/Internal"}},"summary":"[Beta] Pause a subscription"}},"/v1/subscriptions/{id}/resume":{"post":{"description":"Returns to `active`, or to `past_due` when an invoice is still outstanding — the pause did not settle the debt.\n","operationId":"resumeSubscription","parameters":[{"description":"`sub_\u003cuuid\u003e`","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Subscription"}}},"description":"The subscription."},"401":{"$ref":"#/components/responses/Unauthorized"},"404":{"$ref":"#/components/responses/NotFound"},"409":{"$ref":"#/components/responses/SubscriptionConflict"},"429":{"$ref":"#/components/responses/RateLimited"},"500":{"$ref":"#/components/responses/Internal"}},"summary":"[Beta] Resume a paused subscription"}},"/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. Above Rp1.000.000 also `description_required`, `delivery_date_required`, `delivery_date_invalid`, `buyer_contact_required`, `buyer_name_required` or `delivery_terms_not_shown`.\n"},"429":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorResponse"}}},"description":"`rate_limited`, `unverified_count_cap` or `unverified_amount_cap`.\n"},"500":{"$ref":"#/components/responses/Internal"}},"summary":"Create a payment request"}},"/v1/transactions/{id}":{"get":{"description":"At most one read per second per transaction on one key; a second read inside that second is 429 `rate_limited` with `Retry-After`. A finished request (succeeded, failed, canceled, expired) is served from cache. Status changes arrive on the `payment.paid` webhook — poll only when you need certainty.","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://api.fintar.id"}]}