# Receiving & Verifying Webhooks

Echo delivers events as HTTP POST requests with JSON bodies. Your endpoint verifies each request's signature, responds quickly with a success status code, and processes the payload asynchronously.

## What a Webhook Request Looks Like

Every request from Echo has this shape (payload shortened for illustration; see the API Reference for full schemas):

```http
POST /webhooks/echo HTTP/1.1
Host: api.yourcompany.com
Content-Type: application/json
X-Echo-Hmac-SHA256: k4v2kdEU9+Lb+0YYpgKsLLHWFNB8i3w+j191ayygIPM=
X-Echo-Timestamp: 2026-08-12T14:30:00Z
X-Echo-Timestamp-Original: 2026-08-12T14:30:00Z
X-Echo-Retry-Attempt: 0
X-Echo-Webhook-Id: wh_12345abcdef
x-api-key: your-api-key-value
Authorization: Bearer eyJhbGc...

{"ediType":"214 Load Status Update","echoCustomerId":"E16452","loadId":57394717}
```

The `x-api-key` and `Authorization` headers only appear if you configured that [endpoint authentication](/echosync-webhooks/getting-started) at registration, along with any custom headers you provided.

To simulate a delivery against your own endpoint (the signature below is valid for this exact body and the secret `Echo-Example-Secret-2026`):

```bash
curl -X POST https://api-qa.yourcompany.com/webhooks/echo \
  -H 'Content-Type: application/json' \
  -H 'X-Echo-Hmac-SHA256: k4v2kdEU9+Lb+0YYpgKsLLHWFNB8i3w+j191ayygIPM=' \
  -H 'X-Echo-Timestamp: 2026-08-12T14:30:00Z' \
  -H 'X-Echo-Timestamp-Original: 2026-08-12T14:30:00Z' \
  -H 'X-Echo-Retry-Attempt: 0' \
  -H 'X-Echo-Webhook-Id: wh_12345abcdef' \
  --data '{"ediType":"214 Load Status Update","echoCustomerId":"E16452","loadId":57394717}'
```

## Request Headers

| Header | Purpose | Changes on retry? |
|  --- | --- | --- |
| `X-Echo-Hmac-SHA256` | Base64-encoded HMAC-SHA256 of the payload, computed with your Signing Secret | No1 |
| `X-Echo-Timestamp` | Timestamp of the current delivery attempt (UTC ISO 8601) | Yes |
| `X-Echo-Timestamp-Original` | Timestamp of the first delivery attempt | No |
| `X-Echo-Retry-Attempt` | 0 for the initial attempt, increments +1 with each retry | Yes |
| `X-Echo-Webhook-Id` | Unique identifier for the event; identical across all retries, use it for idempotency | No |
| `Authorization` | `Bearer {token}` if OAuth is configuredor`Basic <credentials>` if Basic Auth is configured | Varies2 |
| Your API key header | Only if configured | No |
| Your custom headers | Only if configured | No |


small
sup
1
The payload is identical on every retry, so its signature is too.

br
sup
2
OAuth Bearer tokens are refreshed if expired; a Basic Auth value never changes.

## Verifying the Signature (Required)

Every payload is signed with your **Signing Secret**, the value you created and gave Echo at registration. Verify it before processing anything:

1. Read the base64-encoded signature from the `X-Echo-Hmac-SHA256` header.
2. Compute HMAC-SHA256 over the **raw request body** (the exact bytes received, not parsed and re-serialized JSON) using your Signing Secret, and base64-encode the result.
3. Compare the two using a timing-safe comparison. Match: the payload is authentic and untampered. Mismatch: reject it.


### Worked Example

You can use these exact values to test your implementation:

| Input | Value |
|  --- | --- |
| Signing Secret | `Echo-Example-Secret-2026` |
| Raw body | `{"ediType":"214 Load Status Update","echoCustomerId":"E16452","loadId":57394717}` |
| **Expected signature** | `k4v2kdEU9+Lb+0YYpgKsLLHWFNB8i3w+j191ayygIPM=` |


If your code produces a different signature for these inputs, the usual cause is hashing a modified body (re-serialized JSON, added whitespace, or a trailing newline) instead of the raw bytes.

### Code Examples

**Python**

```python
import hashlib
import hmac
import base64

def generate_hmac(data, secret):
    """Generate HMAC-SHA256 for the given data and secret."""
    hmac_obj = hmac.new(secret.encode(), msg=data.encode(), digestmod=hashlib.sha256)
    return base64.b64encode(hmac_obj.digest()).decode()

def validate_payload(received_hmac, data, secret):
    """Validate webhook payload using timing-safe HMAC comparison."""
    generated_hmac = generate_hmac(data, secret)
    return hmac.compare_digest(received_hmac, generated_hmac)
```

**Node.js**

```javascript
const crypto = require('crypto');

function generateHmac(data, secret) {
    const hmac = crypto.createHmac('sha256', secret);
    hmac.update(data);
    return hmac.digest('base64');
}

function validatePayload(receivedHmac, data, secret) {
    // Timing-safe comparison
    const generatedHmac = generateHmac(data, secret);
    return crypto.timingSafeEqual(Buffer.from(receivedHmac), Buffer.from(generatedHmac));
}
```

**C#**

```csharp
using System;
using System.Security.Cryptography;
using System.Text;

public class WebhookValidator
{
    /// <summary>
    /// Validates webhook payload using HMAC-SHA256 signature verification.
    /// </summary>
    /// <param name="data">The raw webhook payload</param>
    /// <param name="receivedHmac">The value of the X-Echo-Hmac-SHA256 header</param>
    /// <param name="secret">Your Signing Secret</param>
    public bool ValidateWebhook(string data, string receivedHmac, string secret)
    {
        byte[] key = Encoding.UTF8.GetBytes(secret);
        byte[] body = Encoding.UTF8.GetBytes(data);

        using var hmac = new HMACSHA256(key);
        byte[] hash = hmac.ComputeHash(body);
        string calculatedHmac = Convert.ToBase64String(hash);

        // Timing-safe comparison
        return CryptographicOperations.FixedTimeEquals(
            Encoding.UTF8.GetBytes(calculatedHmac),
            Encoding.UTF8.GetBytes(receivedHmac));
    }
}
```

## How Echo Authenticates to Your Endpoint (Optional)

If you configured [endpoint authentication](/echosync-webhooks/getting-started) at registration, Echo presents those credentials with every request:

**API key:** your chosen header name and value are included as-is on every request.

**Basic Auth:** supported as a static header pair: you provide `Authorization` as the header name and `Basic <base64(username:password)>` as the value at registration, and it's included on every request. Mutually exclusive with OAuth, since both occupy the `Authorization` header.

**OAuth 2.0:** Echo runs the client credentials flow against your token endpoint:

1. Echo POSTs to your token endpoint (`application/x-www-form-urlencoded`) with the `client_id`, `client_secret`, and `grant_type=client_credentials` you provided (plus `scope`/`audience` if configured)
2. Your endpoint returns a standard OAuth 2.0 token response:

```json
{
  "token_type": "Bearer",
  "access_token": "eyJhbGc...",
  "expires_in": 3600
}
```
3. Echo caches the token, sends it as `Authorization: Bearer {token}`, and refreshes it before expiration, transparently and without affecting delivery


**Custom headers:** included as-is on every request.

Remember: these authenticate Echo's request *to your infrastructure*. They do not replace signature verification, which is how you confirm the request actually came from Echo.

## Responding

- Respond within **15 seconds**; otherwise the attempt counts as failed and is retried
- Return `200 OK`, `202 Accepted`, or `204 No Content`; any other status code triggers a retry
- Return a success code even if your internal processing fails after receipt; handle those failures in background processing. This prevents unnecessary retries and protects you from [deregistration](/echosync-webhooks/reliability).


## Implementation Best Practices

**Acknowledge fast, process asynchronously.** On receipt: verify the signature, persist the raw webhook (database, queue), return a success response, then process in the background. Don't run business logic before responding.

**Process each webhook exactly once.** Before processing, check whether the `X-Echo-Webhook-Id` was already handled. If yes, skip processing but still return success; retries deliver the same Id with an identical payload.

**Log enough to troubleshoot.** For each webhook, store the `X-Echo-Webhook-Id`, both timestamps, the retry attempt number, the signature verification result, and the processing outcome. Echo support will ask for webhook Ids and timestamps.

**Build for resilience.** Verify the signature before any processing, handle malformed input gracefully, use circuit breakers for downstream dependencies, and route failed background processing to a dead-letter queue. Monitor response times and alert well before the 15-second limit.