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.
Register a subscription
Section titled “Register a subscription”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).
Events
Section titled “Events”| Event | When it fires | What to do |
|---|---|---|
connection.created | A customer finished connecting a handle | Mark the handle payable |
connection.revoked | A customer removed the connection | Stop creating payments for the handle |
payment.authorized | The merchant placed a hold on the card | Optional: reflect “authorized” in your UI |
payment.declined | The charge was declined | Surface 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.
Payload shape
Section titled “Payload shape”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" }}Verify the signature (Standard Webhooks)
Section titled “Verify the signature (Standard Webhooks)”Each delivery carries three headers:
| Header | Meaning |
|---|---|
webhook-id | Unique event id (use it to dedupe). |
webhook-timestamp | Unix seconds when the event was sent. |
webhook-signature | One or more space-separated v1,<base64> signatures. |
Verify before trusting the body:
- Reject if
webhook-timestampis more than 5 minutes old (replay protection). - Strip the
whsec_prefix from your signing secret and base64-decode it to raw key bytes. - Compute
HMAC-SHA256(key, "{webhook-id}.{webhook-timestamp}.{rawBody}"), base64-encode it. - Constant-time compare against each
v1,<base64>value inwebhook-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.
After verifying — re-fetch
Section titled “After verifying — re-fetch”Always re-fetch on receipt; never trust the status in the event body alone:
curl https://api.linqapp.com/api/partner/v3/payments/550e8400-e29b-41d4-a716-446655440000 \ -H "Authorization: Bearer $LINQ_API_KEY"Delivery guarantees
Section titled “Delivery guarantees”- Retries: up to 10 times over ~25 minutes with exponential backoff.
- At-least-once: duplicates are possible — dedupe on
webhook-idso a redelivery is a no-op. - Ack fast: return
2xxquickly and do slow work asynchronously.