Skip to content
Get started
Payments
Agentcard

Webhooks & lifecycle

Subscribe to Agentcard payment and connection events, verify their signatures (Standard Webhooks), and re-fetch for authoritative state.

Payments and connections are asynchronous. Register a webhook endpoint and react to events rather than polling. Every event is a nudge — re-fetch the resource for authoritative state.

Terminal window
curl -X POST https://api.linqapp.com/api/partner/v3/webhook-subscriptions \
-H "Authorization: Bearer $LINQ_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"target_url": "https://yourapp.com/webhooks/agentcard",
"subscribed_events": [
"payment.authorized",
"payment.declined",
"connection.created",
"connection.revoked"
]
}'

The response includes a signing_secret (prefixed whsec_). Store it now — it cannot be retrieved later; to rotate it, delete and recreate the subscription. Each target_url can be used only once per account (differentiate with query params).

EventWhen it firesWhat to do
connection.createdA customer finished connecting a handleMark the handle payable
connection.revokedA customer removed the connectionStop creating payments for the handle
payment.authorizedThe merchant placed a hold on the cardOptional: reflect “authorized” in your UI
payment.declinedThe charge was declinedSurface the failure; create a new payment to retry

There is no payment.succeeded webhook. After payment.authorized, poll GET /payments/{id} until status is succeeded to confirm the charge settled.

Events share a versioned envelope. data carries the resource snapshot:

{
"api_version": "v3",
"webhook_version": "2026-02-03",
"event_type": "payment.authorized",
"event_id": "c33ddeef-3344-5566-7788-99aabbccddee",
"created_at": "2026-03-06T12:00:00Z",
"trace_id": "3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f",
"partner_id": "your-partner-id",
"data": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"object": "payment_request",
"status": "authorized",
"amount": 497,
"currency": "usd",
"checkout_url": "https://zero.linqapp.com/pay/tomo?session=tok_abc123",
"created_at": "2026-03-05T12:00:00Z",
"updated_at": "2026-03-06T12:00:00Z"
}
}

Each delivery carries three headers:

HeaderMeaning
webhook-idUnique event id (use it to dedupe).
webhook-timestampUnix seconds when the event was sent.
webhook-signatureOne or more space-separated v1,<base64> signatures.

Verify before trusting the body:

  1. Reject if webhook-timestamp is more than 5 minutes old (replay protection).
  2. Strip the whsec_ prefix from your signing secret and base64-decode it to raw key bytes.
  3. Compute HMAC-SHA256(key, "{webhook-id}.{webhook-timestamp}.{rawBody}"), base64-encode it.
  4. Constant-time compare against each v1,<base64> value in webhook-signature.
import crypto from "crypto";
function verify(headers: Record<string, string>, rawBody: string, signingSecret: string): boolean {
const id = headers["webhook-id"];
const ts = headers["webhook-timestamp"];
const sigHeader = headers["webhook-signature"]; // "v1,<base64> v1,<base64> ..."
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false; // replay window
const key = Buffer.from(signingSecret.replace(/^whsec_/, ""), "base64");
const expected = crypto.createHmac("sha256", key).update(`${id}.${ts}.${rawBody}`).digest("base64");
return sigHeader.split(" ").some((part) => {
const sig = part.split(",")[1];
return sig && sig.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
});
}

Verify against the raw request body (pre-JSON-parse), not a re-serialized object.

Always re-fetch on receipt; never trust the status in the event body alone:

Terminal window
curl https://api.linqapp.com/api/partner/v3/payments/550e8400-e29b-41d4-a716-446655440000 \
-H "Authorization: Bearer $LINQ_API_KEY"
  • Retries: up to 10 times over ~25 minutes with exponential backoff.
  • At-least-once: duplicates are possible — dedupe on webhook-id so a redelivery is a no-op.
  • Ack fast: return 2xx quickly and do slow work asynchronously.