> For the complete documentation index, see [llms.txt](https://knowledgebase.fabricdata.com/studio/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://knowledgebase.fabricdata.com/studio/integrations-and-apis/api-overview/events-and-webhooks.md).

# Events & Webhooks

### Overview

Origin Studio publishes event notifications when changes occur within your tenant. An event is sent every time a record is created, updated, deleted, restored or permanently deleted.

Events are delivered via **AWS Simple Notification Service (SNS)** to an **HTTPS webhook** or an **Amazon SQS queue** that you own, configured through an event subscription.

You can use these events to:

* Trigger downstream workflows
* Synchronize external systems
* React to record lifecycle changes
* Automate enrichment or publishing pipelines

{% hint style="info" %}
**Events are thin**. An event tells you which record changed and which fields were saved. It never carries the field values. Your integration fetches the record through the API only when it cares about the change.
{% endhint %}

***

### How Event Delivery Works

1. A change occurs in Origin Studio (e.g., a record is created).
2. The change is checked against your tenant's **event subscriptions** and their filters.
3. For each matching subscription, an event is published to your tenant's SNS topic.
4. SNS delivers a notification to the subscription's webhook endpoint or SQS queue.
5. Your system processes the event and, if the change matters to you, fetches the record through the API.

***

### Setting Up an Event Subscription

A tenant Admin creates subscriptions in **Administration > System > Event Subscriptions**, or with `POST /v1/eventSubscriptions`. For a walkthrough of the Administration screens, see Events Notification.

#### Configuring Your Webhook Endpoint

#### Creating a Subscription with the API

The example below creates a webhook subscription that receives every new record, plus updates to `primaryTitle` or `synopsis` on records in the Published phase.

http

```http
POST /v1/eventSubscriptions
```

json

```json
{
  "data": {
    "type": "eventSubscriptions",
    "attributes": {
      "name": "Published title changes",
      "type": "webhook",
      "endpoint": "https://hooks.example.com/origin-studio",
      "format": "cloudevents",
      "resourceType": "records",
      "filter": {
        "eventTypes": ["record.created", "record.updated"],
        "updated": {
          "lifecyclePhases": ["published"],
          "savedKeys": ["primaryTitle", "synopsis"]
        }
      }
    }
  }
}
```

#### Subscription Filters

| Property                    | Required | Applies to       | Description                                                                                |
| --------------------------- | -------- | ---------------- | ------------------------------------------------------------------------------------------ |
| `eventTypes`                | Yes      | All events       | The event types this subscription receives. Always listed explicitly.                      |
| `updated.lifecycleStatuses` | No       | `record.updated` | Only send updates for records in these lifecycle statuses. Cannot be combined with phases. |
| `updated.lifecyclePhases`   | No       | `record.updated` | Only send updates for records in these lifecycle phases. Cannot be combined with statuses. |
| `updated.savedKeys`         | No       | `record.updated` | Only send updates that saved at least one of these fields.                                 |

* `eventTypes` is always explicit, so event types added in future are never sent to you unasked.
* `updated` narrows `record.updated` only. Every other selected event type is sent for every record.
* Use `lifecycleStatuses` **or** `lifecyclePhases`, not both.
* A filter allows at most **150 combinations**: one for each non-update event type, plus statuses (or phases) multiplied by saved keys for updates. Larger filters are rejected with `400`. Split them across two subscriptions to the same endpoint.

{% hint style="info" %}
**Counting combinations.** The example above uses 3: one for `record.created`, plus 1 phase × 2 saved keys for `record.updated`.&#x20;
{% endhint %}

{% hint style="info" %}
**Subscriptions cannot be edited.** To change a filter or endpoint, delete the subscription and create a new one.&#x20;
{% endhint %}

***

### Supported Event Types

Each write produces **one** event, however many fields it saved.

| Event             | Description                                                                                             | `savedFields`                  |
| ----------------- | ------------------------------------------------------------------------------------------------------- | ------------------------------ |
| `record.created`  | A record has been created.                                                                              | The fields submitted on create |
| `record.updated`  | Any other write saved at least one field, including lifecycle, security tag, move and reorder changes.  | The fields the write saved     |
| `record.deleted`  | A record has been deleted. Deleting a series also sends one event for each of its seasons and episodes. | Empty                          |
| `record.restored` | A deleted record has been restored. Each restored ancestor gets its own event.                          | Empty                          |
| `record.purged`   | A record has been permanently deleted.                                                                  | Empty                          |

***

Events are delivered using the AWS SNS `Notification` format, on both webhooks and SQS queues.

The message your endpoint or queue receives contains:

* SNS headers (webhooks only)
* A JSON body containing the SNS envelope
* The actual Origin Studio event, a CloudEvent, inside the `Message` field

#### Example SNS Headers

```
x-amz-sns-message-type: Notification
x-amz-sns-topic-arn: arn:aws:sns:<region>:<account>:events-tenant-<tenantId>-records
x-amz-sns-subscription-arn: arn:aws:sns:<region>:<account>:events-tenant-<tenantId>-records:<subscriptionId>
```

These headers identify:

* The SNS topic
* The subscription
* The message type

#### Example SNS Body (Envelope)

json

```json
{
  "Type": "Notification",
  "MessageId": "a1c4e7f2-3b6d-5e8a-9c0f-1d2e3f4a5b6c",
  "TopicArn": "arn:aws:sns:<region>:<account>:events-tenant-3f9a1c7e5b2d8046-records",
  "Message": "{\"specversion\":\"1.0\",\"id\":\"d8f3a27c046e1b95\",\"source\":\"/tenants/3f9a1c7e5b2d8046/records\",\"type\":\"com.fabricinsights.originstudio.record.updated\",\"subject\":\"a4e81c0f9d2b6573\",\"time\":\"2026-10-09T10:02:47.051Z\",\"dataschema\":\"https://knowledgebase.fabricdata.com/studio/integrations-and-apis/origin-studio-production-api/models#recordcloudevent\",\"datacontenttype\":\"application/json\",\"data\":{\"savedFields\":[{\"key\":\"synopsis\",\"locale\":\"fr-FR\"},{\"key\":\"genres\"}]}}",
  "Timestamp": "2026-10-09T10:02:47.412Z",
  "SignatureVersion": "1",
  "Signature": "<base64 signature>",
  "SigningCertURL": "https://sns.<region>.amazonaws.com/SimpleNotificationService-<id>.pem",
  "UnsubscribeURL": "https://sns.<region>.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=<arn>",
  "MessageAttributes": {
    "type": { "Type": "String", "Value": "record.updated" },
    "lifecycleStatus": { "Type": "String", "Value": "draft" },
    "lifecyclePhase": { "Type": "String", "Value": "DRAFTING" },
    "savedKeys": { "Type": "String.Array", "Value": "[\"synopsis\",\"genres\"]" }
  }
}
```

`MessageAttributes` hold the values your filter matched on. `lifecycleStatus` and `lifecyclePhase` are the record's values **after** the change.

***

### Extracting the Actual Event

The Origin Studio event is contained inside the `Message` field.

> The `Message` field is a **JSON string**, not a nested object. It must be parsed a second time.

javascript

```javascript
const envelope = JSON.parse(requestBody);

if (envelope.Type === "SubscriptionConfirmation") {
  // New webhook: request envelope.SubscribeURL once to start receiving events
  return;
}

const event = JSON.parse(envelope.Message); // the CloudEvent
const recordId = event.subject;
const savedKeys = event.data.savedFields.map(f => f.key);
```

#### Decoded Event Payload

After parsing the `Message` field:

json

```json
{
  "specversion": "1.0",
  "id": "d8f3a27c046e1b95",
  "source": "/tenants/3f9a1c7e5b2d8046/records",
  "type": "com.fabricinsights.originstudio.record.updated",
  "subject": "a4e81c0f9d2b6573",
  "time": "2026-10-09T10:02:47.051Z",
  "dataschema": "https://knowledgebase.fabricdata.com/studio/integrations-and-apis/origin-studio-production-api/models#recordcloudevent",
  "datacontenttype": "application/json",
  "data": {
    "savedFields": [
      { "key": "synopsis", "locale": "fr-FR" },
      { "key": "genres" }
    ]
  }
}
```

***

### Event Payload Fields

Every event is a [CloudEvents 1.0](https://cloudevents.io/) event in structured JSON mode, with the same nine attributes.

| Field              | Value                                             | Description                                                                                    |
| ------------------ | ------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
| `specversion`      | `1.0`                                             | Always present.                                                                                |
| `id`               | Unique string                                     | Unique per change. A redelivery of the same change keeps the same `id`; use it to deduplicate. |
| `source`           | `/tenants/{tenantId}/records`                     | Identifies your tenant and the resource type.                                                  |
| `type`             | `com.fabricinsights.originstudio.record.<change>` | `<change>` is `created`, `updated`, `deleted`, `restored` or `purged`.                         |
| `subject`          | Record ID                                         | Fetch the record with this ID.                                                                 |
| `time`             | RFC 3339 timestamp, millisecond precision         | When the change happened. Use it to order events for the same record.                          |
| `dataschema`       | Link to the `RecordCloudEvent` model              | JSON Schema for the event. See Models.                                                         |
| `datacontenttype`  | `application/json`                                | Always present.                                                                                |
| `data.savedFields` | Array of `{ "key", "locale"? }`                   | Fields the change saved. Empty for deleted, restored and purged.                               |

#### Reading `savedFields`

* `key` is either a record attribute (`primaryTitle`, `workType`, `lifecycleStatus`, `lifecyclePhase`, `parentId`, `sequenceNumber`, `securityTagIds`) or one of your tenant's field keys. Both are treated the same way.
* `locale` appears only for a localized field value, for example `fr-FR`. Non-localized fields have no `locale`.
* Events never carry field values, the user who made the change, or security tag IDs.

***

### Examples

> A new episode created under a season, with a title and an English synopsis.

json

```json
{
  "specversion": "1.0",
  "id": "61b0e8a4c3f9d257",
  "source": "/tenants/3f9a1c7e5b2d8046/records",
  "type": "com.fabricinsights.originstudio.record.created",
  "subject": "a4e81c0f9d2b6573",
  "time": "2026-10-09T09:14:22.318Z",
  "dataschema": "https://knowledgebase.fabricdata.com/studio/integrations-and-apis/origin-studio-production-api/models#recordcloudevent",
  "datacontenttype": "application/json",
  "data": {
    "savedFields": [
      { "key": "primaryTitle" },
      { "key": "workType" },
      { "key": "parentId" },
      { "key": "synopsis", "locale": "en-US" }
    ]
  }
}
```

> The record moved to a new lifecycle status, which also moved it to a new phase. `lifecyclePhase` is listed only when the phase actually changes.

json

```json
{
  "specversion": "1.0",
  "id": "2e9c7f1a5b80d364",
  "source": "/tenants/3f9a1c7e5b2d8046/records",
  "type": "com.fabricinsights.originstudio.record.updated",
  "subject": "a4e81c0f9d2b6573",
  "time": "2026-10-09T15:30:09.774Z",
  "dataschema": "https://knowledgebase.fabricdata.com/studio/integrations-and-apis/origin-studio-production-api/models#recordcloudevent",
  "datacontenttype": "application/json",
  "data": {
    "savedFields": [
      { "key": "lifecycleStatus" },
      { "key": "lifecyclePhase" }
    ]
  }
}
```

> Moving an episode renumbers its siblings. Each renumbered episode gets its own event like this one.

json

```json
{
  "specversion": "1.0",
  "id": "b57e0d3a9c14f826",
  "source": "/tenants/3f9a1c7e5b2d8046/records",
  "type": "com.fabricinsights.originstudio.record.updated",
  "subject": "7c2d95e0b18f4a3e",
  "time": "2026-10-09T16:45:31.902Z",
  "dataschema": "https://knowledgebase.fabricdata.com/studio/integrations-and-apis/origin-studio-production-api/models#recordcloudevent",
  "datacontenttype": "application/json",
  "data": {
    "savedFields": [{ "key": "sequenceNumber" }]
  }
}
```

> `record.restored` and `record.purged` have the same shape as `record.deleted`, with their own `type`. `savedFields` is always empty for these three.

json

```json
{
  "specversion": "1.0",
  "id": "90c4a6e2f7d31b58",
  "source": "/tenants/3f9a1c7e5b2d8046/records",
  "type": "com.fabricinsights.originstudio.record.deleted",
  "subject": "a4e81c0f9d2b6573",
  "time": "2026-10-09T17:20:55.140Z",
  "dataschema": "https://knowledgebase.fabricdata.com/studio/integrations-and-apis/origin-studio-production-api/models#recordcloudevent",
  "datacontenttype": "application/json",
  "data": {
    "savedFields": []
  }
}
```

The field-edit example (`record.updated` saving a French synopsis and genres) is shown under Decoded Event Payload.

***

### SNS Subscription Confirmation

When you first subscribe a webhook, SNS sends a `SubscriptionConfirmation` message.

Example:

json

```json
{
  "Type": "SubscriptionConfirmation",
  "SubscribeURL": "https://sns.<region>.amazonaws.com/?Action=ConfirmSubscription..."
}
```

You must confirm the subscription by requesting the `SubscribeURL`. No events are delivered until you do.

***

### Security & Validation

To ensure a webhook message is authentic:

* Validate the SNS signature using `SigningCertURL`, following the [Amazon SNS message verification steps](https://docs.aws.amazon.com/sns/latest/dg/sns-verify-signature-of-message.html)
* Confirm the `TopicArn` matches your expected tenant topic
* Reject unknown topics

***

### Retry & Delivery Behavior

* SNS retries failed deliveries, following the AWS SNS retry policy
* Events are delivered **at least once**, so your system must handle duplicates
* Events can arrive **out of order**

Recommended:

1. **Deduplicate on the CloudEvents `id`.** A retried change keeps the same `id`.
2. **Order by `time`.** For one record, ignore an event older than the last one you processed.
3. **Fetch, then act.** Events carry no values. When a saved key matters to you, read the record with `GET /v1/records/{recordId}` using the event's `subject`.
4. **Ignore what you don't recognize.** New event types, attributes and `data` properties can be added without notice. Skip anything you don't understand rather than fail.

***

### Recovering Missed Events

Origin Studio does not store or replay events. To catch up after downtime, search for records updated since the `time` of the last event you processed, then handle each one as if it had sent `record.updated`.

http

```http
POST /v1/records/search?filter.locale=fr-FR&page.size=25&page.number=0
```

json

```json
{
  "data": {
    "type": "searchQueries",
    "attributes": {
      "group": {
        "operator": "AND",
        "conditions": [
          { "field": "updated", "operator": "gte", "value": "2026-10-09T10:02:47.051Z" }
        ]
      }
    }
  }
}
```

* **Locales.** Localized fields come back in one locale per request (`en` when `filter.locale` is omitted). If you need non-English localized data, repeat the search once per locale.
* **Paging.** Pages hold at most 25 records. Increase `page.number` until a page returns fewer than 25.
* **Current state only.** You get each record as it is now, not every change in between. Deleted records come back with their deleted status.

> Permanently deleted records do not appear in search results, so a missed `record.purged` event **cannot** be recovered this way.&#x20;

***

### Known Limits

* A rare internal failure can drop an event. If you need every change, run the recovery query on a schedule as a safety net.

***

### Related Pages

* Events Notification
* API Key Management
* Records API
* Record Lifecycles API
* Models
