By the end of this guide you’ll have:
- An API key + signing secret for your merchant.
- A signed deep link that drops a customer into Topiic’s hosted card capture.
- A webhook endpoint that receives
checkout.completedand provisions the customer on your side.
You can run the whole flow against a sandbox merchant before touching real customer cards.
Before you start
Section titled “Before you start”You need:
- A Topiic merchant account with at least one published plan. Ask your Topiic contact for a sandbox merchant if you don’t have one yet.
- A login for the Topiic portal so you can mint credentials.
- An HTTPS endpoint on your side that can receive a
POSTrequest (use webhook.site if you don’t have one yet). - Node 20+, Python 3.10+, or another language that can compute HMAC-SHA256.
1. Mint an API key
Section titled “1. Mint an API key”- Sign into the Topiic portal.
- Navigate to Settings → API keys.
- Click New API key, name it after your integration, then Create key.
- You’ll see the plaintext API key (
tpk_…) and signing secret (tss_…) exactly once. Copy both into your secret store immediately. Topiic only retains hashes; if you lose them you’ll need to mint a new pair.
The API key authenticates direct REST calls. The signing secret is used to HMAC-sign deep links and to verify incoming webhook payloads.
2. Configure your webhook endpoint
Section titled “2. Configure your webhook endpoint”Still in Settings → API keys, find the row for the key you just minted and:
- Set the Endpoint URL to where you’ll receive events.
- Tick the events you care about (
checkout.completed,checkout.failed). - Toggle Enabled on.
- Save webhook.
Topiic will start delivering events to that URL once any are produced. Failed deliveries retry on backoff (see Retries & replay).
3. Pick a plan id
Section titled “3. Pick a plan id”In the portal, open Products → <your product> → Plans and copy the Plan ID (a UUID) of the plan you want your customers to be enrolled in. You’ll embed this in the deep link.
4. Generate a signed deep link
Section titled “4. Generate a signed deep link”When a user on your side clicks “Subscribe”, your server builds a URL like:
https://pay.topiic.com/c?d=<base64url payload>&s=<base64url HMAC-SHA256 signature>The payload is a small JSON object identifying the merchant, plan, your customer reference, and a few security primitives:
{ "akid": "<your-api-key-id>", "mid": "<your-merchant-id>", "plan": "<plan-id>", "ref": "your-internal-customer-id", "contact": { "fn": "Jane", "ln": "Smith", "em": "jane@example.com", "ph": "+61 400 000 000" }, "ret": "https://your-app.example.com/subscribed?customer=42", "evts": ["checkout.completed", "checkout.failed"], "nonce": "<random 16+ bytes, single-use>", "exp": 1769472000}import crypto from 'node:crypto';
const secret = process.env.TOPIIC_SIGNING_SECRET; // tss_…
const payload = { akid: process.env.TOPIIC_API_KEY_ID, mid: process.env.TOPIIC_MERCHANT_ID, plan: 'b6a8a5b8-…-plan-id', ref: `cust_${internalCustomerId}`, contact: { fn: 'Jane', ln: 'Smith', em: 'jane@example.com' }, ret: 'https://your-app.example.com/subscribed?customer=42', evts: ['checkout.completed', 'checkout.failed'], nonce: crypto.randomBytes(16).toString('hex'), exp: Math.floor(Date.now() / 1000) + 60 * 30 // 30 min};
const d = Buffer.from(JSON.stringify(payload)) .toString('base64') .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
const s = crypto.createHmac('sha256', secret).update(d).digest('base64') .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
const url = `https://pay.topiic.com/c?d=${d}&s=${s}`;import base64, hmac, hashlib, json, os, secrets, time
secret = os.environ['TOPIIC_SIGNING_SECRET']
payload = { 'akid': os.environ['TOPIIC_API_KEY_ID'], 'mid': os.environ['TOPIIC_MERCHANT_ID'], 'plan': 'b6a8a5b8-…-plan-id', 'ref': f'cust_{internal_customer_id}', 'contact': {'fn': 'Jane', 'ln': 'Smith', 'em': 'jane@example.com'}, 'ret': 'https://your-app.example.com/subscribed?customer=42', 'evts': ['checkout.completed', 'checkout.failed'], 'nonce': secrets.token_hex(16), 'exp': int(time.time()) + 60 * 30,}
def b64url(b: bytes) -> str: return base64.urlsafe_b64encode(b).decode().rstrip('=')
d = b64url(json.dumps(payload, separators=(',', ':')).encode())s = b64url(hmac.new(secret.encode(), d.encode(), hashlib.sha256).digest())
url = f'https://pay.topiic.com/c?d={d}&s={s}'<?php$secret = getenv('TOPIIC_SIGNING_SECRET');
$payload = [ 'akid' => getenv('TOPIIC_API_KEY_ID'), 'mid' => getenv('TOPIIC_MERCHANT_ID'), 'plan' => 'b6a8a5b8-…-plan-id', 'ref' => "cust_{$internalCustomerId}", 'contact' => ['fn' => 'Jane', 'ln' => 'Smith', 'em' => 'jane@example.com'], 'ret' => 'https://your-app.example.com/subscribed?customer=42', 'evts' => ['checkout.completed', 'checkout.failed'], 'nonce' => bin2hex(random_bytes(16)), 'exp' => time() + 60 * 30,];
function b64url(string $bytes): string { return rtrim(strtr(base64_encode($bytes), '+/', '-_'), '=');}
$d = b64url(json_encode($payload, JSON_UNESCAPED_SLASHES));$s = b64url(hash_hmac('sha256', $d, $secret, true));
$url = "https://pay.topiic.com/c?d={$d}&s={$s}";Read the deep-link signing reference for the full payload schema and validation rules.
5. Redirect your user
Section titled “5. Redirect your user”Your server returns a 303 See Other to the URL you built. The user lands on Topiic, confirms their contact details, enters their card in the gateway-hosted form, and is redirected back to your ret URL with ?status=ok (or ?status=cancelled).
The redirect is not a guarantee that everything succeeded — it’s a UX cue. The webhook is the source of truth.
6. Handle the webhook
Section titled “6. Handle the webhook”When a checkout completes, Topiic POSTs to your webhook URL:
POST /webhooks/topiic HTTP/1.1Content-Type: application/jsonTopiic-Event-Id: 4f8c2a14-…Topiic-Idempotency-Key: 4f8c2a14-…Topiic-Signature: t=1769472312,v1=8f2a1c…
{ "id": "4f8c2a14-…", "type": "checkout.completed", "createdAt": "2026-05-27T10:25:12.123Z", "data": { "sessionId": "…", "externalRef": "cust_42", "memberId": "…", "paymentMethodId": "…", "subscriptionId": "…", "planId": "…" }}Your handler must:
- Verify the
Topiic-Signatureheader with your signing secret — see Verifying webhook signatures. Reject anything that doesn’t verify. - Look up the event id in your database. If you’ve already processed it, return
200 OKand stop. The same event may be delivered more than once. - Provision the customer on your side — link
data.externalRef(yourreffrom step 4) todata.memberIdso future events can be matched. - Return a 2xx response within 10 seconds. Any other response triggers a retry with backoff.
7. You’re done
Section titled “7. You’re done”That’s the whole hosted-checkout loop:
flowchart LR
A[Your server] -- signed link --> B[Topiic checkout]
B --> C[Gateway]
C --> D[Return URL]
B -- webhook (signed) --> E[Your server]
Next up:
- Core concepts — the data model behind members, plans, and subscriptions.
- Webhooks overview — every event type and what it carries.
- Going-live checklist — what to harden before flipping to production traffic.