Integrations & API
Webhooks and API keys
Receive events in your own system with webhooks, and read and write data with an API key: format, signature, retries, operations and limits.
Webhooks send events to your system. API keys let your system read and write data.
Where: Settings › Developer › Webhooks and Settings › Developer › API keys. Creating, changing and deleting need the owner or admin role.
Webhooks
Add an endpoint
- Open Settings › Developer › Webhooks.
- Press New endpoint.
- Fill in Endpoint URL (https only) and, if you want, Description (optional).
- Press Create endpoint.
- Choose events under Subscribed events on the endpoint's card. No choice means every event.
- Press Show next to Signing secret and copy the secret to your receiver.
BunStack now sends the chosen events to the address.
- Pause stops the endpoint and Resume starts it again.
- Rotate secret replaces the secret. BunStack signs everything sent after the rotation with the new secret, retries included. Update your receiver first.
Warning: While an endpoint is paused, BunStack queues nothing for it. It never sends the events from the pause, not even when you press Resume. It marks a delivery already in the queue as failed.
A delivery that failed because of the pause does not count against the endpoint's health.
What BunStack sends
Every delivery is a POST with a JSON body and these headers:
| Header | Content |
|---|---|
x-bunstack-signature | HMAC-SHA256 of the body, with the endpoint's secret, as lowercase hex |
x-bunstack-event | The event type, for example order.paid |
x-bunstack-delivery-id | The id of this delivery |
x-bunstack-attempt | The attempt's number, from 1 |
The body is the event's payload plus the key delivery_id, with the same value as the header. Deduplicate on delivery_id in the body. The body is signed. The header is not.
Payload
Every body has the same five keys:
| Key | Content |
|---|---|
event | The event type, the same as the x-bunstack-event header |
tenant_id | Your workspace's id |
sent_at | When the event was queued. A retry an hour later has the same value. |
data | The event's own fields, per the table below |
delivery_id | The id to deduplicate on |
The keys in data per event:
| Event | data |
|---|---|
order.paid, order.fulfilled, order.refunded, order.cancelled | order_id, order_number, storefront_id, currency, total_amount, status, payment_status, fulfillment_status, refunded_amount |
contact.received | message_id, storefront_id, email, name, subject |
member.signup, event.rsvp | member_id, storefront_id, email, tier_id and rsvp_id, event_id, email, attendees, status respectively. The Members and Events modules are parked. You cannot choose these events on an endpoint. |
When the order events are sent
- BunStack sends
order.paidwhen the order is ready. For most orders that is when the order is created. - An order held by Kustom's fraud check gets
order.paidonly once Kustom has accepted it. - An order Kustom rejected or cancelled never gets
order.paid. - BunStack sends
order.cancelledonce when the order or its reservation is cancelled. That happens whenstatusorpayment_statusbecomescancelled. - A cancellation that books a refund also sends
order.refunded. - An endpoint without a choice gets every event,
order.cancelledincluded. An endpoint with a choice getsorder.cancelledonly once you choose it. - BunStack does not see a cancellation in Kustom's portal of an order Kustom had already accepted. It sends no event.
The contract changed on 12 September 2026 (2026-09-12). Before that, BunStack sent order.paid when it created the order, even for a held order, and order.cancelled did not exist.
Check the signature
The signature is HMAC-SHA256 over the raw body. The key is the secret's characters. Use the secret exactly as shown, as an ASCII string. Do not hex-decode it.
- Read the body's raw bytes before you parse JSON.
- Compute the signature. In Node:
const expected = crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
- Compare
expectedwithx-bunstack-signatureusing a constant-time comparison.
Only the body is signed. Headers are not.
Duplicates
Delivery is at least once (at-least-once). BunStack can deliver the same event several times. Retries can repeat a delivery that already arrived.
delivery_idis the same on every attempt of one event. Two different events never share an id.- Store the ids you have processed. Skip an id you have seen before.
- Do not deduplicate on the content. Two real orders can look identical, and the same order sent twice has the same body.
x-bunstack-attemptis for your logs. Attempt 2 or higher means the previous attempt did not get a2xx.
Answer 2xx once you have stored the event. Do the work afterwards. An answer that is not 2xx, or that takes more than 8 seconds, counts as a failed attempt.
Retries
| Attempt | When |
|---|---|
| 1 | At once |
| 2 | 1 minute after attempt 1 |
| 3 | 5 minutes after attempt 2 |
| 4 | 15 minutes after attempt 3 |
| 5 | 1 hour after attempt 4 |
The last attempt comes 81 minutes after the first. After the last attempt BunStack marks the delivery failed. It does not retry it again, and the delivery stays in the list.
Inspect a delivery
Choose an endpoint and open a delivery. You see the status, the response code, the number of attempts, the delivery's id and the body exactly as it was sent and signed. Use that body when you test your signature check.
API keys
Under Settings › Developer › API keys you create keys with the prefix bunstack_. The key is shown once and stored hashed. A key carries scopes (read:products, read:orders, read:customers, read:reviews, write:products, write:orders, write:reviews). You can revoke it at any time with Revoke. The address, request bodies and error codes are on the reference page under View docs.
Create a key
- Open Settings › Developer › API keys.
- Press New key.
- Fill in Name and choose Scopes.
- Choose a date under Expires (optional) if the key is to stop working.
- Press Create key.
- Copy the key. BunStack does not show it again.
A key with an expiry date works through the end of that day, Swedish time. Writes made with a key are logged in Activity log under the key's name. Reads are not logged.
Operations
Every call is a POST with Authorization: Bearer bunstack_… and a JSON body with op.
| Operation | Scope | Does |
|---|---|---|
list_products | read:products | Lists products, newest first |
list_orders | read:orders | Lists orders in every storefront, newest first |
get_order | read:orders | Fetches one order with its lines |
list_customers | read:customers | Lists customers, newest first |
list_reviews | read:reviews | Lists reviews, newest first |
create_product | write:products | Creates a product in the catalogue |
update_product | write:products | Changes a product |
update_order_status | write:orders | Sets the order's status |
update_review_status | write:reviews | Approves or rejects a review |
create_product and update_product take short_description, the text by the price on the product page, as plain text. It may be at most 600 characters. A longer text answers 400 with invalid_value and field: "short_description". list_products returns it.
A list gives at most 500 rows per page. When the page is full, the answer contains next_before. Send the value as before for the next page.
Order status through the API
update_order_statusanswers409for the statusescancelledandrefunded, both to and from, for every order. Cancel and refund in the admin.- Status
fulfilledanswers409for an order with a hold (Needs attention). - Status
fulfilledanswers409for a Kustom order whose payment is reserved but not captured. - The ProSmart flags
prosmart_partial,prosmart_refundandprosmart_customer_missingdo not count as holds. - An order picked up in store answers
409 store_pickup. Register the handover with Picked up in the admin. It captures the payment and marks the order picked up.
Rate limits
| Limit | Value |
|---|---|
| Calls per key | 600 per minute |
| Writes per key | 60 per minute, within the 600 |
| Calls with a key that does not exist | 60 per minute per IP address |
At a limit, the API answers 429 with { "error": "rate_limited", "retry_after": 60 } and the header Retry-After: 60. Wait out the minute before you try again.