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

# Webhooks

> How to set up school webhooks: available events, request format, signature verification, retries and automatic endpoint disabling.

Webhooks automatically notify your external services about events in the school: sign-up, payment (commercial schools only), course enrollment, course completion. When an event happens, the platform sends a POST request with the event data to the address you specified. This is how you connect the school to CRMs, bots, spreadsheets and your own services without constantly polling the API.

## Where to set them up

Webhooks are in **Control panel → My school → For developers → Webhooks** (in a corporate school — **Control panel → Company → For developers → Webhooks**). A staff member with the **School settings management** permission (in a corporate school — **Company settings management**) can work with them.

* A school can have no more than 5 webhooks. Deleted webhooks do not count toward the limit.
* Each webhook is one address and a set of events it is subscribed to.
* Webhooks are set up only in the control panel. You cannot manage them through the school API.

## How to create a webhook

**1. Click “Create a webhook”**

Open the **Webhooks** section (in a corporate school — **Company webhooks**) and click **Create a webhook**.

**2. Enter the address**

In the **Link** field, enter the address of your service, for example [https://my-service.com/api/exode](https://my-service.com/api/exode). The address can be up to 255 characters long. Use HTTPS.

**3. Add a note**

In the **Service note** field, write what this webhook is for, for example “AmoCRM integration”. The note is visible only in the webhook list and can be up to 255 characters long.

**4. Choose the events**

Select the events in the **Selected events** list. The counter on the right shows how many events are selected.

**5. Check delivery**

Click the arrow next to an event to send a test event to the specified address. Make sure your service received the request.

**6. Save the webhook**

Click **Create**. On creation, the platform generates a secret key for signature verification.

In the webhook list, each row has an **Active** / **Inactive** switch and an action menu: **Edit**, **Copy signature** and **Delete webhook**.

## Events

13 events are available for subscription (in a corporate school — 11):

| Event in the control panel | Identifier | When it fires |
| - | - | - |
| The user has registered | `UserSignedUp` | A new user signed up for the school on their own |
| The user has filled in the data | `UserAcquainted` | The user filled in and submitted the introduction form |
| User connected Telegram | `UserTgConnected` | The user linked or changed Telegram |
| User created via LMS | `UserCreatedViaLms` | A school staff member created the user manually |
| Enrollment in a free course | `ProductEnrolledToFree` | The user enrolled in a free course |
| Registration for the course via LMS | `ProductEnrolledViaLms` | A school staff member granted access manually |
| User registration for the course through payment | `ProductEnrolledViaPayment` | Access was granted after a successful payment (commercial schools only) |
| Enrollment via invite link | `ProductEnrolledByInviteLink` | The user enrolled via an invite link |
| Payment completed | `PaymentCompleted` | The payment was successfully paid (commercial schools only) |
| Course progress updated | `CourseProgressChanged` | A student's progress status on a lesson changed |
| Practice successfully completed (reviewed) | `CourseLessonPracticeCompleted` | The practice moved to the “Completed” status |
| Course completed | `CourseCompleted` | The student completed all available modules and lessons of the course |
| Certificate issued | `CertificateIssued` | The student was issued a certificate |

Corporate schools do not have the **Payment completed** and **User registration for the course through payment** events.

> [!NOTE]
>
> You can subscribe only to the events in the table. There are no events for signing in and out, sign-up via a referral link, submitting practice for review, practice auto-check, refunds or subscription ending.

## Request format

The platform sends a POST request with a JSON body. The body has the same structure for all events:

| Field | What it contains |
| - | - |
| `event` | The event identifier from the table above |
| `timestamp` | The moment of the event in ISO 8601 format, UTC time |
| `idempotencyKey` | A unique event key. It is the same for all your webhooks and does not change on retries |
| `data` | Event data: user, course, payment, access — the contents depend on the event |

```json theme={null}
{
  "data": { "...": "depends on the event" },
  "event": "PaymentCompleted",
  "timestamp": "2026-10-15T09:30:12.512Z",
  "idempotencyKey": "7qqQL3bE0o8R8d3X"
}
```

Only fields from the public event description go into `data`, extra fields are removed. The full contents of `data` for each event are described in the [API documentation](https://docs.exode.biz/en/exode-api/webhooks/about).

## Signature verification

Each request has a `signature` header. It is an HMAC-SHA256 of the raw request body, calculated with the webhook secret key and written in hexadecimal.

**1. Copy the secret key**

In the webhook menu, choose **Copy signature** — the 64-character secret key is copied to the clipboard. The same link is in the edit form above the **Link** field. Save the key in your service settings.

**2. Calculate the signature on your side**

Calculate the HMAC-SHA256 of the request body exactly as it arrived. Do not parse the JSON and build it again: a different field order or whitespace gives a different signature.

**3. Compare the signatures**

If the signatures do not match, reject the request.

```javascript theme={null}
import crypto from 'crypto';
import express from 'express';

const app = express();

app.post('/webhooks/exode', express.raw({ type: 'application/json' }), (req, res) => {
    const rawBody = req.body.toString('utf8');
    const received = req.header('signature') || '';

    const expected = crypto
        .createHmac('sha256', process.env.EXODE_WEBHOOK_SECRET)
        .update(rawBody)
        .digest('hex');

    const isValid = received.length === expected.length
        && crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));

    if (!isValid) {
        return res.status(401).send('invalid signature');
    }

    const payload = JSON.parse(rawBody);

    /* Handle the event and remember payload.idempotencyKey */

    res.sendStatus(200);
});
```

> [!WARNING]
>
> The secret key is confidential data. Keep it only on the server and do not publish it in the school's custom code or in public repositories.

## Delivery and retries

| Parameter | Value |
| - | - |
| Method | POST, JSON body |
| Response timeout | 15 seconds |
| Successful response | HTTP codes 200, 201 and 202 |
| Number of attempts | Up to 5: the first delivery and up to 4 retries |
| Pauses between attempts | About 11, 33, 77 and 165 minutes. The last attempt is about 4 hours 46 minutes after the first one |
| Delivery order | Not guaranteed |

Any other response, exceeding 15 seconds or a network error counts as a failure. After 5 failed attempts, the event is no longer sent. If the webhook was turned off while retries were in progress, the remaining attempts are canceled.

> [!TIP]
>
> Respond with code 200 right after verifying the signature, and do heavy processing in the background. Use `idempotencyKey` so you do not process the same event twice: on a retry, the key is the same.

## Automatic disabling

If a webhook has not accepted a single event for 14 days in a row, the platform turns it off. The school owner gets a notification and a **Webhook disabled** email with the webhook address.

A disabled webhook does not receive new events. To receive events again, fix the address or the service and turn the webhook on with the switch in the list. After you turn it on manually, the 14-day countdown starts again.

## Test events

You send a test event with the arrow button next to an event in the webhook form. The button is shown only for events that can be selected.

* The test is sent to the address from the **Link** field, even if the webhook is not saved yet.
* The body contains sample event data in the same format as real events.
* The test is sent once, without retries.
* A test from the form of a saved webhook is signed with its secret key. A test from the form of a new webhook is signed with a temporary key, so you cannot verify its signature.
* You can send no more than 100 tests in 10 minutes.

## Frequently asked questions

> **Why does my service respond with 204, but events arrive again?**
>
> Only 200, 201 and 202 responses count as successful. The platform treats a 204 response as a failure and retries the delivery. Respond with code 200.

> **Why does the signature not match?**
>
> Most often, the signature is calculated from re-built JSON instead of the raw request body. Calculate the HMAC-SHA256 of the body exactly as it arrived. Also check that you use the key from **Copy signature** of this exact webhook and that the test was sent from the form of a saved webhook.

> **How many times does the platform retry the delivery?**
>
> Up to 5 attempts in total: the first one and 4 retries with pauses of about 11, 33, 77 and 165 minutes. After that, the event is considered undelivered.

> **Why did the webhook turn off by itself?**
>
> A webhook turns off if it has not accepted a single event for 14 days in a row. The school owner gets a notification and an email about it. Fix the address or the service and turn the webhook on with the switch.

> **Can I create more than 5 webhooks?**
>
> No, a school can have no more than 5 webhooks. Delete the ones you do not use or subscribe one webhook to several events. If the limit is not enough, write to support.

> **Do events arrive in the order they happened?**
>
> No, the order is not guaranteed: events are sent in parallel, and failed attempts are retried later. Rely on the `timestamp` field.

> **Why are there no events for signing in or refunds in the list?**
>
> Such events are not sent, so you can't subscribe to them. 13 events from the table above are available for subscription (in a corporate school — 11).

> **Will the webhook arrive if I subscribe two addresses to the same event?**
>
> Yes. The event goes to each active webhook subscribed to it, with the same `idempotencyKey`.

> **Where can I see what exactly comes in the event data?**
>
> Send a test event to your address and look at the request body. The full description of each event's data is in the [API documentation](https://docs.exode.biz/en/exode-api/webhooks/about).

***

*Updated: 2026-10-17 07:07 UTC*


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