Skip to main content

// 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.

subscribe
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.

16 publish today. 5 are defined in the contract with their payload, and listed here so you can plan for them, but nothing emits them yet. Generated from the API's event catalogue at build time; the AsyncAPI document carries the payload schemas.

Store events (10)

  • order.created
    publishing

    A new order was recorded for the store.

    Subject: The order id. · Scope: orders:read

  • order.refunded
    publishing

    A refund was recorded against an order, by Magistry or by a person.

    Subject: The order id. · Scope: orders:read

  • product.price_changed
    defined, not publishing yet

    A variant price or reference price changed on the storefront.

    Subject: The variant id. · Scope: catalog:read

  • cs.thread_escalated
    defined, not publishing yet

    A customer-service thread was escalated to a person.

    Subject: The thread id. · Scope: cs:read

  • campaign.status_changed
    defined, not publishing yet

    An ad campaign was paused, resumed, or had its budget changed.

    Subject: The campaign id. · Scope: ads:read

  • compliance.finding_opened
    defined, not publishing yet

    A compliance audit opened a finding against the store.

    Subject: The finding id. · Scope: automation:read

  • approval.requested
    publishing

    A connected app asked the merchant to approve something.

    Subject: The approval id. · Scope: automation:read

  • approval.decided
    publishing

    A person approved or rejected an app's approval request.

    Subject: The approval id. · Scope: automation:read

  • approval.expired
    publishing

    An approval request reached its deadline with no decision, and is closed as not approved.

    Subject: The approval id. · Scope: automation:read

  • intent.field_overwritten_externally
    publishing

    A 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.posted
    publishing

    A 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.reversed
    publishing

    A 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.closed
    publishing

    A period was closed: nothing more is posted into it until it is reopened.

    Subject: The period id. · Scope: finance:read

  • fin.period.reopened
    publishing

    A closed period was reopened.

    Subject: The period id. · Scope: finance:read

  • fin.bank.imported
    publishing

    A 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.dismissed
    publishing

    A 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.undismissed
    publishing

    A dismissal was undone: the bank row counts again.

    Subject: The bank transaction id. · Scope: finance:read

  • fin.bank_account.created
    publishing

    A 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.updated
    publishing

    A bank account's details changed; `fields` names which.

    Subject: The bank account id. · Scope: finance:read

  • fin.bank.consent_expiring
    defined, not publishing yet

    A 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_changed
    publishing

    An e-invoice moved to a new Peppol status; `previous_status` says where it came from.

    Subject: The e-invoice message id. · Scope: finance:read

asyncapi
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
node · verify a delivery
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