Skip to content

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.

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:

HeaderValueWhat for
X-SHARA-Signature<hex> HMAC-SHA256 of the raw bodyAuthenticates the event’s origin.
X-SHARA-Timestamp<unix-seconds>Closes the ±300 s anti-replay window.
X-SHARA-Event-Idunique event id (evt_…)Idempotency: deduplicates retries.
X-SHARA-Event-Typee.g. run.completedRoutes without opening the body.
Content-Typeapplication/jsonThe body is UTF-8 JSON.

Setting up a destination takes about a minute from the panel. The only irreversible step is copying the secret: it’s shown only once.

  1. Go to Settings → Webhooks (requires an admin role and active two-step verification).
  2. Click New destination and enter your endpoint’s HTTPS URL. Only https:// is accepted; plain http:// is rejected.
  3. Select the event types you want to receive. Only subscribe to the ones you’ll actually process.
  4. Save and copy the signing secret that appears. It won’t be shown again: if you lose it, you’ll have to rotate it.
  5. Send a test event (ping) from the panel and confirm your receiver responds 2xx and verifies the signature correctly.
  6. Activate the destination. From then on, Shara starts delivering your subscribed events in real time.

The ping event 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.

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.

typeWhen it firesKey data
approval.createdAn agent requests a green light before executing a sensitive action (sending an email, moving a CRM record).approval_id, agent, action, expires_at
approval.decidedA person approves or rejects that request from the panel.approval_id, decision (approved/rejected), decided_by
run.completedAn agent execution finishes successfully.run_id, agent, alias, usage.stu
run.failedAn execution ends in error or is stopped by a spending kill-switch.run_id, agent, reason
draft.createdAn agent generates a draft (email, proposal, reply) awaiting human review.draft_id, agent, kind, run_id
integration.connectedA 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 a run.failed with reason indicating the scope of the cut (run · hour · day), useful for alerting your team.

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:

json
{
  "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
    }
  }
}

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:

json
{
  "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:

json
{
  "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:

json
{
  "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:

json
{
  "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):

json
{
  "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"
  }
}

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.

javascript
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.

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.

javascript
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);

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 the id you’ve already seen and discard duplicates instead of reprocessing them.
  • No guaranteed order. Under load, two events can arrive out of order. Use created_at if 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 → Webhooks once 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.

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)
DirectionShara → your serverYour system → Shara
Who signsShara, with your per-tenant secretYou, with your per-tenant secret
Who retriesShara, with backoffYou, the sender
EndpointYour HTTPS URLPOST /v1/webhooks/inbound/{slug}
What forReacting to approvals, runs, draftsFeeding 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.com with your use case.

Technical questions about webhooks? Write to us at api@aiginer.com.