// developers · events
Be told, instead of polling.
21 event types, 16 publishing today. Deliveries are CloudEvents 1.0 envelopes, signed with the same ES256 key the access tokens use, so you verify them with a public key and no shared secret.
// subscribe
One call per endpoint.
curl -X POST https://app.magistry.io/api/v1/subscriptions \
-H "Authorization: Bearer mag_sk_live_…" \
-H "Content-Type: application/json" \
-d '{ "url": "https://your-app.example/hooks/magistry",
"event_types": ["order.*", "approval.decided"],
"description": "Order sync" }'event_types takes exact types or a trailing *; leave it out to receive every type. Creating a subscription needs automation:write, plus the read scope of each type it filters, because a webhook is a read. It is refused when the URL is not https or resolves to a private address, when a filter matches no type in the catalogue, and past 20 subscriptions per store. A filter on a type that does not publish yet is saved, with a no_publisher_yet warning in the response.
Finance events belong to a set of books rather than a store, so they are subscribed per entity at POST /api/v1/finance/subscriptions with an entity_id. Missed ones can be replayed from the finance change feed, which keeps 730 days.
// catalogue
21 event types.
Store events (10)
order.createdpublishingA new order was recorded for the store.
Subject: The order id. · Scope: orders:read
order.refundedpublishingA refund was recorded against an order, by Magistry or by a person.
Subject: The order id. · Scope: orders:read
product.price_changeddefined, not publishing yetA variant price or reference price changed on the storefront.
Subject: The variant id. · Scope: catalog:read
cs.thread_escalateddefined, not publishing yetA customer-service thread was escalated to a person.
Subject: The thread id. · Scope: cs:read
campaign.status_changeddefined, not publishing yetAn ad campaign was paused, resumed, or had its budget changed.
Subject: The campaign id. · Scope: ads:read
compliance.finding_openeddefined, not publishing yetA compliance audit opened a finding against the store.
Subject: The finding id. · Scope: automation:read
approval.requestedpublishingA connected app asked the merchant to approve something.
Subject: The approval id. · Scope: automation:read
approval.decidedpublishingA person approved or rejected an app's approval request.
Subject: The approval id. · Scope: automation:read
approval.expiredpublishingAn approval request reached its deadline with no decision, and is closed as not approved.
Subject: The approval id. · Scope: automation:read
intent.field_overwritten_externallypublishingA field Magistry set (a variant price, a campaign status, budget or bidding, a theme file) was changed afterwards by someone else. Sent once per overwrite, when it is first read back.
Subject: The declaration (intent) id. · Scope: automation:read
Finance entity events (11)
fin.entry.postedpublishingA journal entry was posted. Entries of a bulk import are announced once, by the import, and read through the API.
Subject: The entry id. · Scope: finance:read
fin.entry.reversedpublishingA journal entry was reversed by a new entry. The original stays; this is its tombstone.
Subject: The reversal entry id. · Scope: finance:read
fin.period.closedpublishingA period was closed: nothing more is posted into it until it is reopened.
Subject: The period id. · Scope: finance:read
fin.period.reopenedpublishingA closed period was reopened.
Subject: The period id. · Scope: finance:read
fin.bank.importedpublishingA bank statement was imported: how many rows arrived, how many were new and how many were duplicates.
Subject: The import id. · Scope: finance:read
fin.bank_transaction.dismissedpublishingA bank row was dismissed as not_a_transaction or as a duplicate_of another row; any booking of it was reversed.
Subject: The bank transaction id. · Scope: finance:read
fin.bank_transaction.undismissedpublishingA dismissal was undone: the bank row counts again.
Subject: The bank transaction id. · Scope: finance:read
fin.bank_account.createdpublishingA bank, card, cash or PSP account was added to the books, with the ledger account it books on.
Subject: The bank account id. · Scope: finance:read
fin.bank_account.updatedpublishingA bank account's details changed; `fields` names which.
Subject: The bank account id. · Scope: finance:read
fin.bank.consent_expiringdefined, not publishing yetA bank connection's consent ends soon; the bank feed stops unless the owner renews it.
Subject: The bank connection id. · Scope: finance:read
fin.einvoice.status_changedpublishingAn e-invoice moved to a new Peppol status; `previous_status` says where it came from.
Subject: The e-invoice message id. · Scope: finance:read
curl https://app.magistry.io/api/v1/asyncapi.json// verify
Check the signature before you trust the body.
Standard Webhooks, asymmetric
Each delivery carries webhook-id, webhook-timestamp and webhook-signature: v1a,<signature>: an ES256 signature, raw r‖s in base64url, over {id}.{timestamp}.{body}.
webhook-key-id names the key in /.well-known/jwks.json to verify with, so a key rotation needs no change on your side. A library that already speaks Standard Webhooks verifies these without Magistry-specific code.
Delivery
- Guarantee
- At least once; dedupe on webhook-id
- Attempts
- 6, with jittered backoff
- Timeout
- 10 seconds
- Answer 410
- Unsubscribes the endpoint
- Disabled after
- 20 consecutive failures
import { createPublicKey, verify } from "node:crypto";
const { keys } = await (await fetch("https://app.magistry.io/.well-known/jwks.json")).json();
// rawBody: the request body exactly as received, before any JSON parsing.
export function isFromMagistry(headers, rawBody) {
const id = headers["webhook-id"];
const ts = headers["webhook-timestamp"];
const [scheme, sig] = String(headers["webhook-signature"]).split(",");
if (scheme !== "v1a") return false;
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false; // your replay window
const jwk = keys.find((k) => k.kid === headers["webhook-key-id"]);
if (!jwk) return false;
return verify(
"sha256",
Buffer.from(`${id}.${ts}.${rawBody}`),
{ key: createPublicKey({ key: jwk, format: "jwk" }), dsaEncoding: "ieee-p1363" },
Buffer.from(sig, "base64url"),
);
}Build on the same tools the product uses.
Create a sandbox key on demo data, or start free and connect a real store.
MCP + REST · OAuth 2.1 or API keys · every call audited
