> ## Documentation Index
> Fetch the complete documentation index at: https://docs.stora.co/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Receive real-time notifications when events happen in Stora.

Webhooks are HTTP callbacks that send real-time `POST` requests to your configured endpoints when specific events occur in Stora. When an event happens — such as an invoice being paid or a unit becoming occupied — Stora immediately notifies all endpoints subscribed to that event type.

## Getting started

<Steps>
  <Step title="Create a webhook endpoint">
    Use the [Webhook Endpoints API](/2025-09/api-reference/webhook-endpoints/create-a-webhook-endpoint) to register a publicly accessible HTTPS URL, the event types you want to subscribe to, and the API version.
  </Step>

  <Step title="Store your secret key">
    When you create an endpoint, Stora generates a secret key. Store it securely — you'll use it to verify incoming requests.
  </Step>

  <Step title="Handle incoming events">
    Build a handler at your URL that verifies the signature, processes the event, and returns a `2xx` response.
  </Step>
</Steps>

## Payload structure

Every webhook delivers a JSON payload with this structure:

```json theme={null}
{
  "event": {
    "id": "evt_1234567890",
    "type": "invoice.paid",
    "api_version": "2025-09",
    "created_at": "2025-01-15T10:30:00Z",
    "data": {
      "invoice": {
        // ... full invoice resource
      }
    }
  }
}
```

| Field               | Description                                                         |
| ------------------- | ------------------------------------------------------------------- |
| `event.id`          | Unique identifier for the event — use this for idempotency          |
| `event.type`        | The event type (e.g. `invoice.paid`, `contact.created`)             |
| `event.api_version` | Matches your endpoint's API version — determines the data structure |
| `event.created_at`  | ISO 8601 timestamp of when the event occurred                       |
| `event.data`        | The resource that triggered the event, keyed by resource type       |

## Headers

Every webhook request includes these headers:

| Header               | Description                                                |
| -------------------- | ---------------------------------------------------------- |
| `Content-Type`       | `application/json`                                         |
| `User-Agent`         | `Stora-Webhooks/1.0`                                       |
| `X-Stora-Signature`  | HMAC signature for verification (see below)                |
| `X-Stora-Request-Id` | Unique ID for this delivery attempt — useful for debugging |

## Signature verification

All webhook requests are signed using HMAC SHA256. Always verify the signature before processing.

The signature is in the `X-Stora-Signature` header:

```
t={timestamp},v1={signature}
```

To verify:

1. Extract the timestamp (`t`) and signature (`v1`) from the header
2. Reconstruct the signed payload: `{timestamp}.{raw_request_body}`
3. Compute the HMAC SHA256 using your endpoint's secret key
4. Compare the computed signature with `v1`
5. Optionally, check the timestamp is recent to prevent replay attacks

<CodeGroup>
  ```ruby Ruby theme={null}
  def verify_webhook_signature(request_body, signature_header, secret)
    parts = signature_header.split(',').map { |p| p.split('=').last }
    timestamp = parts[0]
    signature = parts[1]

    signed_payload = "#{timestamp}.#{request_body}"
    computed = OpenSSL::HMAC.hexdigest('SHA256', secret, signed_payload)

    computed == signature
  end
  ```

  ```python Python theme={null}
  import hmac
  import hashlib

  def verify_webhook_signature(request_body, signature_header, secret):
      parts = signature_header.split(',')
      timestamp = parts[0].split('=')[1]
      signature = parts[1].split('=')[1]

      signed_payload = f"{timestamp}.{request_body}"
      computed = hmac.new(
          secret.encode('utf-8'),
          signed_payload.encode('utf-8'),
          hashlib.sha256
      ).hexdigest()

      return hmac.compare_digest(computed, signature)
  ```

  ```php PHP theme={null}
  function verifyWebhookSignature(string $requestBody, string $signatureHeader, string $secret): bool
  {
      $parts = explode(',', $signatureHeader);
      $timestamp = explode('=', $parts[0])[1];
      $signature = explode('=', $parts[1])[1];

      $signedPayload = "{$timestamp}.{$requestBody}";
      $computed = hash_hmac('sha256', $signedPayload, $secret);

      return hash_equals($computed, $signature);
  }
  ```

  ```javascript Node.js theme={null}
  const crypto = require('crypto');

  function verifyWebhookSignature(requestBody, signatureHeader, secret) {
    const parts = signatureHeader.split(',');
    const timestamp = parts[0].split('=')[1];
    const signature = parts[1].split('=')[1];

    const signedPayload = `${timestamp}.${requestBody}`;
    const computed = crypto
      .createHmac('sha256', secret)
      .update(signedPayload)
      .digest('hex');

    return crypto.timingSafeEqual(
      Buffer.from(computed),
      Buffer.from(signature)
    );
  }
  ```
</CodeGroup>

## Retries

Stora automatically retries failed deliveries up to 6 times:

| Attempt   | Delay      |
| --------- | ---------- |
| 1st retry | 1 minute   |
| 2nd retry | 5 minutes  |
| 3rd retry | 30 minutes |
| 4th retry | 2 hours    |
| 5th retry | 6 hours    |
| 6th retry | 12 hours   |

A delivery is retried when your endpoint returns a non-`2xx` status code, a network error occurs, or the request times out (20-second limit).

After 6 failed attempts, the delivery is marked as failed. If the endpoint is deleted or disabled before a scheduled retry, pending retries are cancelled.

## Best practices

<Tip>
  Return a `2xx` response as quickly as possible — even if you process the event asynchronously. This prevents unnecessary retries.
</Tip>

* **Implement idempotency** — use `event.id` to ensure you don't process the same event twice. Store processed event IDs and check before processing.
* **Process asynchronously** — for time-consuming operations, queue the webhook for background processing after returning a success response.
* **Log the request ID** — use `X-Stora-Request-Id` to correlate retry attempts when debugging.
* **Validate signatures** — always verify the HMAC signature before processing.
* **Monitor your endpoint** — extended downtime may exhaust all retry attempts.

## Available events

<AccordionGroup>
  <Accordion title="Contact">
    | Event                                                               | Description                            |
    | ------------------------------------------------------------------- | -------------------------------------- |
    | [`contact.created`](/2025-09/api-reference/webhooks/contactcreated) | Triggered when a contact is created.   |
    | [`contact.updated`](/2025-09/api-reference/webhooks/contactupdated) | Triggered when the contact is updated. |
  </Accordion>

  <Accordion title="Contract">
    | Event                                                                 | Description                           |
    | --------------------------------------------------------------------- | ------------------------------------- |
    | [`contract.created`](/2025-09/api-reference/webhooks/contractcreated) | Triggered when a contract is created. |
    | [`contract.signed`](/2025-09/api-reference/webhooks/contractsigned)   | Triggered when a contract is signed.  |
  </Accordion>

  <Accordion title="Coupon">
    | Event                                                             | Description                           |
    | ----------------------------------------------------------------- | ------------------------------------- |
    | [`coupon.created`](/2025-09/api-reference/webhooks/couponcreated) | Triggered when a coupon is created.   |
    | [`coupon.updated`](/2025-09/api-reference/webhooks/couponupdated) | Triggered when the coupon is updated. |
  </Accordion>

  <Accordion title="Credit Note">
    | Event                                                                       | Description                              |
    | --------------------------------------------------------------------------- | ---------------------------------------- |
    | [`credit_note.created`](/2025-09/api-reference/webhooks/credit_notecreated) | Triggered when a new credit is created.  |
    | [`credit_note.updated`](/2025-09/api-reference/webhooks/credit_noteupdated) | Triggered when a credit note is updated. |
  </Accordion>

  <Accordion title="Deal">
    | Event                                                           | Description                       |
    | --------------------------------------------------------------- | --------------------------------- |
    | [`deal.created`](/2025-09/api-reference/webhooks/dealcreated)   | Triggered when a deal is created. |
    | [`deal.lost`](/2025-09/api-reference/webhooks/deallost)         | Triggered when a deal is lost.    |
    | [`deal.reopened`](/2025-09/api-reference/webhooks/dealreopened) | Triggered when a deal is reopened |
    | [`deal.updated`](/2025-09/api-reference/webhooks/dealupdated)   | Triggered when a deal is updated. |
    | [`deal.won`](/2025-09/api-reference/webhooks/dealwon)           | Triggered when a deal is won.     |
  </Accordion>

  <Accordion title="Identity Verification">
    | Event                                                                                                 | Description                                           |
    | ----------------------------------------------------------------------------------------------------- | ----------------------------------------------------- |
    | [`identity_verification.cancelled`](/2025-09/api-reference/webhooks/identity_verificationcancelled)   | Triggered when an identity verification is cancelled  |
    | [`identity_verification.failed`](/2025-09/api-reference/webhooks/identity_verificationfailed)         | Triggered when an identity verification failed        |
    | [`identity_verification.processing`](/2025-09/api-reference/webhooks/identity_verificationprocessing) | Triggered when an identity verification is processing |
    | [`identity_verification.succeeded`](/2025-09/api-reference/webhooks/identity_verificationsucceeded)   | Triggered when an identity verification succeeded     |
  </Accordion>

  <Accordion title="Invoice">
    | Event                                                                                         | Description                                           |
    | --------------------------------------------------------------------------------------------- | ----------------------------------------------------- |
    | [`invoice.created`](/2025-09/api-reference/webhooks/invoicecreated)                           | Triggered when a new invoice is created.              |
    | [`invoice.finalized`](/2025-09/api-reference/webhooks/invoicefinalized)                       | Triggered when an invoice is finalized.               |
    | [`invoice.marked_uncollectible`](/2025-09/api-reference/webhooks/invoicemarked_uncollectible) | Triggered when an invoice is marked as uncollectible. |
    | [`invoice.paid`](/2025-09/api-reference/webhooks/invoicepaid)                                 | Triggered when an invoice is mark as paid.            |
    | [`invoice.updated`](/2025-09/api-reference/webhooks/invoiceupdated)                           | Triggered when an invoice is updated.                 |
  </Accordion>

  <Accordion title="Note">
    | Event                                                         | Description                       |
    | ------------------------------------------------------------- | --------------------------------- |
    | [`note.created`](/2025-09/api-reference/webhooks/notecreated) | Triggered when a note is created. |
    | [`note.updated`](/2025-09/api-reference/webhooks/noteupdated) | Triggered when a note is updated. |
  </Accordion>

  <Accordion title="Order">
    | Event                                                               | Description                                                 |
    | ------------------------------------------------------------------- | ----------------------------------------------------------- |
    | [`order.completed`](/2025-09/api-reference/webhooks/ordercompleted) | Triggered when the order is completed.                      |
    | [`order.created`](/2025-09/api-reference/webhooks/ordercreated)     | Triggered when a new order is created.                      |
    | [`order.finalized`](/2025-09/api-reference/webhooks/orderfinalized) | Triggered when a new order is finalized (ready to be paid). |
  </Accordion>

  <Accordion title="Protection Level">
    | Event                                                                                 | Description                                   |
    | ------------------------------------------------------------------------------------- | --------------------------------------------- |
    | [`protection_level.created`](/2025-09/api-reference/webhooks/protection_levelcreated) | Triggered when a protection level is created. |
    | [`protection_level.updated`](/2025-09/api-reference/webhooks/protection_levelupdated) | Triggered when a protection level is updated. |
  </Accordion>

  <Accordion title="Subscription">
    | Event                                                                             | Description                                   |
    | --------------------------------------------------------------------------------- | --------------------------------------------- |
    | [`subscription.cancelled`](/2025-09/api-reference/webhooks/subscriptioncancelled) | Triggered when the subscription is cancelled. |
    | [`subscription.created`](/2025-09/api-reference/webhooks/subscriptioncreated)     | Triggered when the subscription is created.   |
    | [`subscription.ended`](/2025-09/api-reference/webhooks/subscriptionended)         | Triggered when the subscription is ended.     |
    | [`subscription.started`](/2025-09/api-reference/webhooks/subscriptionstarted)     | Triggered when the subscription is started.   |
  </Accordion>

  <Accordion title="Task">
    | Event                                                             | Description                                  |
    | ----------------------------------------------------------------- | -------------------------------------------- |
    | [`task.completed`](/2025-09/api-reference/webhooks/taskcompleted) | Triggered when the task is completed.        |
    | [`task.created`](/2025-09/api-reference/webhooks/taskcreated)     | Triggered when a task is created.            |
    | [`task.reopened`](/2025-09/api-reference/webhooks/taskreopened)   | Triggered when a completed task is reopened. |
    | [`task.updated`](/2025-09/api-reference/webhooks/taskupdated)     | Triggered when a task is updated.            |
  </Accordion>

  <Accordion title="Tenancy">
    | Event                                                               | Description                            |
    | ------------------------------------------------------------------- | -------------------------------------- |
    | [`tenancy.created`](/2025-09/api-reference/webhooks/tenancycreated) | Triggered when a tenancy is created.   |
    | [`tenancy.started`](/2025-09/api-reference/webhooks/tenancystarted) | Triggered when the tenancy is started. |
  </Accordion>

  <Accordion title="Unit">
    | Event                                                                 | Description                                |
    | --------------------------------------------------------------------- | ------------------------------------------ |
    | [`unit.available`](/2025-09/api-reference/webhooks/unitavailable)     | Triggered when a unit is made available.   |
    | [`unit.created`](/2025-09/api-reference/webhooks/unitcreated)         | Triggered when a unit is created.          |
    | [`unit.deallocated`](/2025-09/api-reference/webhooks/unitdeallocated) | Triggered when a unit is deallocated.      |
    | [`unit.occupied`](/2025-09/api-reference/webhooks/unitoccupied)       | Triggered when a unit is occupied.         |
    | [`unit.overlocked`](/2025-09/api-reference/webhooks/unitoverlocked)   | Triggered when a unit is overlocked.       |
    | [`unit.repossessed`](/2025-09/api-reference/webhooks/unitrepossessed) | Triggered when a unit is repossessed.      |
    | [`unit.reserved`](/2025-09/api-reference/webhooks/unitreserved)       | Triggered when a unit is reserved.         |
    | [`unit.unavailable`](/2025-09/api-reference/webhooks/unitunavailable) | Triggered when a unit is made unavailable. |
    | [`unit.updated`](/2025-09/api-reference/webhooks/unitupdated)         | Triggered when a unit is updated.          |
  </Accordion>

  <Accordion title="Unit Type">
    | Event                                                                   | Description                            |
    | ----------------------------------------------------------------------- | -------------------------------------- |
    | [`unit_type.created`](/2025-09/api-reference/webhooks/unit_typecreated) | Triggered when a unit type is created. |
    | [`unit_type.updated`](/2025-09/api-reference/webhooks/unit_typeupdated) | Triggered when a unit type is updated. |
  </Accordion>
</AccordionGroup>

For full payload schemas, see the [Webhooks section in the API reference](/2025-09/api-reference/webhooks/).
