Webhooks Guide
Introduction
The Webhooks API allows your application to stay informed about events of interest on the Shipwire platform, in near real-time. By creating a webhook, your application can subscribe to specific topics and tell us where to send notifications for each.
Please note that webhooks, just like our API, contain a maximum of 20 items per resource. If there are more than 20 items for a specific resource that you need (most commonly items, trackings, pieces, or serialNumbers on an orders webhook topic), then you can use the Shipwire API URL given in “next” to retrieve the next page of additional items until you’ve retrieved them all. If there are only 20 or less items, then “next” will be null, and that is the easiest way to know that you have acquired all the items for a given resource.
This is the list of topics you can subscribe to and get information on when the associated event occurs.
Order topics
Topic | Triggered When | Payload Resource |
---|---|---|
order.created | Sent when an order is created | /api/v3/orders/:id |
order.updated | Sent when an order is updated | /api/v3/orders/:id |
order.posted | Sent when the order has been processed (async job) in the system | /api/v3/orders/:id |
order.processed | Sent when an order is cleared of all holds and ready for submission to the warehouse | /api/v3/orders/:id/markProcessed |
order.submitted | Sent when an order is submitted to the warehouse | /api/v3/orders/:id/markSubmitted | order.canceled | Sent when an order is canceled | /api/v3/orders/:id/cancel |
order.completed | Sent when an order is completed | /api/v3/orders/:id/markComplete |
order.completed.with-po-sku-source | Sent when a shipping order associated with a purchase order is completed. Contains product information from purchase order for up to 20 items. If an order has more than 20 items, please retrieve the additional items from the orders API using the purchaseOrderItems subresource like: https://api.shipwire.com/api/v3/orders/{id}/purchaseOrderItems?offset=20&limit=20 | /api/v3/orders/:id |
order.hold.added | Sent when a hold is added to an order | /api/v3/orders/:id/holds |
order.hold.cleared | Sent when a hold is cleared for an order | /api/v3/orders/:id/holds/clear |
tracking.created | Sent when tracking information is first received for an order | /api/v3/orders/:id/trackings |
tracking.delivered | Sent when tracking information indicates the order has been delivered | /api/v3/orders/:id/trackings |
tracking.updated | Sent when tracking information has been updated for an order | /api/v3/orders/:id/trackings |
orders.generateLabels.new | Sent when generate labels job has been successfully created | /api/v3.1/orders/generateLabels/:jobId |
orders.generateLabels.running | Sent when generate labels job is running | /api/v3.1/orders/generateLabels/:jobId |
orders.generateLabels.completed | Sent when generate labels job has been completed | /api/v3.1/orders/generateLabels/:jobId |
orders.generateLabels.retrying | Sent when labels failed to generate and system is restarting this job | /api/v3.1/orders/generateLabels/:jobId |
orders.generatePackingLists.new | Sent when generate packing-list job has been successfully created | /api/v3.1/orders/generatePackingLists/:jobId |
orders.generatePackingLists.running | Sent when generate packing-list job is running | /api/v3.1/orders/generatePackingLists/:jobId |
orders.generatePackingLists.completed | Sent when generate packing-list job has been completed | /api/v3.1/orders/generatePackingLists/:jobId |
orders.generatePackingLists.retrying | Sent when packing-list failed to generate and system is restarting this job | /api/v3.1/orders/generatePackingLists/:jobId |
Purchase Order topics
Topic | Triggered When | Payload Resource |
---|---|---|
purchaseorder.created | Sent when a purchase order is created | /api/v3/purchaseOrders/:id |
purchaseorder.updated | Sent when a purchase order is updated | /api/v3/purchaseOrders/:id |
purchaseorder.needs-approval | Sent when a purchase order enters a state where it needs approval | /api/v3/purchaseOrders/:id |
purchaseorder.approved | Sent when a purchase order is approved | /api/v3/purchaseOrders/:id/approve |
purchaseorder.posted | Sent when all the related fulfillment orders has been processed (async job) in the system | /api/v3/purchaseOrders/:id |
purchaseorder.partially-completed | Sent when a purchase order is partially completed (when one of the related fulfillment order is completed) | /api/v3/purchaseOrders/:id |
purchaseorder.completed | Sent when a purchase order is completed | /api/v3/purchaseOrders/:id |
purchaseorder.canceled | Sent when a purchase order is canceled | /api/v3/purchaseOrders/:id/cancel |
purchaseorder.hold.added | Sent when a hold is added to an purchase order | /api/v3/purchaseOrders/:id/hold |
purchaseorder.hold.cleared | Sent when a hold is cleared for an purchase order | /api/v3/purchaseOrders/:id/hold/clear |
Stock topics
Topic | Triggered When | Payload Resource |
---|---|---|
stock.transition | Sent when product stock moves from one state to another (e.g. an order moves 3 “in-stock” units to “reserved”). Please note that this generates *a lot* of activity as items move through different states as orders are processed – most merchants just need stock.transition.good | see below |
stock.transition.good | Sent when “good” product stock changes (e.g. when an order is placed or new inventory is received at a warehouse). They are a subset of stock.transition where either “toState” or “fromState” is good. | see below |
stock.backordered | Sent when product is backordered (e.g. When order is placed for a product that has no inventory) | see below |
alert.low-stock | Sent when a configured low-stock alert is triggered | see below |
alert | Sent when a generic alert is triggered | see below |
Return topics
Topic | Triggered When | Payload Resource |
---|---|---|
return.created | Sent when a return is created | /api/v3/returns/:id |
return.updated | Sent when a return is updated | /api/v3/returns/:id |
return.canceled | Sent when a return is canceled | /api/v3/returns/:id/cancel |
return.completed | Sent when a return is completed | /api/v3.1/returns/:id/markComplete |
return.hold.added | Sent when a hold is added to a return | /api/v3/returns/:id/holds |
return.hold.cleared | Sent when a hold is cleared for a return | /api/v3/returns/:id/holds |
return.tracking.created | Sent when tracking information is first received for a return | /api/v3/returns/:id/trackings |
return.tracking.updated | Sent when tracking information has been updated for a return | /api/v3/returns/:id/trackings |
return.tracking.delivered | Sent when tracking information indicates a return has been received by the warehouse | /api/v3/returns/:id/trackings |
returns.generateLabels.new | Sent when generate return labels jobs has been successfully created | /api/v3.1/returns/generateLabels/:jobId |
returns.generateLabels.running | Sent when generate return labels jobs is running | /api/v3.1/returns/generateLabels/:jobId |
returns.generateLabels.completed | Sent when generate return labels jobs has been completed | /api/v3.1/returns/generateLabels/:jobId |
returns.generateLabels.retrying | Sent when labels failed to generate and system is restarting this job | /api/v3.1/returns/generateLabels/:jobId |
Receiving (ASN) topics
Topic | Triggered When | Payload Resource |
---|---|---|
receiving.created | Sent when a receiving order is created | /api/v3/receivings/:id |
receiving.updated | Sent when a receiving order is updated | /api/v3/receivings/:id |
receiving.canceled | Sent when a receiving order is canceled | /api/v3/receivings/:id/cancel |
receiving.completed | Sent when a receiving order is completed | /api/v3.1/receivings/:id/markComplete |
receiving.hold.added | Sent when a hold is added to a receiving order | /api/v3/receivings/:id/holds |
receiving.hold.cleared | Sent when a hold is cleared for a receiving order | /api/v3/receivings/:id/holds |
receiving.tracking.created | Sent when tracking information is first received for a receiving order | /api/v3/receivings/:id/trackings |
receiving.tracking.updated | Sent when tracking information has been updated for a receiving order | /api/v3/receivings/:id/trackings |
receiving.tracking.delivered | Sent when tracking information indicates a receiving order has been received by the warehouse | /api/v3/receivings/:id/trackings |
receiving.shipment.received | Sent when a shipment (partial or full) is received as part of a receiving order. For partial shipment it will be sent for each shipment | /api/v3/receivings/:id/shipments |
Product topics
Topic | Triggered When | Payload Resource |
---|---|---|
product.created | Sent when a product order is created | /api/v3/product/:id |
product.updated | Sent when a product is updated | /api/v3/product/:id |
product.retired | Sent when a product is retired | /api/v3/product/:id |
POST bodies
Webhooks are POSTed to endpoints with a common envelope wrapping a topic-dependent resource. For most topics,
the resource, or “body”, is identical to the resource you would get from a related API endpoint. For example, on
a tracking.updated webhook, you might get:
{
"attempt": 1,
"body": {
"status": 200,
"message": "Successful",
"resourceLocation": "https://api.shipwire.com/trackings/638903765",
"resource": {
"id": 638903765,
"orderId": 186903450,
"orderExternalId": null,
"tracking": "294631149443923572",
"carrier": null,
"url": null,
"summary": "Shipment information sent to FedEx",
"summaryDate": "2015-05-05 03:17:10",
"trackedDate": "2015-05-05 03:17:10",
"deliveredDate": null
}
},
"timestamp": "2015-05-12T11:01:51-07:00",
"topic": "tracking.updated",
"uniqueEventID": "ba898330ad9b9dfd41de247c09bf7b96",
"webhookSubscriptionID": 222
}
The object above, rooted at “body”, is the same object that you would get from the order-tracking API. The exceptions to this pattern are “stock.transition”, “alert”, and “alert.low-stock”, which contain data unique to those webhook topics, and are described below:
“stock.transition” webhook body
{
"orderId": 1244,
"productId": 528,
"warehouseId": 1038,
"physicalWarehouseId": null,
"fromState": "good",
"toState": "reserved",
"delta": 12,
"toStateStockAfterTransition": 24,
"fromStateStockAfterTransition":64,
"type": "shipment",
}
It is important to note that the toStateStockAfterTransition and fromStateStockAfterTransition fields are meant to be references, and may not necessarily contain the most up-to-date stock values. This is because webhooks may get sent in a different order than how the events occurred. Possible stock transition states are “pending”, “good”, “reserved”, “backordered”, “shipping”, “shipped”, “created”, “damaged”, “returned”, “inreview”. Possible values for inventory transition type are “shipment”, “return”, “receiving”, “adjustment”, “unknown”. For correct and up-to-date information, use the Stock endpoint.
Note that the Adjustment Reason Codes (“reason” and “reasonCode”) will only be populated when there has been an adjustment to inventory, typically within an inventory state, within the warehouse or manual disposition by operations/engineering support. Adjustments occur in the following inventory buckets: “good”, “damaged”, “quarantined”, “in review”, and “consuming” and are not part of the inbound receiving and outbound shipping processes.
“stock.transition” webhook body for an inventory adjustment, reducing 3 units from “good” due to cycle count
{
"delta": -3,
"fromState": "none",
"fromStateAfterTransition": 0,
"physicalWarehouseId": null,
"productId": 1234567,
"productSku": "SkuExample",
"reason": "Cycle Count",
"reasonCode": "CYC",
"toState": "good",
"toStateAfterTransition": 0,
"type": "adjustment",
"warehouseId": 25
}
Deciphering Reasons and Reason Codes in stock.transitions webhooks
reasonCode | reason | Examples |
---|---|---|
RER | Receiving Error | Operator error at receiving, ASN Carton Shortage/Overage |
SPK | Special Projects/Kitting | Value Added Services/Special Projects |
CYC | Cycle Count | Ad hoc or scheduled cycle count |
LIQ | Liquidation, customer initiated | Customer-requested destruction of material |
RET | Returns/Refusals | order voided per customers request/refusal after it has already been picked and billed |
SER | Shipping Error | Corrections due to short/over/cross shipment |
SYS | System Error | Data problem found |
QHA | Increase or Movement into Quality Hold | Potential quality issue with material |
QHR | Decrease or Movement out of Quality Hold | Potential quality issue with material addressed |
CHA | Increase or Movement into C-Hold | Potential loss of material |
CHR | Decrease or Movement out of C-Hold | Potentially lost material has been found |
OTH | Other | Other, rare adjustment reasons |
“alert” and “alert.low-stock” webhook body
{
"id": "2",
"type": "backordered",
"name": "A backordered alert",
"triggeredFor": {
"warehouseId": 1038,
"productId": 528
},
"triggerCondition": {
"type": "backordered",
"value": 1104,
"threshold": 1
}
}
Webhook versions
Currently, the Webhooks API has only the “v1” version. In the future, additional Webhook API versions will be added as needed to support new capabilities and topics to subscribe to.
If you want your integration with Shipwire to be tied to a specific version of the Webhook API which has a particular payload structure and content version, you can specify the API version as a prefix to the topic.
- "v1.order.created"
- "v1.order.updated"
- "v1.order.canceled"
- "v1.order.completed"
- "v1.order.hold.added"
- "v1.order.hold.cleared"
- "v1.tracking.created"
- "v1.tracking.updated"
- "v1.tracking.delivered"
- "v1.stock.transition"
- "v1.stock.transition.good"
- "v1.alert.low-stock"
- "v1.alert"
Subscribing to topics without a prefixed version number means that your integration will use the latest API payload
version at the time of subscription. For example, if you register now for the topic order.created, your subscription
will be tied to v1. After v2 is made available, your existing subscriptions will remain with v1 and that distinction will
not change. However, if you now add new subscriptions without specifying the API version, those new subscriptions will
be tied to v2. By specifying a version, you can ensure that each of your topic subscriptions are associated with the
version of the Webhook API that you want.
Guarantees and caveats
Webhooks are currently delivered on a best-effort basis. Shipwire will typically deliver a webhook message within minutes of an event occurring and usually much sooner. The message will be sent once and only once while Shipwire and your application’s servers both remain connected and available.
Requirements for webhook endpoints
Webhook recipients are expected to conform to the following:
- URLs must be for HTTPS hosts with valid and verifiable SSL certificates
- Servers must reply to POST requests with a 200-level response within 5 seconds
- Servers must reply to HEAD requests with a 200-level response within 5 seconds (HEAD requests are occasionally used to validate the endpoint, e.g. at subscription time)
When a failure occurs on a POST request to the consumer, the message will be retried. Note that the rate and number of retries is not constant.
If your server consistently fails to respond to webhook messages, we may, at our discretion, unregister the webhook topics that are failing. Once you’ve resolved any issues, you may then re-register for the webhook topics.
Secrets
Verification
Webhooks should be verified by the consumer to confirm that information is authentic. Accounts which have at least one active API Secret will have webhook events with a signature for each valid secret as an HTTP header. Example:
X-Shipwire-Signature: abc123;secret-id=2
X-Shipwire-Signature: bcd345;secret-id=5
The hash value is the HMAC-SHA256 of the unaltered POST request body. Having multiple secrets allows you to rotate your keys without missing messages.
Signature verification code samples
Here is a sample of how you could verify your webhook secrets in PHP
<?php
$key = hex2bin(" (your key here) ");
if ($key === FALSE) {
echo "invalid key";
return;
}
$request = file_get_contents("php://input");
if (strlen($request) == 0) {
$request = file_get_contents("php://stdin");
}
echo strlen($request) . " bytes. ";
echo hash_hmac("sha256", $request, $key);
Here is a sample of how you could verify your webhook secrets in Go
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"flag"
"fmt"
"io"
"log"
"os"
)
func main() {
flag.Parse()
if flag.NArg() < 1 {
log.Fatal("need key")
}
// decode the key from hex to binary ([]byte)
key, err := hex.DecodeString(flag.Arg(0))
if err != nil {
log.Fatal(err)
}
// compute hash
h := hmac.New(sha256.New, key)
l, err := io.Copy(h, os.Stdin)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%d bytes: %x\n", l, h.Sum(nil))
}
API reference
View the webhooks API reference here