Event Catalogue
Every webhook event type Brainerce can deliver, when it fires, and what its payload contains.
The merchant-facing webhook system delivers events from the following catalogue. Subscribe via Settings → Webhooks; fetch the full list at runtime via GET /api/stores/{storeId}/webhook-subscriptions/event-catalog.
All payloads follow the standard envelope:
{
"id": "evt_<32-hex>",
"type": "<event-type>",
"createdAt": "<ISO-8601>",
"data": {
/* see per-event tables below */
}
}The envelope id is also sent as the X-Brainerce-Event-Id header. It is the same on every retry and on an event that is resent later from the event log (a resend adds X-Brainerce-Redelivery: true), so deduplicate on it. An endpoint can also be given a body template that reshapes the request body for a third-party API; its {{path}} placeholders resolve against this envelope, for example {{data.order.id}}.
Payload enrichment. order.* and checkout.completed events ship with the full Order resource in the envelope alongside the original orderId. Example for order.created:
{
"id": "evt_a3f9b1c2...",
"type": "order.created",
"createdAt": "2026-05-18T14:00:00.000Z",
"data": {
"orderId": "ord_abc123",
"order": {
"id": "ord_abc123",
"orderNumber": "1042",
"status": "PENDING",
"financialStatus": "pending",
"fulfillmentStatus": "unfulfilled",
"totalAmount": 142.5,
"subtotal": 130.0,
"taxAmount": 12.5,
"shippingAmount": 0,
"currency": "USD",
"items": [
/* line items */
],
"customer": { "email": "...", "firstName": "...", "lastName": "..." },
"shippingAddress": { "...": "..." },
"billingAddress": { "...": "..." },
"createdAt": "2026-05-18T13:59:58.000Z",
"updatedAt": "2026-05-18T13:59:58.000Z"
/* + appliedDiscounts, deliveryType, paymentMethod, etc. */
}
}
}The ID fields stay beside order, so a handler that only reads orderId keeps working.
The other families use the same pattern. The sibling key is named after the resource:
| Event family | Sibling resource key | Where it is missing |
|---|---|---|
order.*, checkout.completed | order | Never added when the lookup fails |
customer.* | customer | customer.deleted (the row is gone) |
product.* | product | product.deleted (the row is gone) |
payment.refunded | payment | payment.succeeded and payment.failed carry no paymentId, so they have no payment sibling |
inventory.* | inventory | A product with no stock row of its own |
donation.* | donation | Never added when the lookup fails |
blog.post.* | none | The payload is the flat fields in the Blog table |
Resource fields
Every field below is always present on the resource, with null where the value is unset. Dates are ISO 8601 strings. Money on orders, customers, products and payments is a JSON number in the major unit (142.5 is 142.50 in currency). Money on a donation is a decimal string ("12.50"), the only family that does this.
order: id, storeId, orderNumber, externalId, platform, status (DRAFT, PENDING, PROCESSING, ON_HOLD, PAID, SHIPPED, DELIVERED, COMPLETED, FULFILLED, CANCELLED, REFUNDED, PARTIALLY_REFUNDED), financialStatus (for example pending, paid, partially_refunded, refunded), fulfillmentStatus (unfulfilled, partial, fulfilled), totalAmount, subtotal, taxAmount, shippingAmount, discountAmount, couponDiscount, surchargeAmount (numbers; surchargeAmount is 0 when there is none), couponCode, currency, items (array of line items, your internal cost removed), customer (the buyer snapshot stored on the order), shippingAddress, billingAddress, appliedDiscounts, appliedSurcharges, customFieldValues, deliveryType (shipping unless the order is a pickup), paymentMethod, sourceName, notes, createdAt, updatedAt.
customer: id, storeId, email, phone, firstName, lastName, emailVerified, tags (string array), metadata, preferredLocale, acceptsMarketing, privacyPolicyAcceptedAt, totalSpent (number), ordersCount, lastOrderAt, createdAt, updatedAt.
product: id, storeId, sku, name, slug, description, descriptionFormat, metaDescription, basePrice, salePrice (numbers), gtin, mpn, attributes, images, status, type (SIMPLE, VARIABLE, KIT), isDownloadable, translations, taxBehavior, avgRating, reviewCount, createdAt, updatedAt. Variants, per-variant prices and stock are not included; read them from the API with the productId.
payment (only on payment.refunded): id, storeId, orderId, checkoutId, externalId, amount, refundedAmount (numbers), currency, status (PENDING, AUTHORIZED, CAPTURED, FAILED, REFUNDED, PARTIALLY_REFUNDED, CANCELLED), paymentMethod, paymentMethodDetails, errorCode, errorMessage, createdAt, updatedAt.
inventory (the product-level stock row): id, productId, trackingMode (TRACKED, UNLIMITED, DISABLED), total, reserved, available (integers), lastInventorySyncAt, updatedAt. A change to one variant's stock still reports the product-level row.
donation: id, storeId, status (PENDING, PAID, FAILED, CANCELLED), amount, feeCoverAmount, chargedAmount (decimal strings), currency, donorEmail, donorName, isAnonymous, tributeType (IN_HONOR, IN_MEMORY, or null), tributeName, message, customerId, recurringSubscriptionId, campaignId, fundId, dedicationLabel, receiptName, donorPhone, receiptTaxIdLast4, paidAt, createdAt, and payment (null, or { id, status, amount, refundedAmount, currency } with the amounts as decimal strings).
A few caveats:
- The sibling is best-effort, so
data.ordercan be missing on anyorder.*event. If loading the resource fails when the event is dispatched (a database timeout, or the row already gone), the event is still delivered, carrying only the ID fields:{ "orderId": "..." }with noorderkey. The same applies to every family above. Never treat a missing sibling as an error and never drop the event over it. Fetch the order instead:GET /api/v1/orders/{orderId}withAuthorization: Bearer brainerce_…and theorders:readscope (SDK:client.getOrder(orderId)inapiKeymode). That returns the REST order resource, which is not guaranteed to be field-for-field identical to the webhook snapshot, so map from it explicitly. customer.deleted/product.deleted: the row no longer exists at dispatch time, so the sibling resource is omitted. The merchant still receives the event with the ID and can decide how to react (typically: remove it from their CRM / catalog index).payment.failedfires before the Payment row exists (capture never landed), so the payload carriescheckoutId + erroronly. Nopaymentsibling. It is never sent for a checkout that already has an order or a captured payment: a late or repeated failure notice from the provider after a successful payment is ignored, so apayment.failedis not followed by an order for the same checkout.order.refundedcarries extra fields alongsideorderIdand the fullordersnapshot, and not all of them are always present — branch on what you get rather than destructuring blindly:refundType("full" | "partial") — always present.amount— always present. The refunded amount only, in the order's currency — not the order total. For a manual "mark as refunded" status change this does equal the order's total at the time of refund (that path always setsrefundType: "full"). For a refund taken in the payment provider's own dashboard (source: "provider"),amountis what that provider reports refunding on that one payment — on an order settled across more than one tender (e.g. part gift card, part card), a provider-reportedrefundType: "full"can still carry anamountsmaller than the order total, because it only reflects that payment's leg.refundId— present when a Brainerce refund record backs the event: a refund created through Brainerce (dashboard refund orPOST /orders/:id/refunds), and a refund taken in the payment provider's own dashboard that Brainerce recorded in the order's refund list (it then carries the id of that record). It isnullfor a manual "mark as refunded" status change, and for a provider-dashboard refund that carried no refund id of its own, because no refund record backs those. Use it to tell two partial refunds on one order apart.source: "provider"— present only when the refund was taken in the payment provider's own dashboard (Stripe, PayPal and some other providers: only those that report a refund back to us) and reached us on their webhook. Absent means the refund came from Brainerce. Treat it as a discriminator, not a guarantee of ordering: the event is delivered at least once, so key your own bookkeeping onorderId+refundTypeand make the handler idempotent.
- Never included in any payload:
costPriceon products (your margin) and thecoston order line items,passwordHashand login tokens on customers, rawproviderDataon payments, signed download URLs on orders, internal account ids (accountId), and the store id at the top level ofdata. A donation carries the donor's tax id only asreceiptTaxIdLast4. - Delivery order is not guaranteed. Each event is a separate delivery with a short random delay, and a retry can land after a later event, so
order.paidcan arrive beforeorder.created. UsecreatedAton the envelope to order events, and key your handler on the resource id. - A test event carries only the ID fields plus
"_test": true, never the resource sibling. Its envelopeidisevt_test_followed by 24 hex characters. Use a real event to see the full shape.
Orders
| Event | When it fires | data |
|---|---|---|
order.created | A new order exists, whatever its payment status: a storefront checkout, a paid checkout, an order created from the dashboard or POST /v1/orders | orderId, order |
order.updated | The order status changed, the shipping address was edited, or the order was marked as paid. Not sent for notes, tracking numbers, custom field values or line item edits | orderId, order |
order.paid | The order is paid: a provider captured the funds, or the merchant recorded an out-of-band payment (cash on delivery, bank transfer, card machine), or it was created already paid | orderId, order |
order.fulfilled | The order was fulfilled with the fulfil action (the dashboard's Add tracking window, or the fulfil tools of the MCP server and the AI assistant; there is no fulfil route on the public API), or a shipping label's carrier reported the parcel shipped. Not sent when the status is set to SHIPPED from the status dropdown or PATCH /v1/orders/{id} (that sends order.updated only), and not for a tracking correction on an order that already shipped | orderId, order |
order.cancelled | The order status became CANCELLED (dashboard, API, or the customer cancelling) | orderId, order |
order.refunded | The order was fully or partially refunded, including a refund taken in the payment provider's own dashboard | orderId, order, refundType ("full" or "partial"), amount (number, refunded amount only), refundId (string or null), and source: "provider" on a provider-dashboard refund |
The most common subscription is order.created + order.paid. Use order.paid to trigger fulfillment in your ERP; use order.created to log every order the moment it exists, whether or not it has been paid for yet.
order.created is not an attempt log. A checkout that fails payment never produces an order, so it never produces an order.created. If you want the attempts as well as the orders, subscribe to payment.failed (the provider rejected the transaction) and checkout.abandoned (only a logged-in customer's idle cart, and only for stores with cart recovery turned on, so it does not cover every shopper who left).
A refund taken in the provider's dashboard also moves the order
If someone refunds a charge directly in the provider's own dashboard (Stripe or PayPal, for example) rather than in Brainerce, and the provider reports refunds back, its refund webhook reaches us and we apply the same order-side transition a Brainerce refund does:
order.financialStatusbecomes"refunded"or"partially_refunded".order.statusmoves toREFUNDED/PARTIALLY_REFUNDED, but only from a status where that transition is legal. ACANCELLEDor already-REFUNDEDorder keeps its status, and aPAIDorder that is only partially refunded keepsPAID—financialStatusis the field that always tells the truth about the money, so read that rather thanstatusif you are reconciling refunds.order.statusHistorygets the transition appended.order.refundedfires, carryingsource: "provider".
Two things this still does not do, deliberately: inventory is not restocked (a restock is a merchant choice with no equivalent input on a provider webhook), and payment.refunded does not reach you for every provider. Stripe, PayPal and some other providers report refunds back to Brainerce, so a provider-dashboard refund on one of those fires payment.refunded as well as order.refunded. The remaining providers (Sola included) do not report refunds at all, so neither event fires for a refund taken in their dashboard — Brainerce never learns about it. Do not branch on the provider name: treat a provider-dashboard refund as something that may never reach you. Subscribe to order.refunded and expect it for refunds taken in Brainerce on any provider.
Customers
| Event | When it fires | data |
|---|---|---|
customer.created | A new customer account is created, either by a merchant or by a shopper signing up on your storefront. Guest checkout is the exception; see below. | customerId, customer |
customer.updated | A customer was updated through the customer update path: the dashboard, PATCH /v1/customers/{id}, the storefront profile edit. Not sent for changes the platform makes itself: order totals (totalSpent, ordersCount), marketing opt-in and unsubscribe, email verification, password changes | customerId, customer |
customer.deleted | The customer account was deleted | customerId (no customer) |
What customer.created covers, and the one case it does not
Read this before you build a CRM sync on it.
customer.created fires when a customer account comes into existence, on all of these:
- a merchant creating a customer from the dashboard or
POST /v1/customers - a shopper signing up on your storefront with a password
- a shopper completing an email-verified signup, where the event fires on verification, when the record is actually written, not when the code is emailed
- a shopper signing up through Google, Facebook or GitHub OAuth (the only three providers there are), or a guest linking their first OAuth credential
Internally the shopper paths emit customer.registered, which is aliased onto
customer.created for delivery. customer.registered is not subscribable on its own, and
you will never receive both events for the same signup.
The exception is guest checkout. A shopper who checks out without registering has a
customer record written for them, and that write emits no customer.created. It is not
a signup: no credential is created and the shopper never asked for an account.
You still receive those shoppers, on the order events. order.created and
checkout.completed both carry the enriched customer object, so a guest reaches your CRM
attached to the order that created them. Like order itself, that object is best-effort: an
event can arrive with orderId only, so fetch the order (which names its customer) when it
is missing.
Expect customer.created for a customer you already know. Two ordinary sequences
produce one: a guest who checked out months ago and now registers, and a merchant-created
customer who later signs up on the storefront with the same email. The customerId is
identical both times, and only the envelope id differs, so event-id deduplication will not
collapse them. Handle the event as an upsert keyed on customerId, never a blind
create. That is the one handler shape that is correct on every path above.
Note there is no GET /v1/customers list route to reconcile against, only
GET /v1/customers/{id} and GET /v1/customers/by-email, so if you need a full customer
export, take it from the dashboard.
Common use: pipe new customers into a CRM (HubSpot, Salesforce) or email tool (Mailchimp, Klaviyo).
Products
| Event | When it fires | data |
|---|---|---|
product.created | A product is created: the dashboard, POST /v1/products, bulk create, duplicate | productId, product |
product.updated | One product was edited through the product update (dashboard product page, PATCH /v1/products/{id}). Not sent by bulk price, status or description edits, variant edits, or catalog imports | productId, product |
product.deleted | One product was deleted. Not sent by a bulk delete | productId (no product) |
Common use: sync catalog changes into a marketplace listing (Amazon, eBay), or rebuild a static product index.
Inventory
| Event | When it fires | data |
|---|---|---|
inventory.updated | Stock changed for any reason (sale, restock, manual edit, import), for a product or one of its variants | productId, variantId (only on a variant change), inventory |
inventory.low | Stock fell to or below the low-stock threshold, and is still above zero. At most one delivery per product per 24 hours. See below | productId, available, threshold, inventory |
Common use: trigger restock automations, alert ops in Slack when a popular SKU is running low.
inventory.low is deduplicated for 24 hours, per product
Stronger suppression than "once per crossing", and worth designing around: after one
inventory.low fires for a product, no further inventory.low is delivered for that
product for 24 hours, no matter how many times stock crosses the threshold in between.
Sell down, restock, sell down again inside the same day and you receive exactly one event.
It is never sent when stock reaches zero. The check is skipped at available 0 or below, so a sale that sells the last unit fires no inventory.low; watch inventory.updated (its inventory.available) for out-of-stock. The event carries the available of the row that changed and fires for variant sales too, but it is per product: there is no variantId on it.
Consequences for your handler:
- Do not treat the event as an authoritative "is low right now" signal. Read
GET /v1/products/{id}/inventorywhen you act on it. - Do not build a low-stock dashboard purely from these events; they undercount by design.
inventory.updatedhas no such dedup, so use it if you need every movement.
How the threshold is resolved, in order:
- The product's own
lowStockThreshold, when set. - Otherwise the maximum
lowStockThresholdacross the store's sales channels. The maximum, not the minimum, so the most cautious channel wins and the alert fires earlier than a per-channel reading would suggest. - Otherwise, when the store has no sales channels at all, the platform default of
5(theLOW_STOCK_THRESHOLDsetting of the platform).
The resolved number is echoed back as threshold in the payload; read it there rather
than reimplementing this precedence.
Checkout
| Event | When it fires | data |
|---|---|---|
checkout.completed | An order was created. Fires together with order.created from every place that creates an order (including the dashboard and POST /v1/orders), so subscribe to one or the other, not both | orderId, order |
checkout.abandoned | A logged-in customer's cart sat idle for 60 minutes and was marked abandoned. Only for stores that turned on cart recovery (the option "Also recover abandoned carts (logged-in customers)") | cartId, abandonedAt (ISO 8601), customerId |
Common use: trigger abandonment-recovery email campaigns; pipe completed checkouts into analytics.
checkout.abandoned does not cover a checkout that was started and left. It is sent from the cart sweep, which looks at carts that never reached checkout, belong to a logged-in customer (so customerId is always set, never null), have at least one item, were last touched between 60 minutes and 48 hours ago, and sit in a store with cart recovery on and a storefront channel with a domain. The sweep runs every 10 minutes. A guest cart, or a cart whose owner already opened the checkout, produces no event.
checkout.abandoned can fire more than once for the same cartId. A shopper who comes back to an abandoned cart gets it back as an active cart (the platform restores it for 30 days), and if they go idle again the cart is marked abandoned a second time. Read the event as "this cart is idle right now", and make your handler idempotent on cartId, for example by skipping a cart you have already emailed. The platform's own recovery emails are not repeated this way: they stop being scheduled for a checkout once the first one has been sent.
Payments
| Event | When it fires | data |
|---|---|---|
payment.succeeded | A payment provider confirmed that funds were captured for a checkout | orderId only (no payment, no order) |
payment.failed | A payment provider rejected the transaction. No order exists yet | checkoutId, error (string). No payment |
payment.refunded | A refund reached a payment (Brainerce refund, or a provider-dashboard refund on a provider that reports refunds back, such as Stripe or PayPal) | paymentId, refundAmount (number, major unit), isFullRefund (boolean), payment |
Note that payment.succeeded and order.paid fire on similar events but at different layers:
payment.succeededis the payment provider confirming. Use it when you care about the payment record itself (chargebacks, settlement reconciliation).order.paidis the order state transitioning. Use it when you care about the order, for example to start fulfillment.
For most merchants, order.paid is the right choice. Subscribe to payment.* only when you're building payment-specific automation.
They are not interchangeable, and the gap matters. An order paid outside Brainerce, whether by cash on delivery, a bank transfer, a card machine, or an invoice settled by a business customer, fires order.paid but never fires payment.succeeded: no provider was involved, and no settlement will ever arrive to reconcile against. If you trigger fulfillment from payment.succeeded, every one of those orders will be silently skipped. Use order.paid.
Donations
⛔ A donation is not an order, so no order.* event ever fires for one. A donation has no line items, no fulfilment and no shipping, and it is reported separately from sales. If you subscribe to order.paid expecting to hear about gifts, you will hear nothing.
These two events are also how an organisation's own receipting system finds out a gift arrived. Brainerce issues tax receipts only through a connected receipt app (turned on by the merchant in the Donations page's Receipts tab). Without one, Brainerce issues no receipt and reports nothing to any tax authority, so a store with its own receipting system listens to donation.paid and donation.refunded instead. Receipt documents themselves fire no webhook.
| Event | When it fires | data |
|---|---|---|
donation.paid | The provider confirmed the charge. Fires for a one-off gift from a donation page and for each cycle of a recurring donation (standing order). | donationId, donation |
donation.refunded | A paid donation was refunded, in full or in part | donationId, donation |
The donation object carries exactly these fields: id, storeId, status (PENDING, PAID, FAILED or CANCELLED; a paid one is PAID), amount, feeCoverAmount, chargedAmount (the three money values are decimal strings with two decimals, in the store currency), currency, donorEmail, donorName, isAnonymous, tributeType (IN_HONOR, IN_MEMORY or null), tributeName, message, customerId (or null), recurringSubscriptionId, campaignId, fundId, dedicationLabel, receiptName, donorPhone, receiptTaxIdLast4, paidAt, createdAt and payment (id, status, amount, refundedAmount, currency; null when the donation never had a payment). Nothing else is sent: the full receipt ID number, the failure reason and the donor's management link are never in a webhook.
Read amount, not chargedAmount, for the gift. amount is what the donor gave and what a receipt is written for. chargedAmount adds the processing fee the donor volunteered to cover, which is not part of the gift. On a fee-covered donation the two differ.
There is no refunded status on a donation. A refunded gift stays PAID, and the refunded figure is on donation.payment.refundedAmount — one fact in one place, rather than a copy on the donation that can drift from the money it describes.
donorName is sent even on an anonymous gift. isAnonymous is a promise about public surfaces, and this payload goes to your own systems; the flag rides along so you can honour it where you display donors. A recurring cycle carries recurringSubscriptionId; a one-off gift has it as null.
campaignId names the donation campaign the gift was for (a fundraising drive such as a building fund, not an email campaign), and is null for a general gift. Use it to file the gift against the right appeal in your own systems.
fundId names the fund (designation) the gift is for: where the money goes, such as scholarships or the building, as opposed to the campaign that raised it. null when the gift has none. A gift to a campaign carries the campaign's default fund unless the donor chose another. dedicationLabel is the store's own dedication wording as the donor picked it ("For the recovery of"), a snapshot that a later rename does not change; tributeName is the honoree.
Receipt details for your receipting system. receiptName is who the receipt is made out to when it is not the donor (a spouse, a company), donorPhone the donor's phone, and receiptTaxIdLast4 the last four characters of the ID or company number the donor gave for a receipt. The full number is stored encrypted and never leaves the platform in a webhook. All three are null when the donor gave none.
Blog
| Event | When it fires | data |
|---|---|---|
blog.post.published | A post goes live, by manual publish, scheduled publish, or SEO Autopilot | postId, slug, title, salesChannelIds (array, empty means every channel), publishedAt (ISO 8601 or null) |
blog.post.updated | An already-published post's content changes | postId, slug, title, salesChannelIds, publishedAt (as above) |
This is the pair a statically-generated storefront needs. Subscribe to both and
revalidate on receipt: slug is enough to target the ISR path, and salesChannelIds
tells you which storefronts the post belongs to, so a multi-channel setup can skip
rebuilds it does not need.
These events are the reason SEO Autopilot content appears on a static storefront without a redeploy. Without a subscription, autopilot-published posts exist in Brainerce and are invisible on your site until the next build.
Two things these events do not tell you:
- Unpublishing is silent.
blog.post.updatedfires only while the post is stillPUBLISHED. Taking a post down emits nothing at all, so a cached storefront keeps serving a page the merchant has retired. If that matters, re-fetch the post on revalidation and 404 it when the API no longer returns it, rather than trusting an event to arrive. - A slug change arrives as the new slug only. Renaming a published post emits
blog.post.updatedcarrying the newslug; the old path is never named, so revalidating the payload's slug leaves the previous URL stale in your cache. (Brainerce records the redirect on its own side, but your ISR cache does not learn about it.)
Subscribing programmatically
You can manage subscriptions via the API in addition to the dashboard:
# Create
curl -X POST https://api.brainerce.com/api/stores/{storeId}/webhook-subscriptions \
-H "Authorization: Bearer brainerce_..." \
-H "Content-Type: application/json" \
-d '{
"url": "https://hooks.example.com/brainerce",
"events": ["order.created", "order.paid"],
"description": "Production order handler"
}'
# List
curl https://api.brainerce.com/api/stores/{storeId}/webhook-subscriptions \
-H "Authorization: Bearer brainerce_..."
# Update (e.g. add an event type)
curl -X PATCH https://api.brainerce.com/api/stores/{storeId}/webhook-subscriptions/{id} \
-H "Authorization: Bearer brainerce_..." \
-H "Content-Type: application/json" \
-d '{ "events": ["order.created", "order.paid", "order.refunded"] }'
# Rotate the signing secret
curl -X POST https://api.brainerce.com/api/stores/{storeId}/webhook-subscriptions/{id}/rotate-secret \
-H "Authorization: Bearer brainerce_..."The API key needs the webhooks:write scope (mapped from MANAGE_WEBHOOKS store permission) for write operations, webhooks:read for reads.
Every route lives under /api/stores/{storeId}/webhook-subscriptions:
| Route | Scope | Purpose |
|---|---|---|
GET /event-catalog | read | The subscribable event types with one-line descriptions ({ events: [{ type, description }] }). The same list as this page. |
GET /health | read | Store-wide snapshot: total, active and circuit-open counts, 24 h success rate and response time, 7-day trend, the 10 most recent failures. |
GET /{id}/deliveries | read | The endpoint's event log. Filter with ?outcome=all|succeeded|failed|missed. |
POST /{id}/test | write | Send a test event (evt_test_*), 10 per hour per subscription. An endpoint with custom headers or a body template needs confirmLiveCall: true. |
POST /{id}/preview | read | Render the body, destination and header names for a stored real event without sending anything. |
POST /{id}/redeliver | write | Resend 1 to 25 chosen log rows ({ "deliveryIds": [...] }). 30 calls per hour. |
POST /{id}/replay-missed | write | Resend the events recorded while the circuit was open, oldest first, at most 25 per call. 3 calls per hour. |
DELETE /{id} | write | Delete the subscription. |
Custom headers, the body template and resending are explained in Webhooks: resending events, custom headers and body template.
Merchant Integration
Receive real-time POSTs from your Brainerce store when orders, customers, inventory, or payments change, resend events that were missed, and shape the request for a third-party API.
Verifying Signatures
Copy-paste HMAC-SHA256 verification snippets in TypeScript, Python, PHP, Ruby, and Go.