# The webhook your server gets

`POST` to your webhook address

WordPlus Cloud posts what happens in your app to the webhook address on your app page in your WordPlus Cloud
account, in batches, signed as a request to the API is, with a key of its own
([webhooks](/api/webhooks/)). Check the signature against the body as it came, then answer 2xx. Anything else is
tried again, later each time, for 24 hours: drop a repeat by its `id`.

## Parameters

| Name | In | |
|---|---|---|
| `X-WordPlus-Site` (required) | header | Your app key. |
| `X-WordPlus-Timestamp` (required) | header | Unix seconds. |
| `X-WordPlus-Signature` (required) | header | Hex HMAC-SHA256 under the webhook key. |

## Request

```json
{
  "events": [
    {
      "id": "0b6f1c9e-8f5d-4c38-9d2b-6a1e2f3c4d5e",
      "at": 1791638400000,
      "type": "room.occupied",
      "room": "order:42"
    },
    {
      "id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
      "at": 1791638401000,
      "type": "emit",
      "room": "chat:34:42",
      "event": "message",
      "data": "ch:chat:34:42:1:1.AQEB…",
      "from": "34",
      "event_id": "1791638401000-0"
    }
  ]
}
```

## Answer

Taken. Any 2xx is.

## Refusals

- **default**: Not taken: any other answer, or none within 10 seconds. The batch is sent again 30 seconds later, then after 2, 10 and 30 minutes, and 1, 2, 4 and 8 hours, for 24 hours at most.

## Examples

The helper checks the signature and gives the events: https://docs.wordplus.cloud/api/signing/ has it in each language, and https://docs.wordplus.cloud/api/webhooks/ what it checks.

**Node**

```js
import { createServer } from 'node:http';
import { webhookEvents } from './wordplus.mjs';

createServer((req, res) => {
    let raw = '';
    req.on('data', (chunk) => (raw += chunk));
    req.on('end', async () => {
        try {
            for (const event of webhookEvents(req.headers, req.url, raw)) await handle(event);
            res.writeHead(204).end();
        } catch (error) {
            res.writeHead(401).end();
        }
    });
}).listen(8080);
```

**Python**

```python
from wordplus import webhook_events

@app.post("/api/realtime/webhook")
def realtime_webhook():
    headers = {name.lower(): value for name, value in request.headers.items()}
    try:
        events = webhook_events(headers, request.full_path.rstrip("?"), request.get_data())
    except ValueError:
        return "", 401
    for event in events:
        handle(event)
    return "", 204
```

**PHP**

```php
require 'wordplus.php';

$headers = array_change_key_case(getallheaders(), CASE_LOWER);
try {
    $events = wordplus_webhook_events($headers, $_SERVER['REQUEST_URI'], file_get_contents('php://input'));
} catch (RuntimeException $e) {
    http_response_code(401);
    exit;
}
foreach ($events as $event) {
    handle($event);
}
http_response_code(204);
```

**Go**

```go
http.HandleFunc("/api/realtime/webhook", func(w http.ResponseWriter, r *http.Request) {
    raw, _ := io.ReadAll(r.Body)
    events, err := app.Webhook(r.Header, r.URL.RequestURI(), raw)
    if err != nil {
        w.WriteHeader(401)
        return
    }
    for _, event := range events {
        handle(event)
    }
    w.WriteHeader(204)
})
```

