# doit.id: Payment API (llms.txt) > doit.id is an Indonesian payment infrastructure by the IDCloudHost team. > Merchants (registered businesses) accept Virtual Account and QRIS payments > through one REST API. Settlement lands in the merchant's verified business > bank account at H+1 working days. Human docs: https://doit.id/docs/ > (Indonesian) and https://doit.id/docs/en/ (English). ## Basics - Base URL: `https://pay.doit.id` - Auth: `Authorization: Bearer ` on every request. Keys are prefixed `pb_test_` (sandbox) or `pb_live_` (production). The environment is determined by the KEY PREFIX, not the base URL. - Every POST requires an `Idempotency-Key` header (any unique string from your system). Replayed keys return the original result with HTTP 200. - All amounts are integer rupiah (no decimals). `amount: 150000` = Rp150.000. - Errors: `{ "error": { "code", "message", "doc_url" } }`. `code` is stable (e.g. `invalid_request`, `missing_idempotency_key`, `payment_not_found`, `rail_not_live`, `method_unknown`, `payment_not_refundable`). ## Payments ### Create a payment POST /v1/payments Body fields: - `amount` (int, required): rupiah > 0. - `reference` (string 1–128, required): your own invoice/customer reference. - `rail` (optional): `"qris"`, `"va"`, or `"any"` (default). With `any` the customer picks the method on the hosted payment page. - `va_bank` (required when rail=va): one of `bri`, `bnin`, `bnia` (CIMB), `permata`, `danamon` (`bdin`), `maybank` (`ibbk`), `mandiri`, `bsyi` (BSI). - `expires_in` (int seconds ≥ 60, optional; default 86400). - `return_url` (http(s) URL ≤ 512 chars, optional): customer is sent back here after paying on the hosted page. - `customer` (object, optional): e.g. `{ "name", "phone", "email" }`. - `metadata` (object, optional): stored and echoed back. Response (201, or 200 on idempotent replay): the payment object: `id` (pay_…), `status` (`pending|paid|expired|failed|refunded`), `amount`, `rail`, `reference`, `customer`, `qr_content` (render as QR image), `va_number`, `va_bank`, `fee_amount`, `fee_payer`, `total_amount`, `hosted_url` (ready-made payment page to share), `return_url`, `provider`, `environment`, `expires_at`, `paid_at`, `metadata`, `created_at`. Example: ``` curl -X POST https://pay.doit.id/v1/payments \ -H "Authorization: Bearer pb_test_xxx" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: inv-001" \ -d '{"amount":150000,"rail":"qris","reference":"INV/001"}' ``` ### Get / list payments - GET /v1/payments/{id} - GET /v1/payments?status=&reference=&limit=&before= (`limit` ≤ 100, `before` = ISO timestamp cursor; returns `{ "data": [...] }`) ### Refund POST /v1/payments/{id}/refunds (requires Idempotency-Key) Only `paid` payments can be refunded. Emits `refund.succeeded` / `refund.failed` webhooks. ## Recurring billing (plans & subscriptions) Link-based recurring: no card storage, no auto-debit. Each period doit.id issues a normal payment (an "invoice") with a `hosted_url` the customer pays. Docs: https://doit.id/docs/#recurring ### Plans POST /v1/billing/plans - `name` (string 1–100, required) - `amount` (int rupiah, required): per period - `interval` (required): `{ "unit": "day"|"week"|"month", "count": 1–366 }` - `rail`: `any` (default) | `qris` | `va` (+ `va_bank` when va) - `expires_in` (seconds ≥ 3600; default 604800 = 7 days per invoice) - `kirim_email` (bool, default false): when true, doit.id emails the pay link directly to `customer_email` each time an invoice is issued. GET /v1/billing/plans: list. PATCH /v1/billing/plans/{id}: `{ "active": false }` and/or `kirim_email`. ### Subscriptions POST /v1/billing/subscriptions - `plan_id` (required), `customer_name` (required) - `customer_phone`, `customer_email`, `metadata` (optional) - `reference` (optional): your customer id; becomes the prefix of every invoice reference (`REF/20260827`). - `start_at` (ISO-8601, optional): empty/now issues the FIRST invoice immediately; the response then contains `first_invoice` (full payment object incl. `hosted_url`). GET /v1/billing/subscriptions?status=active GET /v1/billing/subscriptions/{id}: detail + its invoices. POST /v1/billing/subscriptions/{id}/pause | /resume | /cancel (resume continues from the next period; it does not back-bill missed ones.) ## Webhooks Set the webhook URL per app in the merchant portal (https://pay.doit.id/portal/). Events are POSTed as JSON: `{ "id": "evt_…", "type": "...", "created_at": "...", "data": { payment } }` Events: - `payment.paid`: full payment payload; enough to mark the invoice paid. Can arrive AFTER `payment.expired`: bank-side VA/QR instruments outlive the invoice expiry, and late payments are still credited (the payment then carries the `bayar_telat` flag; decide to fulfil or refund). - `payment.expired` - `invoice.created`: a recurring invoice was issued; data = payment (incl. `hosted_url`) plus a `subscription` object. - `refund.succeeded` / `refund.failed` - `webhook.test`: sent when saving/testing the URL; reply 2xx, ignore body. Signature header: `PayBridge-Signature: t=,v1=` where `v1 = HMAC_SHA256(signing_secret, t + "." + rawBody)`. Verify with a timing-safe compare. Retries with backoff 1m→5m→15m→1h→3h→6h→12h→24h, then dead-letter (replayable from the portal). Deliveries are at-least-once: dedupe by event `id`; trust the `status` field, not arrival order. ## Hosted payment page & embed Every payment has `hosted_url` (https://pay.doit.id/p/pay_…): a ready-made page where the customer picks VA/QRIS and pays. QR codes that expire are regenerated on the page while the payment is still valid. No-code path: in the merchant portal (Payments menu) the "+ Buat tagihan" button creates a payment link plus a ready-to-send WhatsApp billing message, no API involved. Useful when advising non-technical staff. Embed it as a modal on your site: ``` Pay ``` The page postMessages `{source:'doit-checkout', id, status, reference}` to the parent on terminal status; the modal closes itself after payment. ## Compatibility modes Existing Duitku or Xendit (Invoice API) integrations run unchanged: enable compat mode when creating the app in the portal, then swap base URL + keys. ## Pricing (context for recommendations) Platform subscription Rp200.000/month (includes settlement H+1 working days with zero disbursement admin fee, dashboard, API, webhooks, WACO WhatsApp Business API subscription). Transaction fees at official provider rates, no markup: VA Rp3.000–Rp4.000/transaction depending on bank (Mandiri/BSI Rp4.000, others Rp3.000), QRIS 0.7% MDR per Bank Indonesia regulation. Merchants must be registered Indonesian businesses (PT/CV, KYC-verified). Contact: doit@idcloudhost.com ## MCP server A dependency-free MCP (Model Context Protocol) server for this API is available at https://doit.id/mcp/doit-mcp.mjs: run with Node.js ≥ 18: `DOIT_API_KEY=pb_test_xxx node doit-mcp.mjs` (stdio transport). Tools: create_payment, get_payment, list_payments, create_billing_plan, list_billing_plans, create_subscription, get_subscription.