> ## 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.

# Build an approvals notifier

> Post approval activity into a chat channel with webhooks, and act on pending queues through MCP.

OnTime notifies people **in-app only** — no email, no chat. This guide builds the bridge to wherever your team actually looks.

## What you can and cannot do

<Warning>
  **Webhooks fire on *decisions*, not on *requests*.** There is no `leave.requested` event. You will be told when a leave request is approved or rejected; you will **not** be told when one arrives needing a decision.

  That is the opposite of what a "pending approvals" bot needs. So this splits into two mechanisms:
</Warning>

| Goal                                  | Mechanism                     |
| ------------------------------------- | ----------------------------- |
| **Announce decisions** as they happen | Webhooks — push, immediate.   |
| **Surface pending work**              | MCP — pull, on your schedule. |

## Part 1 — Announce decisions

Register a webhook subscribing to the decision events, then post each one to chat.

```typescript theme={null}
import crypto from "node:crypto";
import express from "express";

const app = express();
const SECRET = process.env.ONTIME_WEBHOOK_SECRET!;
const CHAT_WEBHOOK = process.env.CHAT_WEBHOOK_URL!;

const LABELS: Record<string, string> = {
  "leave.approved": "✅ Leave approved",
  "leave.rejected": "❌ Leave rejected",
  "regularization.approved": "✅ Attendance correction approved",
  "regularization.rejected": "❌ Attendance correction rejected",
  "overtime.approved": "✅ Overtime approved",
  "overtime.rejected": "❌ Overtime rejected",
};

const seen = new Set<string>(); // use a durable store in production

app.post(
  "/webhooks/ontime",
  express.raw({ type: "application/json" }),
  async (req, res) => {
    const expected =
      "sha256=" +
      crypto.createHmac("sha256", SECRET).update(req.body).digest("hex");
    const got = req.get("X-Ontime-Signature") ?? "";

    const a = Buffer.from(got);
    const b = Buffer.from(expected);
    if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
      return res.sendStatus(401);
    }

    // Acknowledge before doing anything — the delivery times out at 30s.
    res.sendStatus(200);

    const deliveryId = req.get("X-Ontime-Delivery")!;
    if (seen.has(deliveryId)) return; // at-least-once: deduplicate
    seen.add(deliveryId);

    const payload = JSON.parse(req.body.toString("utf8"));
    const label = LABELS[payload.event];
    if (!label) return; // unknown event: acknowledged, ignored

    // The payload carries a reference, not the record. Resolve the employee name
    // from your own cache of GET /api/public/v1/employees.
    const who = await employeeName(payload.data.recipientId);

    await fetch(CHAT_WEBHOOK, {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({ text: `${label} — ${who}` }),
    });
  },
);
```

<Note>
  **Resolving names costs a lookup.** The payload gives you `recipientId`, not a name. Cache the directory from `GET /api/public/v1/employees` and refresh it daily rather than fetching per event.
</Note>

Full signature-verification detail is in [Webhooks](/developers/webhooks/overview).

## Part 2 — Surface pending work

For "what is waiting on me", use the **MCP server**. An MCP key is bound to a specific employee and acts with exactly that person's permissions and scope — so a manager's bot sees their direct reports' pending requests, and nobody else's.

Relevant tools:

| Tool                          | Returns                                   |
| ----------------------------- | ----------------------------------------- |
| `ontime_pending_approvals`    | Everything waiting on the bound employee. |
| `ontime_leave_requests`       | The leave queue.                          |
| `ontime_regularization_queue` | The regularization queue.                 |
| `ontime_overtime_mine`        | Overtime requests.                        |

And to act:

| Tool                           | Does                            |
| ------------------------------ | ------------------------------- |
| `ontime_leave_decide`          | Approve or reject leave.        |
| `ontime_regularization_decide` | Approve or reject a correction. |
| `ontime_overtime_decide`       | Approve or reject overtime.     |

Setup and the full catalog are in the **AI & MCP** tab.

<Warning>
  **Scope applies, and so does the same-actor rule.** A bot acting as an employee cannot approve that employee's own requests, and cannot see anything outside their scope. This is a feature — it means a compromised bot key is no more powerful than the person it acts as.
</Warning>

## A reasonable architecture

```mermaid theme={null}
flowchart LR
  O[OnTime] -->|webhook: decisions| B[Your bot]
  B -->|post| C[Chat channel]
  T[Timer, e.g. 9am daily] --> B
  B -->|MCP: pending approvals| O
  B -->|digest| C
```

Push for decisions — they are time-sensitive and low-volume. Pull for pending work — a morning digest beats a notification per request.

## Operating it

<Steps>
  <Step title="Monitor your own endpoint">
    The webhook flips to `Failing` on the first failure, but the OnTime alert only fires after all eight retries — roughly 22 hours later.
  </Step>

  <Step title="Deduplicate durably">
    The in-memory `Set` above is illustrative. Restart it and you re-post events.
  </Step>

  <Step title="Check notification preferences">
    Webhook delivery is gated by the organization's notification preferences. If a category is switched off in Settings → Notifications, that webhook never fires — check there before the delivery log.
  </Step>

  <Step title="Replay from the delivery log">
    Failed deliveries are not lost. Fix the endpoint, then replay.
  </Step>
</Steps>
