Webhooks
A webhook endpoint is a URL //SCHEMA sends events to as they happen — a
release going live, a delivery being processed, a member joining. Each
delivery is a signed POST carrying the event; the event types and
their payloads are listed in Webhook events.
Endpoints belong to your organization, and can be narrowed to some of its catalogs. Each has a signing secret, shown once, when the endpoint is created or its secret rotated.
Every owner of your organization is emailed when an endpoint is created, or changed so that it receives more than before — a new URL, more event types or catalogs, or turned back on. A webhook is a standing copy of your events sent somewhere else, so the people who could stop one hear about it at once. If no owner can be emailed, the change is refused.
Webhook configuration is not available to agents over MCP.
- querywebhookEventTypes
- querywebhookEndpoints
- querywebhookEndpoint
- mutationcreateWebhookEndpoint
- mutationupdateWebhookEndpoint
- mutationrotateWebhookSecret
- mutationdeleteWebhookEndpoint
- mutationsendTestWebhook
- mutationredeliverWebhook
The WebhookEndpoint object#
A destination for your organization's events. Never carries its signing secret.
Attributes
endpointPkInt!Unique identifier.urlString!Where events are sent.- What it is for, if set.
eventTypes[String!]!The event types it receives; see Webhook events.- The catalogs whose events it receives, or
nullfor every catalog of your organization, including ones created later. Membership events, which belong to no catalog, reach it either way. enabledBoolean!Whether it is being sent to.enabledAtDateTime!When it was created or last turned back on. It receives only events that happened after this.- When it was turned off, or
null. - Why it was turned off — by whom, or automatically after five days of failed deliveries.
- Since when every delivery to it has failed, while that is so. Five days of it turns the endpoint off and emails your owners.
createdAtDateTime!When it was created.updatedAtDateTime!When it was last changed.- Its deliveries, newest first. Kept for 30 days.
The WebhookDelivery object#
One event's delivery to one endpoint.
Attributes
deliveryPkInt!Unique identifier.eventIdString!The event's id, sent aswebhook-id. The same on every attempt.eventTypeString!The event type, e.g.release.available.occurredAtDateTime!When the event happened.stateString!pending(still being tried),succeeded(answered 2xx) orfailed(out of attempts, or refused).attemptsInt!Attempts made.- When the last attempt was made.
- The HTTP status your receiver last answered with, if it answered.
- Why the last attempt failed:
http 503,timeout after 10s,redirect to …(redirects are never followed),refused: …(an address webhooks cannot be sent to). - Up to 4 KiB of your receiver's last answer.
- How long the last attempt took, in milliseconds.
- When the next attempt is due, while
pending. - When it was delivered, if it was.
The CreatedWebhookEndpoint object#
A new endpoint, and the only copy of its signing secret.
Attributes
endpointWebhookEndpoint!The WebhookEndpoint.secretString!The signing secret, beginningwhsec_. Shown once. Verify each delivery'swebhook-signaturewith it; see Receive webhooks.
The CreateWebhookEndpointInput input object#
An endpoint to create.
Fields
- Where to send.
https://on the default port; not a private, loopback or link-local address. descriptionStringWhat it is for, e.g.Storefront sync.- At least one, from webhookEventTypes.
catalogPks[Int!]Only events from these of your catalogs. Omit for every catalog, including ones created later.
The UpdateWebhookEndpointInput input object#
Changes to an endpoint. Omitted fields are left as they are.
Fields
- The endpoint to change.
urlStringA new destination. The signing secret stays the same.descriptionStringA new description;nullclears it.eventTypes[String!]The event types it should receive, replacing the current list.catalogPks[Int!]The catalogs it should receive events from;nullfor every catalog.enabledBooleanfalseturns it off, failing what was pending;trueturns it back on, receiving events from then on.
List event types#
Every event type an endpoint can subscribe to, with a one-line summary. webhook.test is not among them — it is sent on request to one endpoint.
Arguments
None.
Returns
A list of WebhookEventType objects.
query {
webhookEventTypes {
type
description
}
}curl https://api.schema.fm/graphql \
-H "Authorization: Bearer $SCHEMA_API_KEY" \
-H "Content-Type: application/json" \
--data-binary @- <<'EOF'
{
"query": "query {\n webhookEventTypes {\n type\n description\n }\n}"
}
EOFconst res = await fetch("https://api.schema.fm/graphql", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SCHEMA_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
query: `
query {
webhookEventTypes {
type
description
}
}
`,
}),
});
const { data, errors } = await res.json();{
"data": {
"webhookEventTypes": [
{
"type": "release.available",
"description": "A release is active and its availability date has arrived."
}
]
}
}List endpoints#
Your organization's webhook endpoints, oldest first. Never includes a signing secret.
Arguments
None.
Returns
A list of WebhookEndpoint objects.
query {
webhookEndpoints {
endpointPk
url
eventTypes
catalogPks
enabled
failingSince
}
}curl https://api.schema.fm/graphql \
-H "Authorization: Bearer $SCHEMA_API_KEY" \
-H "Content-Type: application/json" \
--data-binary @- <<'EOF'
{
"query": "query {\n webhookEndpoints {\n endpointPk\n url\n eventTypes\n catalogPks\n enabled\n failingSince\n }\n}"
}
EOFconst res = await fetch("https://api.schema.fm/graphql", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SCHEMA_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
query: `
query {
webhookEndpoints {
endpointPk
url
eventTypes
catalogPks
enabled
failingSince
}
}
`,
}),
});
const { data, errors } = await res.json();{
"data": {
"webhookEndpoints": [
{
"endpointPk": 4,
"url": "https://hooks.paperradio.example/schema",
"eventTypes": ["release.available", "release.state_changed"],
"catalogPks": null,
"enabled": true,
"failingSince": null
}
]
}
}Retrieve an endpoint#
One endpoint, with its delivery log. null if your organization has no endpoint by that id.
Arguments
- The endpoint.
Returns
A WebhookEndpoint, or null.
query {
webhookEndpoint(endpointPk: 4) {
url
deliveries(first: 2) {
deliveryPk
eventType
state
attempts
lastStatus
lastError
}
}
}curl https://api.schema.fm/graphql \
-H "Authorization: Bearer $SCHEMA_API_KEY" \
-H "Content-Type: application/json" \
--data-binary @- <<'EOF'
{
"query": "query {\n webhookEndpoint(endpointPk: 4) {\n url\n deliveries(first: 2) {\n deliveryPk\n eventType\n state\n attempts\n lastStatus\n lastError\n }\n }\n}"
}
EOFconst res = await fetch("https://api.schema.fm/graphql", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SCHEMA_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
query: `
query {
webhookEndpoint(endpointPk: 4) {
url
deliveries(first: 2) {
deliveryPk
eventType
state
attempts
lastStatus
lastError
}
}
}
`,
}),
});
const { data, errors } = await res.json();{
"data": {
"webhookEndpoint": {
"url": "https://hooks.paperradio.example/schema",
"deliveries": [
{
"deliveryPk": 1182,
"eventType": "release.available",
"state": "pending",
"attempts": 2,
"lastStatus": 503,
"lastError": "http 503"
},
{
"deliveryPk": 1181,
"eventType": "release.state_changed",
"state": "succeeded",
"attempts": 1,
"lastStatus": 204,
"lastError": null
}
]
}
}
}Create an endpoint#
Create an endpoint. Copy secret from the response immediately —
it is never shown again.
The URL must be https:// on the default port, and must not be a
private, loopback or link-local address. Every owner of your
organization is emailed, in the same transaction, with the
destination, what it receives, and who created it; if no owner can
be emailed, the endpoint is not created. At most 10 endpoints per
organization.
Arguments
- What to create; see CreateWebhookEndpointInput.
Show child parameters
- Where to send.
https://on the default port; not a private, loopback or link-local address. descriptionStringWhat it is for, e.g.Storefront sync.- At least one, from webhookEventTypes.
catalogPks[Int!]Only events from these of your catalogs. Omit for every catalog, including ones created later.
Returns
A CreatedWebhookEndpoint, including the secret.
mutation {
createWebhookEndpoint(input: {
url: "https://hooks.paperradio.example/schema"
eventTypes: ["release.available", "release.state_changed"]
}) {
secret
endpoint {
endpointPk
enabled
}
}
}curl https://api.schema.fm/graphql \
-H "Authorization: Bearer $SCHEMA_API_KEY" \
-H "Content-Type: application/json" \
--data-binary @- <<'EOF'
{
"query": "mutation {\n createWebhookEndpoint(input: {\n url: \"https://hooks.paperradio.example/schema\"\n eventTypes: [\"release.available\", \"release.state_changed\"]\n }) {\n secret\n endpoint {\n endpointPk\n enabled\n }\n }\n}"
}
EOFconst res = await fetch("https://api.schema.fm/graphql", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SCHEMA_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
query: `
mutation {
createWebhookEndpoint(input: {
url: "https://hooks.paperradio.example/schema"
eventTypes: ["release.available", "release.state_changed"]
}) {
secret
endpoint {
endpointPk
enabled
}
}
}
`,
}),
});
const { data, errors } = await res.json();{
"data": {
"createWebhookEndpoint": {
"secret": "whsec_MfKQ9r8GKYqrTwjUPD8ILPZIo2LaLaSwq3c0Jm7ZP4c=",
"endpoint": {
"endpointPk": 4,
"enabled": true
}
}
}
}Update an endpoint#
Change an endpoint's URL, description, event types or catalogs, or turn it off or back on. Fields you omit are left as they are.
A change that lets it receive more than before — a new URL, more event types or catalogs, or turning it back on — emails every owner, in the same transaction. Narrowing announces nothing. Turning an endpoint off fails what was still pending; turning it back on receives events from then on, never what it missed — use redeliverWebhook for those.
Arguments
- What to change; see UpdateWebhookEndpointInput.
Show child parameters
- The endpoint to change.
urlStringA new destination. The signing secret stays the same.descriptionStringA new description;nullclears it.eventTypes[String!]The event types it should receive, replacing the current list.catalogPks[Int!]The catalogs it should receive events from;nullfor every catalog.enabledBooleanfalseturns it off, failing what was pending;trueturns it back on, receiving events from then on.
Returns
The updated WebhookEndpoint.
mutation {
updateWebhookEndpoint(input: { endpointPk: 4, enabled: false }) {
enabled
disabledReason
}
}curl https://api.schema.fm/graphql \
-H "Authorization: Bearer $SCHEMA_API_KEY" \
-H "Content-Type: application/json" \
--data-binary @- <<'EOF'
{
"query": "mutation {\n updateWebhookEndpoint(input: { endpointPk: 4, enabled: false }) {\n enabled\n disabledReason\n }\n}"
}
EOFconst res = await fetch("https://api.schema.fm/graphql", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SCHEMA_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
query: `
mutation {
updateWebhookEndpoint(input: { endpointPk: 4, enabled: false }) {
enabled
disabledReason
}
}
`,
}),
});
const { data, errors } = await res.json();{
"data": {
"updateWebhookEndpoint": {
"enabled": false,
"disabledReason": "Turned off by Sam Lee <[email protected]>."
}
}
}Rotate an endpoint's secret#
Replace the signing secret, returning the new one once. For 24 hours deliveries carry a signature from each secret, so your receiver can switch over without dropping one.
Arguments
- The endpoint.
Returns
The new secret, beginning whsec_.
mutation {
rotateWebhookSecret(endpointPk: 4)
}curl https://api.schema.fm/graphql \
-H "Authorization: Bearer $SCHEMA_API_KEY" \
-H "Content-Type: application/json" \
--data-binary @- <<'EOF'
{
"query": "mutation {\n rotateWebhookSecret(endpointPk: 4)\n}"
}
EOFconst res = await fetch("https://api.schema.fm/graphql", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SCHEMA_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
query: `
mutation {
rotateWebhookSecret(endpointPk: 4)
}
`,
}),
});
const { data, errors } = await res.json();{
"data": {
"rotateWebhookSecret": "whsec_C2FvCcBdMdD1e9xY3bQh7nLk5RtWp0uA8JsiXgZoEyM="
}
}Delete an endpoint#
Delete an endpoint and its delivery log. Nothing pending is sent.
Arguments
- The endpoint.
Returns
true if an endpoint of your organization was deleted.
mutation {
deleteWebhookEndpoint(endpointPk: 4)
}curl https://api.schema.fm/graphql \
-H "Authorization: Bearer $SCHEMA_API_KEY" \
-H "Content-Type: application/json" \
--data-binary @- <<'EOF'
{
"query": "mutation {\n deleteWebhookEndpoint(endpointPk: 4)\n}"
}
EOFconst res = await fetch("https://api.schema.fm/graphql", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SCHEMA_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
query: `
mutation {
deleteWebhookEndpoint(endpointPk: 4)
}
`,
}),
});
const { data, errors } = await res.json();{
"data": {
"deleteWebhookEndpoint": true
}
}Send a test event#
Queue a webhook.test event to this one endpoint. It goes through
the same path as every other event — signed, retried and logged — so
a test that arrives and verifies means your integration works. The
endpoint must be enabled.
Arguments
- The endpoint.
Returns
The event's id, as your receiver will see it in webhook-id.
mutation {
sendTestWebhook(endpointPk: 4)
}curl https://api.schema.fm/graphql \
-H "Authorization: Bearer $SCHEMA_API_KEY" \
-H "Content-Type: application/json" \
--data-binary @- <<'EOF'
{
"query": "mutation {\n sendTestWebhook(endpointPk: 4)\n}"
}
EOFconst res = await fetch("https://api.schema.fm/graphql", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SCHEMA_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
query: `
mutation {
sendTestWebhook(endpointPk: 4)
}
`,
}),
});
const { data, errors } = await res.json();{
"data": {
"sendTestWebhook": "evt_4ee8793102184b15940831a1e5f3ce54"
}
}Redeliver an event#
Send one delivery again, with the same webhook-id, as a fresh run of
attempts. Safe on a delivery that already succeeded: your receiver
should be deduplicating on the id. The endpoint must be enabled.
Arguments
- The delivery, from the endpoint's
deliveries.
Returns
true once it is queued.
mutation {
redeliverWebhook(deliveryPk: 1182)
}curl https://api.schema.fm/graphql \
-H "Authorization: Bearer $SCHEMA_API_KEY" \
-H "Content-Type: application/json" \
--data-binary @- <<'EOF'
{
"query": "mutation {\n redeliverWebhook(deliveryPk: 1182)\n}"
}
EOFconst res = await fetch("https://api.schema.fm/graphql", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.SCHEMA_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
query: `
mutation {
redeliverWebhook(deliveryPk: 1182)
}
`,
}),
});
const { data, errors } = await res.json();{
"data": {
"redeliverWebhook": true
}
}