# Webhooks Guide

Several types of objects are processed asynchronously in the EasyPost system (e.g., Batches, Trackers).
To provide updates on these background tasks, EasyPost dispatches webhook events whenever a new Event occurs.

### Understanding Webhooks

Webhooks are push notifications (or callbacks) that keep applications up-to-date on the status of EasyPost objects.

- Webhooks eliminate the need to poll for updates.
- When triggered, each webhook sends an Event via HTTP `POST` request to the configured webhook URL.
- A successful `2XX` response is exepected from each webhook; in the event of a failure, EasyPost retries the webhook.

### Webhook Examples

- Webhook POST JSON
- Retrieve a Webhook Event JSON
- Receiving a Webhook

Related:

- [API Documentation](https://docs.easypost.com/docs/events)

---

## Prerequisites

- An active EasyPost account.
- Configure webhook URLs in the EasyPost account page.
- Application readiness to handle and process webhook events.
- Use HMAC validation or basic authentication with HTTPS to secure the webhook endpoint.

---

## Webhook Authentication

Securing webhook endpoints is crucial to ensure data integrity and authenticity. EasyPost recommends two primary methods for securing webhooks:

### HMAC Validation

HMAC validation ensures webhook data is untampered during transmission. Include a `webhook_secret` when creating or updating a webhook. EasyPost generates a signature with this secret, sent via the `X-Hmac-Signature` header in each event.

Use `validate_webhook()` from EasyPost’s client libraries (the function name may vary depending on the programming language–refer to the respective
library documentation for the exact name). Pass the webhook secret, headers, and event body to this function. If the signature matches, the
webhook is valid; otherwise, the event should be rejected to prevent fraudulent data.

For a full description of how validation works, including timestamp verification and signature calculation, see the Webhook HMAC Validation Help Center article. This article describes
the complete validation process for teams that need to understand or implement the logic manually.

Related:

- [EasyPost Client Libraries](https://docs.easypost.com/libraries)
- [Webhook HMAC Validation](https://support.easypost.com/hc/en-us/articles/39826034964237-Webhook-HMAC-Validation)

### Basic Authentication

Basic authentication requires a username and password in the webhook URL. For example:

`https://username:secret@www.example.com/easypost-webhook`

EasyPost will deliver a webhook to this endpoint, including an Authorization header. Validate the credentials to ensure they match stored values; otherwise, reject the webhook.

#### Create a Webhook

[Content omitted: API request example. Review the rendered documentation for complete request payloads, parameters, and code samples.]

---

## TLS and Certificate Validation

EasyPost performs certificate validation and requires that any webhook recipient using TLS (HTTPS) must have a certificate signed by a trusted public
certification authority. This helps ensure that the data being transmitted is secure.

Webhooks sent over outdated or insecure protocols, such as SSLv2 or SSLv3, are not supported. Additionally, any connection using export-grade ciphers will be rejected.

Refer to Mozilla's guide to Server-Side TLS
and Qualys's SSL/TLS Deployment Best Practices for guidance on properly securing server configurations.

---

## Event Filtering and Safelisting

### Event Type Header

To streamline event handling, EasyPost adds an `X-Easypost-Event-Type` header to webhook requests. Load balancers can use this header to filter out non-essential
events without parsing the entire payload, reducing infrastructure load.

---

## Custom Outbound headers

EasyPost supports custom headers on outbound webhook requests, allowing up to **three custom headers**, each with a name and value under **1000 ASCII characters**. The header
names and values are stored and returned as plaintext.

This feature allows customers to meet specific API gateway needs, such as load balancing or custom authorization checks, for more flexible integrations.

### Naming Conventions and Format

The `custom_headers` attribute in the webhook object is a list of key-value pairs, where each entry consists of:

- `<header_name>` and `<header_value>` must be valid strings, with a **character limit of 1000**.
- Each `<header_name>` must be **unique** within a webhook request.

### Validation Requirements

- Custom headers must be specified as an **array of key-value pairs**.
- If the `custom_headers` attribute is included in a `POST` or `PATCH` request, both `name` and `value` must be set for each header.
- Only ASCII characters are allowed in both `<header_name>` and `<header_value>`.

### Blocked Header Names and Prefixes

Certain headers are restricted to prevent conflicts with system-defined values:

- Headers prefixed with `X-EasyPost-*` **cannot be used**. Any attempt to set a custom header with this prefix will result in a failure.
- Headers using an existing `X-*` standard name or non-standard `X-*` headers will be overwritten in case of a conflict by EasyPost during webhook dispatch.

### Security

- Custom header values are stored in plaintext in EasyPost’s secure database. Encryption is **not** applied, so it is **strongly recommended** to use pre-encrypted values where necessary.
- Headers should **not contain sensitive information** unless properly secured before transmission.

### Updating Custom Headers

- Custom headers can be modified using a `PATCH` request.
- The entire list of headers will be replaced with the new list provided in the request. Individual additions or deletions of specific headers are **not supported**.

### RFC Compliance

All custom header names must conform to RFC 7230/ RFC 9112 formatting rules to ensure compatibility with standard HTTP header specifications.

---

## Response Time

Webhooks require a response within seven seconds. If no response is received, EasyPost will automatically retry the webhook.

**Recommendation:** Process the event in a background worker and immediately return a `2XX` status code to avoid duplicate notifications.

---

## Retrying Failed Webhooks

EasyPost will retry failed webhook deliveries up to six times, with an increasing delay between retries.

---

## Handling Sensitive Data

Avoid exposing sensitive information within webhook payloads. Secure transmission methods such as HTTPS and encryption are strongly recommended.

---

## Testing Webhook Integrations

To understand webhook event contents, use an endpoint mocking service like Beeceptor to create a simple endpoint for inspecting webhook requests.

> Note: EasyPost does not endorse Beeceptor; verify its security measures before sending actual user data.

---

## FAQs

**Q: How many times will EasyPost attempt to deliver a webhook?**

A: EasyPost will retry failed webhooks six times, with increasing delay intervals between attempts.

**Q: What HTTP status code is required for webhooks?**

A: A `2XX` status code is required, with `200 OK` or `200 Accepted` preferred.

**Q: How long is there to respond to a webhook?**

A: Respond within seven seconds. If no response is sent, the webhook event is considered a failure and will be retried.

**Q: How can webhook integrations be tested?**

A: Webhooks triggered in the **Test** environment behave the same way as in **Production**. Use services like Beeceptor to mock public endpoints for local testing.

**Q: Can webhooks be received for packages not shipped through EasyPost?**

Yes, create an EasyPost Tracker object using the desired tracking code to receive webhooks for non-EasyPost shipments.

**Q: How many webhook endpoints can be designated?**

Most users configure 1–5 endpoints, but up to 30 endpoints can be designated. If more are needed, contact EasyPost support.

---

## Webhook Examples

#### Webhook POST JSON Example

```json
{
  "mode": "production",
  "description": "batch.created",
  "previous_attributes": { "state": "purchasing" },
  "pending_urls": ["example.com/easypost-webhook"],
  "completed_urls": [],
  "created_at": "2015-12-03T19:09:19Z",
  "updated_at": "2015-12-03T19:09:19Z",
  "result": {
    "id": "batch_...",
    "object": "Batch",
    "mode": "production",
    "state": "purchased",
    "num_shipments": 1,
    "reference": null,
    "created_at": "2015-12-03T19:09:19Z",
    "updated_at": "2015-12-03T19:09:19Z",
    "scan_form": null,
    "shipments": [
      {
        "batch_status": "postage_purchased",
        "batch_message": null,
        "id": "shp_a5b1348307694736aaqqqq8fqda53f93"
      }
    ],
    "status": {
      "created": 0,
      "queued_for_purchase": 0,
      "creation_failed": 0,
      "postage_purchased": 1,
      "postage_purchase_failed": 0
    },
    "pickup": null,
    "label_url": null
  },
  "id": "evt_...",
  "object": "Event"
}
```

#### Retrieve a Webhook Event JSON Example

```json
{
  "description": "tracker.updated",
  "mode": "test",
  "previous_attributes": {
    "status": "pre_transit"
  },
  "created_at": "2022-10-26T20:18:21.000Z",
  "pending_urls": [],
  "completed_urls": [],
  "updated_at": "2022-10-26T20:18:21.000Z",
  "id": "evt_55a53eb2556b11ed8945059f515d2b6d",
  "user_id": "user_060ab38db3c04ffaa60f262e5781a9be",
  "status": "pending",
  "object": "Event"
}
```

#### Receiving a Webhook Example

```ruby
require 'easypost'
require 'sinatra'

post '/easypost-webhook' do
  result = params['result']

  case result['object']
  when 'Batch'
    batch = client.batch.create(result)

    case batch.state
    when 'purchase_failed'
      batch.shipments.each do |shipment|
        if shipment.batch_status == 'postage_purchase_failed'
          client.batch.remove_shipments(batch.id, shipments: [shipment])
        end
      end
    end
  end
end
```

---

## Additional Resources

Related:

- [Configuring Webhooks](https://support.easypost.com/hc/en-us/articles/360040154392-Configuring-Webhooks)
- [Help Center](https://support.easypost.com/hc/en-us)