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 schema
Every webhook payload is comprised of three main parts:
-
data — a representation of the updated resource
- For request resources the schema is identical to the retrieve request API endpoint
- For shipment resources the schema is identical to the retrieve shipment API endpoint
- object — the name of the resource type; at present the value will always be either
request
orshipment
- 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)