Sign in

Webhooks

A webhook sends a POST request to your server about every new post in the MAX channels you choose — usually 1–2 minutes after we read it. No need to poll the feed every few minutes.

Note

Webhooks are available from the “API.Max Growth” plan: 1 webhook with up to 500 channels. “API.Max Profi” — 5 webhooks with up to 5,000 channels each. Managing webhooks does not use requests.

A read-and-write key

Only a key marked “read and write” can create and change webhooks. The mark is set when you create the key on the “API keys” page in your account and cannot be changed later. A key without it gets 403 with the code read_only_key.

Create a webhook

# create a webhook: your server address and datool.ru channel numbers
# requires a "read and write" key; the response has the signing secret (shown once)
curl -X POST https://api.datool.ru/v1/max/webhooks/ \
  -H "Authorization: Bearer dtk_your_key" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com/hooks/datool", "channels": [4821, 5310]}'
  • url — an https:// address of your server (private network addresses and localhost are not accepted);
  • channels — datool.ru numbers of public MAX channels (the id field in the catalog and channel card).

The response contains secret. Save it: it is shown only once and signs every event. To get a new one — POST /v1/max/webhooks/{id}/rotate-secret/.

To test the address: POST /v1/max/webhooks/{id}/test/ sends a ping event without posts — one attempt with no retries, at most once a minute per webhook (more often — 429 throttled). The result is in the last_success_at and last_error fields.

Webhooks can be created, changed and deleted with a “read and write” key of the organization’s owner or administrator; a regular member’s key gets 403 manager_required. The URL may use an internationalized domain (.рф).

What arrives

{
  "event": "post.created",
  "webhook": 12,
  "created_at": "2026-10-18T12:00:00+03:00",
  "posts": [
    {
      "id": 9876543,
      "channel": 4821,
      "text": "Мэрия открыла новый парк на набережной…",
      "views": 18450
    }
  ]
}
Field Contents
event post.created — new posts, ping — a test delivery
webhook Webhook number
created_at When the event was assembled (ISO 8601, Moscow time)
posts Posts — an empty list for ping

posts holds up to 100 posts, each in the same shape as GET /v1/max/posts/{id}/. We only send fresh posts (published within the last two days); edits and deletions are not sent yet.

Request headers:

Header Contents
X-Datool-Event post.created or ping
X-Datool-Delivery delivery number; the same on retries
X-Datool-Signature t=<time>,v1=<signature>

Verify the signature

The signature is HMAC-SHA256 of the string <t>.<request body> with your secret. Check it before parsing JSON and reject events older than 5 minutes — this way nobody can forge an event or replay an old one.

import hashlib
import hmac
import time


def is_valid(secret: str, body: bytes, header: str) -> bool:
    """Check an event signature: secret — the webhook secret, body — the raw request body."""
    # the X-Datool-Signature header: "t=<time>,v1=<signature>"
    try:
        parts = dict(item.split("=", 1) for item in header.split(","))
        timestamp = int(parts["t"])
        signature = parts["v1"]
    except (ValueError, KeyError):
        # no header or a broken one — the event is not from datool.ru
        return False
    # reject events older than 5 minutes so an old event can't be replayed
    if abs(time.time() - timestamp) > 300:
        return False
    # the signature is HMAC-SHA256 of "<t>.<body>" with your secret
    expected = hmac.new(
        secret.encode(), f"{timestamp}.".encode() + body, hashlib.sha256
    ).hexdigest()
    # constant-time comparison — the signature can't be guessed from response timing
    return hmac.compare_digest(expected, signature)

Receiver example

# an event receiver on Flask: pip install flask
from flask import Flask, abort, request

app = Flask(__name__)
# the secret from the webhook creation response (keep it in settings, not in code)
SECRET = "whsec_…"


@app.post("/hooks/datool")
def datool_hook():
    # the raw body — the signature is computed over it, before parsing JSON
    body = request.get_data()
    if not is_valid(SECRET, body, request.headers.get("X-Datool-Signature", "")):
        # the signature doesn't match — the event is not from datool.ru
        abort(401)
    event = request.get_json()
    if event["event"] == "post.created":
        for post in event["posts"]:
            # store the post; the same event may arrive twice — check post["id"]
            print(post["id"], post["text"][:80])
    # respond 2xx quickly, do heavy processing after responding
    return "", 204

Response and retries

Respond with a 2xx code within 10 seconds — do heavy processing after responding. We do not follow redirects.

If there is no response or the code is different, we retry after 1, 5, 15 minutes, 1, 3, 6 and 12 hours — the last attempt comes about 22 hours later. An event that fails for a whole day is not delivered. We don’t read your response body — only the code matters. The same event may arrive twice — tell posts apart by id.

If not a single delivery succeeds for a day, the webhook is disabled and the organization’s owners and administrators get an email. Fix the server and enable the webhook: PATCH /v1/max/webhooks/{id}/ with {"is_active": true} or the “Enable” button on the “API keys” page. Posts will be sent from the moment it is enabled. A webhook disabled by datool.ru staff (disabled_reason: staff) cannot be enabled this way — 409 conflict; contact support.

If your plan has ended

Webhooks are not deleted, but events stop arriving. After renewal they arrive again, from that moment on.

All methods are in the reference (the “MAX: webhooks” group).

Was this page helpful?

You are leaving datool.ru

The link leads to an external website. datool.ru is not responsible for its content — check the address before entering passwords or payment details.

Redirecting in 10 s

Continue