Result callbacks
When a shopper finishes, we POST the outcome to your app, signed with the shop's HMAC key. Verify every callback before you trust it.
The request
A POST with a JSON body to the callback URL set for the app (SHOPIFY_APP_CALLBACK_URL, https only). Only a pass or fail result is sent. For rejected, timeout or failed, read the Verification with GET. The callback goes to the shop's client that is active when the outcome is recorded, so a Verification started before you disconnected and reconnected the app still reaches the new connection. A Verification whose shopper never came back is closed by your GET after expiresAt, and that read also sends the callback if the shopper had confirmed. Match the callback to your order by externalReference or the id you stored at start.
{
"verificationId": "<verification-id>",
"transactionId": "<transaction-id>",
"externalReference": "order-1001",
"result": "pass",
"requiredAge": 18,
"livemode": true,
"occurredAt": "2026-01-01T00:00:00.000Z"
}| Header | Value |
|---|---|
Abyrg-Signature | t=<unix-seconds>,v1=<hex>, where v1 is HMAC-SHA256 with your hmac_key over <t>.<raw body>. |
Abyrg-Event-Id | Stable across retries of the same event. Use it to dedupe. |
Verify it
t is more than 300 seconds from your clock, and compute the HMAC over the raw body bytes, not a re-encoded JSON object.import { createHmac, timingSafeEqual } from "node:crypto";
const TOLERANCE_SECONDS = 300;
export function verifyCallback(rawBody, header, hmacKey) {
const parts = Object.fromEntries(
String(header ?? "")
.split(",")
.map((p) => p.trim().split("=", 2)),
);
const t = Number(parts.t);
const v1 = parts.v1;
if (!Number.isInteger(t) || typeof v1 !== "string") return false;
if (Math.abs(Date.now() / 1000 - t) > TOLERANCE_SECONDS) return false;
const expected = createHmac("sha256", hmacKey)
.update(`${t}.${rawBody}`)
.digest();
const given = Buffer.from(v1, "hex");
return given.length === expected.length && timingSafeEqual(given, expected);
}The @abyrg/shared package has the same check as verifyResultCallback if you use it.
Answer and retries
- Answer any 2xx once you have stored the event. Anything else, or no answer, is a failure.
- We try up to 5 times, waiting 10 seconds, 1 minute, 10 minutes and 1 hour between attempts. Each attempt carries a fresh timestamp and signature.
- You may get the same event more than once. Dedupe on
Abyrg-Event-Id, and make processing idempotent. - Treat the callback as a hint. Before you act, you can confirm it with
GET /api/v1/verifications/{verificationId}.
Next: finalize the order.