Build a hosted checkout page
Let 505pay host the payment UI — create an intent with your API key and redirect the payer to the 505pay checkout page
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.
The hosted checkout flow lets 505pay handle the entire payment UI. Your server mints a payment intent with your merchant API key, receives a checkout_url, and redirects the payer there. 505pay collects payment details, handles PSP redirects, and fires a webhook when done.
This is the fastest integration path — no frontend code required. The payer sees the 505pay-branded checkout page at pg.505pay.link (or your branded equivalent).
The hosted-checkout endpoint accepts your secret API key (sk_test_… in sandbox, sk_live_… in production). It also accepts a console JWT for backward compatibility with the dashboard, but server-to-server integrations should always use an API key. Create one at Settings → API keys in the dashboard.
Create a payment intent with your API key
Send your server-side sk_test_… (sandbox) or sk_live_… (production) key as the bearer. An Idempotency-Key header is required for API-key callers — re-using the same key with the same body replays the original response, so network retries are safe.
Attach metadata for any integrator-supplied bookkeeping (order id, customer ref, etc.). It round-trips on both the response and the payment_intent.created + payment.completed webhook events.
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-9876-$(uuidgen)" \
-d '{
"merchant_id": "018f4d9f-7b8a-7c2d-9a2d-1f8b92f6d101",
"amount_idr": 150000,
"max_uses": 1,
"description": "Order #9876",
"metadata": {
"order_id": "9876",
"customer_ref": "cust_42"
}
}'Response (201 Created):
{
"id": "018f...",
"shortcode": "abc123def456",
"amount_idr": 150000,
"status": "open",
"metadata": { "order_id": "9876", "customer_ref": "cust_42" },
"checkout_url": "https://pg.505pay.link/pay/abc123def456"
}import express from 'express';
import { randomUUID } from 'node:crypto';
const app = express();
app.use(express.json());
const PAYMENTS_BASE = 'https://sandbox.505pay.link';
const SK = process.env.PAYOPS_SECRET_KEY; // sk_test_... or sk_live_...
app.post('/checkout/start', async (req, res) => {
const { orderId } = req.body;
const r = await fetch(`${PAYMENTS_BASE}/v1/payments/intents`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${SK}`,
'Content-Type': 'application/json',
// Tie the idempotency key to your order id so retrying a failed
// request returns the SAME intent instead of minting a new one.
'Idempotency-Key': `order-${orderId}`,
},
body: JSON.stringify({
merchant_id: process.env.PAYOPS_MERCHANT_ID,
amount_idr: 150_000,
max_uses: 1,
description: `Order #${orderId}`,
metadata: { order_id: orderId },
}),
});
if (!r.ok) {
const err = await r.json();
return res.status(r.status).json(err);
}
const intent = await r.json();
// Persist intent.id alongside your order BEFORE redirecting, so the
// webhook handler can resolve back to the order.
await persistIntent({ orderId, intentId: intent.id });
res.redirect(303, intent.checkout_url);
});import os
import requests
from flask import Flask, request, redirect, jsonify
app = Flask(__name__)
PAYMENTS_BASE = 'https://sandbox.505pay.link'
SK = os.environ['PAYOPS_SECRET_KEY'] # sk_test_... or sk_live_...
@app.post('/checkout/start')
def start_checkout():
order_id = request.json['orderId']
r = requests.post(
f'{PAYMENTS_BASE}/v1/payments/intents',
headers={
'Authorization': f'Bearer {SK}',
# Tie idempotency key to the order id — retries return the SAME intent.
'Idempotency-Key': f'order-{order_id}',
},
json={
'merchant_id': os.environ['PAYOPS_MERCHANT_ID'],
'amount_idr': 150_000,
'max_uses': 1,
'description': f'Order #{order_id}',
'metadata': {'order_id': order_id},
},
timeout=10,
)
if not r.ok:
return jsonify(r.json()), r.status_code
intent = r.json()
# Persist intent.id alongside your order BEFORE redirecting.
persist_intent(order_id=order_id, intent_id=intent['id'])
return redirect(intent['checkout_url'], code=303)package main
import (
"bytes"
"encoding/json"
"net/http"
"os"
)
const paymentsBase = "https://sandbox.505pay.link"
func startCheckout(w http.ResponseWriter, r *http.Request) {
var in struct{ OrderID string `json:"orderId"` }
if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
http.Error(w, err.Error(), 400)
return
}
body, _ := json.Marshal(map[string]any{
"merchant_id": os.Getenv("PAYOPS_MERCHANT_ID"),
"amount_idr": 150_000,
"max_uses": 1,
"description": "Order #" + in.OrderID,
"metadata": map[string]string{"order_id": in.OrderID},
})
req, _ := http.NewRequest(http.MethodPost,
paymentsBase+"/v1/payments/intents", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+os.Getenv("PAYOPS_SECRET_KEY"))
req.Header.Set("Content-Type", "application/json")
// Tie the idempotency key to the order id — retries return the SAME intent.
req.Header.Set("Idempotency-Key", "order-"+in.OrderID)
resp, err := http.DefaultClient.Do(req)
if err != nil {
http.Error(w, err.Error(), 502)
return
}
defer resp.Body.Close()
var intent struct {
ID string `json:"id"`
CheckoutURL string `json:"checkout_url"`
}
if err := json.NewDecoder(resp.Body).Decode(&intent); err != nil {
http.Error(w, err.Error(), 502)
return
}
// Persist intent.ID alongside your order BEFORE redirecting.
persistIntent(in.OrderID, intent.ID)
http.Redirect(w, r, intent.CheckoutURL, http.StatusSeeOther)
}Payer completes payment on the hosted page
505pay handles the payment UI at the checkout_url. No frontend work needed on your side.
Receive the payment.completed webhook
Fulfilment must trigger on the webhook, never on the redirect. The payment.completed event echoes your metadata so you can resolve back to your order without a side-table lookup.
{
"event": "payment.completed",
"intent_id": "018f...",
"merchant_id": "018f...",
"shortcode": "abc123def456",
"amount_idr": 150000,
"status": "paid",
"completed_at": "2026-05-15T12:34:56Z",
"metadata": { "order_id": "9876", "customer_ref": "cust_42" }
}See the Webhooks guide for the signature verification recipe and the canonical event list.
Always verify the payment.completed webhook signature before fulfilling. The redirect back to your site is a UX hint, not a payment confirmation.
Handle replay and drift
- Same
Idempotency-Key+ same body → 201 Created with the original response (theIdempotent-Replayed: trueheader is set). Safe to retry on network blips. - Same
Idempotency-Key+ different body → 409idempotency_key_mismatch. Surface this as a bug in your retry logic; do not strip the key and retry. - Missing
Idempotency-Keyfrom an API-key caller → 400idempotency_key_required.
See the Idempotency guide for the full algorithm.
Related
- API keys guide — sandbox vs production keys, scopes, rotation
- Idempotency guide
- Webhooks guide
- Payment lifecycle — state machine
Was this page helpful?