# 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. The `resultId` is the result of the step that sent the document to the partner (the SFTP upload, or the AS2 send, which fires when the transfer starts; the partner's MDN arrives later): its `output` artifact is the generated EDI and its `detail.x12` carries the control numbers. Optional.
- **Error Webhook**: fires when a processing error occurs in any of this partner's flows (inbound or outbound). The `resultId` is the failed result written by the step that failed, such as an implementation validation or an SFTP upload, so fetching it gives you `status: "error"`, `detail.errorMessage`, `detail.errors` when the step itemized its findings (implementation validation lists one per violation), and `detail.incomingResultId`, the result the step was processing, when it was given one. See [Results and Artifacts](/resources/api-docs/results). Optional.

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

Use a separate webhook for the outbound and error roles. Both deliveries send the same body, `{timestamp, output: {resultId, traceGuid}}`, with the same headers and no field naming the event, so a partner that points both roles at one webhook gives your endpoint no way to tell a delivered document from a failed one without fetching the result. Two webhooks with two URLs make the distinction on arrival. Inbound and error deliveries look alike too, so share a webhook across those roles only if your handler fetches every result before acting on it.

`GET /platform/webhooks` and `GET /platform/webhooks/:id` return `signingSecretSet`, a boolean that says whether the webhook has a signing secret and so whether its deliveries carry a verifiable `X-Webhook-Signature`. The secret itself is never returned.

## 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, so store it securely. Each webhook has its own secret key, so deliveries for a partner whose three slots use three webhooks are signed with three keys. Give each webhook its own URL (a distinct path or query string is enough), so your endpoint knows which secret to verify with and which event it received.
- **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 result was ready for delivery (ISO 8601 UTC). It is the same on every attempt and on a redelivery, so the body is identical each time. The send time is the `t` value in `X-Webhook-Signature`
- `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`: the delivery's idempotency key (see Retry Behavior below). Stable across every attempt of one delivery; a redelivery gets a new one
- `X-Webhook-Attempt`: which attempt of this delivery the request is, starting at 1. A delivery gets at most 6; a redelivery starts again at 1
- `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. Some failures look transient and are retried on a schedule; the rest are final on the first answer:

```
| Condition                                             | Retried?                               |
|-------------------------------------------------------|----------------------------------------|
| 500, 502, 503, 504                                    | yes                                    |
| 429                                                   | yes, Retry-After can lengthen the wait |
| Connection timed out (10s), refused, reset, or closed | yes                                    |
| Any 4xx other than 429, or any other 5xx              | no                                     |
| A destination Tediware's outbound guard blocks        | no                                     |
| DNS failure or TLS error                              | no                                     |
| A redirect                                            | no (redirects are not followed)        |
```

A 500 is retried like the gateway codes, so a handler that fails partway through will see the same delivery again. Make your handler safe to run twice for one `X-Request-ID`. A 4xx other than 429 is final: it says the request itself is wrong, and sending it again would not change the answer.

A retryable failure gets 6 attempts in total, waiting about 3 seconds, 18 seconds, 83 seconds, 4 minutes, then 10 minutes between them, roughly 16 minutes end to end, which covers a typical deploy window. Retries run on the job queue rather than holding a connection open. A `Retry-After` header on a 429 or 503, in seconds or as an HTTP date, is used when it asks for a longer wait than the schedule, capped at 10 minutes. It never shortens a wait.

Every attempt of one delivery sends the same `X-Request-ID`, so a 502 from a gateway does not prove your handler never ran; dedupe on `X-Request-ID` rather than assuming each header value is a new event. A redelivery (see below) sends a new `X-Request-ID`, since it is a new delivery on purpose, but the same `resultId` in the body.

An intermediate failed attempt writes a delivery record and a line on the trace, not an error result or an error feed entry. Those are written once, either when every attempt has failed or immediately on a failure that was never retryable.

**Exception:** the error webhook, the one that notifies you of a failed result, is not retried. It runs inside the failing step's own error handling, and retrying it there would re-run that step, so it gets a single attempt.

This is worth reading twice, because the common assumption is the opposite. If your endpoint returns a 4xx other than 429, or a retryable failure lasts through every attempt, 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 on its own.

The document itself is not lost. For an inbound document, redeliver it (see below). You can also fetch it from the feed, 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.

## Redelivering an Inbound Document

For an inbound document, you can have Tediware deliver it again at any time, not only after a failure: a receiver can lose a document after already answering 2xx. Use the **Redeliver** button on the transaction's page, or `POST /platform/edi_transactions/:id/redeliver` (see [EDI Transactions](/resources/api-docs/edi-transactions)).

Redelivery re-runs the same delivery on the same trace: nothing is re-parsed and no new transaction is created. The body carries the same `resultId` as the original delivery, with a new `X-Request-ID`, so a receiver that already processed that result can recognize and skip the duplicate. A 997 generated for the document is not re-sent; resend it from its own transaction page.

## 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 a final error, or kept failing through every retry, and you need the document**: For an inbound document, redeliver it (see Redelivering an Inbound Document above). You can also 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).
