Webhook events
What each event means, when exactly it fires, and the fields of its data — generated from the same definitions //SCHEMA's code is checked against.
Subscribe an endpoint to any of these with createWebhookEndpoint. Payloads are deliberately thin: identifiers and what changed, not the catalog data itself. Fetch what you need through the API — by the time an event arrives, everything it describes is already readable there.
A payload never changes shape under a type. A change that would break a receiver is published as a new type, delivered beside the old one until it is retired.
The envelope#
Every delivery's body is one JSON object of this shape, with the event's own fields in data.
| Field | Type | Description |
|---|---|---|
id | string | The event's id, evt_ and 32 hex characters. The same on every retry, and the same as the webhook-id header — dedupe on it. |
type | string | The event type, e.g. release.state_changed. A payload never changes shape under a type; an incompatible change is a new type. |
occurredAt | timestamp | When it happened, in UTC. For release.available from a future-dated release, the date it became available rather than when we noticed. |
orgPk | integer | The organization the endpoint belongs to. |
catalogPk | integer | null | The catalog it happened in. null for events about the organization itself, such as membership. |
data | object | The event's own fields, below. |
Headers#
Each request carries the Standard Webhooks headers. See Receive webhooks for verifying them.
| Header | Value |
|---|---|
webhook-id | The event id, as in the body. |
webhook-timestamp | Seconds since the epoch at which this attempt was signed. Reject anything more than a few minutes old. |
webhook-signature | v1, and a base64 HMAC-SHA256 of {webhook-id}.{webhook-timestamp}.{body}, keyed by the endpoint's secret. During a rotation, two signatures separated by a space; accept either. |
Event types#
webhook.test#
When you send a test from the console or call sendTestWebhook. Addressed
to that one endpoint, whatever it subscribes to, and delivered through the
same path as every other event — signed, retried and logged.
Field of data | Type | Description |
|---|---|---|
endpointPk | integer | The endpoint the test was sent to. |
message | string | A fixed sentence for a human reading the delivery. |
{
"endpointPk": 4,
"message": "This is a test event from //SCHEMA. If you can read it, your endpoint is receiving and can verify our signature."
}message.processed#
A DDEX message was parsed and merged into the catalog; its files may still be arriving.
Once per message, when it has been parsed and merged — whether it arrived
over SFTP or through the console. Not for a message we have already
processed: a redelivery of the same MessageId is a no-op and announces
nothing.
status is ingested when nothing more is expected, and partial when
files the message declared have not arrived yet. A partial message is
followed by message.completed once they have.
Field of data | Type | Description |
|---|---|---|
messagePk | integer | The message, as deliveries in the API identifies it. |
messageId | string | The sender's MessageId, exactly as delivered. |
senderDpid | string | The sender's DDEX party id. |
status | ingested | partial | Whether every file it declared is already here. |
missingFileCount | integer | Declared files not yet arrived. 0 when status is ingested. |
{
"messagePk": 5521,
"messageId": "MSG-2026-10-17-0001",
"senderDpid": "PADPIDA2014071701E",
"status": "partial",
"missingFileCount": 3
}message.completed#
Every file a message declared has arrived and been matched.
When the last file a partial message declared arrives and is matched —
the XML came first and the media has caught up. Never for a message that
was ingested from the start. Completing a message is necessary, not
sufficient, for its releases to go live; watch release.state_changed
for that.
Field of data | Type | Description |
|---|---|---|
messagePk | integer | The message. |
messageId | string | The sender's MessageId. |
senderDpid | string | The sender's DDEX party id. |
{
"messagePk": 5521,
"messageId": "MSG-2026-10-17-0001",
"senderDpid": "PADPIDA2014071701E"
}release.state_changed#
A release entered a new availability state: pending, active or withdrawn.
Every time a release enters pending, active or withdrawn: created by
a delivery, held for missing files, promoted when they arrive, published
from a draft, withdrawn by a DDEX purge or withdrawRelease, reinstated
by reinstateRelease. from is null when the release did not exist
before.
A reinstated release that was already announced as release.available
is not announced again — the withdrawn → active change is the news.
Drafts are never announced — they are authoring, visible only with
catalog_write — so a published draft reports from: "draft" and its
first event is that one.
Deliveries are not ordered. Two changes to one release can arrive in either order; fetch the release for its current state rather than trusting the last event you saw.
Field of data | Type | Description |
|---|---|---|
releasePk | integer | The release. |
upc | string | null | Its UPC/EAN, when it has one. |
grid | string | null | Its GRid, when it has one. |
from | string | null | The state it left, or null for a release that is new. |
to | string | The state it entered. |
releaseRevision | integer | Bumped by every edit to a draft. Higher is newer. |
availableFrom | timestamp | null | When it may be made available, if the sender or author gave a date. |
{
"releasePk": 9182,
"upc": "0602445790135",
"grid": null,
"from": "pending",
"to": "active",
"releaseRevision": 1,
"availableFrom": "2026-10-24T00:00:00Z"
}release.available#
A release is active and its availability date has arrived.
The moment to do your part: the release is live and its date has come. Once per release and date.
For a release whose date had already passed (or that has none) when it
went live, this arrives alongside its release.state_changed. For one
dated in the future, it arrives when the date comes, with occurredAt
set to the date itself. If the sender moves the date, the new date is a
new event.
Field of data | Type | Description |
|---|---|---|
releasePk | integer | The release. |
upc | string | null | Its UPC/EAN, when it has one. |
grid | string | null | Its GRid, when it has one. |
availableFrom | timestamp | null | The date it became available from; null when none was given and it was available on going live. |
{
"releasePk": 9182,
"upc": "0602445790135",
"grid": null,
"availableFrom": "2026-10-24T00:00:00Z"
}member.added#
Someone was added to the organization.
When someone is added from the console, the API or by //SCHEMA staff. Not for the owner who created the organization at signup, which happens before any endpoint can exist.
Field of data | Type | Description |
|---|---|---|
userPk | integer | The person. |
email | string | Their address, as entered. |
role | owner | admin | member | The role they were given. |
invitedBy | integer | null | Who added them, or null when //SCHEMA did. |
{
"userPk": 88,
"email": "[email protected]",
"role": "member",
"invitedBy": 41
}member.removed#
Someone was removed from the organization.
When someone's membership ends. Their //SCHEMA account may live on in another organization; this is only about yours. The address and the role they held are included because there is no membership left to look them up by.
Field of data | Type | Description |
|---|---|---|
userPk | integer | The person. |
email | string | Their address. |
role | owner | admin | member | The role they held. |
{
"userPk": 88,
"email": "[email protected]",
"role": "admin"
}member.role_changed#
A member's role in the organization changed.
When a member's role changes. Not when a role is set to the one they already have.
Field of data | Type | Description |
|---|---|---|
userPk | integer | The person. |
email | string | Their address. |
from | owner | admin | member | The role they had. |
to | owner | admin | member | The role they have now. |
{
"userPk": 88,
"email": "[email protected]",
"from": "member",
"to": "admin"
}resource.analyzed#
A resource's file was measured: its duration, format and loudness are readable.
Once per resource, when //SCHEMA has read its file and measured what it
actually is — duration, codec, sample rate, bit depth, whether it is
lossless, and EBU R128 loudness. Read them from Resource.technical.
Usually seconds after the file lands. Not again when //SCHEMA later
re-measures with a newer analyzer.
The measurement belongs to the bytes, so a file your catalog shares with a redelivery is measured once and each resource holding it is announced.
Field of data | Type | Description |
|---|---|---|
resourcePk | integer | The resource. |
isrc | string | null | Its ISRC, when it has one. |
{
"resourcePk": 40211,
"isrc": "USUM72612345"
}resource.encoded#
An encoding of a resource is ready, and its URL can be minted.
When an encoding of a resource is ready: the ones //SCHEMA makes for
every master (aac-256, aac-128, and the aac-128 preview clip),
moments after the file lands, and any you asked for with
requestEncoding. Mint it with
url(encoding: <encoding>, clip: <clip>). Once per resource and
encoding.
Field of data | Type | Description |
|---|---|---|
resourcePk | integer | The resource. |
isrc | string | null | Its ISRC, when it has one. |
encoding | string | The pinned encoding, e.g. aac-256-v1, as encodings lists it. |
clip | boolean | Whether this is the preview clip of that encoding rather than full length. |
{
"resourcePk": 40211,
"isrc": "USUM72612345",
"encoding": "aac-128-v1",
"clip": true
}delivery.file_rejected#
A delivered file was refused at intake and not stored.
When a file you delivered is refused. Today that means a lossy audio master — MP3, AAC, Ogg, WMA, or a WAV holding compressed audio — because //SCHEMA accepts WAV, AIFF, FLAC and ALAC masters only, decided from the file's header rather than its name or the message's claim. Images and video are never refused.
A release waiting for a refused file stays pending: deliver an accepted
file at the same path to complete it. Over SFTP, the refused file is moved
to rejected/, beside done/.
Field of data | Type | Description |
|---|---|---|
rejectionPk | integer | The rejection, as deliveryRejections in the API lists it. |
deliveryNamespace | string | The batch directory the file arrived in, or the console's namespace. |
relativeUri | string | The file's path within the batch, as delivered. |
detectedFormat | string | What the header says the file is, e.g. mp3 or aac. |
reason | string | What arrived and what is accepted, for a person. |
{
"rejectionPk": 77,
"deliveryNamespace": "BATCH_20261023",
"relativeUri": "resources/A1.mp3",
"detectedFormat": "mp3",
"reason": "MP3 is not accepted as a master: deliver WAV, AIFF, FLAC or ALAC"
}