---
title: Webhooks & lifecycle | API Docs
description: 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

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).

## 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

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)

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:

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.

## After verifying — re-fetch

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"
```

## Delivery guarantees

- **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.

## Next

- [Create & complete a payment](/guides/agentcard/create-a-payment/index.md)
- [Errors, statuses & go-live](/guides/agentcard/errors-and-go-live/index.md)
