Skip to main content
Realtime keys are short-lived tokens for subscribing to live payment events on a checkout session. Dollr uses Supabase Realtime under the hood.
The Dollr Open API does not expose inbound HTTP webhooks. For backend fulfillment, see Payment status patterns.

When to use

ApproachBest for
PollingSimple backends; few concurrent payments
Realtime keysLive checkout UI while customer approves MoMo or card
Hosted checkout + source statusSuccess page verification

Prerequisites

CredentialSource
Dollr Bearer tokenAuthentication
SUPABASE_URLEnvironments — merchant portal Developer settings
SUPABASE_ANON_KEYSame section

Step 1 — Obtain a collection realtime key

POST /v1/realtime-keys/collection
FieldTypeRequiredDescription
session_idintegerYesCheckout session ID from POST /v1/sessions/checkout
source_typestringYesINVOICE or ORDER
reference_idstringYesYour 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:
FieldDescription
access_tokenShort-lived JWT for Supabase Realtime auth
expires_inToken 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