Warehouse / Guides

Event subscriptions

Receive a signed webhook when an order, shipment or other resource changes.

Choose the delivery scope

Manage subscriptions through this schema's GraphQL endpoint. Subscriptions and stored queries inherit its warehouse scope; they do not receive an warehouseId variable.

https://api.pack.cloud/v1/warehouses/{warehouseId}/graphql

The Organization and Warehouse APIs use the same management operation names. Their event topics, server variables, and projection schema differ. Topic, action, and trigger values are case-sensitive.

TopicActionsServer variable
Shipmentcreate, updateshipmentId
Fulfillmentcreate, updatefulfillmentId
Picklistcreate, updatepicklistId
GoodsReceiptcreate, updategoodsReceiptId
ReturnReceiptcreate, updatereturnReceiptId
StockMutationcreatestockMutationId

Create a subscription

Create requires a unique key, a supported topic, at least one action, and an HTTPS uri. Actions that include update also require at least one trigger. Delivery is enabled by default when enabled is omitted. The key cannot be changed later.

Destinations must use HTTPS. PackCloud rejects URLs containing credentials or fragments, as well as private or otherwise unsafe destinations.

mutation CreateSubscription($input: EventSubscriptionCreateInput!) {
eventSubscriptionCreate(input: $input) {
eventSubscription {
id
key
topic
actions
triggers
uri
enabled
status
}
secret
}
}
Variables
{
"input": {
"key": "fulfillment-updates",
"topic": "Fulfillment",
"actions": ["create", "update"],
"triggers": ["fulfillment.status", "fulfillment.lines.quantity"],
"uri": "https://events.example.com/packcloud/fulfillments",
"query": "query FulfillmentEvent($fulfillmentId: ID!) { fulfillment(id: $fulfillmentId) { id reference status } }",
"queryFilter": "fulfillment.reference:*",
"enabled": true
}
}

PackCloud generates the signing secret. It is returned only by create and rotation and cannot be queried later. Store it immediately; rotate it if it is lost.

The eventSubscriptionCreate reference lists every input field and the full response.

Select update triggers

Triggers apply only to the update action: a subscription whose actions include update requires at least one trigger, and a create-only subscription may not have any. An update is delivered when at least one changed trigger matches your list. Create events are never filtered by triggers.

Relation-only paths such as fulfillment.lines select membership changes. More specific paths such as fulfillment.lines.quantity select changes to that field.

Every prefix with deeper triggers also has a wildcard form, such as fulfillment.*, fulfillment.lines.* and fulfillment.allocations.*. A wildcard selects every change beneath that prefix, and on a relation it also covers that relation gaining or losing a record, so fulfillment.lines.* covers a line being added or removed as well as any of a line's fields changing. Use fulfillment.* to follow every update on the topic, and a wildcard keeps selecting fields that are added to the catalog later. A relation without deeper triggers, such as fulfillment.events, has no wildcard form.

Paths ending in .events select new history entries on the resource.

Shipment update triggers
Wildcards
  • shipment.*
  • shipment.brand.*
  • shipment.carrier.*
  • shipment.contents.*
  • shipment.customer.*
  • shipment.deliveryAddress.*
  • shipment.pieces.*
  • shipment.pieces.shippingPhase.*
  • shipment.recipientAddress.*
  • shipment.returnAddress.*
  • shipment.senderAddress.*
  • shipment.shippingPhase.*
  • shipment.shippingService.*
  • shipment.shippingServiceMethod.*
  • shipment.shippingStatus.*
Shipment fields
  • shipment.barcode
  • shipment.trackingCode
  • shipment.trackingUrl
  • shipment.labelType
  • shipment.piecesCount
  • shipment.reference
  • shipment.insuranceAmount
  • shipment.status
  • shipment.shippingStatus.code
  • shipment.shippingPhase.code
  • shipment.type
  • shipment.customsInvoiceNumber
  • shipment.customsShipmentType
  • shipment.canceledAt
  • shipment.brand.id
  • shipment.customer.id
  • shipment.carrier.code
  • shipment.shippingService.id
  • shipment.shippingServiceMethod.id
Sender address
  • shipment.senderAddress
  • shipment.senderAddress.id
  • shipment.senderAddress.companyName
  • shipment.senderAddress.name
  • shipment.senderAddress.address
  • shipment.senderAddress.address2
  • shipment.senderAddress.houseNumber
  • shipment.senderAddress.postalCode
  • shipment.senderAddress.city
  • shipment.senderAddress.stateCode
  • shipment.senderAddress.countryCode
  • shipment.senderAddress.email
  • shipment.senderAddress.phone
  • shipment.senderAddress.eori
  • shipment.senderAddress.vat
  • shipment.senderAddress.oss
Return address
  • shipment.returnAddress
  • shipment.returnAddress.id
  • shipment.returnAddress.companyName
  • shipment.returnAddress.name
  • shipment.returnAddress.address
  • shipment.returnAddress.address2
  • shipment.returnAddress.houseNumber
  • shipment.returnAddress.postalCode
  • shipment.returnAddress.city
  • shipment.returnAddress.stateCode
  • shipment.returnAddress.countryCode
  • shipment.returnAddress.email
  • shipment.returnAddress.phone
  • shipment.returnAddress.eori
  • shipment.returnAddress.vat
  • shipment.returnAddress.oss
Recipient address
  • shipment.recipientAddress
  • shipment.recipientAddress.id
  • shipment.recipientAddress.companyName
  • shipment.recipientAddress.name
  • shipment.recipientAddress.address
  • shipment.recipientAddress.address2
  • shipment.recipientAddress.houseNumber
  • shipment.recipientAddress.postalCode
  • shipment.recipientAddress.city
  • shipment.recipientAddress.stateCode
  • shipment.recipientAddress.countryCode
  • shipment.recipientAddress.email
  • shipment.recipientAddress.phone
  • shipment.recipientAddress.eori
  • shipment.recipientAddress.vat
  • shipment.recipientAddress.oss
Delivery address
  • shipment.deliveryAddress
  • shipment.deliveryAddress.id
  • shipment.deliveryAddress.companyName
  • shipment.deliveryAddress.name
  • shipment.deliveryAddress.address
  • shipment.deliveryAddress.address2
  • shipment.deliveryAddress.houseNumber
  • shipment.deliveryAddress.postalCode
  • shipment.deliveryAddress.city
  • shipment.deliveryAddress.stateCode
  • shipment.deliveryAddress.countryCode
  • shipment.deliveryAddress.email
  • shipment.deliveryAddress.phone
  • shipment.deliveryAddress.eori
  • shipment.deliveryAddress.vat
  • shipment.deliveryAddress.oss
Contents
  • shipment.contents
  • shipment.contents.quantity
  • shipment.contents.description
  • shipment.contents.price
  • shipment.contents.weight
  • shipment.contents.countryCodeOfOrigin
  • shipment.contents.hsCode
Pieces
  • shipment.pieces
  • shipment.pieces.barcode
  • shipment.pieces.trackingUrl
  • shipment.pieces.dimensions
  • shipment.pieces.weight
  • shipment.pieces.shippingPhase.code
History events
  • shipment.events
Fulfillment update triggers
Wildcards
  • fulfillment.*
  • fulfillment.allocations.*
  • fulfillment.allocations.originalStock.*
  • fulfillment.allocations.product.*
  • fulfillment.allocations.stock.*
  • fulfillment.lines.*
  • fulfillment.lines.product.*
  • fulfillment.organization.*
Fulfillment fields
  • fulfillment.reference
  • fulfillment.status
  • fulfillment.releaseAt
  • fulfillment.shippedAt
  • fulfillment.organization.id
Lines
  • fulfillment.lines
  • fulfillment.lines.quantity
  • fulfillment.lines.product.id
Allocations
  • fulfillment.allocations
  • fulfillment.allocations.quantity
  • fulfillment.allocations.quantityException
  • fulfillment.allocations.quantityPicked
  • fulfillment.allocations.quantityShipped
  • fulfillment.allocations.product.id
  • fulfillment.allocations.stock.id
  • fulfillment.allocations.originalStock.id
History events
  • fulfillment.events
Picklist update triggers
Wildcards
  • picklist.*
  • picklist.assignedToUser.*
  • picklist.picklistPreset.*
  • picklist.trolley.*
Picklist fields
  • picklist.reference
  • picklist.status
  • picklist.type
  • picklist.assignedToUser.id
  • picklist.picklistPreset.id
  • picklist.trolley.id
GoodsReceipt update triggers
Wildcards
  • goodsReceipt.*
  • goodsReceipt.lines.*
  • goodsReceipt.lines.goodsReceiptPiece.*
  • goodsReceipt.lines.product.*
  • goodsReceipt.organization.*
  • goodsReceipt.pieces.*
Goods receipt fields
  • goodsReceipt.remarks
  • goodsReceipt.status
  • goodsReceipt.expectedDeliveryAt
  • goodsReceipt.arrivedAt
  • goodsReceipt.completedAt
  • goodsReceipt.organization.id
Pieces
  • goodsReceipt.pieces
  • goodsReceipt.pieces.carrierCode
  • goodsReceipt.pieces.barcode
  • goodsReceipt.pieces.status
  • goodsReceipt.pieces.type
  • goodsReceipt.pieces.arrivedAt
  • goodsReceipt.pieces.completedAt
Lines
  • goodsReceipt.lines
  • goodsReceipt.lines.goodsReceiptPiece.id
  • goodsReceipt.lines.product.id
  • goodsReceipt.lines.quantityReceived
ReturnReceipt update triggers
Wildcards
  • returnReceipt.*
  • returnReceipt.lines.*
  • returnReceipt.lines.returnRequestLine.*
  • returnReceipt.organization.*
  • returnReceipt.returnRequest.*
Return receipt fields
  • returnReceipt.remarks
  • returnReceipt.status
  • returnReceipt.undeliverableReturnReason
  • returnReceipt.completedAt
  • returnReceipt.returnRequest.id
  • returnReceipt.organization.id
Lines
  • returnReceipt.lines
  • returnReceipt.lines.condition
  • returnReceipt.lines.remarks
  • returnReceipt.lines.quantityReceived
  • returnReceipt.lines.returnRequestLine.id
History events
  • returnReceipt.events
StockMutation — create only; no update triggers

A delivery reports the concrete paths in fields_changed, grouped by what happened to them. A record added to a relation lands in added, a record removed from one lands in removed, and a changed field value lands in updated. All three lists are always present and any of them can be empty. These paths identify the changed records and fields more precisely than the trigger catalog.

{
"added": ["fulfillment[id: '<fulfillment-id>'].lines[id: '<added-line-id>']"],
"updated": [
"fulfillment[id: '<fulfillment-id>'].status",
"fulfillment[id: '<fulfillment-id>'].lines[id: '<line-id>'].quantity"
],
"removed": ["fulfillment[id: '<fulfillment-id>'].lines[id: '<removed-line-id>']"]
}

Read and change subscriptions

List subscriptions with cursor pagination, or retrieve one by ID.

query GetAndListSubscriptions($subscriptionId: ID!) {
eventSubscription(id: $subscriptionId) {
id
key
topic
actions
triggers
uri
enabled
status
pauseReason
pausedAt
}
eventSubscriptions(first: 20) {
edges { node { id key topic enabled status } }
pageInfo { hasNextPage endCursor }
}
}
{ "subscriptionId": "<event-subscription-id>" }

Update only the fields that should change. A valid update that corrects a structural problem makes the subscription active again while preserving its key and secret.

mutation UpdateSubscription($subscriptionId: ID!, $input: EventSubscriptionUpdateInput!) {
eventSubscriptionUpdate(id: $subscriptionId, input: $input) {
eventSubscription { id key uri enabled status pauseReason }
userErrors { field message }
}
}
{
"subscriptionId": "<event-subscription-id>",
"input": {
"uri": "https://events.example.com/packcloud/current",
"enabled": true
}
}

Always request userErrors from update and deactivate mutations.

Rotate or deactivate

Rotation replaces only the secret. Store the replacement before discarding the old value.

mutation RotateSubscriptionSecret($subscriptionId: ID!) {
eventSubscriptionRotateSecret(id: $subscriptionId) {
eventSubscription { id key status }
secret
}
}

Deactivation disables and pauses the subscription while retaining its delivery history.

mutation DeactivateSubscription($subscriptionId: ID!) {
eventSubscriptionDeactivate(id: $subscriptionId) {
eventSubscription { id key enabled status pauseReason pausedAt }
userErrors { field message }
}
}
{ "subscriptionId": "<event-subscription-id>" }
There is no delete or replay mutation. Re-enabling a subscription affects future matching events; events excluded while it was inactive are not replayed.

Add a stored GraphQL projection

The optional query adds its result as data in the delivery. Without a stored query, the envelope omits data.

Stored projections can read the complete current Warehouse Query schema, including sensitive fields. Create subscriptions only for trusted receivers and select only the data they need.
  • Provide exactly one named, read-only query operation.
  • Mutations, subscriptions, and introspection are rejected.
  • Use only the server variable listed for the selected topic.
  • The query runs without a user identity and uses the endpoint's scope.
  • Every connection must include exactly one literal first or last integer from 1 through 250. Omitting both or using a pagination variable is unsupported.
Query size16 KiB (16,384 bytes) or less
Depth7 levels or less
Root fieldsAt most 5
Computed cost250 points or less
Connection pageA literal page size from 1 through 250
Delivery payloadAt most 1 MiB for the complete envelope

PackCloud supplies the declared resource variable from the event.

Filter projected data

An optional queryFilter uses the same Shopify-style grammar as PackCloud search queries. It requires a stored query, and every filter field must be a scalar response path selected by that query. Aliases become part of the response path.

fulfillment.reference:*
fulfillment.status:shipped AND fulfillment.reference:"FUL-1001"
(fulfillment.status:shipped OR fulfillment.status:canceled) AND fulfillment.reference:FUL*
fulfillment.reference:>=FUL-1000
-fulfillment.reference:*
  • Use field:value for equality and field:>value, :>=, :<, or :<= for ordering.
  • Use AND, OR, NOT, a leading minus sign, parentheses, quoted values, and a trailing * for prefix matching. A standalone *tests whether a scalar is present; negate that term to test for an absent or null value.
  • AND binds more tightly than OR.
  • For list paths, a term matches when any selected list member matches.

A non-match is filtered before transport, so no request is sent for that subscription and event.

Receive and verify deliveries

PackCloud sends an HTTPS POST with an application/json body.

{
"event_id": "<event-id>",
"delivery_id": "<delivery-id>",
"topic": "Fulfillment",
"action": "update",
"subscription_key": "fulfillment-updates",
"occurred_at": "<ISO-8601 timestamp>",
"fields_changed": {
"added": [],
"updated": ["fulfillment[id: '<fulfillment-id>'].status"],
"removed": []
},
"query_variables": { "fulfillmentId": "<fulfillment-id>" },
"data": {
"fulfillment": {
"id": "<fulfillment-id>",
"reference": "<fulfillment-reference>",
"status": "<fulfillment-status>"
}
}
}

fields_changed always carries added, updated and removed. On a create delivery, added holds the path of the new resource and the other two lists are empty. data appears only when a stored query is configured and can be null.

Event-IdStable event ID; use it to correlate related deliveries
Event-Delivery-IdUnique delivery ID; use it for idempotent processing
Event-TimestampUnix timestamp of occurred_at, unchanged on retries
Event-SignatureBase64-encoded HMAC-SHA256 of the exact raw body bytes
Event-TopicTopic for the event
Event-Actioncreate or update
Event-Subscription-KeySubscription key
Event-Resource-IdID of the event's root resource

Verify the signature against the raw request body and compare it in constant time.

$expected = base64_encode(hash_hmac('sha256', $rawBody, $secret, true));
$valid = hash_equals($expected, $receivedSignature);
Do not decode and re-encode JSON before verification. PackCloud freezes the destination, exact body bytes, system headers, timestamp, and signature once; every retry sends those same values.

Retry failed deliveries

SuccessAny 2xx response
RetriedConnection or DNS failures, 408, 429, and 5xx responses
Failed immediatelyRedirects and every other non-2xx response
AttemptsAt most 10 total; delays double from 1 minute up to a 24-hour maximum
RetentionEvents, deliveries, and attempt history expire after 30 days

Redirects are not followed. Deduplicate with delivery_id because transport retries are expected. There is no manual replay or payload recomputation.

After repeated terminal failures, PackCloud temporarily stops sending while continuing to queue matching deliveries. It tests the oldest pending delivery periodically and resumes oldest-first delivery after a successful response. Projection errors do not affect this recovery behavior.

Inspect delivery history

Delivery history is read-only, newest first, and remains available through the subscription until its 30-day retention expires. Attempts are ordered from oldest to newest.

query DeliveryHistory($subscriptionId: ID!) {
eventSubscription(id: $subscriptionId) {
deliveries(first: 20) {
edges {
node {
id
eventId
destination
status
attemptCount
nextAttemptAt
completedAt
lastErrorCode
lastError
expiresAt
createdAt
attempts {
attemptNumber
outcome
startedAt
completedAt
httpStatus
errorCode
error
}
}
}
pageInfo { hasNextPage endCursor }
}
}
}

Use status, nextAttemptAt, and the error fields to troubleshoot a delivery. The EventDelivery type reference reference lists all statuses and fields.

Fix projection failures

A structural failure means the stored document or its shape is no longer valid for the current schema. PackCloud records the failed delivery and pauses the subscription because another record cannot fix it. Correct the query or filter with a valid update to reactivate delivery.

A record-specific resolver, data, or payload-size failure affects only that delivery. It does not pause the subscription.