Webhook endpoints
The addresses the shop sends its events to: orders, products, customers, quotations and enquiries.
Reach them through admin.webhooks. Reading needs the webhooks:read scope, changing them webhooks:write. An endpoint takes only events your key may read: order events need orders:read, product events and catalog.changed products:read, customer events customers:read, and quotation and enquiry events quotations:read. An app sees and manages only the endpoints it made, which are deleted when the merchant disconnects it. Check each delivery with verifyWebhookSignature.
List webhook endpoints
admin.webhooks.list(query?: ListQuery): Promise<CursorPage<WebhookEndpoint>>
One page of the endpoints your key may see, newest first, or oldest change first with updatedSince. Read the next page with meta.nextCursor as cursor. Scope: webhooks:read.
Parameters
| Parameter |
Type |
|
query |
object |
The query fields below, by name. |
Query fields
| Parameter | Type | Description |
|---|
limit | integer | At least 1. At most 100. Default 50. |
cursor | string | meta.nextCursor (or the cursor in links.next) of the previous page. At most 1000 characters. |
updatedSince | string (date-time) | Records changed at or after this time (ISO 8601, any offset or Z), oldest change first, for incremental sync. |
Returns
A CursorPage<WebhookEndpoint>:
| Field | Type | Description |
|---|
links | object | |
links.first | null | |
links.last | null | |
links.prev | string (uri) or null | |
links.next | string (uri) or null | |
meta | object | |
meta.path | string (uri) | |
meta.perPage | integer | |
meta.nextCursor | string or null | |
meta.prevCursor | string or null | |
data | array of WebhookEndpoint | |
data[].id | integer | |
data[].url | string (uri) | |
data[].events | array of string | |
data[].active | boolean | |
data[].disabledAt | string (date-time) or null | When it was turned off after 20 failed deliveries in a row. |
data[].createdAt | string (date-time) or null | |
data[].updatedAt | string (date-time) or null | |
Errors
| Status | When |
|---|
401 | invalid_api_key: no active Admin key in Authorization: Bearer. |
403 | admin_key_in_browser (an Origin header was sent), ip_not_allowed, plan_required(the shop's plan lacks the Integration API, or the feature the endpoint needs), orinsufficient_scope. |
422 | Validation errors ({ message, errors }), or an idempotency refusal(idempotency_key_reused, idempotency_key_invalid). |
429 | Rate limited. |
Example
const { data: endpoints } = await admin.webhooks.list();
Iterate over every endpoint
admin.webhooks.all(query?: ListQuery): AsyncGenerator<WebhookEndpoint, void, undefined>
Every endpoint matching the query, one at a time, fetching the next page as you read. It takes the same query as list, without cursor. Scope: webhooks:read.
Returns
An async iterable of WebhookEndpoint, each:
| Field | Type | Description |
|---|
[].id | integer | |
[].url | string (uri) | |
[].events | array of string | |
[].active | boolean | |
[].disabledAt | string (date-time) or null | When it was turned off after 20 failed deliveries in a row. |
[].createdAt | string (date-time) or null | |
[].updatedAt | string (date-time) or null | |
Example
for await (const endpoint of admin.webhooks.all()) {
if (!endpoint.active) {
await admin.webhooks.update(endpoint.id, { active: true });
}
}
Get a webhook endpoint
admin.webhooks.get(id: number): Promise<WebhookEndpoint>
One endpoint by its id. Scope: webhooks:read.
Parameters
| Parameter |
Type |
|
id |
number |
The endpoint's id. |
Returns
A WebhookEndpoint:
| Field | Type | Description |
|---|
id | integer | |
url | string (uri) | |
events | array of string | |
active | boolean | |
disabledAt | string (date-time) or null | When it was turned off after 20 failed deliveries in a row. |
createdAt | string (date-time) or null | |
updatedAt | string (date-time) or null | |
Errors
| Status | When |
|---|
401 | invalid_api_key: no active Admin key in Authorization: Bearer. |
403 | admin_key_in_browser (an Origin header was sent), ip_not_allowed, plan_required(the shop's plan lacks the Integration API, or the feature the endpoint needs), orinsufficient_scope. |
404 | not_found: the shop has no such record. |
429 | Rate limited. |
Add a webhook endpoint
admin.webhooks.create(input: WebhookEndpointInput, options?: RequestOptions): Promise<CreatedWebhookEndpoint>
A new endpoint, which starts on. Its whsec_… signing secret is returned in secret, only this once: keep it to verify deliveries. A shop has at most 10 endpoints (422 limit_reached). Scope: webhooks:write, plus the read scope of each event.
Parameters
| Parameter |
Type |
|
input |
object |
The endpoint's fields, below. |
options |
RequestOptions |
idempotencyKey, signal and headers (see retries). |
Request fields
| Field | Type | Description |
|---|
url required | string (uri) | An https address on the public internet. At most 500 characters. |
events required | array of string | |
active | boolean | Validated but ignored when creating; a new endpoint is on. |
Returns
A CreatedWebhookEndpoint: the endpoint in data, and its secret:
| Field | Type | Description |
|---|
data | WebhookEndpoint | |
data.id | integer | |
data.url | string (uri) | |
data.events | array of string | |
data.active | boolean | |
data.disabledAt | string (date-time) or null | When it was turned off after 20 failed deliveries in a row. |
data.createdAt | string (date-time) or null | |
data.updatedAt | string (date-time) or null | |
secret | string | The whsec_… signing secret. |
Errors
| Status | When |
|---|
401 | invalid_api_key: no active Admin key in Authorization: Bearer. |
403 | admin_key_in_browser (an Origin header was sent), ip_not_allowed, plan_required(the shop's plan lacks the Integration API, or the feature the endpoint needs), orinsufficient_scope. |
409 | idempotency_in_progress: a request with this Idempotency-Key is still running. Retry with the same key after Retry-After seconds; the first answer is then replayed. |
422 | Validation errors, limit_reached, or an idempotency refusal. |
429 | Rate limited. |
Example
const { data: endpoint, secret } = await admin.webhooks.create({
url: 'https://erp.example.com/storehugs/webhooks',
events: ['order.created', 'order.status_changed', 'product.stock_changed'],
});
// Keep secret to verify deliveries.
Update a webhook endpoint
admin.webhooks.update(id: number, changes: Partial<WebhookEndpointInput>, options?: UpdateOptions): Promise<WebhookEndpoint>
Changes the address, the events or whether the endpoint is on; only what is sent changes. Turning it on resets its failure count. A new address, new events or turning it on needs the read scope of every event it then takes (403 insufficient_scope). Scope: webhooks:write.
Parameters
| Parameter |
Type |
|
id |
number |
The endpoint's id. |
changes |
object |
The fields to change, below. |
options |
UpdateOptions |
unmodifiedSince (the updatedAt you read; a newer endpoint is refused with 409 edit_conflict) and idempotencyKey. |
Request fields
| Field | Type | Description |
|---|
url | string (uri) | An https address on the public internet. At most 500 characters. |
events | array of string | |
active | boolean | |
updatedAt | string (date-time) or null | The record's updatedAt as you read it; a newer record gets 409 edit_conflict. |
Returns
The changed WebhookEndpoint:
| Field | Type | Description |
|---|
id | integer | |
url | string (uri) | |
events | array of string | |
active | boolean | |
disabledAt | string (date-time) or null | When it was turned off after 20 failed deliveries in a row. |
createdAt | string (date-time) or null | |
updatedAt | string (date-time) or null | |
Errors
| Status | When |
|---|
401 | invalid_api_key: no active Admin key in Authorization: Bearer. |
403 | admin_key_in_browser (an Origin header was sent), ip_not_allowed, plan_required(the shop's plan lacks the Integration API, or the feature the endpoint needs), orinsufficient_scope. |
404 | not_found: the shop has no such record. |
409 | edit_conflict (the record changed after If-Unmodified-Since or updatedAt), or idempotency_in_progress. |
422 | Validation errors ({ message, errors }), or an idempotency refusal(idempotency_key_reused, idempotency_key_invalid). |
429 | Rate limited. |
Example
const endpoint = await admin.webhooks.get(2);
await admin.webhooks.update(
2,
{ events: [...endpoint.events, 'order.refunded'] },
{ unmodifiedSince: endpoint.updatedAt ?? undefined },
);
Delete a webhook endpoint
admin.webhooks.delete(id: number): Promise<void>
Deletes an endpoint. Scope: webhooks:write.
Parameters
| Parameter |
Type |
|
id |
number |
The endpoint's id. |
Returns
Nothing.
Errors
| Status | When |
|---|
401 | invalid_api_key: no active Admin key in Authorization: Bearer. |
403 | admin_key_in_browser (an Origin header was sent), ip_not_allowed, plan_required(the shop's plan lacks the Integration API, or the feature the endpoint needs), orinsufficient_scope. |
404 | not_found: the shop has no such record. |
409 | idempotency_in_progress: a request with this Idempotency-Key is still running. Retry with the same key after Retry-After seconds; the first answer is then replayed. |
422 | Validation errors ({ message, errors }), or an idempotency refusal(idempotency_key_reused, idempotency_key_invalid). |
429 | Rate limited. |