Use Webhooks
Now that you know how webhooks work, you can now manage Webhooks through the webhooks API.
Prerequisites
To properly configure a webhook, you need one webhook URL with the following:
- Available on the public internet
- Valid SSL certificate that is served over
https
- Supports
POST
calls from Shipwell - Attaches to the software or server from which you parse or transform Shipwell data
Webhook Configuration Concepts
A webhook configuration is an instance of webhook configuration details such as:
url
: Your publicly accessiblehttps
webhook handler URL endpoint with a validSSL certificate
enabled_events
: A list of the Shipwell events for which you want the webhook to receiveerror_contact_email
: An email address to contact if your webhook fails to receive every payload 5 timesevent_version
: The version of the event type configured to your webhook. Refer to Types of events to see the event version. Event types may have different versions. You may have to create multiple webhook configurations for the same event type if you want to the same webhook URL to listen for events with different versions. We recommend using one event type per webhook endpoint URL (even if this URL is used across different webhook configurations). If you choose not to indicate an event version, the event payload will default to thelatest
version.custom_data
: Optional key-value string pairs that will be stored with the webhook configuration and added to each webhook payloads triggered for the webhook configuration
We recommend that you create webhook configurations in a manner that makes maintenance and deployment processes for your webhook handlers easy. You may have multiple webhook configurations per environment in your account.
Here is a diagram that shows approaches to webhook configurations and how events interact with different webhook configurations (click for a larger version of the image):
Security & Verification
- SSL Certificates and HTTPS
- Ensure that your webhook URLs have a valid
SSL certificate
and begin withhttps://
as the URL scheme.
- Ensure that your webhook URLs have a valid
- IP Addresses and Verifying Webhook Requests/Payloads
- Webhooks are delivered from dynamic IP addresses and not delivered from a static list of IPs.
- The verification of the payloads of webhooks as being from Shipwell and valid may be performed with this cryptographically secure verification method.
Add the webhook URL
After you've configured your server or webhook handler endpoint URL to receive Shipwell HTTP POST calls, create a webhook configuration entry in Shipwell using the Create (POST) Webhook Shipwell API endpoint. In the webhook configuration creation example below, a test webhook configuration is created in the sandbox environment (sandbox-api.shipwell.com
) to listen for the shipment.created
and shipment.updated
events:Note
production
usage, remember to replace sandbox-api.shipwell.com
with the production environment of api.shipwell.com
.curl --request POST \
--url https://sandbox-api.shipwell.com/webhooks/ \
--header 'accept: application/json' \
--header 'content-type: application/json' \
--data '{"enabled_events":["shipment.created","shipment.updated"]
,"status":"ENABLED","include_updated_object":false
,"event_version":1, "error_contact_email":"test@shipwell.com"
,"url":"https://webhook.site/f4b4f289-9a4f-41ba-a0cb-47f72fb12b53"
,"custom_data":{"your_custom_key1":"your_custom_value1"}}'
import requests
base_path = ""
host_path = "sandbox-api.shipwell.com"
target_url = (
"https://"
+ host_path
+ base_path
+ "/webhooks/"
)
headers = {
"Authorization": "YOUR_AUTHORIZATION_HEADER",
"Content-Type": "application/json",
}
payload = {
'enabled_events': [
'shipment.created',
'shipment.updated'
],
'status': 'ENABLED',
'include_updated_object': False,
'event_version': 1,
'error_contact_email': 'test@shipwell.com',
'url': 'https://webhook.site/f4b4f289-9a4f-41ba-a0cb-47f72fb12b53',
'custom_data':
{
'your_custom_key1':'your_custom_value1',
}
}
response = requests.post(target_url, headers=headers, json=payload)
data = response.json()
print(data)
Update the webhook URL
If details about your webhook URL need to be updated, you may change the webhook configuration or webhook URL.
There are two approaches to updating the webhook configuration details using the HTTP verbs ofPATCH
or PUT
.We recommend updating webhooks using HTTP PATCH which accepts a partial payload of only the elements that you wish to change or update.
The following example shows updating the following data for an existing webhook configuration:
enabled_events
- The list of events that the webhook URL will receive
error_contact_email
- The
optional
email address that receives notification of errors
- The
Note
webhookId/webhook_id
of an existing webhook configuration, utilize the List (GET) Webhooks endpoint.Partial PATCH Update
The following is an example of utilizing a HTTP PATCH request to partially update the details of an existing webhook configuration.
curl -i -X PATCH \
'https://sandbox-api.shipwell.com/webhooks/{webhookId}' \
-H 'Authorization: YOUR_AUTHORIZATION_HEADER' \
-H 'Content-Type: application/json' \
-d '{
"enabled_events": [
"shipment.created"
],
"error_contact_email": "error-distribution-list@example.com"
}'
import requests
webhook_id = "YOUR_webhookId_PARAMETER"
base_path = ""
host = "sandbox-api.shipwell.com"
target_url = "https://" + host + base_path + "/webhooks/" + webhook_id
headers = {
"Content-Type": "application/json",
"Authorization": "YOUR_AUTHORIZATION_HEADER",
}
payload = {
"enabled_events": [
"shipment.created"
],
"error_contact_email": "error-distribution-list@example.com"
}
response = requests.patch(target_url, headers=headers, json=payload)
data = response.json()
print(data)
Full PUT Update
The following is an example of a full HTTP PUT request for updating the details of an existing webhook configuration.
curl -i -X PUT \
'https://sandbox-api.shipwell.com/webhooks/{webhookId}' \
-H 'Authorization: YOUR_AUTHORIZATION_HEADER' \
-H 'Content-Type: application/json' \
-d '{
"id": "{webhookId}",
"resource_type": "webhook",
"created_at": "2024-07-18T14:15:22Z",
"enabled_events": [
"shipment.created",
"shipment.updated"
],
"error_contact_email": "user@example.com",
"username_password_authentication": {
"username": "johndoe",
"password": "Yga90E)a23z!"
},
"status": "ENABLED",
"include_updated_object": false,
"url": "https://api.yourcompany.com/webhook_url",
"event_version": 1,
"custom_data": {
"your_custom_key1": "your_custom_value1",
"your_custom_key2": "your_custom_value2"
}
}'
import requests
webhook_id = "YOUR_webhookId_PARAMETER"
base_path = ""
host = "sandbox-api.shipwell.com"
target_url = "https://" + host + base_path + "/webhooks/" + webhook_id
headers = {
"Content-Type": "application/json",
"Authorization": "YOUR_AUTHORIZATION_HEADER",
}
payload = {
"id": f"{webhook_id}",
"resource_type": "webhook",
"created_at": "2014-07-18T14:15:22Z",
"enabled_events": [
"shipment.created",
"shipment.updated"
],
"error_contact_email": "user@example.com",
"username_password_authentication": {
"username": "johndoe",
"password": "Yga90E)a23z!"
},
"status": "ENABLED",
"include_updated_object": False,
"url": "https://api.yourcompany.com/webhook_url",
"event_version": 1,
"custom_data":
{
"your_custom_key1":"your_custom_value1",
"your_custom_key2":"your_custom_value2"
}
}
response = requests.put(target_url, headers=headers, json=payload)
data = response.json()
print(data)
Basic Auth Support
If your webhook endpoint or URL supports HTTP Basic Authentication, you may also specify the username and password for this method ofAuthentication
in the webhook configuration details. When Shipwell delivers webhooks payloads associated with this particular webhook configuration, then it will HTTP POST (over HTTPS) to the webhook configuration's endpoint or URL and set the HTTP Basic Authentication headers in the request headers (it is up to your webhook endpoint or URL to handle the HTTP Basic Authentication if you specify this setting).The username_password_authentication
data for a webhook endpoint may be specified when initially creating the webhook configuration or after the initial creation by updating the webhook configuration.curl -i -X PATCH \
'https://sandbox-api.shipwell.com/webhooks/{webhookId}' \
-H 'Authorization: YOUR_AUTHORIZATION_HEADER' \
-H 'Content-Type: application/json' \
-d '{
"username_password_authentication": {
"username": "johndoe",
"password": "Yga90E)a23z!"
}
}'
import requests
webhook_id = "YOUR_webhookId_PARAMETER"
base_path = ""
host = "sandbox-api.shipwell.com"
target_url = "https://" + host + base_path + "/webhooks/" + webhook_id
headers = {
"Content-Type": "application/json",
"Authorization": "YOUR_AUTHORIZATION_HEADER",
}
payload = {
"username_password_authentication": {
"username": "johndoe",
"password": "Yga90E)a23z!"
}
}
response = requests.patch(target_url, headers=headers, json=payload)
data = response.json()
print(data)
Custom Data Support
Shipwell webhook configurations support specifying a dictionary ofcustom_data
key-value string pairs that are repeated or echoed into the payload of every webhook payload instance associated with a particular webhook configuration.- For example, if you specify
"abc_webhook_environment": "development-west-2"
for a particular webhook configuration, then every webhook payload for that configuration and delivered to theurl
will also contain that data in thecustom_data
section of the webhook payload (it is echoed into the payload).
custom_data
data for a webhook endpoint may be specified when initially creating the webhook configuration or after the initial creation by updating the webhook configuration.curl -i -X PATCH \
'https://sandbox-api.shipwell.com/webhooks/{webhookId}' \
-H 'Authorization: YOUR_AUTHORIZATION_HEADER' \
-H 'Content-Type: application/json' \
-d '{
"custom_data": {
"your_custom_key1": "your_custom_value1",
"your_custom_key2": "your_custom_value2"
}
}'
import requests
webhook_id = "YOUR_webhookId_PARAMETER"
base_path = ""
host = "sandbox-api.shipwell.com"
target_url = "https://" + host + base_path + "/webhooks/" + webhook_id
headers = {
"Content-Type": "application/json",
"Authorization": "YOUR_AUTHORIZATION_HEADER",
}
payload = {
"custom_data": {
"your_custom_key1": "your_custom_value1",
"your_custom_key2": "your_custom_value2"
}
}
response = requests.patch(target_url, headers=headers, json=payload)
data = response.json()
print(data)
Test your webhook
When configuring a webhook through the sandbox environment in Shipwell, you can test your webhook to make sure it receives event payloads. Using the example from "Add the webhook URL".:
Trigger an event
Trigger one of the events configured with your webhook either through the Shipwell UI or through the Shipwell API. For example, if you configured a webhook to listen forshipment.created
, create a shipment. Verify payload
Benefits of Webhook Payload Verification
Shipwell provides a cryptographically secure method to validate and verify that webhook payloads sent to your endpoint(s) are from Shipwell and that the payload has not been altered in transmission. Shipwell uses dynamically provisioned servers to enable scaling which means that we do not have static IP addresses.
Webhook payloads are already delivered via HTTP POST securely overHTTPS
(Shipwell does not allow invalid or expired SSL certificates on webhook endpoints) and this adds an extra layer of cryptographically secure protection that may be adapted to your programming language(s) and environments.Upon creation of the event payload, your webhook receives two additional custom headers used to verify that the payload was sent to the webhook URL:
X-Shipwell-Signature
- a unique signature generated from the body,X-Shipwell-Timestamp
and the Webhooksecret
X-Shipwell-Timestamp
- an ISO-8601 timestamp indicating when the signature was generated, used for signature verification
These 2 headers can be used to verify that you are receiving payloads from Shipwell.
To verify a payload:
- Generate a
signature_payload
by concatenating:X-Shipwell-Timestamp
header- A semicolon,
;
- The JSON payload you received (use the raw request body without formatting)
- Using the
signature_payload
, generate a SHA-256signature
using the Webhook's secret as the key. - The computed
signature
should match theX-Shipwell-Signature
. Don't worry, sandbox data is not real data for customers, bills, invoices, or shipments or any other objects in your account. If your endpoint is working successfully, you’ll see the webhook get executed on your server. Additionally, you can call the Events API and look for the specific event you received.
The following is a flask example of verifying a webhook signature:
import hashlib
import hmac
from flask import Flask, request, abort
app = Flask(__name__)
webhook_secret = ... # the Webhook secret is generated and returned when creating a Webhook
@app.route('/webhook-handler', methods=['POST'])
def webhook_handler():
if request.method == 'POST':
received_signature = request.headers.get('X-Shipwell-Signature')
timestamp = request.headers.get('X-Shipwell-Timestamp')
if not received_signature and not timestamp:
abort(400)
signature_payload = timestamp + ";" + request.get_data() # need the raw String to compute the signature
signature = hmac.new(key=webhook_secret.encode(),
msg=signature_payload.encode(),
digestmod=hashlib.sha256).hexdigest()
if signature != received_signature:
abort(400)
print(f'Received data from Shipwell! {request.json}')
# This must return in less than 10 seconds
return '', 200
abort(400)
if __name__ == '__main__':
app.run()
We recommend using a cryptographically secure signing key when configuring your endpoints as well as having all endpoints verify the signature matches the payload to ensure the request is authentically from Shipwell.
Example payload
All events received by your webhook use a custom JSON payload depending on the event type, according to the OpenAPI specification, the following is an example of a generalized event payload:
{
"id": "545b40eb-1adf-43a2-9159-64190400900b",
"occurred_at": "2020-04-20",
"source": {
"user_id": "fdc6a67b-0533-485c-8e96-ee98d703b8bb",
"company_id": "f75099e9-a057-46a5-9fc6-490332a110f6",
"request_id": "919386f2-c8cf-4670-a1ee-1a5e09b6e1f9",
"publishing_system": "backend",
"environment": "sandbox"
},
"event_name": "shipment.created",
"custom_data": {
"your_custom_key1": "your_custom_value1",
"your_custom_key2": "your_custom_value2"
},
"details": {
// ... details varies by event
}
}
The Event object schema uses the OpenAPI 3 for the event body:
openapi: 3.0.2
info:
version: "1.0.0"
title: Webhook payload
components:
schemas:
Source:
type: object
required:
- publishing_system
description: >-
Object containing data associated with the source of the event.
properties:
user_id:
type: string
nullable: true
description: "The user that originated the event, if applicable."
company_id:
type: string
nullable: true
description: "The company that originated the event, if applicable."
request_id:
type: string
nullable: true
description: If the event came from an API request, the ID of that request is null unless system generated.
publishing_system:
type: string
description: "The system name where the event originated"
example: "shipwell.rating"
WebhookPayload:
type: object
description: |
What the external endpoint (specified by the Webhook `url`) receives as a `POST` body.
readOnly: true
required:
- id
- occurred_at
- source
- event_name
- details
properties:
id:
type: string
description: Unique ID of the event.
occurred_at:
type: string
format: 'date-time'
description: When the event occurred.
source:
allOf:
- $ref: '#/components/schemas/Source'
- type: object
required:
- environment
properties:
environment:
type: string
description: >-
The environment from which the event was generated. Useful in scenarios where the same hook subscribes to events in multiple environments.
enum: [dev, sandbox, prod]
event_name:
type: string
description: Which particular event is being received. This will always be a subset of what was specified in enabled_events
webhook_id:
type: string
description: Unique id of the Webhook that prompted delivery
custom_data:
type: object
nullable: true
description: Custom data registered with the Webhook
details:
type: object
description: The event payload that was subscribed to. The contents differ both on the event name as well as the event_version that was subscribed to
new_object:
type: object
description: If the original Webhook `include_updated_object` was true, this becomes the new object. Otherwise the object does not appear.
Respond to webhook events
Your endpoint must return a 2xx HTTP status code within 10 seconds when it has successfully received an event. This might mean you need to run business logic as a background job if it cannot be completed within the 10-second response window.
If your endpoint does not return any successful responses to Shipwell events we may automatically disable the webhook. You will receive a notification to the configurederror_contact_email
if this occurs. To re-enable the webhook, send a PATCH
request to update the status
field to ENABLED
. See the docs for more informationEvent delivery
Timeouts and retries
When a webhook is triggered, Shipwell queues the event and webhook to be published. If the webhook is unsuccessful in delivering the event (the remote server was unreachable, returned a non-2xx HTTP response, or the took longer than 10s to respond), the webhook handler will attempt to deliver the payload to the webhook again after a 20 min wait. We'll try to deliver the webhook up to 5 times, doubling the delay between each attempt. If the webhook handler gives up, Shipwell will send an automatic email to the email address submitted with the webhook configuration.
Shipwell will automatically follow all redirects returned by the server; however, more than 30 redirections are considered a failure. Additionally, the status code found to determine if the webhook request succeeded or not is the last one received in the chain of redirections. Shipwell ignores any other information returned in the request headers or request body.
The majority of webhook issues result in 4xx and 5xx HTTP status codes.
- 4xx indicates Shipwell was able to contact your server, but the endpoint was not valid. Double-check that the endpoint URL is correct and ready to receive POST requests.
- 5xx usually indicates an exception on your server.
Event ordering
Shipwell does not guarantee the order of the delivery of webhook events, i.e. events may be delivered in a different order than the events or data was generated. For example, creating anpurchase order
then a shipment
may generate the following events where the shipment
details arrive before the purchase order
details:shipment.created
purchase_order.created
shipment
before an purchase order
and your workflow requires the purchase order
then you may retrieve the purchase order
, customer
, shipment
, etc. in API calls using information from the shipment
if you happen to receive the shipment
first).Handling duplicate events
Shipwell does not guarantee exactly once event delivery. This is common for event delivery systems that support scale and speed and there are common ways to handle if you receive a duplicate event.
- Ensure that your event processing is idempotent
- You may log or save events processed, after creation/save of an item look that up in a database/cache/data store, etc., and then do not process those events
- You may utilize the event id, event name, etc. when determining if you have already logged/processed/recorded an event in your system
Debugging webhooks
Sometimes you need to debug or investigate webhook endpoint issue(s) for a new or existing webhook configuration. For example:
- Shipwell may have a delay delivering events to your webhook endpoint
- Your webhook endpoint URL is not the URL you expect and the webhook configuration needs to be updated
- Your webhook endpoint may have an SSL issue
- Your webhook endpoint or server times out
- Your webhook endpoint is not returning a 200 series (i.e. 200-299) HTTP status code
- Your webhook endpoint is not receiving the events that you expect
- Your HTTP basic auth information has changed
Webhook Delivery Attempts API Endpoint
You may view the webhook delivery attempts and responses by querying the Webhook Delivery Attempts API Endpoint.
- The webhook attempts are sorted in descending order by the webhook attempt date/time so the most recent attempts are returned at the top of the response list. You may also page through the results using date/time windows if needed.
- Similar to other Shipwell endpoints that support filtering, the filter criteria is
ANDed
together and notORed
(i.e.a=1 and b=2
).
GET /webhooks/attempts?response_code.gte=400
will return 400-499 (client errors) and 500-599 (server errors) HTTP response codes from all webhook attempts across your Shipwell account in that specific environment (i.e. sandbox-api (sandbox)
, api (production)
, etc.). Other common webhook attempt query examples:GET /webhooks/attempts?response_code.in=400,401,403,404,408,500,501,502,503,504,null
- Retrieves webhook attempts where the response contains specific HTTP status codes or the endpoint or server timed out
- Specifying the string
null
for a response code query indicates no response code (i.e. the server timed out)
GET /webhooks/attempts?response_code=null
- Retrieves webhook attempts that timed out where your endpoint or server did not respond within the 10-second threshold
GET /webhooks/attempts?response_code=null&webhook_config_id=YOUR_WEBHOOK_CONFIG_ID
- Retrieves webhook attempts that timed out for a particular webhook endpoint or webhook_config_id
GET /webhooks/attempts?response_code.gte=200&response_code.lte=299
- Retrieves webhook attempts that were successful with any HTTP 200-series status code (200-299 (server success))
curl --location --request GET 'https://sandbox-api.shipwell.com/webhooks/attempts?response_code.gte=400' \
--header 'Authorization: YOUR_AUTHORIZATION_HEADER'
import requests
base_path = ""
host = "sandbox-api.shipwell.com"
target_url = "https://" + host + base_path + "/webhooks/attempts"
headers = {"Authorization": "YOUR_AUTHORIZATION_HEADER"}
query_params = {
"response_code.gte": 400
}
# Specific Error Codes Example:
# response_code_list = [400, 401, 403, 404, 408, 500, 501, 502, 503, 504, "null"]
# response_codes_delimited = ",".join([str(item) for item in response_code_list])
# query_params = {
# "response_code.in": response_codes_delimited
# }
response = requests.get(target_url, headers=headers, params=query_params)
data = response.json()
print(data)
To view events in your account (in case you need to import events after a webhook handler or endpoint downtime, etc.), utilize the Events API Endpoint
Tip
Two of the most common issues with webhook endpoints are: 1. That the handler, receiver, or server takes longer than 10 seconds to respond and times out without returning a HTTP status code within the threshold. 2. The SSL certificate for the webhook endpoint is invalid.
Go live
Once you've verified your webhook URL is receiving, acknowledging, and handling events correctly, go through the configuration step again to configure a webhook URL for your production integration. If you're using the same webhook URL for both sandbox and production modes, theX-Shipwell-Signature
is unique to each environment. Make sure to add the new signature to your endpoint code if you're checking webhook signatures.