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.
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. (Refer to Guarantees and caveats in case of failures)
- 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)
- Servers must reply to GET requests with a 200-level response within 5 seconds
Here 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.canceled | Sent when an order is canceled | /api/v3/orders/:id |
order.created | Sent when an order is created | /api/v3/orders/:id |
order.updated | Sent when an order is updated. Note: order routing is not considered an update. Also note that this generates a lot of traffic, and most likely you would be better served by using a more specific webhook, depending on your need. | /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 |
order.submitted | Sent when an order is submitted to the warehouse | /api/v3/orders/:id |
order.completed | Sent when an order has been ship confirmed by the warehouse | /api/v3/orders/:id |
order.completed.with-po-sku-source | Sent when an order associated with a purchase order has been ship confirmed. 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 |
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 |
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 |
purchaseorder.posted | Sent when all the related fulfillment orders have finished being created | /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 |
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 |
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 |
return.completed | Sent when a return is completed | /api/v3.1/returns/:id |
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 |
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 |
receiving.completed | Sent when a receiving order is completed | /api/v3.1/receivings/:id |
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 |
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:
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:
{
"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.good” webhook body, showing a shipping order consuming a quantity of 1 good of SKU ABC123 once it has been routed to warehouse 12
{
"attempt": 1,
"body": {
"productId": 515783606,
"productSku": "ABC123",
"warehouseId": 12,
"fromState": "good",
"toState": "reserved",
"delta": 1,
"toStateStockAfterTransition": 3,
"fromStateStockAfterTransition": 2993,
"type": "shipment",
"orderId": 622263970,
"physicalWarehouseId": null
},
"timestamp": "2021-10-21T08:09:11-07:00",
"topic": "stock.transition.good",
"uniqueEventID": "0fbb2dd3d8f23000",
"webhookSubscriptionID": 123456
}
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.good” webhook body for an inventory adjustment, moving 6 units from good to damaged
{
"attempt": 1,
"body": {
"productId": 6349911,
"productSku": "ABC123",
"warehouseId": 12,
"fromState": "good",
"toState": "damaged",
"delta": 6,
"toStateStockAfterTransition": 144,
"fromStateStockAfterTransition": 21926,
"type": "adjustment",
"reason": "None",
"reasonCode": "NONE",
"lotId": "123",
"lotQuantity": 6,
"lotExpirationDate": "20230329",
"physicalWarehouseId": null
},
"timestamp": "2021-10-21T07:31:03-07:00",
"topic": "stock.transition.good",
"uniqueEventID": "0fbb258126723000",
"webhookSubscriptionID": 123456
}
“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
}
}
{
"id": "123",
"type": "low_stock",
"name": "Inventory Alert",
"triggeredFor": {
"warehouseId": 1,
"productId": 12345
},
"triggerCondition": {
"type": "quantity",
"value": 99,
"threshold": 100
}
}
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 |
NONE | No reason code given by warehouse | No reason code given by warehouse |
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.
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.
- "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.
When a failure occurs on a POST request to the consumer, the message will be retried. Between each subsequent attempt for the same webhook, we wait progressively longer. We make a total of 10 attempts, where the final attempt occurs at least 5 and a half hours after the initial attempt.
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.
Please do not implement IP address whitelisting on your end – we send webhooks from a number of different IP addresses, and this list regularly changes. If webhook authentication is required, use Secrets.
Secrets
So that applications can validate the authenticity of events, Shipwire can optionally sign events with a shared secret. Secrets are managed through the Secrets resource. Secret keys are 64 bytes, encoded as 128-digit hexadecimal strings. You may have multiple shared secrets active at a given time, in which case Shipwire will sign events with all active secrets (see example below).
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