Quickstart
A working integration in five steps. Time: ~5 minutes assuming you have your app credentials.
1. Get your app credentials
Section titled “1. Get your app credentials”Each uncle-z product is a “app” on the gateway with three secrets, generated by the operator at provisioning time:
public_key: pk_<24 hex chars> # non-secret app id; sent on every requestsecret: <64 hex chars> # request-signing secretwebhook_secret: <64 hex chars> # the gateway uses this to sign inbound webhooks; you verify with itYou’ll typically have two apps per product: one for mode=sandbox (development) and one for mode=live (production). See Modes for why.
If you don’t have credentials yet, ask the operator to run:
docker exec payment-server-1 /app/admin apps create \ --name <product-slug>-sandbox \ --webhook-url https://<your-product>.uncle-z.com/api/payment-webhook \ --mode sandbox2. Sign your first request
Section titled “2. Sign your first request”Every request to the gateway carries three headers:
| Header | Value |
|---|---|
X-PAY-Key | Your public_key |
X-PAY-Timestamp | Current Unix epoch seconds |
X-PAY-Signature | Hex HMAC-SHA256(secret, "<ts>.<METHOD>.<URL.Path>.<sha256(body) as hex>") |
Reference signer in Node:
import { createHmac, createHash } from 'node:crypto';
function sign({ method, path, body, timestamp, secret }) { const bodyHash = createHash('sha256').update(body || '').digest('hex'); const canonical = `${timestamp}.${method}.${path}.${bodyHash}`; return createHmac('sha256', secret).update(canonical).digest('hex');}In Go:
import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "fmt")
func sign(secret, method, path string, body []byte, ts int64) string { bodyHash := sha256.Sum256(body) canonical := fmt.Sprintf("%d.%s.%s.%x", ts, method, path, bodyHash[:]) mac := hmac.New(sha256.New, []byte(secret)) mac.Write([]byte(canonical)) return hex.EncodeToString(mac.Sum(nil))}In PHP:
function sign(string $secret, string $method, string $path, string $body, int $ts): string { $bodyHash = hash('sha256', $body); $canonical = "$ts.$method.$path.$bodyHash"; return hash_hmac('sha256', $canonical, $secret);}3. Create a payment
Section titled “3. Create a payment”PK=pk_xxx # your public keySK=xxx # your secretBODY='{"external_user_id":"u-1","email":"buyer@example.com","amount_minor":2900,"currency":"USD","method":"polar","metadata":{"polar_product_id":"<polar-product-uuid>"},"return_url":"https://yourproduct.uncle-z.com/payment/return","invoice_prefix":"PROD-PRO"}'TS=$(date +%s)H=$(printf '%s' "$BODY" | openssl dgst -sha256 -hex | awk '{print $NF}')S=$(printf '%s' "$TS.POST./v1/payments.$H" | openssl dgst -sha256 -hmac "$SK" -hex | awk '{print $NF}')
curl -sL -X POST https://payment.uncle-z.com/v1/payments \ -H "X-PAY-Key: $PK" \ -H "X-PAY-Timestamp: $TS" \ -H "X-PAY-Signature: $S" \ -H "Content-Type: application/json" \ -d "$BODY"Response:
{ "payment_id": "abc123…", "status": "initiated", "checkout_url": "https://sandbox.polar.sh/checkout/polar_c_…", "invoice_number": "PROD-PRO-000042"}4. Redirect the buyer to checkout_url
Section titled “4. Redirect the buyer to checkout_url”Pop it open in a new tab, embed it in an iframe, or hand it to a desktop app’s webview — whatever fits your UX. The buyer pays through the PSP’s hosted UI; the PSP redirects back to your return_url when done.
Don’t trust the redirect’s query params for status — the PSP may also send a webhook before the buyer gets back, and the redirect is best-effort. Use one of:
- Webhook (preferred) — implement step 5; you’ll get
payment.succeededwithin seconds. - Polling —
GET /v1/payments/{id}untilstatusis terminal.
5. Verify the gateway’s inbound webhook
Section titled “5. Verify the gateway’s inbound webhook”The gateway POSTs JSON to your app’s webhook_url with two headers:
| Header | Value |
|---|---|
X-PAY-Timestamp | Unix seconds at delivery time |
X-PAY-Signature | Hex HMAC-SHA256(webhook_secret, "<ts>.<rawBody>") |
Note: this is the body verbatim, not the canonical request format from step 2. Webhook signing is simpler.
Reference verifier in Node:
import { createHmac, timingSafeEqual } from 'node:crypto';
function verifyWebhook(secret, ts, rawBody, signature) { const expected = createHmac('sha256', secret).update(`${ts}.${rawBody}`).digest('hex'); return timingSafeEqual(Buffer.from(expected), Buffer.from(signature));}Make your handler idempotent — the gateway may deliver the same event twice if your endpoint times out and retries. Dedupe on payment_id + event (not on the random delivery id).
That’s the full integration. Detailed per-PSP guides linked in the sidebar; reference docs cover every endpoint, header, and error code.