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
  • Support POST calls from Shipwell
  • Attaches to the software or server from which you parse or transform Shipwell data.

We recommend that you create one webhook per event type. Event types can have different versions. Creating one webhook per maintenance of your webhook easier.

Add the webhook URL

After you've configured your server to receive Shipwell Post calls, post the Webhook URL using the Post webhook endpoint. You'll need at least the following information in the request body:

  • url: Your webhook URL
  • enabled_events: A list of the Shipwell events events for which you want the webhook to receive
  • error-contact-email: An email address to contact if your webhook fails to receive every payload 5 times
  • event_version: The version of the event type configured to your webhook. Refer to Types of events) to see the event version. You might have to POST the webhook URL twice should you want to the same webhook URL to listen for events with different versions. We recommend using one event type per webhook. If you choose not to indicate an event version, the event payload will default to the latest version.

In this example, curl is used to post a test webhook URL to the sandbox environment to listen for the shipment.created and shipment.updated events:

Copy
Copied
curl --request POST \
  --url https://api.shipwell.com/webhooks/ \
  --header 'accept: application/json' \
  --header 'content-type: application/json' \
  --data
  '{"enabled_events":["shipment.created"]
,"status":"ENABLED","include_updated_object":false
,"event_version":1, "error-contact-email":"test@shipwell.com",
"url":"https://webhook.site/f4b4f289-9a4f-41ba-a0cb-47f72fb12b53"}'

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 for shipment.created, create a shipment.

Verify payload

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 Webhook secret
  • 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:

  1. Generate a signature_payload by concatenating:
    1. X-Shipwell-Timestamp header
    2. A semicolon, ;
    3. The JSON payload you received (use the raw request body without formatting)
  2. Using the signature_payload, generate a SHA-256 signature using the Webhook's secret as the key.
  3. The computed signature should match the X-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:

Copy
Copied
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', methods=['POST'])
def webhook():
    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:

Copy
Copied
{
  "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",
  "details": {
    ... // details varies by event
  }
}

The Event object schema uses the OpenAPI 3 for the event body:

Copy
Copied
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
        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 returns a 2xx HTTP status code when successfully receiving an event. Return a response to the Shipwell server before executing any logic on the event since we impose a 10s timeout for webhook servers to respond. Your endpoint may be disabled if it fails to acknowledge any events over seven consecutive days.

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.

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, the X-Shipwell-Signature is unique to each environment. Make sure to add the new signature to your endpoint code if you're checking webhook signatures.
Copyright © Shipwell 2023. All right reserved.