Realtime keys are short-lived tokens for subscribing to live payment events on a checkout session. Dollr uses Supabase Realtime under the hood.
When to use
| Approach | Best for |
|---|
| Polling | Simple backends; few concurrent payments |
| Realtime keys | Live checkout UI while customer approves MoMo or card |
| Hosted checkout + source status | Success page verification |
Prerequisites
| Credential | Source |
|---|
| Dollr Bearer token | Authentication |
SUPABASE_URL | Environments — merchant portal Developer settings |
SUPABASE_ANON_KEY | Same section |
Step 1 — Obtain a collection realtime key
POST /v1/realtime-keys/collection
| Field | Type | Required | Description |
|---|
session_id | integer | Yes | Checkout session ID from POST /v1/sessions/checkout |
source_type | string | Yes | INVOICE or ORDER |
reference_id | string | Yes | Your UUID v4 (same as execution idempotency key) |
curl -X POST "https://api.heydollr.app/v1/realtime-keys/collection" \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"session_id": 55,
"source_type": "INVOICE",
"reference_id": "550e8400-e29b-41d4-a716-446655440000"
}'
Response:
| Field | Description |
|---|
access_token | Short-lived JWT for Supabase Realtime auth |
expires_in | Token lifetime in seconds |
session_id is an integer here. Execution endpoints expect session_id as a string — see API conventions.
Step 2 — Connect with Supabase Realtime
import { createClient } from "@supabase/supabase-js";
const supabase = createClient(
process.env.SUPABASE_URL,
process.env.SUPABASE_ANON_KEY,
{
auth: { autoRefreshToken: false, persistSession: false },
accessToken: async () => realtimeKey.access_token,
}
);
supabase.realtime.setAuth(realtimeKey.access_token);
const channel = supabase.channel(
`payment-intent:${sessionId}:${referenceId}`
);
channel
.on(
"postgres_changes",
{
event: "*",
schema: "public",
table: "checkout_payment_intent_public_status",
filter: `reference_id=eq.${referenceId}`,
},
(payload) => {
const row = payload.new;
console.log("Status update:", row);
// Update your UI — then verify via GET /v1/status/collection/{reference_id}
}
)
.subscribe((status) => {
if (status === "SUBSCRIBED") console.log("Watching payment");
if (status === "CHANNEL_ERROR") fallbackToPolling();
});
Channel naming
payment-intent:{session_id}:{reference_id}
Table
checkout_payment_intent_public_status — filtered by reference_id.
Step 3 — Fallback to polling
If realtime disconnects or times out:
GET /v1/status/collection/{reference_id}
Always persist reference_id before execute. Use realtime for UX; use status API for fulfillment decisions.
Timeout guidance
Subscribe for up to 30 seconds after execute, then fall back to polling. MoMo payments may take several minutes in PROCESSING.
Last modified on June 23, 2026