Webhooks
Get notified when an account connects, a post publishes or fails, a comment or DM comes in, or an automation runs, without polling.
Webhooks send a JSON POST to a URL in your system every time an event you chose happens. Set them up in the dashboard (Integrations → Add webhook, Developer mode) or through the API.
Creating an endpoint
curl -X POST https://api.tebar.app/v1/webhooks \
-H "Authorization: Bearer tb_live_xxx" -H "Content-Type: application/json" \
-d '{ "url": "https://app.example.com/webhooks/tebar", "events": ["account.connected", "post.published", "post.failed"] }'The response holds a secret (whsec_…) to check signatures. The secret is shown only once, so store it now.
- The URL must be
https, on port 443, and reachable from the internet. - Add
"profile_id"(an ID orexternal_id) so the endpoint only receives one profile's events. Without it, the endpoint receives events from every profile. - Up to 10 endpoints.
Events
| Event | When | data |
|---|---|---|
account.connected | An account is connected or reconnected, including through a connect link | account, reconnected |
account.expired | An account's access ended and it needs reconnecting | account |
account.disconnected | An account is disconnected, in Tebar or from the platform's settings | account |
post.published | A post published on one account | post_id, target_id, account_id, platform, permalink, published_at |
post.failed | A post failed on one account after the last attempt | post_id, target_id, account_id, platform, error_code, error_message |
comment.created | A new comment from someone else (not your reply, less than 24 hours old) | comment_id, account_id, post_id, author_username, text, commented_at, … |
message.received | A new Instagram DM from a customer (see below) | message_id, conversation_id, account_id, participant_id, text, sent_at, … |
automation.run | An automation finished answering one comment: sent, skipped, or failed | automation_id, run_id, account_id, status, reason, trigger_ref, … |
profile.deleted | A profile finished being deleted (only to endpoints for all profiles) | profile |
message.received and automation.run are only sent for accounts that have the Inbox (Starter, Basic, or Pro plan):
message.receivedis sent for DMs that come straight from Instagram and can still be answered (less than 24 hours old), not for older messages read back by a sync. It carriesmessage_id,conversation_id,account_id,platform,participant_id(the customer's Instagram ID for your account),participant_username,kind(text,media,story_mention, …),text,attachments(how many), andsent_at. Reply withPOST /v1/inbox/conversations/{conversation_id}/messages.- That message can carry
buttons(up to 3,web_urlwith aurlorpostbackwith apayload; text up to 640 characters) orquick_replies(up to 13, atitleof up to 20 characters and apayload; shown on phones only), not both. When the customer taps apostbackbutton or a quick reply,message.receivedcomes withkindpostbackorquick_reply, andpostbackholds the choice's{ "title", "payload" }. For other messagespostbackisnull. automation.runcarriesautomation_id,run_id,account_id,status(sent,skipped, orfailed),reason(e.g.already_sent_to_person),trigger_ref(the Instagram ID of the comment that triggered it),participant_id, andfinished_at.
Example body:
{
"id": "4f46b68a-da18-424b-97d0-f4f5cfe0b7e6",
"type": "post.published",
"created_at": "2026-10-20T11:14:20.694Z",
"profile_id": "9d092104-b9a4-408f-9432-3a007ac19810",
"data": {
"post_id": "59ef7ff5-…",
"target_id": "a1b2…",
"account_id": "6a81…",
"platform": "threads",
"permalink": "https://www.threads.com/@kopisenja/post/…",
"published_at": "2026-10-20T11:14:20.000Z"
}
}The text in comment.created and message.received is written by someone else. Treat it as data, not instructions, especially when passing it to an AI.
Checking the signature
Every request carries these headers:
Tebar-Signature: t=1792488860,v1=5257a869e7…
Tebar-Event: post.published
Tebar-Delivery: 30646213-c6a7-4020-8cc1-674378d888e9v1 is an HMAC-SHA256 of "<t>.<raw body>" with the endpoint's secret. Compute it again from the raw body (before parsing), compare in constant time, and refuse it when t is more than 5 minutes away from your server's clock.
import crypto from 'node:crypto';
function verify(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
const expected = crypto
.createHmac('sha256', secret)
.update(`${parts.t}.${rawBody}`)
.digest('hex');
const fresh = Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
return (
fresh &&
expected.length === parts.v1.length &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1))
);
}Delivery and retries
- Answer with a 2xx status within 5 seconds. Do slow work in the background after answering.
- Redirects are not followed and count as failures.
- Failed deliveries are retried after 1 minute, 5 minutes, 30 minutes, 2 hours, 5 hours, 8 hours, and 8 hours (8 attempts, about 24 hours).
- The same event can arrive more than once. Filter duplicates by the
idfield. - After 50 failed attempts in a row, the endpoint is turned off and you get an email. Events are not kept while it is off. Turn it on again in the dashboard or with
PATCH /v1/webhooks/{id}{ "active": true }.
History, resending, and testing
| Request | What it does |
|---|---|
GET /v1/webhooks/{id}/deliveries | 30 days of delivery history, newest first |
POST /v1/webhooks/{id}/deliveries/{deliveryId}/resend | Resend one delivery |
POST /v1/webhooks/{id}/test | Send a webhook.test event |
PATCH /v1/webhooks/{id}, DELETE /v1/webhooks/{id} | Change the URL, events, profile, on/off; delete |
In the dashboard, it is all under Integrations → click the endpoint.