> ## Documentation Index
> Fetch the complete documentation index at: https://docs.textsetu.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Signed HTTP callbacks that tell your systems the moment something changes.

Webhooks tell your systems the moment something changes in a project, so you
don't have to poll the API on a timer. TextSetu sends a signed HTTP request to a
URL you choose, every time an event you've subscribed to happens.

Typical uses:

* **Rebuild or redeploy** when translations are approved.
* **Invalidate a cache or CDN** when a value changes.
* **Kick off a CI job** when a branch is merged.
* **Sync to another system** when keys are added or removed.

<Note>
  **Over the API.** Webhooks can be managed entirely over the API as well as in
  the UI, and the payload schemas, headers and signature algorithm are all
  documented per-endpoint. See
  [the Webhooks endpoints](/api/introduction) in the API Reference.
</Note>

***

## Creating a webhook

1. Open **Project settings → Webhooks** and choose **New webhook**.
2. Enter the **Endpoint URL**. It must be publicly reachable over HTTPS.
3. Pick the **events** you want. Start from a preset — *All events*,
   *Translation content*, *Pipeline events* — then fine-tune, or tick individual
   events.
4. Leave **Include AI-generated changes** off unless you want them
   (see [Keeping the volume sane](#keeping-the-volume-sane)).
5. Save. TextSetu shows a **signing secret** beginning `whsec_`.

<Warning>
  **Copy the signing secret immediately**

  It is shown **once** and cannot be retrieved afterwards. If you lose it, rotate
  the secret to get a new one.
</Warning>

Your receiver uses that secret to verify each request genuinely came from
TextSetu. Until you verify signatures, treat the endpoint as untrusted — anyone
who learns the URL could post to it.

### Confirming it works

Use **Test** on the endpoint row. That queues a `ping` event, which shows up in
the delivery log within seconds. It's the quickest way to prove the URL is
reachable and your signature check is correct *before* you depend on real
events.

***

## Events

| Event                        | Fires when                                                            |
| ---------------------------- | --------------------------------------------------------------------- |
| **Key created**              | A translation key is added                                            |
| **Key updated**              | A key's name, description, tags or screenshot changes                 |
| **Key deleted**              | A key is removed                                                      |
| **Translation updated**      | A translation's text or approval status changes, for one language     |
| **Branch created**           | A translation branch is created                                       |
| **Branch merged**            | A branch is merged into the main line, with a summary of what changed |
| **Import completed**         | A file import finishes successfully                                   |
| **Export completed**         | A file export finishes and is ready to download                       |
| **Language added / removed** | A project language changes                                            |

**Subscription is explicit.** An endpoint only ever receives the events you
tick. There is no "everything" option that silently grows — if TextSetu adds a
new event type later, existing endpoints keep receiving exactly what they
received before until you opt in. New events can't become surprise traffic.

### Branches

**Changes made on a branch are not delivered.** Branch edits are staged as a
diff and are deliberately not part of project activity, so editing a translation
or adding a key *on a branch* fires nothing.

You're notified at the boundaries instead:

* **Branch created** when the work starts.
* **Branch merged** when it lands — carrying the number of keys added, keys
  deleted and values updated.

For a [branch-based workflow](/docs/guides/branches), treat **Branch merged** as your
signal to re-pull. That is the point at which the branch's changes become live
on main.

### Keeping the volume sane

Three things stop webhooks becoming a firehose:

* **An import fires one event, not one per key.** A 5,000-key import is a single
  **Import completed** delivery.
* **AI-generated changes are excluded by default.** A bulk
  [AI translation](/docs/guides/ai-translation) run can produce thousands of
  **Translation updated** events, so the *Include AI-generated changes* switch
  is off until you turn it on.
* **Unsubscribed events are never queued at all** — not sent, not retried, and
  absent from the log.

***

## The delivery log

Every attempt is recorded beneath the endpoints table for **30 days**, whether
it succeeded or not. Click a row to see the exact payload sent, the response
received, and how many attempts have been made.

| Status      | Meaning                                                                    |
| ----------- | -------------------------------------------------------------------------- |
| **pending** | Queued, not attempted yet                                                  |
| **success** | Delivered — your endpoint returned a 2xx                                   |
| **failed**  | Failed, and a retry is scheduled                                           |
| **dead**    | Gave up — attempts exhausted, or the response was permanently unacceptable |
| **skipped** | Never sent (endpoint disabled, or the source change no longer exists)      |

Filter by endpoint, event type or status, and sort any column. Use **Refresh**
to pull in deliveries that have landed since you opened the page — retries
arrive minutes or hours later, and the log doesn't update on its own.

### Replaying a delivery

**Replay** re-sends a delivery's original payload — useful after fixing an
outage on your side. The replay is a **new** delivery with its own webhook ID,
so a receiver that de-duplicates on that ID will still process it. The original
record is left untouched.

***

## Retries

A failed delivery is retried up to **five times** over roughly **seven and a
half hours**:

```
5s → 5min → 30min → 2h → 5h
```

What gets retried:

* **Retried:** `408`, `429`, any `5xx`, plus timeouts and connection or TLS
  failures.
* **Not retried:** other `4xx` responses (`401`, `403`, `404`, `410`, …). Those
  won't fix themselves, and retrying for seven hours wouldn't help — the
  delivery log tells you exactly what to fix.
* **Not retried:** redirects. TextSetu never follows them, so a `3xx` means the
  configured URL is wrong.

If your endpoint returns a `Retry-After` header on a `429` or `503`, TextSetu
honours it.

### Auto-disabling

After **10 consecutive failed deliveries** an endpoint is switched off
automatically and shown as **Auto-disabled**. That represents roughly ten
exhausted retry windows, so it means genuinely dead rather than a brief blip.

Fix your side, then **Enable** it again — which also clears the failure count,
giving the endpoint a full retry budget.

***

## Rotating the signing secret

Choose **Rotate secret** to issue a new one. The previous secret keeps working
for **24 hours**, and during that window TextSetu signs each request with
**both**. A receiver that accepts any valid signature can roll over without
dropping a single delivery.

Deploy the new secret within those 24 hours. After that, only the new one is
sent.

***

## URL requirements

URLs are checked when you save **and again on every delivery** — DNS can change
after an endpoint is created.

* **HTTPS** is required.
* **No credentials** in the URL (`https://user:pass@…`) and no `#fragment`.
* **Must resolve to a public address.** Private, loopback, link-local (including
  cloud metadata addresses) and carrier-NAT ranges are rejected, so a webhook
  can't be used to reach internal infrastructure.
* **Redirects are never followed** — point the endpoint at its final URL.

***

## Delivery guarantees

Design your receiver around these three properties:

* **At-least-once.** A delivery can arrive more than once. De-duplicate on the
  `webhook-id` header.
* **Unordered.** Two changes made in quick succession may arrive in either
  order. Don't infer sequence from arrival time.
* **Acknowledge fast.** Return a 2xx quickly and do the real work
  asynchronously — TextSetu gives up on an attempt after 10 seconds.

***

## Privacy

**Translation updated** includes the old and new translation text, sent to the
URL you configure. That's the point of the event, but it does mean translation
content leaves TextSetu — subscribe deliberately, and only to endpoints you
control.

***

## Troubleshooting

| Symptom                                     | Cause                                                                                                                                                  |
| ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Every delivery is `dead` with a `401`/`403` | Your endpoint is rejecting the request — usually signature verification. Check you're using the base64-decoded secret bytes, not the `whsec_…` string. |
| Deliveries are `dead` with a `404`          | Wrong path, or the route only accepts `GET`. Webhooks are always `POST`.                                                                               |
| The signature never matches                 | Verify against the **raw request body**, before JSON parsing. Re-serializing changes the bytes.                                                        |
| The endpoint shows **Auto-disabled**        | 10 consecutive failures. Fix the receiver, then **Enable**.                                                                                            |
| Nothing arrives at all                      | Check the event is ticked and the endpoint is **Enabled** — and if the change was made on a branch, see [Branches](#branches).                         |
| Nothing arrives from AI translation         | Turn on *Include AI-generated changes*.                                                                                                                |
| A delivery is `skipped`                     | The endpoint was disabled when it came up for sending, or the key it described was deleted before it went out.                                         |

***

## Permissions

Creating, editing and deleting webhooks — and viewing delivery payloads —
requires **project management** access, the same level as project API tokens,
since a webhook secret can be used to impersonate TextSetu to your receiver.
Listing endpoints and the delivery log needs project read access. See
[Roles and permissions](/docs/guides/roles-and-permissions).
