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

# Webhook events

> Every event Saasybill sends, when it fires, and what its payload holds

Twelve events cover customers, subscriptions and invoices. Choose the ones you need when you [add an endpoint](/api-reference/webhooks/overview#set-up-an-endpoint).

***

## Event types

| Event | Fires when |
| - | - |
| `customer.created` | A customer is added, including one imported from Xero. |
| `customer.updated` | A customer's code, name, email or status changes. |
| `customer.deleted` | A customer is removed. |
| `subscription.created` | A subscription is created. |
| `subscription.updated` | Units, plan, renewal date, discount or label changes, or cancel at period end is switched. This includes each renewal. |
| `subscription.ended` | A subscription ends or completes. |
| `subscription.deleted` | A subscription is deleted. |
| `invoice.created` | An invoice is raised. |
| `invoice.updated` | An invoice's total, number, status or Xero sync changes. |
| `invoice.paid` | An invoice is paid. |
| `invoice.voided` | An invoice is voided. |
| `invoice.deleted` | An invoice is deleted. |

There's also `webhook.test`, sent by <Badge>Send Test Event</Badge>. You can't subscribe to it.

***

## Tracked fields

An event's `data.object` holds these fields and no others. Only a change to one of them creates an `updated` event. A change to any other field is not announced, which stops a busy subscription announcing itself every minute.

Every `data.object` also has `id` and `object`.

<Tabs>
  <Tab title="Customer">
    | Field | Description |
    | - | - |
    | `code` | The customer's code. |
    | `name` | The customer's display name. |
    | `email` | The customer's email. |
    | `status` | `active` or `inactive`. |
  </Tab>

  <Tab title="Subscription">
    | Field | Description |
    | - | - |
    | `status` | `pending`, `active`, `complete` or `ended`. |
    | `customer` | The customer's id. |
    | `plan` | The plan's id. |
    | `units` | The current units. |
    | `renewal_date` | The next renewal date, `YYYY-MM-DD`. |
    | `discount_percent` | The discount, such as `"10.00"`. |
    | `label` | The subscription's label. |
    | `cancel_at_period_end` | `true` when set to end at its renewal date. |
  </Tab>

  <Tab title="Invoice">
    | Field | Description |
    | - | - |
    | `status` | The invoice's [status](/api-reference/objects#invoice-status). |
    | `customer` | The customer's id. |
    | `subscription` | The subscription's id, or `null`. |
    | `number` | Xero's invoice number, or `null`. |
    | `total` | The invoice total, such as `"613.8000"`. |
    | `xero_sync_status` | `synced`, `pending` or `failed`. |
  </Tab>
</Tabs>

<Info>
  These are the same values the API returns, in the same formats. `data.object` is a summary, not the full object. Fetch the full object with `GET /v1/{customers|subscriptions|invoices}/{id}`.
</Info>

***

## How the events relate

* **`created`** has no `previous_attributes`.
* **`updated`** carries `previous_attributes` with the earlier value of each field that changed.
* **`deleted`** carries the object as it was just before it was deleted.
* **`invoice.paid`, `invoice.voided` and `invoice.deleted`** replace `invoice.updated` when the status changes to that value. A status change to anything else, or any other tracked change, is `invoice.updated`.
* **`subscription.ended`** replaces `subscription.updated` when the status moves to `ended` or `complete`. If other fields change in the same minute, you'll also get a `subscription.updated`.
* **A move between `ended` and `complete`** isn't announced.

***

## Example payloads

### Customer created

```json theme={null}
{
  "id": "evt_0f7a3c9e1b2d4e6a8c5b7d9f1a3c5e70",
  "object": "event",
  "type": "customer.created",
  "created": "2026-09-30T04:10:12.000Z",
  "livemode": true,
  "data": {
    "object": {
      "id": "3b9f1c1e-6a52-4c0e-9d7e-1f6f2f0b7a11",
      "object": "customer",
      "code": "ACME-001",
      "name": "Acme Pty Ltd",
      "email": "accounts@acme.example",
      "status": "active"
    }
  }
}
```

### Subscription updated

Units went from 10 to 15.

```json theme={null}
{
  "id": "evt_9b1d7f3a5c2e4a6c8e0b2d4f6a8c0e12",
  "object": "event",
  "type": "subscription.updated",
  "created": "2026-10-08T01:03:00.000Z",
  "livemode": true,
  "data": {
    "object": {
      "id": "9d4e2b7a-13c5-4f8e-b6a0-2e1d7c9f5b33",
      "object": "subscription",
      "status": "active",
      "customer": "3b9f1c1e-6a52-4c0e-9d7e-1f6f2f0b7a11",
      "plan": "b6c1e0a4-2f7d-4a61-8f0c-5d3e9a7b2c44",
      "units": 15,
      "renewal_date": "2026-10-30",
      "discount_percent": "10.00",
      "label": "Acme – Team plan",
      "cancel_at_period_end": false
    },
    "previous_attributes": {
      "units": 10
    }
  }
}
```

### Invoice paid

```json theme={null}
{
  "id": "evt_6c3f0b9e2a7d4f1c8e5a9b0d3c2f1e47",
  "object": "event",
  "type": "invoice.paid",
  "created": "2026-10-04T04:12:00.000Z",
  "livemode": true,
  "data": {
    "object": {
      "id": "5f8a3c9d-7e21-4b04-9c6d-0a2b4e8f1d55",
      "object": "invoice",
      "status": "paid",
      "customer": "3b9f1c1e-6a52-4c0e-9d7e-1f6f2f0b7a11",
      "subscription": "9d4e2b7a-13c5-4f8e-b6a0-2e1d7c9f5b33",
      "number": "INV-0042",
      "total": "613.8000",
      "xero_sync_status": "synced"
    },
    "previous_attributes": {
      "status": "approved"
    }
  }
}
```

### Invoice failed to sync

An `invoice.updated` event when Xero refuses an invoice.

```json theme={null}
{
  "id": "evt_2a4c6e8b0d1f3a5c7e9b1d3f5a7c9e34",
  "object": "event",
  "type": "invoice.updated",
  "created": "2026-09-30T04:12:30.000Z",
  "livemode": true,
  "data": {
    "object": {
      "id": "5f8a3c9d-7e21-4b04-9c6d-0a2b4e8f1d55",
      "object": "invoice",
      "status": "draft",
      "customer": "3b9f1c1e-6a52-4c0e-9d7e-1f6f2f0b7a11",
      "subscription": "9d4e2b7a-13c5-4f8e-b6a0-2e1d7c9f5b33",
      "number": null,
      "total": "613.8000",
      "xero_sync_status": "failed"
    },
    "previous_attributes": {
      "xero_sync_status": "pending"
    }
  }
}
```

### Test event

<Badge>Send Test Event</Badge> sends an event about no object, so you can check your wiring.

```json theme={null}
{
  "id": "evt_8e0a2c4f6b1d3a5c7e9f1b3d5a7c9e56",
  "object": "event",
  "type": "webhook.test",
  "created": "2026-09-30T04:15:00.000Z",
  "livemode": true,
  "data": {
    "object": { "id": "test", "object": "test" }
  }
}
```

***

## Handling events

<Steps>
  <Step title="Verify the signature">
    Reject anything that fails. See [Verify signatures](/api-reference/webhooks/verify-signatures).
  </Step>

  <Step title="Skip repeats">
    Look up `Saasybill-Event-Id`. If you have processed it, reply `200` and stop.
  </Step>

  <Step title="Reply straight away">
    Return `200` before you do slow work.
  </Step>

  <Step title="Fetch the current state">
    Use `data.object.id` to fetch the full object from the API, and act on what it is now. This copes with events that arrive late or out of order.
  </Step>
</Steps>
