Incoming webhooks
A webhook is a secret URL that posts into one text channel. Anything that can send an HTTP request (a CI job, a monitoring alert, a cron script, GitHub, a tool written for Discord or Slack) can write messages, embeds and files there. No bot, no token and no library needed. Messages show the webhook’s name and picture with a BOT label. Base URL: https://app.diyor.chat/api/v1
| Method | Path | Does |
|---|---|---|
| POST | /webhooks/{id}/{token} | Send a message (JSON, form or multipart with files). |
| GET | /webhooks/{id}/{token} | The webhook: name, channel, server. |
| GET | /webhooks/{id}/{token}/messages/{message} | A message this webhook sent. |
| PATCH | /webhooks/{id}/{token}/messages/{message} | Edit its content or embeds. |
| DELETE | /webhooks/{id}/{token}/messages/{message} | Delete it (its files too). |
| POST | /webhooks/{id}/{token}/github | GitHub events, written as readable messages. |
Create a webhook
- In the Diyor app open Server settings → Integrations (Sunucu ayarları → Entegrasyonlar). You need the Manage server permission.
- Click Create webhook (Webhook oluştur). Copy the URL shown; it looks like
https://app.diyor.chat/api/v1/webhooks/{id}/{token}. There is also a button that copies the GitHub variant. - In the list, give the webhook a name and a picture and pick the channel its messages go to. Announcement channels cannot have webhooks.
From the same list you can get a new URL (Yeni adres; the old one stops working) or delete the webhook. A server can have up to 15 webhooks.
Send a message
curl -X POST "https://app.diyor.chat/api/v1/webhooks/{id}/{token}" \
-H "Content-Type: application/json" \
-d '{"content": "Nightly backup completed ✅"}'
-> 204 No ContentJavaScript (Node.js 18+)
await fetch(process.env.DIYOR_WEBHOOK_URL, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ content: 'Deploy finished', username: 'Deploy bot' }),
});Python
import os, requests
requests.post(os.environ["DIYOR_WEBHOOK_URL"], json={"content": "Disk usage over 90% on db-1"}).raise_for_status()The body can also be form data with a payload field holding the JSON (as Slack sends it).
Message fields
| Field | Type | Meaning |
|---|---|---|
content | string | The message text, up to 2000 characters. Markdown like any message. Alias: text (Slack). |
username | string | Shown as the author for this message only. Up to 40 characters; cannot be “Diyor”. |
avatar_url | string | Picture for this message only: an https URL, up to 500 characters. |
embeds | array | Up to 3 embeds (see below). |
At least one of content, embeds or a file is required. Unknown fields are ignored.
{
"content": "Release **v2.4.0** is out",
"username": "Release notes",
"avatar_url": "https://example.com/logo.png"
}Embeds
Embeds are cards with a colored edge. Up to 3 per message; all their text together at most 6000 characters.
{
"embeds": [{
"title": "Build #128 passed", // 256
"description": "All **42** tests passed", // 4000, Markdown
"url": "https://ci.example.com/128", // https only
"color": 3066993, // or "#2ecc71"
"fields": [ // up to 10; name 256, value 1024
{ "name": "Branch", "value": "main", "inline": true },
{ "name": "Duration", "value": "3m 12s", "inline": true }
],
"image": { "url": "https://example.com/coverage.png" },
"footer": { "text": "CI" } // 2048
}]
}Every embed field and its limit: Embed object.
Files
To attach files, send multipart/form-data: the message as JSON in a payload_json part and each file in its own part named files[0], files[1] … files[9] (file, file1 … also work). Each file keeps its name and type.
curl "https://app.diyor.chat/api/v1/webhooks/{id}/{token}" \
-F 'payload_json={"content": "Weekly report"}' \
-F 'files[0]=@report.pdf' \
-F 'files[1]=@chart.png'JavaScript (Node.js 20+)
import { openAsBlob } from 'node:fs';
const form = new FormData();
form.append('payload_json', JSON.stringify({ content: 'Weekly report' }));
form.append('files[0]', await openAsBlob('report.pdf', { type: 'application/pdf' }), 'report.pdf');
await fetch(process.env.DIYOR_WEBHOOK_URL, { method: 'POST', body: form });Python
import json, os, requests
with open("report.pdf", "rb") as pdf, open("chart.png", "rb") as png:
requests.post(os.environ["DIYOR_WEBHOOK_URL"], data={"payload_json": json.dumps({"content": "Weekly report"})},
files={"files[0]": ("report.pdf", pdf, "application/pdf"), "files[1]": ("chart.png", png, "image/png")}).raise_for_status()- Up to 10 files per message and 100 MB per request. The request needs a
Content-Lengthheader (curl, fetch and requests send it). - Files belong to the server owner: the size of each file, the daily upload quota and how long files are kept follow the owner’s plan, as if they had uploaded them. Per file: 25 MB on Diyor, 250 MB on Pro, 1 GB on Pro Max.
- Members of the server see and download the files in the channel; a bot with message content access gets them in
attachments. - All files are checked before any is stored. If one is rejected, or the message cannot be sent, nothing is kept and nothing counts against the quota.
- Diyor 1.7.81 and later show every file. Older apps show the first one.
Get the message back
By default a successful post answers 204 No Content. Add ?wait=true to get 200 with the created message; keep its id to edit or delete it later.
POST /webhooks/{id}/{token}?wait=true
-> 200
{
"id": "5c8d12d6-…", "server_id": "622rfdt5f7uj", "channel_id": "general",
"author": { "id": "qoha6xse1sorpi4m", "name": "Deploy bot", "avatar_url": "https://example.com/logo.png", "webhook": true },
"content": "Weekly report", "timestamp": "2026-10-22T10:19:51.095Z", "edited_timestamp": null,
"attachments": [{ "id": "9f1c…", "name": "report.pdf", "filename": "report.pdf", "size": 52311, "content_type": "application/pdf", "url": "…" }],
"embeds": [], "reactions": [], "pinned": false, "components": [], "reply_to": null, "interaction": null
}Edit and delete
A webhook can read, edit and delete only the messages it sent itself.
GET /webhooks/{id}/{token}/messages/{message_id} -> 200 message
PATCH /webhooks/{id}/{token}/messages/{message_id} -> 200 message
{ "content": "Build #128 passed (re-run)", "embeds": [] }
DELETE /webhooks/{id}/{token}/messages/{message_id} -> 204PATCHtakes JSON withcontentand/orembeds; a field you leave out stays as it is,[]removes the embeds. Files cannot be changed. The message shows “edited”.DELETEremoves the message and its files for everyone.
A status message that you keep updating (a deploy in progress, a long job) is a good use: post with ?wait=true, then PATCH the same id.
The webhook itself
GET /webhooks/{id}/{token}
-> 200
{ "id": "qoha6xse1sorpi4m", "type": 1, "name": "Deploy bot", "avatar": null, "avatar_url": "https://app.diyor.chat/resim/w/…",
"channel_id": "general", "guild_id": "622rfdt5f7uj", "application_id": null, "token": "…" }Handy to check a URL before saving it. guild_id is the server id (the name Discord tools expect).
Coming from Discord
Most tools that post to Discord webhooks work unchanged: replace the start of the URL.
https://discord.com/api/webhooks/{id}/{token}
https://app.diyor.chat/api/webhooks/{id}/{token} // /api/v10/webhooks/… works too| Discord | Diyor |
|---|---|
| content, username, avatar_url, embeds | Supported |
| files[n] + payload_json | Supported: 10 files, 100 MB per request, per-file size by the owner’s plan |
| ?wait=true, GET/PATCH/DELETE messages/{id}, GET webhook | Supported |
attachment://name | Not supported: embed images that point to an attached file are dropped (the file itself is sent) |
| thread_id, thread_name | Ignored: the message goes to the webhook’s channel |
| tts, allowed_mentions, flags, components, poll | Ignored |
| Rate limit headers (X-RateLimit-*) | Only Retry-After on 429 |
Slack and GitHub
- Slack:
{"text": "…"}is accepted on the same URL, also aspayload=form data. Many Slack integrations can post to Diyor this way. - GitHub: in the repository go to Settings → Webhooks → Add webhook, use
…/{token}/githubas the payload URL and application/json as the content type. Diyor turns pushes, pull requests (opened, merged, closed, ready for review), reviews, issues, comments, releases, stars, forks and finished workflow runs into short messages; other events are accepted and skipped.
Limits
| What | Limit |
|---|---|
| Messages | 30 per minute per webhook |
| content | 2000 characters |
| username | 40 characters |
| avatar_url | https, 500 characters |
| Embeds | 3 per message, 6000 characters in total |
| JSON or form body | 1 MB |
| Files | 10 per message, 100 MB per request |
| Size of one file | Server owner’s plan: 25 MB (Diyor), 250 MB (Pro), 1 GB (Pro Max) |
| Daily upload quota and how long files stay | Server owner’s plan, shared with the owner’s own uploads |
| Webhooks | 15 per server |
Errors
Errors are JSON: {"code": "rate_limited", "message": "You are being rate limited."}. On 429 and 503 wait the seconds in Retry-After before trying again.
| Status | code | When |
|---|---|---|
| 400 | invalid_json | The body (or payload_json) is not a JSON object. |
| 400 | invalid_form | The multipart body could not be read. |
| 400 | empty_message | No content, embeds or files. |
| 400 | content_too_long | content is over 2000 characters. |
| 400 | invalid_embeds | An embed breaks a rule; message says which. |
| 400 | invalid_avatar_url | avatar_url is not an https URL of at most 500 characters. |
| 400 | too_many_files | More than 10 files. |
| 404 | unknown_webhook | Wrong id or token, or the webhook was deleted or got a new URL. |
| 404 | unknown_channel | The webhook’s channel no longer exists. |
| 404 | unknown_message | No such message, it was deleted, or another author sent it. |
| 405 | method_not_allowed | Wrong method for this path (see the Allow header). |
| 411 | length_required | A request with files has no Content-Length. |
| 413 | payload_too_large | JSON body over 1 MB or a request with files over 100 MB. |
| 413 | file_too_large | A file is over the per-file limit of the server owner’s plan. |
| 429 | rate_limited | Over 30 messages a minute. Wait Retry-After seconds. |
| 429 | upload_quota_exceeded | The server owner’s daily upload quota is used up. |
| 502 | upload_failed | A file could not be stored. Nothing was sent; try again. |
| 503 | storage_unavailable | Large files cannot be stored right now. Retry after Retry-After. |
| 507 | storage_full | The file storage is full. |
Security
- The URL is the password: anyone who has it can post to the channel. Keep it in a secret store or an environment variable, not in code or public repositories.
- If it leaks, create a new URL for it in Server settings → Integrations (Yeni adres). The old URL stops working at once.
- Diyor stores only a hash of the token, so a lost URL cannot be shown again; create a new one instead.
- A webhook can only post to its own channel and touch its own messages.
Bots, slash commands and the full API: bot API documentation. Machine-readable API: openapi.json.