Webhooks

ARTA will optionally deliver webhook events to your HTTP webhook endpoints at key moments during the fulfillment workflow for request and shipment resources. These webhooks may be used to build your own custom email/SMS/push notification systems or to simply help keep any cached data in sync with ARTA's resources.

Configuration

Your organization's webhook endpoints exist in either Live or Test modes. You can, for example, preview and validate configurations in test mode prior to updating your live mode settings.

Your webhook settings may be managed via the ARTA API or directly within your API Client Dashboard when building/testing/operating your integrations.

API Reference

Webhook endpoints

Webhook schema

Every webhook payload is comprised of three main parts:

  • data — a representation of the updated resource

  • object — the name of the resource type; at present the value will always be either request or shipment
  • type — a label describing the type of webhook being sent

Currently, ARTA will send shipment webhook notifications with the following types:

  • ping — Sent when a ping test message is manually triggered via the ARTA Dashboard or API
  • request.created — Sent when the quote request is created
  • request.status.updated — Sent when the request resource's status has transitioned from one state to another
  • shipment.created — Sent when the shipment is created
  • shipment.eei_form_status.updated — Sent when the shipment's eei_form_status has transitioned from one state to another as part of the ARTA international fulfillment workflow
  • shipment.status.updated — Sent when the shipment's status has transitioned from one state to another
  • shipment.schedule.updated — Sent when a change has been made to the shipment's collection and/or delivery schedule
  • shipment.tracking.updated — Sent when the tracking details are updated for a shipment

The following is a sample webhook event payload for a shipment resource:

{
  "data": {
    "created_at": "2021-02-10T18:12:37.795261",
    "destination": {
      "access_restrictions": [],
      "address_line_1": "87 Richardson St",
      "address_line_2": null,
      "address_line_3": null,
      "city": "New York",
      "contacts": [
        {
          "email_address": "al@example.com",
          "name": "Alfred Barr",
          "phone_number": "(222) 222-2222"
        }
      ],
      "country": "US",
      "postal_code": "11249",
      "region": "NY",
      "title": "Home"
    },
    "id": "c7660839-4fa5-4c39-a2f8-78348f1f7643",
    "insurance_policy": null,
    "internal_reference": null,
    "object_count": 1,
    "origin": {
      "access_restrictions": [],
      "address_line_1": "11 W 53rd St",
      "address_line_2": null,
      "address_line_3": null,
      "city": "New York",
      "contacts": [
        {
          "email_address": "mary@example.com",
          "name": "Mary Quinn Sullivan",
          "phone_number": "(333) 333-3333"
        }
      ],
      "country": "US",
      "postal_code": "10019",
      "region": "NY",
      "title": "Warehouse"
    },
    "package_count": 1,
    "packages": [
      {
        "depth": "6.0",
        "handle_with_care": false,
        "height": "14.5",
        "id": 1536,
        "is_sufficiently_packed": false,
        "objects": [
          {
            "current_packing": [],
            "depth": "2",
            "details": {
              "creation_date": null,
              "creator": "Robert Irwin",
              "is_cites": false,
              "is_fragile": false,
              "materials": [],
              "notes": null,
              "title": "All That Jazz"
            },
            "height": "10.5",
            "images": [],
            "internal_reference": null,
            "public_reference": null,
            "subtype": "painting_unframed",
            "type": "art",
            "unit_of_measurement": "in",
            "value": "15000",
            "value_currency": "USD",
            "weight": "3.5",
            "weight_unit": "lb",
            "width": "10"
          }
        ],
        "packing_materials": ["strongbox"],
        "unit_of_measurement": "in",
        "weight": "3.5",
        "weight_unit": "lb",
        "width": "14.0"
      }
    ],
    "public_reference": "SC-799212: New York, NY, US to New York, NY, US",
    "quote_type": "select",
    "schedule": {
      "delivery_end": null,
      "delivery_start": null,
      "pickup_end": null,
      "pickup_start": null
    },
    "services": [
      {
        "amount": "1.00",
        "amount_currency": "USD",
        "included_services": [],
        "is_requested": false,
        "is_required": true,
        "name": "Consolidated Trucking",
        "sub_subtype": "road_groupage",
        "subtype": "consolidated",
        "type": "transport"
      },
      {
        "amount": "1.00",
        "amount_currency": "USD",
        "included_services": [],
        "is_requested": false,
        "is_required": true,
        "name": "Strongbox",
        "sub_subtype": "strongbox",
        "subtype": "packing_materials",
        "type": "packing"
      },
      {
        "amount": "1.00",
        "amount_currency": "USD",
        "included_services": [],
        "is_requested": false,
        "is_required": true,
        "name": "Fuel Surcharge",
        "sub_subtype": "fuel_surcharge",
        "subtype": "fees",
        "type": "taxes_duties_fees"
      },
      {
        "amount": "1.00",
        "amount_currency": "USD",
        "included_services": [],
        "is_requested": false,
        "is_required": true,
        "name": "Debris Disposal",
        "sub_subtype": "debris_disposal",
        "subtype": "debris_disposal",
        "type": "handling"
      }
    ],
    "shipping_notes": null,
    "shortcode": "SC-799212",
    "status": "pending",
    "total": "4.00",
    "total_currency": "USD",
    "tracking": [],
    "updated_at": "2021-02-10T18:12:37.882318",
    "url": "https://connect.arta.io/shipments/c7660839-4fa5-4c39-a2f8-78348f1f7643/rHr9fi87GAmziLjwoXFRrQl3"
  },
  "object": "shipment",
  "type": "shipment.created"
}

Retries

ARTA's webhook delivery system expects a 2xx HTTP status code in response to receiving the event to indicate the event's successful receipt. If your endpoint returns any other status code during a delivery, ARTA will retry sending the event with exponential backoff in both live and test modes.

Verifying webhooks

ARTA delivers each webhook event with an Arta-Signature HTTP header. This enables you to verify that the events were sent by ARTA, not by a third party.

Before you can verify signatures, you need to retrieve your webhook endpoint’s secret from the "get a webhook secret token" API endpoint or via the webhook endpoint's detail page in the API Client Dashboard. ARTA generates a unique secret token for each endpoint.

You may optionally generate a new secret token with the "reset a webhook secret token" endpoint or from within the dashboard at any point. Note that changing the secret token will immediately cause future webhook events to use the newly generated token.

The Arta-Signature header included in each webhook delivery contains a timestamp and a signature. The timestamp is prefixed by t=, and the signature is prefixed with s=.

The following is an example header:

Arta-Signature: t=1623359782,s=j+WrZkH4UyzFXlgC3UsuRfnvYjeovCcAZPdKnG41sj4=

ARTA generates signatures using a hash-based event authentication code (HMAC) with SHA-256. The signature is then Base64 encoded when sent with the webhook.

The step-by-step guide below describes how to prepare and verify each event.

Step 1: Extract the timestamp and signatures from the header

Split the header, using the , character as the separator, to get a list of elements. Then split each element, using the = character as the separator, to get a prefix and value pair.

The value for the prefix t corresponds to the timestamp, and s corresponds to the signature.

Step 2: Prepare the signed_payload string

The signed_payload string is created by concatenating:

  • The timestamp (as a string)
  • The character .
  • The actual JSON payload (i.e., the request body)

Step 3: Determine the expected signature

Compute an HMAC with the SHA256 hash function. Use the webhook endpoint’s secret token as the key, and use the signed_payload string as the event.

Then Base64 encode the HMAC to complete generation of the expected signature.

Step 4: Compare the signatures

Compare the signature (or signatures) in the header to the expected signature.

Additionally, compute the difference between the current timestamp and the received timestamp, then decide if the difference is within your tolerance.

Example verification implementations

The next section provides reference verification implementations in a few different programming languages. Each implementation assumes that a webhook event is received with the following Arta-Signature header and event body:

# Webhook Arta-Signature header
t=1623359782,s=Hau27QgzVq3vr+ocQSx5bxoX1TLdz0IhcvGdBdvgsjg=

# Webhook event body
{\"data\":{\"id\":134},\"object\":\"webhook\",\"type\":\"ping\"}

As well, the associated webhook's secret token was retrieved from the "get a webhook secret token" API endpoint and had the following value:

2jjKqld6rjlUcl8pRB4mCM6hQrCuQ2GTGvp_otzQHGjpKYJ1-0AD3yToE2cKk25e

Using Elixir

# Retrieved via the ARTA API /webhooks/:id/secret_token endpoint
secret = "2jjKqld6rjlUcl8pRB4mCM6hQrCuQ2GTGvp_otzQHGjpKYJ1-0AD3yToE2cKk25e"

# Received via the webhook's Arta-Signature header
signature = "Hau27QgzVq3vr+ocQSx5bxoX1TLdz0IhcvGdBdvgsjg="

# Prepare the expected signed_payload in the `TIMESTAMP.PAYLOAD` format
signed_payload = "1623359782.{\"data\":{\"id\":134},\"object\":\"webhook\",\"type\":\"ping\"}"

# Generate the expected Base64 encoded hmac
expected = :crypto.mac(:hmac, :sha256, secret, signed_payload) |> Base.encode64()

# Validate that the expected signature matches the signature you received
IO.inspect(expected == signature)

Using Ruby

require 'base64'
require 'openssl'

# Retrieved via the ARTA API /webhooks/:id/secret_token endpoint
secret = "2jjKqld6rjlUcl8pRB4mCM6hQrCuQ2GTGvp_otzQHGjpKYJ1-0AD3yToE2cKk25e"

# Received via the webhook's Arta-Signature header
signature = "Hau27QgzVq3vr+ocQSx5bxoX1TLdz0IhcvGdBdvgsjg="

# Prepare the expected signed_payload in the `TIMESTAMP.PAYLOAD` format
signed_payload = "1623359782.{\"data\":{\"id\":134},\"object\":\"webhook\",\"type\":\"ping\"}"

# Generate the expected Base64 encoded hmac
hmac = OpenSSL::HMAC.digest("SHA256", secret, signed_payload)
expected = Base64.strict_encode64(hmac) 

# Validate that the expected signature matches the signature you received
print expected == signature

Using Python

import hashlib
import hmac
import base64

# Retrieved via the ARTA API /webhooks/:id/secret_token endpoint
secret = bytes('2jjKqld6rjlUcl8pRB4mCM6hQrCuQ2GTGvp_otzQHGjpKYJ1-0AD3yToE2cKk25e', 'utf-8')

# Received via the webhook's Arta-Signature header
signature = bytes('Hau27QgzVq3vr+ocQSx5bxoX1TLdz0IhcvGdBdvgsjg=', 'utf-8')

# Prepare the expected signed_payload in the `TIMESTAMP.PAYLOAD` format
signed_payload = bytes('1623359782.{\"data\":{\"id\":134},\"object\":\"webhook\",\"type\":\"ping\"}', 'utf-8')

# Generate the expected Base64 encoded hmac
hash = hmac.new(secret, signed_payload, hashlib.sha256)
expected = base64.b64encode(hash.digest())

# Validate that the expected signature matches the signature you received
print(signature == expected)