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

# API keys and webhooks

> Issue and revoke API keys, register webhook endpoints, and inspect or replay outbound deliveries.

export const PermissionNote = ({perm}) => <Info>
    Requires the <code>{perm}</code> permission. Ask your administrator if an
    action described here is missing from your screen — see
    [Roles & permissions](/help/concepts/roles-and-permissions).
  </Info>;

<PermissionNote perm="api:manage" />

**API & webhooks** in the admin sidebar. This is where OnTime's machine interfaces are managed.

## API keys

### Issuing one

<Steps>
  <Step title="Issue a key">
    Give it a name that says what it is for — "Payroll export job", not "key 2".
  </Step>

  <Step title="Choose a scope">
    `ReadOnly` or `ReadWrite`.

    <Note>
      **Both scopes currently grant exactly the same access.** The public API surface is read-only — there are no public write routes — so a `ReadWrite` key behaves identically to a `ReadOnly` one. The scope records your intent and is what a future write route would gate on.
    </Note>
  </Step>

  <Step title="Copy the key">
    <Warning>
      **The key is shown exactly once.** OnTime stores only a hash. If you lose it, revoke it and issue a new one — there is no way to retrieve it.
    </Warning>

    Keys look like `ont_` followed by a hex string.
  </Step>
</Steps>

### Using one

Send it as a bearer token:

```bash theme={null}
curl https://app.ontime.hosai.app/api/public/v1/employees \
  -H "Authorization: Bearer ont_..."
```

Full details, including the endpoints available, are in the **Developers** tab.

### Revoking

Immediate. A revoked key is refused on the next request.

<Note>
  **There is no rotate action.** Rotating means issuing a new key, updating your consumer, then revoking the old one — in that order, so you never have a gap.
</Note>

### Rate limit

600 requests per minute, per key.

<Warning>
  The limiter holds its counters **in process memory**. Restarting the backend resets every key's counter, and if OnTime is ever run as more than one instance each would allow 600 per minute independently. Treat 600 as the intended limit rather than a guaranteed ceiling.
</Warning>

## Webhooks

### Registering one

<Steps>
  <Step title="Give the endpoint URL">
    An HTTPS URL you control.
  </Step>

  <Step title="Copy the signing secret">
    <Warning>
      **Shown exactly once**, like an API key. You need it to verify that deliveries genuinely came from OnTime.
    </Warning>
  </Step>
</Steps>

Each delivery is signed with HMAC-SHA256 and carries a delivery ID header your consumer can use to deduplicate. Verification is documented under the **Developers** tab.

### Status

| Status    | Meaning                                   |
| --------- | ----------------------------------------- |
| `Healthy` | Deliveries are succeeding.                |
| `Failing` | At least one delivery attempt has failed. |

### Which events fire

<Warning>
  **Nine event types**, not the full notification catalog: leave approved and rejected, regularization approved and rejected, overtime approved and rejected, payslip published, Form 16 available, and message received.

  Everything else — comp-off earned, swap decisions, document verified — raises an in-app notification but **never reaches a webhook**.
</Warning>

## The delivery log

Every outbound attempt, with its status.

### Retries and the dead-letter queue

A failed delivery is retried with escalating backoff — roughly a minute, then quadrupling, up to eight attempts over about 22 hours. An event that exhausts all eight moves to **Failed**: the dead-letter queue.

**Replay** re-queues a failed delivery. Fix your endpoint, then replay.

### Alerting

When an event lands in the dead-letter queue, every `api:manage` holder gets an in-app notification.

<Warning>
  **The alert fires on exhaustion, not on the first failure.** A webhook flips to `Failing` on its very first failed attempt, but the notification only arrives once an event has burned through all eight retries — roughly 22 hours later.

  So an endpoint can show a red `Failing` badge for most of a day before anyone is actively told. Check the badge; do not wait for the alarm. (A transient failure that recovers on retry correctly never alerts at all.)
</Warning>

Nothing is lost in the meantime — a delivery that cannot get through lands in the queue and is replayable. The gap is in how promptly you learn, not in whether the event survives.
