Payments Service API
Merchant onboarding, payment intents, refunds, settlements, disputes, webhooks, and public checkout
The Payments Service is the transaction core of 505pay. It handles the full merchant lifecycle — from KYB onboarding through live payment intents, refunds, disbursements, and webhook delivery. Every payment method (QRIS, VA, GoPay, OVO, etc.) runs through this service.
Base URLs
| Environment | Hostname |
|---|---|
| Sandbox | https://sandbox.505pay.link |
| Production | https://api.505pay.link |
Server-to-server merchant calls (intents, refunds, settings) authenticate with an API key (sk_test_… in sandbox, sk_live_… in production) created at Settings → API Keys in the dashboard. Admin / console flows authenticate with a Bearer JWT issued by the Auth Service. Public checkout endpoints (/v1/public/payments/intents/*) are unauthenticated — they are called by 505pay's hosted payment page, not your server.
Payment intent lifecycle
A payment intent is the authoritative record of a payment attempt. It moves through states as the payer interacts with the PSP.
Idempotency
Pass Idempotency-Key: <uuid> on POST /v1/payments/intents to guarantee at-most-once creation. Replaying the same key returns the original intent without double-charging.
First payment in 3 steps
# 1. Create a payment intent (QRIS Dynamic — change allowed_channels for other methods)
curl -X POST https://sandbox.505pay.link/v1/payments/intents \
-H "Authorization: Bearer $PAYOPS_SECRET_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-1234-v1" \
-d '{
"merchant_id": "'"$PAYOPS_MERCHANT_ID"'",
"amount_idr": 50000,
"max_uses": 1,
"description": "Order #1234",
"allowed_channels": ["qris"]
}'
# 2. Show the payer the checkout — redirect to intent.checkout_url, or render intent.qr_string
# 3. Listen for the payment.completed webhook on your registered endpointconst intent = await fetch('https://sandbox.505pay.link/v1/payments/intents', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.PAYOPS_SECRET_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': 'order-1234-v1',
},
body: JSON.stringify({
merchant_id: process.env.PAYOPS_MERCHANT_ID,
amount_idr: 50_000,
max_uses: 1,
description: 'Order #1234',
allowed_channels: ['qris_dynamic'],
}),
}).then((r) => r.json());
console.log(intent.checkout_url); // hosted page; or render intent.qr_string yourselfimport os, requests
intent = requests.post(
'https://sandbox.505pay.link/v1/payments/intents',
headers={
'Authorization': f"Bearer {os.environ['PAYOPS_SECRET_KEY']}",
'Idempotency-Key': 'order-1234-v1',
},
json={
'merchant_id': os.environ['PAYOPS_MERCHANT_ID'],
'amount_idr': 50_000,
'max_uses': 1,
'description': 'Order #1234',
'allowed_channels': ['qris_dynamic'],
},
).json()
print(intent['checkout_url'])import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"github.com/google/uuid"
)
body, _ := json.Marshal(map[string]any{
"merchant_id": os.Getenv("PAYOPS_MERCHANT_ID"),
"amount_idr": 50_000,
"max_uses": 1,
"description": "Order #1234",
"allowed_channels": []string{"qris"},
"metadata": map[string]string{"order_id": "1234"},
})
req, _ := http.NewRequest("POST",
"https://sandbox.505pay.link/v1/payments/intents",
bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+os.Getenv("PAYOPS_SECRET_KEY"))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", "order-1234-"+uuid.New().String())
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
// intent.checkout_url is in the JSON response
var intent map[string]any
_ = json.NewDecoder(resp.Body).Decode(&intent)
fmt.Println(intent["checkout_url"])<?php
require 'vendor/autoload.php'; // Guzzle
$client = new \GuzzleHttp\Client();
$res = $client->post('https://sandbox.505pay.link/v1/payments/intents', [
'headers' => [
'Authorization' => 'Bearer ' . getenv('PAYOPS_SECRET_KEY'),
'Content-Type' => 'application/json',
'Idempotency-Key' => 'order-1234-' . \Ramsey\Uuid\Uuid::uuid4(),
],
'json' => [
'merchant_id' => getenv('PAYOPS_MERCHANT_ID'),
'amount_idr' => 50000,
'max_uses' => 1,
'description' => 'Order #1234',
'allowed_channels' => ['qris_dynamic'],
'metadata' => ['order_id' => '1234'],
],
]);
$intent = json_decode($res->getBody(), true);
echo $intent['checkout_url']; // hosted page; or render $intent['qr_string'] yourselfSee the full Accept your first payment recipe for the webhook + fulfilment loop.
Tenant legal terms
payments-service renders six bilingual legal documents — the MSA, the DPA, the acceptable use policy, the marketplace seller terms, the marketplace buyer terms and the privacy notice — from one tenant-scoped parameter set. The operator fills in a form; the documents its merchants sign and its buyers read are rendered from those values.
The shipped legal text is an engineering-prepared draft
The templates in this repository were prepared by an engineering agent from public sources. They are not legal advice, have not been reviewed by a licensed Indonesian advocate, and must not be published to merchants or buyers as counsel-approved text.
An operator must have its own Indonesian counsel review the rendered documents — with that operator's parameters filled in — before they are put in front of a merchant or a buyer.
Reading the settings and the form schema
GET /v1/payments/legal/settings returns the effective parameter set, the schema the console renders its form from, and the current revision. Effective means platform defaults merged with the tenant's stored values, with fallbacks already applied — an empty complaints email resolves to the support email — so the form shows what the documents will actually say.
{
"params": {
"operator.legal_name": "PT Sinar Jaya Digital",
"operator.brand_name": "Sinar Jaya",
"contacts.support_email": "[email protected]",
"contacts.complaints_email": "[email protected]",
"commercial.terms_change_notice_days": 30,
"data.breach_notice_hours": 48,
"data.hosting_countries": ["ID"],
"marketplace.buyer_min_age": 18,
"marketplace.return_window_days": 7,
"marketplace.courier_list": ["JNE", "SiCepat", "J&T"]
},
"schema": [
{
"key": "data.breach_notice_hours",
"label_en": "Breach notification (hours)",
"label_id": "Pemberitahuan insiden data (jam)",
"type": "int",
"min": 1,
"max": 72,
"required": false,
"material": true,
"help_id": "Ceiling 72 hours. UU 27/2022 art. 46: a processor slower than this makes its controller non-compliant."
}
],
"revision": 4,
"updated_at": "2026-09-18T11:02:44+07:00"
}Keys are dotted and grouped by the prefix before the first dot — operator., contacts., commercial., legal., data., aup., marketplace. — and the console groups the form on that prefix. Parameter types are text, textarea, int, percent, enum, email and list.
Admin routes do not auto-gate
requireBearer only checks that a JWT is present. Every handler here calls requirePermission explicitly: payments.tenant.read at scope * for the reads, payments.tenant.write at scope * for the write. A merchant-scoped caller — a sub-merchant owner holds payments.* at merchant:<id> only — gets a 403.
Writing
PUT /v1/payments/legal/settings is a partial update: only the keys present in params are written, and every other stored value survives.
{
"params": {
"commercial.terms_change_notice_days": 45,
"marketplace.courier_list": ["JNE", "SiCepat"]
}
}- Unknown keys are refused, never dropped. A silent drop is how an operator comes to believe it has shortened a notice period that still renders at 30 days.
- Bounds are statutory where a statute sets one — breach notification at most 72 hours, data-subject-request assistance at most 3 days, refunds at most 7 days, buyer age at least 18, AML retention at least 5 years. The
400 invalid_legal_settingsbody names the failing key and the reason, including the legal basis:
{
"error": "invalid_legal_settings",
"message": "one or more parameters are invalid: data.breach_notice_hours",
"request_id": "01K5Q7J2Z0000000000000000",
"fields": {
"data.breach_notice_hours": "must be at most 72. Ceiling 72 hours. UU 27/2022 art. 46: a processor slower than this makes its controller non-compliant."
}
}- Every write is audit-logged as
payments.tenant.legal_settings.updatedwith the changed keys only — never the values, several of which are contact email addresses.
Versioning, material changes and re-acceptance
revision advances only when a PUT actually changes a stored value. An idempotent re-save of the same form leaves it where it was, because the revision feeds the rendered document version and a bump asks every merchant of the tenant to re-accept.
A rendered document's version is <template version>+r<revision> — and the +r… suffix appears only once a material parameter differs from its default. Each schema entry carries a material bit; changing a material parameter moves the document version and raises needs_reaccept on the agreements list. Changing a non-material one (a support phone number, say) does not disturb anyone's acceptance.
Terms that cannot be set
Some terms are fixed by statute and are absent from the schema by construction, not guarded at the edge — a key with no schema entry can never be stored and can never be rendered. A PUT naming one is rejected as an unknown key. They are:
- The precedence of the Indonesian text. Locked. UU 24/2009 art. 31 and Perpres 63/2019 art. 26: the Indonesian text exists and prevails for an Indonesian counterparty.
- Governing law and the buyer dispute forum.
- The base prohibited-business list.
aup.prohibited_additionsappends bullets and cannot remove one.
Previewing
GET /v1/payments/legal/preview?doc=marketplace-buyer-terms&locale=id renders one document with the caller tenant's current parameters and returns title, body_markdown, version and hash (the sha256 of the exact body returned). doc accepts msa, dpa, aup, marketplace-seller-terms, marketplace-buyer-terms and privacy-notice.
A required parameter the tenant has not set yet does not fail the preview: it renders as a loud, clearly marked placeholder token so the operator can see what is still owed while filling the form in. Nothing that reaches a merchant or a buyer ever contains one.
locale selects the document title. The body is bilingual in every locale, by design — the Indonesian text must exist and prevails, so no locale strips it. Omitting locale yields Indonesian.
The storefront read
GET /v1/payments/legal/documents/{doc} is the service-to-service read that marketplace-service proxies to its buyer-facing /v1/marketplace/legal/{buyer-terms|privacy-notice}.
It is an ordinary /v1 route rather than a separate internal plane because that is the existing S2S channel: marketplace-service exchanges its service-account credential at auth-service's POST /v1/internal/token-exchange for a bearer minted for the seller's tenant, and calls this with it. The tenant is therefore taken from the caller's claims, never from a query parameter, so there is no cross-tenant read primitive here.
Only the two buyer-facing documents are served — marketplace-buyer-terms and privacy-notice. The MSA, DPA, acceptable use policy and seller terms are B2B documents behind the merchant's own acceptance flow, and serving them on the route a public storefront proxies would publish the operator's commercial terms.
| Status | Meaning |
|---|---|
404 | the tenant has no legal settings row, or the document is not buyer-facing |
409 | legal_settings_incomplete — a settings row exists but required parameters are missing; the body lists them in missing_keys. The document is not served half-rendered. |
No settings row
A tenant that has never written legal settings keeps its previous behaviour unchanged: revision is 0, updated_at is empty, and the buyer-facing routes simply have nothing to serve, so the storefront's legal pages answer the neutral 404. Nothing else about the tenant's payments behaviour depends on this row.
Endpoint reference
Merchant onboarding (KYB)
Create and submit merchants through KYB. Progress through draft → submitted → approved/rejected.
Get current agreement template
List KYB document requirements
List non-prohibited industries
Get a merchant
List beneficial owners
List documents
List caller's merchants
Create a merchant draft
Record agreement acceptance
Add a beneficial owner
Upload a KYB document
Submit merchant through KYB
Partial-update a draft merchant
Update beneficial owner
Delete a beneficial owner
Team
Add and remove merchant team members.
Payment intents
Create, list, and cancel payment intents. Track attempt history.
Refunds
Issue full or partial refunds and list them.
Settlements
View settlement batches and payout settings.
Disbursements
Create, approve/reject disbursements with two-eyes workflow. Manage recipients and batch uploads.
List disbursements
Get a single disbursement
List uploaded disbursement batches
Get a disbursement batch (with row-level status)
List disbursement recipients (address book)
Get a disbursement recipient
List disbursement batches (paginated)
Get a disbursement batch
Get line items for a disbursement batch
Create a new disbursement (status=pending under threshold, pending_approval over)
Approve a pending_approval disbursement (two-eyes; AC-42.5)
Reject a pending_approval disbursement (two-eyes; AC-42.5)
Upload a CSV batch of disbursements (AC-42.6 — idempotent via csv_sha256)
Create a disbursement recipient
Verify a recipient's bank account via PSP account validation
PSP disbursement webhook receiver — Flip and other providers (AC-42.8)
UOB advisory disbursement webhook (JWKS-kid signed notification)
Update a disbursement recipient
Deactivate a disbursement recipient
Payouts
Merchant payout accounts, schedules, and payout history.
List a merchant's payout accounts
Get a merchant's payout schedule
List the merchant's historical payouts (paginated)
Get a single payout (with line items)
Get the projected next payout for the merchant
Reject a pending payout account (admin)
Revoke a verified payout account (admin; opens a new pending row)
Mark a pending payout account as verified (admin)
Pause a merchant's payout schedule (admin)
Resume a merchant's payout schedule (admin)
Submit a payout bank account (lands as pending_verification)
Update a merchant's payout schedule
Pricing & billing
Tenant-level pricing templates, per-merchant overrides, billing charges, and statements.
Reporting & transactions
Revenue KPIs, transaction ledger, drill-through detail, and async CSV export.
Merchant payments dashboard snapshot
List transactions (paginated)
Drill-through detail for one payment intent (AC-6.3, AC-6.5)
Export transactions as CSV or XLSX
List async export jobs for the caller's tenant (AC-6.12)
Get an export job by id (AC-6.12)
Enqueue an async export job (AC-6.12)
Disputes (chargebacks)
View, respond to, and accept chargeback disputes.
Payment channels & routing
Enable payment channels and manage routing rules.
PSP credentials
Manage Payment Service Provider credentials and rotation.
API keys
Manage API keys — create, update, revoke, and monitor for leaked keys.
Webhooks
Webhook subscriptions for event delivery.
Public checkout (unauthenticated)
Called by 505pay's hosted payment page — no Authorization header required.
PSP webhooks (inbound)
Inbound webhook endpoint called by PSP providers.
Tenant legal terms
The tenant's configurable legal parameters, the schema the console renders its form from, the document preview, and the service-to-service read the storefront proxies for its buyer-facing documents.
Health & metadata
Liveness and readiness probes.
Ungrouped operations
87 operations in the spec are not listed in the groups file. Add their operationIds to content/docs/api/payments.groups.json.
List owner downstream fee schedules
List owner per-period downstream-fee invoices
Get an owner invoice with per-event attribution lines
Get the owner payout reserve percentage
List all tenants' Straventa subscriptions (platform admin)
List Straventa plans
List PSP settlement reconciliation statements
Get a settlement statement with its reconciled lines
List subscriptions for a merchant
Get a single subscription
List unified payments cases (aggregates disputes + refunds)
Get a single case by composite id ({type}:{uuid})
Per-channel revenue breakdown for a date range
Conversion funnel (Created / Viewed / Charge submitted / Paid)
Metric + grain timeseries
Top payment links by paid amount for a date range
Export an approved manual_file batch as a bank worksheet (CSV or XLSX)
Get the effective disbursement approval threshold
List merchant invoices (paginated)
Get a single merchant invoice
Download the rendered invoice PDF
List accepted agreements
List customers for a merchant
Get customer profile with aggregates
GDPR Article 15 export — full customer record as JSON
Get the active GDPR delete job for a customer (404 when none)
List payment intents for a customer
List notes for a customer (most-recent first)
List refunds tied to a customer (most-recent first)
Get merchant KYC/KYB verification state
List the caller's KYC documents (merchant-facing)
Fetch metadata for a single KYC document
List the platform KYC reviewer queue
List the platform KYC/KYB verification review queue
Get public refund status (HMAC-authenticated, no JWT required)
Stream the merchant's disbursement batches as a CSV file
Stream the merchant's settlements as a CSV file
Get merchant customer-email settings (reply_to + branding + verification state)
Read merchant notification preferences
List webhook delivery attempts for a subscription
List dead-letter queue rows for a subscription
List the canonical event catalogue
Get the webhook dispatcher's retry policy
Get the tenant's active Straventa subscription
Download invoice PDF (public)
View subscription portal (anonymous)
Anonymous HMAC-token verification of merchant reply_to (mailbox round-trip)
Create an owner downstream fee schedule
Create or update a Straventa plan (platform admin)
Ingest and reconcile a PSP settlement statement
Create a recurring subscription
Merchant-initiated cancel
Upload the bank/portal result file to settle or fail disbursement rows
Preview fee + total debit before submitting a disbursement (AC-42.9)
Sandbox 3-D-Secure callback for the card_3ds channel
Re-send the invoice email
Void an invoice
GDPR Article 17 — schedule customer erasure
Append a free-form note to a customer profile
Bulk-import customers from a CSV file
Upload a KYC document (merchant-facing, multipart)
Re-upload (replace) a KYC document (multipart)
Approve a KYC document (terminal transition)
Reject a KYC document (terminal transition)
Transition document from uploaded to under_review
Dispose a needs_review verification (verified or rejected)
File a recon question / settlement-variance dispute
Rotate an API key
Send mailbox-round-trip verification email to the current reply_to
Replay a single webhook delivery
Replay every dead-letter row for a subscription
Rotate a webhook subscription's signing secret
Subscribe the tenant to a Straventa plan
Cancel the tenant's Straventa subscription
Record a VA Copy-button click (anonymous telemetry)
Subscriber-initiated cancel
Pause (active → paused)
Resume (paused → active)
Update an owner downstream fee schedule
Set the owner payout reserve percentage
Set the tenant-level disbursement approval threshold
Update merchant customer-email settings
Update merchant notification preferences
Transition a case status (delegates to underlying dispute/refund)
Update a customer note (replaces body; bumps updated_at)
Update subscriber email (rotates portal token)
Hard-delete a customer note (irreversible)
Was this page helpful?