Webhooks
Webhooks make Shara notify you when something relevant happens in your workspace: a pending approval, a finished run, a new draft or a connector that was just linked. Instead of polling the API in a loop, you register an HTTPS URL of your own and Shara sends it a signed POST for every event, the moment it happens. All the infrastructure is hosted in the EU.
How they work
Section titled “How they work”A webhook is an outbound delivery: when an event occurs, Shara builds a JSON payload, signs it with HMAC and sends it via POST to the URL you configured. It’s the reverse pattern of inbound webhooks (where your systems send events to Shara): here traffic goes from Shara to you, with no need to poll GET /v1/runs.
You configure them from the panel at Settings → Webhooks. Each destination has: an HTTPS URL you own, the list of event types you want to receive and its own per-tenant signing secret (never shared between workspaces), shown only once when created. Save it in your secrets manager: it’s the only way to verify an event really comes from Shara.
Every delivery arrives with these headers. Your receiver must read the raw body (the exact bytes) before parsing the JSON, because the signature is calculated over those bytes:
| Header | Value | What for |
|---|---|---|
X-SHARA-Signature | <hex> HMAC-SHA256 of the raw body | Authenticates the event’s origin. |
X-SHARA-Timestamp | <unix-seconds> | Closes the ±300 s anti-replay window. |
X-SHARA-Event-Id | unique event id (evt_…) | Idempotency: deduplicates retries. |
X-SHARA-Event-Type | e.g. run.completed | Routes without opening the body. |
Content-Type | application/json | The body is UTF-8 JSON. |
Registering a webhook step by step
Section titled “Registering a webhook step by step”Setting up a destination takes about a minute from the panel. The only irreversible step is copying the secret: it’s shown only once.
- Go to
Settings → Webhooks(requires an admin role and active two-step verification). - Click New destination and enter your endpoint’s HTTPS URL. Only
https://is accepted; plainhttp://is rejected. - Select the event types you want to receive. Only subscribe to the ones you’ll actually process.
- Save and copy the signing secret that appears. It won’t be shown again: if you lose it, you’ll have to rotate it.
- Send a test event (
ping) from the panel and confirm your receiver responds2xxand verifies the signature correctly. - Activate the destination. From then on, Shara starts delivering your subscribed events in real time.
The
pingevent doesn’t count as activity in your workspace and doesn’t consume STU: it only serves to validate the URL, the signature and the response time end to end before you trust the destination.
Available events
Section titled “Available events”Only subscribe to the types you need. Every event identifies the agent involved by name (Amadeus orchestrates; the departmental ones are Carnegie, Kotler, Graham, Holmes, Maslow, Deming, Rosling, Porter, Turing and Carlzon) and, when applicable, the model’s alias used (Symphony · Sonata · Prelude · Concerto) and the consumption in STU.
type | When it fires | Key data |
|---|---|---|
approval.created | An agent requests a green light before executing a sensitive action (sending an email, moving a CRM record). | approval_id, agent, action, expires_at |
approval.decided | A person approves or rejects that request from the panel. | approval_id, decision (approved/rejected), decided_by |
run.completed | An agent execution finishes successfully. | run_id, agent, alias, usage.stu |
run.failed | An execution ends in error or is stopped by a spending kill-switch. | run_id, agent, reason |
draft.created | An agent generates a draft (email, proposal, reply) awaiting human review. | draft_id, agent, kind, run_id |
integration.connected | A connector (Gmail, Slack, CRM…) is linked in the workspace. | integration, provider, connected_by |
The safety
kill-switches cut spending at €5 per run, €50 per hour and €400 per day. When one of them stops an execution, you’ll receive arun.failedwithreasonindicating the scope of the cut (run·hour·day), useful for alerting your team.
Event structure
Section titled “Event structure”Every event shares a common envelope: a unique id, the type, the created_at timestamp, your workspace’s tenant_id and a data object whose shape depends on the type. Text generated by agents already comes redacted (never raw material from providers). Example of a run.completed:
{
"id": "evt_7c4a91f2",
"type": "run.completed",
"created_at": "2026-07-10T09:42:15Z",
"tenant_id": "wksp_3f9a2c",
"data": {
"run_id": "run_a1b2c3",
"agent": "carnegie",
"alias": "Sonata",
"status": "completed",
"usage": {
"input_tokens": 184,
"output_tokens": 412,
"stu": 1280
}
}
}
Payload examples by type
Section titled “Payload examples by type”The envelope is always the same; what changes is data. These are the bodies you’ll receive for the most common types. A pending approval (approval.created), meant to alert the person who must give the sign-off:
{
"id": "evt_4Lm9Zx01",
"type": "approval.created",
"created_at": "2026-07-10T10:05:33Z",
"tenant_id": "wksp_3f9a2c",
"data": {
"approval_id": "apr_4Lm9Zx",
"run_id": "run_a1b2c3",
"agent": "carnegie",
"action": "email.send",
"summary": "Email de seguimiento a 3 clientes con incidencias abiertas.",
"expires_at": "2026-07-11T10:05:33Z"
}
}
The resolution of that approval (approval.decided), with who decided and what:
{
"id": "evt_4Lm9Zx02",
"type": "approval.decided",
"created_at": "2026-07-10T10:18:11Z",
"tenant_id": "wksp_3f9a2c",
"data": {
"approval_id": "apr_4Lm9Zx",
"decision": "approved",
"decided_by": "usr_kv71",
"run_id": "run_a1b2c3"
}
}
A new draft (draft.created) awaiting human review:
{
"id": "evt_8b1d0af5",
"type": "draft.created",
"created_at": "2026-07-10T10:06:02Z",
"tenant_id": "wksp_3f9a2c",
"data": {
"draft_id": "drf_9k2p",
"run_id": "run_a1b2c3",
"agent": "carnegie",
"kind": "email",
"subject": "Seguimiento de tus incidencias de esta semana"
}
}
A stopped run (run.failed). Here a daily spending cut stopped it; reason indicates the scope so you can alert your team:
{
"id": "evt_c3d4e5f6",
"type": "run.failed",
"created_at": "2026-07-10T11:41:20Z",
"tenant_id": "wksp_3f9a2c",
"data": {
"run_id": "run_z9y8x7",
"agent": "porter",
"reason": "kill_switch:day",
"status": "failed"
}
}
And a newly linked connector (integration.connected):
{
"id": "evt_a1a2a3a4",
"type": "integration.connected",
"created_at": "2026-07-10T12:00:00Z",
"tenant_id": "wksp_3f9a2c",
"data": {
"integration": "gmail",
"provider": "google",
"connected_by": "usr_kv71"
}
}
HMAC signature verification
Section titled “HMAC signature verification”The signature is HMAC-SHA256 calculated over the raw body in bytes with the webhook’s per-tenant secret, arriving as lowercase hex (64 characters) in X-SHARA-Signature. Always verify before processing: (1) check that X-SHARA-Timestamp falls within the ±300 s anti-replay window relative to your clock; (2) recalculate the HMAC over the exact bytes; (3) compare in constant time (timingSafeEqual) to avoid leaking information via timing. If anything doesn’t match, discard the event with a 401.
import { createHmac, timingSafeEqual } from 'node:crypto';
// per-tenant signing secret (Settings → Webhooks); never shared between workspaces
const SECRET = process.env.SHARA_WEBHOOK_SECRET;
// rawBody = Buffer with the EXACT bytes received (not the re-serialized JSON)
export function verifySharaEvent(rawBody, headers) {
const signature = headers['x-shara-signature'] ?? '';
const timestamp = Number(headers['x-shara-timestamp'] ?? 0);
// 1) ±300 s anti-replay window, BEFORE calculating the HMAC
const skewSec = Math.floor(Date.now() / 1000) - timestamp;
if (!Number.isFinite(timestamp) || Math.abs(skewSec) > 300) return false;
// 2) HMAC-SHA256 over the raw body
const expected = createHmac('sha256', SECRET).update(rawBody).digest();
const received = Buffer.from(signature, 'hex');
// 3) Constant-time comparison (protects against timing attacks)
return received.length === expected.length && timingSafeEqual(received, expected);
}
The same signing scheme (HMAC-SHA256 over the raw body, lowercase hex, ±300 s window, constant-time comparison) governs the inbound webhooks you sign towards Shara. Learn one and you know both.
A complete receiver
Section titled “A complete receiver”Minimal Express server that receives the delivery, verifies signature and window, deduplicates by X-SHARA-Event-Id and responds fast, deferring the heavy work. Note the express.raw: you need the raw body for the HMAC to match byte for byte.
import express from 'express';
import { createHmac, timingSafeEqual } from 'node:crypto';
const SECRET = process.env.SHARA_WEBHOOK_SECRET;
const seen = new Set(); // in production, use Redis or your database
const app = express();
app.use(express.raw({ type: 'application/json' }));
app.post('/hooks/shara', (req, res) => {
const sig = req.get('X-SHARA-Signature') ?? '';
const ts = Number(req.get('X-SHARA-Timestamp') ?? 0);
const eventId = req.get('X-SHARA-Event-Id') ?? '';
// 1) ±300 s anti-replay window
const skew = Math.floor(Date.now() / 1000) - ts;
if (!Number.isFinite(ts) || Math.abs(skew) > 300) {
return res.status(401).json({ error: 'timestamp_out_of_window' });
}
// 2) Constant-time HMAC signature
const expected = createHmac('sha256', SECRET).update(req.body).digest();
const received = Buffer.from(sig, 'hex');
const ok = received.length === expected.length && timingSafeEqual(received, expected);
if (!ok) return res.status(401).json({ error: 'signature_invalid' });
// 3) Idempotency: discard the duplicate, but respond 2xx anyway
if (eventId && seen.has(eventId)) return res.status(200).json({ ok: true, duplicate: true });
if (eventId) seen.add(eventId);
// 4) Respond RIGHT AWAY; process the heavy work asynchronously
const event = JSON.parse(req.body.toString('utf8'));
res.status(200).json({ ok: true });
queueForProcessing(event); // your queue / worker
function queueForProcessing(e) {
console.log('verified event:', e.type, e.id);
}
});
app.listen(8080);
Retries, idempotency and errors
Section titled “Retries, idempotency and errors”Respond with a 2xx within seconds (ideally < 10 s) and process the heavy work asynchronously. Delivery rules:
- Retries with backoff. If your endpoint responds
5xx, times out or is unreachable, Shara retries with growing exponential backoff over several hours before giving up. - Idempotency via
X-SHARA-Event-Id. The same event can reach you more than once (from a retry after a timeout where you actually processed it). Log theidyou’ve already seen and discard duplicates instead of reprocessing them. - No guaranteed order. Under load, two events can arrive out of order. Use
created_atif order matters; don’t assume strict sequence. - Auto-pause. After many consecutive failures, Shara pauses the destination and flags it in the panel; reactivate it from
Settings → Webhooksonce your receiver is healthy again. - Secret rotation. You can rotate the signing secret at any time; deploy the new one on your receiver and verify subsequent deliveries with it.
Security best practices
Section titled “Security best practices”A webhook is a door you open in your infrastructure towards the internet. Treat it as such:
- Always verify the signature, and fail closed. No valid signature, no processing. Never accept an event “because the JSON looks fine”.
- Read the raw body. Calculate the HMAC over the received bytes, not over a JSON re-serialized by your framework, or the signature won’t match.
- Compare in constant time. Use
timingSafeEqual(or equivalent) so you don’t leak the correct signature through timing differences. - Store the secret in a secrets manager. Never in the repository, in the frontend or in logs. It’s a server-side credential.
- Serve the endpoint only over HTTPS. Traffic is encrypted and Shara doesn’t deliver to plain
http://. - Respect the anti-replay window. Reject timestamps outside ±300 s: it’s your defense against malicious replays of a captured event.
- Validate the payload schema. Even though it comes signed, validate the fields before acting; don’t blindly trust types or sizes.
- Respond fast and process separately. Queue the work and return
2xx; that way you avoid timeouts that trigger unnecessary retries. - Rotate the secret periodically and immediately at any suspicion of a leak.
- Monitor auto-pause. If Shara pauses the destination due to failures, your integration is down: watch it in the panel.
Inbound vs. outbound: don’t confuse them
Section titled “Inbound vs. outbound: don’t confuse them”Shara has two webhook mechanisms with similar names but opposite meanings. This page covers the outbound ones (Shara notifies you). The inbound ones (you send events to Shara, for example a lead from a form) are documented in the REST API.
| Outbound (this page) | Inbound (REST API) | |
|---|---|---|
| Direction | Shara → your server | Your system → Shara |
| Who signs | Shara, with your per-tenant secret | You, with your per-tenant secret |
| Who retries | Shara, with backoff | You, the sender |
| Endpoint | Your HTTPS URL | POST /v1/webhooks/inbound/{slug} |
| What for | Reacting to approvals, runs, drafts | Feeding an agent from an external system |
Need an event type that isn’t on the list, or delivery to a special destination? Write to us at
support@aiginer.comwith your use case.