Create a webhook endpoint
Register a new HTTPS endpoint to receive outbound event notifications.
The signing_secret is returned once at creation — store it
immediately. It cannot be retrieved again; rotate it via a DELETE +
re-create if lost.
Failed deliveries are retried automatically with exponential backoff. If an
endpoint fails 20 deliveries in a row (each after exhausting its retries),
it is automatically disabled (enabled: false) and stamped with a
disabled_reason. Re-enable it with a PATCH once the endpoint is healthy
again; re-enabling resets the failure counter.
An endpoint belongs to the environment of the key that created it
(environment): an endpoint created with a test key receives only sandbox
events, and one created with a live key receives only production events.
Endpoints are listed, read, updated, deleted and tested only with a key of
the same environment; with a key of the other environment they return 404.
Delivery follows redirects, and each redirect target is held to the same rules as the registered URL (HTTPS, public address). A redirect to a disallowed target fails the delivery.
curl --request POST \
--url https://api.tybritelabs.com/v1/webhook_endpoints \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"url": "https://yourapp.com/webhooks/tybrite",
"events": [
"order.paid",
"order.fulfilled",
"payment.succeeded"
],
"enabled": true,
"metadata": {
"label": "production-erp",
"owner": "platform-team"
}
}
'import requests
url = "https://api.tybritelabs.com/v1/webhook_endpoints"
payload = {
"url": "https://yourapp.com/webhooks/tybrite",
"events": ["order.paid", "order.fulfilled", "payment.succeeded"],
"enabled": True,
"metadata": {
"label": "production-erp",
"owner": "platform-team"
}
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
url: 'https://yourapp.com/webhooks/tybrite',
events: ['order.paid', 'order.fulfilled', 'payment.succeeded'],
enabled: true,
metadata: {label: 'production-erp', owner: 'platform-team'}
})
};
fetch('https://api.tybritelabs.com/v1/webhook_endpoints', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.tybritelabs.com/v1/webhook_endpoints",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'url' => 'https://yourapp.com/webhooks/tybrite',
'events' => [
'order.paid',
'order.fulfilled',
'payment.succeeded'
],
'enabled' => true,
'metadata' => [
'label' => 'production-erp',
'owner' => 'platform-team'
]
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.tybritelabs.com/v1/webhook_endpoints"
payload := strings.NewReader("{\n \"url\": \"https://yourapp.com/webhooks/tybrite\",\n \"events\": [\n \"order.paid\",\n \"order.fulfilled\",\n \"payment.succeeded\"\n ],\n \"enabled\": true,\n \"metadata\": {\n \"label\": \"production-erp\",\n \"owner\": \"platform-team\"\n }\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.tybritelabs.com/v1/webhook_endpoints")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"url\": \"https://yourapp.com/webhooks/tybrite\",\n \"events\": [\n \"order.paid\",\n \"order.fulfilled\",\n \"payment.succeeded\"\n ],\n \"enabled\": true,\n \"metadata\": {\n \"label\": \"production-erp\",\n \"owner\": \"platform-team\"\n }\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.tybritelabs.com/v1/webhook_endpoints")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"url\": \"https://yourapp.com/webhooks/tybrite\",\n \"events\": [\n \"order.paid\",\n \"order.fulfilled\",\n \"payment.succeeded\"\n ],\n \"enabled\": true,\n \"metadata\": {\n \"label\": \"production-erp\",\n \"owner\": \"platform-team\"\n }\n}"
response = http.request(request)
puts response.read_body{
"webhook_endpoint": {
"id": "ac6737d6-12b9-446d-a6c4-1077493f498f",
"url": "https://yourapp.com/webhooks/tybrite",
"events": [
"order.paid",
"order.fulfilled"
],
"enabled": true,
"metadata": {
"label": "production-erp"
},
"environment": "production",
"created_at": "2026-06-20T13:39:11.631+00:00",
"updated_at": "2026-06-20T13:39:11.631+00:00",
"signing_secret": "68c216748eb34be5adf93e39cda61386337883e7420c4a6d1994eb11346bce20"
}
}{
"error": {
"code": "invalid_request",
"message": "Invalid request parameters",
"details": "The 'email' field is required"
}
}{
"error": {
"code": "unauthorized",
"message": "Missing or invalid Authorization header"
}
}{
"error": {
"code": "forbidden",
"message": "This operation requires a secret key. Publishable keys cover catalogue reads and the actions a shopper takes for themselves."
}
}{
"error": {
"code": "rate_limited",
"message": "Too many requests for this API key. Please slow down and try again shortly.",
"remaining": 0,
"reset": 1706198400
}
}{
"error": {
"code": "server_error",
"message": "An unexpected error occurred"
}
}Authorizations
API Key Authentication
Use your API key in the Authorization header:
Authorization: Bearer tybrite_sk_live_YOUR_KEY
Key Types:
Secret Keys (Server-Side Only):
- Format:
tybrite_sk_live_*(production) ortybrite_sk_test_*(sandbox) - Full read/write access to all endpoints
- ⚠️ NEVER expose in client-side code or public repositories
- Required for: every write a shopper does not make for themselves, authentication, payment verification, AI recommendations
Publishable Keys (Client-Safe):
- Format:
tybrite_pk_live_*(production) ortybrite_pk_test_*(sandbox) - Catalogue reads, plus the actions a shopper takes for themselves
- ✅ Safe for client-side JavaScript, mobile apps, and public code
- Allowed for: browsing products, search, CMS content, pricing queries, cart and wishlist, reviews, storefront events, and the Agent API's quotes, cart drafts and checkout intents
Endpoint-Specific Requirements:
- Authentication endpoints (
/v1/auth/*): Secret key required - Payment verification (
POST /v1/payments/verify): Secret key required - AI Recommendations (
POST /v1/recommendations): Secret key required - Semantic Search (
POST /v1/search): Both key types allowed (read-only operation) - Shopper actions (cart, wishlist, reviews, storefront events, Agent API quotes, cart drafts and checkout intents): Both key types allowed
- All other write operations: Secret key required
- All read operations: Both key types allowed
Using a publishable key for restricted operations returns 403 Forbidden.
Body
HTTPS destination URL. HTTP is rejected.
"https://yourapp.com/webhooks/tybrite"
Array of event types to subscribe to. Use ["*"] for all events.
Order lifecycle: order.created, order.paid, order.fulfilled,
order.shipped, order.cancelled, order.refunded, order.updated
In-store sales: sale.completed, sale.refunded — a sale rung up at the
point of sale rather than online. Subscribe to these alongside the order events
to receive every sale a store makes, whichever channel it came through.
Your connection: connection.revoked — the key pair your application holds
for this store was revoked, rotated, deactivated or expired. Only reaches
applications connected through Login with GC, and carries pair_id so you revoke
the affected connection rather than every one you hold for the store.
Payment lifecycle: payment.succeeded, payment.failed, payment.refunded
Agent checkouts: agent.checkout_intent.created,
agent.checkout_intent.confirmed, agent.checkout_intent.expired — a checkout
an assistant prepared through the Agent API, the shopper's confirmation of it
(the order now exists), and one that lapsed unconfirmed. agent.mandate.triggered
— a shopper's standing instruction found its condition met and a checkout was
prepared; the payload carries confirmation_url for the shopper to confirm it.
Customer lifecycle: customer.created, customer.updated, customer.deleted
Inventory & catalog: product.created, product.updated,
product.stock_low, product.out_of_stock
Cart & checkout: cart.created, cart.updated, cart.abandoned, checkout.abandoned, wishlist.added
Gift cards: gift_card.issued, gift_card.redeemed, gift_card.expired
Promotions: promotion.applied (a promotion was applied to a checkout),
promotion.created, promotion.activated (a promotion went live or was
scheduled to), promotion.deactivated (a live promotion ended, was paused, or
expired). A promotion awaiting approval is not yet a promotion: no event fires
while it is a draft, and promotion.created arrives when it is approved.
Pricing: pricing_rule.created, pricing_rule.activated (a dynamic-pricing
rule went live), pricing_rule.updated (a live rule's discount, scope, priority,
or window changed — the prices it produces are now different), and
pricing_rule.deactivated. Like promotions, a rule awaiting approval emits
nothing until it goes live. Subscribe to these if you render prices and need to
re-read when the rules behind them change.
Content & collections: collection.created, collection.updated
(a product collection was added, its homepage placement / banner changed, or
its membership changed — products added, removed, or re-ordered). A membership
change carries members_changed: true, and fires once per collection per change,
not once per product.
post.published, lookbook.published, review.approved (a review passed
moderation and is now visible)
Deletion & restore: product.deleted, product_variant.deleted,
promotion.deleted, collection.deleted, post.deleted, lookbook.deleted,
pricing_rule.deleted, customer.deleted, subcategory.deleted — the record
has left the API. It no longer appears in any list, cannot be fetched by id, and
stops taking effect (a deleted promotion stops discounting; a deleted product
cannot be bought). Each carries the record's id and name, plus deleted_at
and restorable_until.
Deleting is reversible for 90 days, so every one of these has a matching
.restored event (product.restored, promotion.restored, and so on) that
fires when the merchant brings the record back. restorable_until tells you how
long you have: you may tear down your copy immediately and rebuild it on
.restored, or hold it until the window closes.
Deleting a product also deletes its variants, so a product.deleted is followed
by a product_variant.deleted for each one. Restoring the product restores them
together.
Visibility: category.activated, category.deactivated,
subcategory.activated, subcategory.deactivated. A category that is switched
off is filtered out of every catalog read, so it disappears from your storefront
exactly as a deleted one would — these events tell you when that happens.
Categories cannot be deleted (they are a fixed platform set a merchant enables or
disables), so this is the only lifecycle signal they produce. Subscribe if you
render navigation, a category strip, or any layout built from the taxonomy.
Store lifecycle & configuration: store.updated (the store's name,
logo, branding, contact, base currency, or the shopping assistant's name or
logo changed — carries a changed_fields list, which names
agent_assistant_name and agent_assistant_logo_url for the assistant),
payment_provider.connected (a payment provider was
connected), shipping_provider.connected (a shipping-rate provider was
connected), channel.connected (a sales channel was connected)
Store profile & policies: store.profile_updated (the store's support
contact details, addresses, legal identity or social profiles changed — carries
changed_fields; read the new values from GET /v1/store/profile),
store.policy_published (a legal document was published or republished —
carries slug, title, version, effective_date, published_at and
external_url), store.policy_unpublished (a document was taken down and
GET /v1/store/policies/{slug} now returns 404 — carries slug, title,
version and unpublished_at)
Design system: store.design_system_updated (the store's design system
changed, or was switched on or off — carries store_id, changed_fields and
updated_at; read the new values from store.brand in GET /v1/store/info)
Feature availability: feature.status_changed — a capability's
feature_status in GET /v1/store/info changed, e.g. the store publishes its
first post and cms becomes available. Carries feature,
status, and previous_status, each status one of available,
awaiting_data, not_in_plan or unavailable. feature includes
agentic_commerce and agent_hosted_helpers, so a shopping assistant can
follow when the hosted helper tools become available or unavailable. Use this
to light up a storefront surface the moment its data exists. See the
automation-webhooks guide.
Catalog sync & syndication: feed.sync.completed (a scheduled
inbound feed-pull finished — carries created/updated/failed counts),
channel.sync.completed (a sales-channel push to Google/Meta/… finished —
carries pushed/rejected counts)
Wholesale (B2B): for stores with wholesale enabled — the trade itself:
b2b.rfq.created, b2b.quote.sent, b2b.quote.accepted, b2b.quote.rejected,
b2b.po.issued, b2b.po.confirmed, b2b.po.fulfilled,
b2b.invoice.issued, b2b.invoice.partially_paid (a payment arrived and a
balance remains — fires for each such payment), b2b.invoice.paid,
b2b.invoice.overdue
Wholesale credit and approvals: b2b.credit.threshold_reached and
b2b.credit.exhausted fire when a buyer crosses a share of their credit limit,
and b2b.buyer.on_hold / b2b.buyer.released when they are stopped or let go
again. Each fires on the CROSSING, not the level, so a buyer who stays above a
threshold produces one event rather than one per order. b2b.approval.requested,
b2b.approval.approved and b2b.approval.rejected follow an order the buying
organisation routes for internal sign-off before it reaches the supplier.
Wholesale replenishment: b2b.replenishment.needed fires when stock falls to
its reorder point, and is the one wholesale event that fires in the BUYER's own
store about their own stock rather than in the supplier's. It carries the supplier
that line was last bought from, so an automation can raise the reorder rather than
only report the need.
The store-lifecycle, content, and feature-availability events are designed for automation tools that keep a storefront in step with the store as it grows — the moment a promotion goes live, a collection becomes homepage-eligible, or a licensed capability gains its first data.
[ "order.paid", "order.fulfilled", "payment.succeeded" ]
Whether the endpoint is active. Disabled endpoints are skipped on delivery.
Opaque key-value pairs for your own reference (not sent in deliveries).
{ "label": "production-erp", "owner": "platform-team" }
Response
Endpoint created
Show child attributes
Show child attributes
curl --request POST \
--url https://api.tybritelabs.com/v1/webhook_endpoints \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"url": "https://yourapp.com/webhooks/tybrite",
"events": [
"order.paid",
"order.fulfilled",
"payment.succeeded"
],
"enabled": true,
"metadata": {
"label": "production-erp",
"owner": "platform-team"
}
}
'import requests
url = "https://api.tybritelabs.com/v1/webhook_endpoints"
payload = {
"url": "https://yourapp.com/webhooks/tybrite",
"events": ["order.paid", "order.fulfilled", "payment.succeeded"],
"enabled": True,
"metadata": {
"label": "production-erp",
"owner": "platform-team"
}
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({
url: 'https://yourapp.com/webhooks/tybrite',
events: ['order.paid', 'order.fulfilled', 'payment.succeeded'],
enabled: true,
metadata: {label: 'production-erp', owner: 'platform-team'}
})
};
fetch('https://api.tybritelabs.com/v1/webhook_endpoints', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.tybritelabs.com/v1/webhook_endpoints",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'url' => 'https://yourapp.com/webhooks/tybrite',
'events' => [
'order.paid',
'order.fulfilled',
'payment.succeeded'
],
'enabled' => true,
'metadata' => [
'label' => 'production-erp',
'owner' => 'platform-team'
]
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api.tybritelabs.com/v1/webhook_endpoints"
payload := strings.NewReader("{\n \"url\": \"https://yourapp.com/webhooks/tybrite\",\n \"events\": [\n \"order.paid\",\n \"order.fulfilled\",\n \"payment.succeeded\"\n ],\n \"enabled\": true,\n \"metadata\": {\n \"label\": \"production-erp\",\n \"owner\": \"platform-team\"\n }\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api.tybritelabs.com/v1/webhook_endpoints")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"url\": \"https://yourapp.com/webhooks/tybrite\",\n \"events\": [\n \"order.paid\",\n \"order.fulfilled\",\n \"payment.succeeded\"\n ],\n \"enabled\": true,\n \"metadata\": {\n \"label\": \"production-erp\",\n \"owner\": \"platform-team\"\n }\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.tybritelabs.com/v1/webhook_endpoints")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"url\": \"https://yourapp.com/webhooks/tybrite\",\n \"events\": [\n \"order.paid\",\n \"order.fulfilled\",\n \"payment.succeeded\"\n ],\n \"enabled\": true,\n \"metadata\": {\n \"label\": \"production-erp\",\n \"owner\": \"platform-team\"\n }\n}"
response = http.request(request)
puts response.read_body{
"webhook_endpoint": {
"id": "ac6737d6-12b9-446d-a6c4-1077493f498f",
"url": "https://yourapp.com/webhooks/tybrite",
"events": [
"order.paid",
"order.fulfilled"
],
"enabled": true,
"metadata": {
"label": "production-erp"
},
"environment": "production",
"created_at": "2026-06-20T13:39:11.631+00:00",
"updated_at": "2026-06-20T13:39:11.631+00:00",
"signing_secret": "68c216748eb34be5adf93e39cda61386337883e7420c4a6d1994eb11346bce20"
}
}{
"error": {
"code": "invalid_request",
"message": "Invalid request parameters",
"details": "The 'email' field is required"
}
}{
"error": {
"code": "unauthorized",
"message": "Missing or invalid Authorization header"
}
}{
"error": {
"code": "forbidden",
"message": "This operation requires a secret key. Publishable keys cover catalogue reads and the actions a shopper takes for themselves."
}
}{
"error": {
"code": "rate_limited",
"message": "Too many requests for this API key. Please slow down and try again shortly.",
"remaining": 0,
"reset": 1706198400
}
}{
"error": {
"code": "server_error",
"message": "An unexpected error occurred"
}
}
