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

# Webhooks and signatures

> Get message, campaign and account events as they happen.

Add an https link in the app (**Developer → Webhooks**) or with `POST /v1/webhooks`, and pick the events you want. We post each event to your link as JSON. Copy the **signing secret** when it is shown: you see it once.

## The events

| Event | When |
| - | - |
| `message.sent` | A message left WhatsApp's servers for the person. |
| `message.delivered` | It reached the person's phone. |
| `message.read` | The person opened it. |
| `message.failed` | It could not be sent or delivered. |
| `message.received` | Someone sent you a message, or tapped a button. |
| `wallet.low` | Your wallet went below ₹500. |
| `number.quality_changed` | Your number's quality changed. |
| `number.status_changed` | Your number's status changed. |
| `number.name_changed` | Your display name was decided. |
| `template.status_changed` | A template was approved, rejected, paused or disabled. |
| `template.quality_changed` | A template's quality changed. |
| `template.created` | A template was made (in the app or by the API). |
| `template.deleted` | A template was deleted (in the app or by the API). |
| `campaign.started` | A campaign started sending. |
| `campaign.finished` | A campaign finished. |
| `campaign.paused` | A campaign was paused. |
| `campaign.stopped` | A campaign was stopped, or could not go on. |

## What we send

```json theme={null}
{
  "id": "evt_6f1c…",
  "type": "message.delivered",
  "created_at": "2026-10-22T19:41:08.120+05:30",
  "data": { "id": "…", "to": "+9198250*****", "status": "delivered", "price": "0.9131", "reference": "order-4521" }
}
```

`data` is the same as the API's own answer for that thing: a message event carries the message as `GET /v1/messages/{id}` shows it. Headers: `X-GetReply-Event` (the type) and `X-GetReply-Signature`.

## Check the signature

`X-GetReply-Signature` looks like `t=1761142268,v1=5f2a…`. `v1` is the HMAC-SHA256 of `t`, a dot, and the **raw** body, with your secret. Right after you make a new secret there are two `v1` values: accept either. Check the time too, so an old request can't be sent again.

```javascript Node.js theme={null}
import crypto from "node:crypto";

// rawBody: the request body exactly as it arrived (a string), before any JSON parsing.
export function verify(rawBody, header, secret) {
  const parts = header.split(",");
  const t = Number(parts.find((p) => p.startsWith("t="))?.slice(2));
  const given = parts.filter((p) => p.startsWith("v1=")).map((p) => p.slice(3));
  if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false;
  const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  return given.some((g) => g.length === expected.length && crypto.timingSafeEqual(Buffer.from(g), Buffer.from(expected)));
}
```

```php PHP theme={null}
<?php
// PHP 8 or newer.
// $rawBody = file_get_contents('php://input');
function verify(string $rawBody, string $header, string $secret): bool {
  $t = 0; $given = [];
  foreach (explode(',', $header) as $part) {
    if (str_starts_with($part, 't=')) $t = (int) substr($part, 2);
    if (str_starts_with($part, 'v1=')) $given[] = substr($part, 3);
  }
  if ($t === 0 || abs(time() - $t) > 300) return false;
  $expected = hash_hmac('sha256', $t . '.' . $rawBody, $secret);
  foreach ($given as $g) if (hash_equals($expected, $g)) return true;
  return false;
}
```

```python Python theme={null}
import hashlib, hmac, time

# raw_body: the request body as bytes, exactly as it arrived.
def verify(raw_body: bytes, header: str, secret: str) -> bool:
    parts = header.split(",")
    t = next((int(p[2:]) for p in parts if p.startswith("t=")), 0)
    given = [p[3:] for p in parts if p.startswith("v1=")]
    if not t or abs(time.time() - t) > 300:
        return False
    expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return any(hmac.compare_digest(expected, g) for g in given)
```

## Answer fast, then work

Answer with any **2xx** straight away, then do your work. Each event has an `id`: if the same `id` comes twice, handle it once.

* We wait up to 10 seconds for your answer. Any 2xx answer counts as delivered, and that event is never sent again.
* Anything else is tried again, 6 tries in all (see the table).
* A webhook that only fails for 24 hours is switched off. Switch it on again in Developer → Webhooks.
* After you make a new secret, the old one keeps signing for 24 hours.
* Every try is kept 7 days (Developer → Webhooks → Deliveries).

| Try | When |
| - | - |
| 1 | At once |
| 2 | 1 minutes after the try before |
| 3 | 5 minutes after the try before |
| 4 | 30 minutes after the try before |
| 5 | 2 hours after the try before |
| 6 | 6 hours after the try before |

A webhook that keeps failing is switched off. Turn it back on in the app: Developer → Webhooks → Switch on.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.