//SCHEMADocs
Console
Reference

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.

FieldTypeDescription
idstringThe event's id, evt_ and 32 hex characters. The same on every retry, and the same as the webhook-id header — dedupe on it.
typestringThe event type, e.g. release.state_changed. A payload never changes shape under a type; an incompatible change is a new type.
occurredAttimestampWhen it happened, in UTC. For release.available from a future-dated release, the date it became available rather than when we noticed.
orgPkintegerThe organization the endpoint belongs to.
catalogPkinteger | nullThe catalog it happened in. null for events about the organization itself, such as membership.
dataobjectThe event's own fields, below.

Headers#

Each request carries the Standard Webhooks headers. See Receive webhooks for verifying them.

HeaderValue
webhook-idThe event id, as in the body.
webhook-timestampSeconds since the epoch at which this attempt was signed. Reject anything more than a few minutes old.
webhook-signaturev1, 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 dataTypeDescription
endpointPkintegerThe endpoint the test was sent to.
messagestringA 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 dataTypeDescription
messagePkintegerThe message, as deliveries in the API identifies it.
messageIdstringThe sender's MessageId, exactly as delivered.
senderDpidstringThe sender's DDEX party id.
statusingested | partialWhether every file it declared is already here.
missingFileCountintegerDeclared 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 dataTypeDescription
messagePkintegerThe message.
messageIdstringThe sender's MessageId.
senderDpidstringThe 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 dataTypeDescription
releasePkintegerThe release.
upcstring | nullIts UPC/EAN, when it has one.
gridstring | nullIts GRid, when it has one.
fromstring | nullThe state it left, or null for a release that is new.
tostringThe state it entered.
releaseRevisionintegerBumped by every edit to a draft. Higher is newer.
availableFromtimestamp | nullWhen 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 dataTypeDescription
releasePkintegerThe release.
upcstring | nullIts UPC/EAN, when it has one.
gridstring | nullIts GRid, when it has one.
availableFromtimestamp | nullThe 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 dataTypeDescription
userPkintegerThe person.
emailstringTheir address, as entered.
roleowner | admin | memberThe role they were given.
invitedByinteger | nullWho 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 dataTypeDescription
userPkintegerThe person.
emailstringTheir address.
roleowner | admin | memberThe 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 dataTypeDescription
userPkintegerThe person.
emailstringTheir address.
fromowner | admin | memberThe role they had.
toowner | admin | memberThe 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 dataTypeDescription
resourcePkintegerThe resource.
isrcstring | nullIts 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 dataTypeDescription
resourcePkintegerThe resource.
isrcstring | nullIts ISRC, when it has one.
encodingstringThe pinned encoding, e.g. aac-256-v1, as encodings lists it.
clipbooleanWhether 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 dataTypeDescription
rejectionPkintegerThe rejection, as deliveryRejections in the API lists it.
deliveryNamespacestringThe batch directory the file arrived in, or the console's namespace.
relativeUristringThe file's path within the batch, as delivered.
detectedFormatstringWhat the header says the file is, e.g. mp3 or aac.
reasonstringWhat 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"
}