# Webhooks

Webhooks are the recommended way to receive processing outcomes when your system can host a public HTTPS endpoint. Tediware calls your endpoint the moment a document is processed, delivered, or hits an error, so you get a signed, event-driven push with nothing to schedule. Each delivery includes a `resultId` that your system uses to fetch the full result and download the translated data. If your platform cannot accept inbound HTTP requests, use Polling instead: the same outcomes are available from a feed you pull on your own schedule. See the Overview page for how the two approaches compare and how they fit into the inbound and outbound data flows.

## The Webhook Flow

These two flows show where the webhook fits. Tediware notifies your system the moment a document is processed (inbound) or delivered (outbound), and your system then fetches the full data through the Results API.

[INBOUND DIAGRAM]

[OUTBOUND DIAGRAM]

## Webhook Types

Each partner can have up to three webhooks configured:

- **Inbound Webhook** -- fires when inbound EDI from this partner has been processed and converted to JSON. This is typically the trigger for your system to fetch the translated data -- use the `resultId` from the payload to call `GET /platform/results/:id`, then download the output artifact. Required for inbound flows.
- **Outbound Webhook** -- fires when outbound EDI has been successfully generated and delivered to the partner's connection. Use the `resultId` to fetch the result if you need the generated EDI content or control numbers. Optional.
- **Error Webhook** -- fires when a processing error occurs in any of this partner's flows (inbound or outbound). The result's `errorMessage` field contains details about what went wrong. Optional.

Webhooks are assigned on the partner detail page under the **Configuration** section.

## Configuration

Webhooks are created on the **Webhooks** page and then assigned to partners. Each webhook has the following fields:

- **Name** -- a label for your reference
- **URL** -- the HTTP endpoint that will receive POST requests
- **Content Type** -- the `Content-Type` header sent with requests (defaults to `application/json`)
- **Secret Key** -- used to sign payloads so you can verify they came from Tediware. Auto-generated when the webhook is created. Displayed in full only at creation time -- store it securely.
- **API Key** -- an optional key sent in the `X-Api-Key` header. Use this if your receiving endpoint requires its own authentication.

## Payload Format

Every webhook delivery sends a JSON POST request with the following body:

```json
{
  "timestamp": "2026-03-30T14:22:33Z",
  "output": {
    "resultId": "4c2021f8-8310-4628-80f9-b578d4d68e11",
    "traceGuid": "5ab72145-4a4b-40f6-95de-e2579f163f79"
  }
}
```

- `timestamp` -- when the webhook was dispatched (ISO 8601 UTC)
- `output.resultId` -- the ID of the processing result. Use this with `GET /platform/results/:id` to retrieve the full result including artifacts.
- `output.traceGuid` -- the trace identifier linking all results from the same processing run

The payload is intentionally minimal. Tediware sends only identifiers, never the document content itself. Your system uses these identifiers to pull the full data through the Results API at its own pace. This keeps webhook payloads small, avoids transmitting sensitive business data over the webhook channel, and lets your system process results asynchronously.

## HTTP Headers

Each webhook request includes the following headers:

- `Content-Type` -- the content type configured on the webhook (default `application/json`)
- `X-Webhook-Signature` -- an HMAC-SHA256 signature for verifying the payload
- `X-Request-ID` -- a unique identifier for this delivery attempt
- `X-Api-Key` -- the API key configured on the webhook (only included if set)

## Verifying Signatures

Every webhook delivery is signed using your webhook's secret key. Verifying the signature confirms that the request came from Tediware and that the payload has not been tampered with.

### Signature Format

The `X-Webhook-Signature` header contains a timestamp and a signature separated by a comma:

```
t=1743350553,v1=5d41402abc4b2a76b9719d911017c592aeca6fba3b4e2f8c1d2e3f4a5b6c7d8e
```

- `t` -- the Unix timestamp when the signature was generated
- `v1` -- the HMAC-SHA256 hex digest

### Verification Steps

1. Extract the `t` and `v1` values from the `X-Webhook-Signature` header
2. Construct the signed payload string by joining the timestamp and the raw request body with a period: `{t}.{request_body}`
3. Compute the HMAC-SHA256 of the signed payload string using your webhook's secret key
4. Compare the computed hex digest with the `v1` value. If they match, the signature is valid.
5. Optionally, check that the timestamp is recent (e.g., within the last 5 minutes) to guard against replay attacks.

### Example (Node.js)

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

function verifyWebhookSignature(rawBody, signatureHeader, secretKey) {
  const parts = Object.fromEntries(
    signatureHeader.split(",").map((p) => p.split("=", 2))
  );

  const signedPayload = `${parts.t}.${rawBody}`;
  const expected = crypto
    .createHmac("sha256", secretKey)
    .update(signedPayload)
    .digest("hex");

  return crypto.timingSafeEqual(
    Buffer.from(expected, "hex"),
    Buffer.from(parts.v1, "hex")
  );
}
```

### Example (Python)

```python
import hmac
import hashlib

def verify_webhook_signature(raw_body, signature_header, secret_key):
    parts = dict(p.split("=", 1) for p in signature_header.split(","))
    signed_payload = f"{parts['t']}.{raw_body}"
    expected = hmac.new(
        secret_key.encode(), signed_payload.encode(), hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, parts["v1"])
```

### Test Vector

Check your verifier against these fixed values before your first live delivery. They are a real signature produced by the same code that signs your webhooks, so if your implementation reproduces this digest it will verify a real delivery, and if it does not you have found the bug on your own schedule rather than during an incident.

```
Secret key: 3d8f1a05c7b24e69f0a1c2d3e4b5a6978c0d1e2f
Request body: {"timestamp":"2026-03-30T14:22:33Z","output":{"resultId":"4c2021f8-8310-4628-80f9-b578d4d68e11","traceGuid":"5ab72145-4a4b-40f6-95de-e2579f163f79"}}
X-Webhook-Signature: t=1774880553,v1=22cf1eac4cbd3dc41170979528da75e20b4bc925e2faabf872a00939c5387906
```

Feed the secret, the body, and the header to your verification function; it should pass. Two details this catches:

- The signed string is `1774880553.{"timestamp":...}`: the `t` value from the header, a literal period, then the request body exactly as received. Re-serializing a parsed body is the usual reason a verifier that looks correct fails on real traffic, since even a change in spacing produces a different digest.
- The `timestamp` inside the body (ISO 8601) and the `t` in the header (Unix seconds) are two separate values. Sign with `t`.

## Retry Behavior

Tediware expects your webhook endpoint to respond within 10 seconds and return a 2xx status code. If the request times out or a network error occurs, Tediware retries up to 2 additional times (3 total attempts) with progressive delays:

- First retry: after 1 second
- Second retry: after 2 seconds

Non-2xx HTTP responses (such as 400 or 500) are treated as failures but are **not retried**. Only timeouts and connection errors trigger retries.

This is worth reading twice, because the common assumption is the opposite. If your endpoint returns a 500, Tediware does not try again later. The delivery is recorded as failed and that is the end of it: nothing will arrive at your endpoint a second time.

The document itself is not lost. Fetch it from the feed instead, which is populated for every partner whether or not it uses webhooks. See [Recovering a Delivery Your Endpoint Failed](/resources/api-docs/polling) for the steps. If your handler can fail in a way you cannot fix in the moment, prefer returning 2xx once you have durably accepted the payload and doing the real work asynchronously.

## Delivery History

Tediware records every webhook delivery attempt, including the payload sent, the response received, and any errors. These records are available for 30 days and are then automatically cleaned up. See the Data Retention documentation for details on all retention periods.

## Troubleshooting

- **Webhook not firing** -- Verify that the partner's flow is set to **Active** and that the webhook is assigned to the correct slot (inbound, outbound, or error) on the partner configuration.
- **Signature verification failing** -- Ensure you are using the raw request body (not a parsed and re-serialized version) when constructing the signed payload string. The secret key must match exactly what was provided at webhook creation.
- **Delivery timeouts** -- Your endpoint must respond within 10 seconds. If processing takes longer, accept the webhook immediately and handle the work asynchronously.
- **Repeated failures** -- Check the webhook delivery history in the application for status codes and error messages. Common issues include expired TLS certificates, firewall rules blocking Tediware's IP, or the endpoint being temporarily unavailable.
- **Your endpoint returned an error and you need the document** -- It will not be delivered again. Pull it from the feed with `GET /platform/feed_entries?status=error`, take the `resultId`, and download the output artifact. The full steps are under [Recovering a Delivery Your Endpoint Failed](/resources/api-docs/polling).
