# Webhooks

> Send organisation-level Infuse events to your own HTTPS endpoint.

Use webhooks to receive Infuse platform events in your own systems. A webhook endpoint belongs to an organisation and can subscribe to one or more event types.

Webhooks are configured from **Organisation settings** > **Webhooks**.

## When to Use Webhooks

Use webhooks when your backend needs to react to Infuse activity without polling APIs.

Common examples:

- update an internal entitlement or licensing system after a marketplace purchase,
- notify an operations workflow when a purchase state changes,
- mirror Infuse events into your own audit or analytics pipeline.

Webhooks are platform-level integrations. Infuse-Marketplace is currently the first event source, but webhook endpoints are managed at the organisation level.

## Available Events

The initial Infuse-Marketplace event types are:

<table>
<thead>
  <tr>
    <th>
      Event type
    </th>
    
    <th>
      Description
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        marketplace.purchase.completed
      </code>
    </td>
    
    <td>
      A marketplace purchase was completed and activated.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        marketplace.purchase.refunded
      </code>
    </td>
    
    <td>
      A marketplace purchase was refunded.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        marketplace.purchase.revoked
      </code>
    </td>
    
    <td>
      A marketplace purchase was revoked.
    </td>
  </tr>
</tbody>
</table>

Only `marketplace.purchase.completed` is emitted today.

## Create an Endpoint

1. Open **Organisation settings** > **Webhooks**.
2. Select **Create endpoint**.
3. Enter your receiver URL.
4. Optionally add a description.
5. Leave the endpoint enabled, unless you want to create it without receiving events yet.
6. Select at least one event type.
7. Save the endpoint.
8. Copy the signing secret immediately and store it in your backend secret manager.

The signing secret is only shown once after creation. It cannot be retrieved again later.

## Endpoint URL Requirements

Production webhook endpoints must use HTTPS.

Your endpoint should:

- accept `POST` requests,
- read a JSON request body,
- return a `2xx` status quickly after accepting the event,
- verify the Infuse signature before processing the event,
- be idempotent, because a delivery can be retried or replayed.

If your processing is slow, acknowledge the webhook first and queue the work internally.

## Request Headers

Infuse sends these headers with each webhook delivery:

<table>
<thead>
  <tr>
    <th>
      Header
    </th>
    
    <th>
      Description
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        Infuse-Webhook-Id
      </code>
    </td>
    
    <td>
      Unique webhook event ID.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        Infuse-Webhook-Event-Type
      </code>
    </td>
    
    <td>
      Event type, such as <code>
        marketplace.purchase.completed
      </code>
      
      .
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        Infuse-Webhook-Timestamp
      </code>
    </td>
    
    <td>
      Unix timestamp in seconds used in the signature payload.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        Infuse-Webhook-Signature
      </code>
    </td>
    
    <td>
      Lowercase hex HMAC-SHA256 signature.
    </td>
  </tr>
</tbody>
</table>

Use `Infuse-Webhook-Id` as an idempotency key when recording processed events.

## Verify Signatures

Each endpoint has a signing secret that starts with `whsec_`. Store the raw secret securely.

To verify a delivery:

1. Read the raw request body exactly as received.
2. Read `Infuse-Webhook-Timestamp`.
3. Read `Infuse-Webhook-Signature`.
4. Hash the raw `whsec_...` secret with SHA-256 and hex-encode it.
5. Build the signed payload as `{timestamp}.{rawBody}`.
6. Compute `HMACSHA256(secretHash, signedPayload)`.
7. Compare the computed lowercase hex signature with `Infuse-Webhook-Signature` using a constant-time comparison.

Example in Node.js:

```ts
import crypto from 'node:crypto';

export function verifyInfuseWebhook({
  rawBody,
  timestamp,
  signature,
  signingSecret,
}: {
  rawBody: string;
  timestamp: string;
  signature: string;
  signingSecret: string;
}) {
  const secretHash = crypto
    .createHash('sha256')
    .update(signingSecret, 'utf8')
    .digest('hex');

  const expected = crypto
    .createHmac('sha256', secretHash)
    .update(`${timestamp}.${rawBody}`, 'utf8')
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(expected, 'utf8'),
    Buffer.from(signature.toLowerCase(), 'utf8'),
  );
}
```

Do not parse and re-stringify the JSON before verifying. Use the raw request body.

## Example Payload

`marketplace.purchase.completed` sends purchase, buyer, device, payment, and marketplace item information.

```json
{
  "id": "5051eeeb-acf1-4793-80cf-5559b97cd976",
  "purchaseId": "5051eeeb-acf1-4793-80cf-5559b97cd976",
  "subject": "marketplace_purchase/local-test-20260603040716",
  "status": "active",
  "livemode": false,
  "buyer": {
    "organisationId": "22222222-2222-2222-2222-222222222222",
    "userId": "0560272b-98b7-49e7-821c-0eba9f0a4b8b",
    "email": "user@example.com"
  },
  "device": {
    "id": "device-001",
    "serialNumber": "DEVICE-001"
  },
  "marketplace": {
    "name": "Example Infuse-Marketplace Item",
    "itemId": "fad3247d-edc1-4cf8-952d-d4c12406d7d5",
    "listingId": "40c5016c-390b-4b3a-b1e2-a881788d5014",
    "offeringId": "30162bd0-7683-444c-8208-cf8eee8729b5"
  },
  "payment": {
    "currency": "USD",
    "provider": "stripe",
    "totalAmountPaid": 12.34,
    "providerListingMarkup": 1.23,
    "providerPaymentReference": "payment-reference",
    "providerCustomerReference": "customer-reference"
  },
  "occurredAtUtc": "2026-06-03T04:07:16.711787+00:00",
  "activatedAtUtc": "2026-06-03T04:07:16.711787+00:00"
}
```

Treat payload fields as event data for your integration. Use Infuse APIs as the source of truth if you need to fetch the latest purchase or device state.

## Edit an Endpoint

From **Organisation settings** > **Webhooks** > **Endpoints**, you can:

- update the endpoint URL or description,
- enable or disable deliveries,
- change subscribed events,
- rotate the signing secret,
- delete the endpoint.

Rotating the signing secret shows the new secret once. Update your receiver before or immediately after rotating so deliveries continue to verify.

Disabling an endpoint stops new deliveries to that endpoint. It does not delete delivery history.

## Delivery History

Open the **Deliveries** tab to review webhook delivery attempts.

The table shows:

- event type,
- subject,
- endpoint URL,
- delivery status,
- attempt count,
- last attempt time,
- next retry time,
- response code.

Delivery statuses are:

<table>
<thead>
  <tr>
    <th>
      Status
    </th>
    
    <th>
      Meaning
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        Pending
      </code>
    </td>
    
    <td>
      The delivery is queued or waiting for its next attempt.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        Succeeded
      </code>
    </td>
    
    <td>
      The endpoint returned a <code>
        2xx
      </code>
      
       response.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        Failed
      </code>
    </td>
    
    <td>
      The last attempt failed and another retry may be scheduled.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        Exhausted
      </code>
    </td>
    
    <td>
      Infuse will not retry automatically.
    </td>
  </tr>
</tbody>
</table>

Failed and exhausted deliveries can be replayed from the delivery table.

## Retry Behavior

Infuse treats `2xx` responses as successful.

The following failures are retryable:

- timeout or network failure,
- `408`,
- `409`,
- `429`,
- `5xx`.

Non-retryable responses, such as `400`, are marked failed without normal automatic retry.

Your receiver should return:

- `2xx` only after it has accepted the event,
- `4xx` when the request should not be retried,
- `5xx` when the problem is temporary and Infuse should retry.

## Security Recommendations

- Keep signing secrets in a server-side secret manager.
- Never expose signing secrets in browser code.
- Verify every delivery before processing it.
- Use a constant-time comparison for signatures.
- Record processed `Infuse-Webhook-Id` values to avoid duplicate processing.
- Rotate signing secrets if they are exposed or no longer trusted.
- Restrict webhook receiver logs so secrets and sensitive payloads are not leaked.
