Accept your first payment
End-to-end — create a payment intent with your sandbox API key, display the hosted checkout, fulfil on the webhook
Execute curls interactively
To run these API calls directly from your browser, open the Payments API reference and use the built-in Try-it playground. Paste your sk_test_… key in the Auth panel, pick Sandbox, and send without leaving the docs.
This recipe gets you from zero to a confirmed payment in your sandbox. It uses QRIS Dynamic (simplest integration — no redirect, payer just scans a QR generated from the response).
What you'll build: server creates an intent with its sandbox API key → receives a checkout_url + qr_string → payer scans → server receives payment.completed webhook → fulfils the order.
Prerequisites:
- A sandbox merchant in dashboard.505pay.link (sign-up + KYB-lite — sandbox merchants are auto-approved).
- A sandbox API key with the
payments.payment_intent.createscope. Create one at Settings → API Keys → Create. The value is shown ONCE — store it asPAYOPS_SECRET_KEY(prefixsk_test_…). - Your merchant's UUID. Find it at Settings → General or via
GET /v1/payments/merchants/me. Store asPAYOPS_MERCHANT_ID.
Why an API key, not an OAuth token?
Server-to-server merchant integrations authenticate with a long-lived secret API key (sk_test_… / sk_live_…). OAuth client_credentials is for the dashboard / console flows, not for your checkout backend. See API keys guide for rotation and scoping.
Create a payment intent
The single mandatory request for QRIS Dynamic — the response contains everything you need to display the QR or redirect to the hosted page.
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-$(uuidgen)" \
-d '{
"merchant_id": "'"$PAYOPS_MERCHANT_ID"'",
"amount_idr": 50000,
"max_uses": 1,
"description": "Order #1234",
"allowed_channels": ["qris"],
"metadata": { "order_id": "1234" }
}'const res = 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-${crypto.randomUUID()}`,
},
body: JSON.stringify({
merchant_id: process.env.PAYOPS_MERCHANT_ID,
amount_idr: 50_000,
max_uses: 1,
description: 'Order #1234',
allowed_channels: ['qris'],
metadata: { order_id: '1234' },
}),
});
const intent = await res.json();
// intent.checkout_url, intent.shortcode, intent.idimport os, uuid, requests
intent = requests.post(
'https://sandbox.505pay.link/v1/payments/intents',
headers={
'Authorization': f"Bearer {os.environ['PAYOPS_SECRET_KEY']}",
'Idempotency-Key': f'order-1234-{uuid.uuid4()}',
},
json={
'merchant_id': os.environ['PAYOPS_MERCHANT_ID'],
'amount_idr': 50_000,
'max_uses': 1,
'description': 'Order #1234',
'allowed_channels': ['qris'],
'metadata': {'order_id': '1234'},
},
timeout=10,
).json()$res = (new \GuzzleHttp\Client())->post(
'https://sandbox.505pay.link/v1/payments/intents',
[
'headers' => [
'Authorization' => 'Bearer ' . getenv('PAYOPS_SECRET_KEY'),
'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'],
'metadata' => ['order_id' => '1234'],
],
]
);
$intent = json_decode($res->getBody(), true);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)Response (201 Created):
{
"id": "018f7c2d-1f8b-7c2d-9a2d-1f8b92f6d101",
"shortcode": "abc123def456",
"merchant_id": "018f4d9f-7b8a-7c2d-9a2d-1f8b92f6d101",
"amount_idr": 50000,
"status": "open",
"allowed_channels": ["qris"],
"description": "Order #1234",
"checkout_url": "https://sandbox.505pay.link/pay/abc123def456",
"metadata": { "order_id": "1234" },
"created_at": "2026-05-17T12:00:00Z"
}Show the payer the checkout
Two options, depending on whether you want 505pay to host the UI:
- Hosted (recommended): redirect the browser to
intent.checkout_url. 505pay renders the QR, polls for completion, and handles UX edge cases. - Self-hosted: call
GET /v1/public/payments/intents/{shortcode}(unauthenticated, intended for your own frontend) and render the returned channel-specificqr_stringas a QR image.
In sandbox you can simulate payment completion from the dashboard's Payments → Intents → simulate button — no scan required.
Receive the payment.completed webhook
Subscribe to webhooks at Settings → Webhooks → New. Set url = https://your-server.example/webhooks/505pay and copy the displayed secret into your env (PAYOPS_WEBHOOK_SECRET).
When the payment completes 505pay POSTs the event to your endpoint:
POST /webhooks/505pay HTTP/1.1
Content-Type: application/json
X-Straventa-Signature: 4f1c…hex
X-Straventa-Event-Id: 018f8a…
X-Straventa-Delivery-Id: 018f8a…
X-Straventa-Event-Type: payment.completed
{
"event": "payment.completed",
"intent_id": "018f7c2d-1f8b-7c2d-9a2d-1f8b92f6d101",
"merchant_id": "018f4d9f-…",
"shortcode": "abc123def456",
"amount_idr": 50000,
"status": "paid",
"completed_at": "2026-05-17T12:01:23Z",
"metadata": { "order_id": "1234" }
}Verify with HMAC-SHA256 over the raw request body using PAYOPS_WEBHOOK_SECRET. See Webhooks guide for the per-language recipe.
Fulfil on the webhook, not the response
The 201 response confirms the intent was created. The payment.completed webhook confirms funds were captured. Always fulfil on the webhook.
Confirm end-to-end
You can poll the intent at any time:
curl https://sandbox.505pay.link/v1/payments/intents/$INTENT_ID \
-H "Authorization: Bearer $PAYOPS_SECRET_KEY"
# → { "status": "paid", ... }Next steps
- Payment lifecycle — full state machine + per-method webhook events
- Issue a refund — for the unhappy path
- Other payment methods — VA, GoPay, OVO, Card 3DS, and more (all use the same
POST /v1/payments/intentsroute with differentallowed_channels) - Webhooks guide — HMAC verification + retry semantics
Was this page helpful?