Skip to content
camada
Menu

Reference

Webhook payload

A webhook channel is one HTTPS URL per project, added under Alerts → Channels in the app. camada sends one POST per alert event, JSON body, signed. Any 2xx response counts as delivered; anything else is retried. Times in the body are ISO 8601 UTC.

Request headers

HeaderValue
content-typeAlways application/json.
x-camada-eventThe event id, e.g. block.applied. Route on this without parsing the body.
x-camada-deliveryDelivery id, the same value as delivery_id in the body. Use it to deduplicate.
x-camada-signaturet=<unix seconds>,v1=<hex HMAC-SHA256>. Can carry more than one v1; accept the request if any one matches. See below.

Payload

Every event uses the same envelope. entity is null for events that are not about one address or session (quota, payment, learning); each link in links is absolute or null. data carries the fields specific to the event. schema_version is 1.

POST body
{
  "schema_version": 1,
  "event": "block.applied",
  "severity": "warning",
  "occurred_at": "2026-09-03T14:22:07.311Z",
  "delivery_id": "dlv_7f3c1a9e2b40",
  "org": { "id": "org_2ZKq", "name": "Acme" },
  "project": { "id": "prj_8Ha2", "name": "Production", "slug": "acme-production", "env": "production" },
  "entity": { "key_type": "ip", "key": "203.0.113.66" },
  "verdict": "malicious",
  "category": "scraping",
  "confidence": 0.94,
  "action": "blocked",
  "summary": "1,284 requests in 4 minutes across 37 product pages, no beacon, headless fingerprint.",
  "evidence": [
    "no beacon post on any of 37 page views",
    "TLS fingerprint matches a known headless client",
    "request interval standard deviation 41 ms"
  ],
  "links": {
    "entity": "https://my.camada.app/activity/entities/ip/203.0.113.66",
    "run": "https://my.camada.app/activity/runs/run_5512",
    "allow": "https://my.camada.app/lists?allow=203.0.113.66",
    "block": "https://my.camada.app/lists?tab=blocks&block=203.0.113.66",
    "project": "https://my.camada.app/"
  },
  "data": { "ttl_hours": 24, "source": "verdict" }
}

Compatibility

Inside schema_version: 1 fields are only added, never removed or renamed — so parse leniently and ignore what you do not know. A breaking change bumps schema_version, and the old version keeps being delivered for 90 days after the new one ships, which is your window to migrate.

Event catalog v1

EventDefault severityMeaning
block.appliedwarningA block was written and published to your snapshot.
block.recommendedinfoA block was recommended during the learning window, not enforced.
verdict.suspiciousinfoThe analyst wrote a suspicious or malicious verdict that was not blocked.
attack.spikecriticalBlocked plus recommended activity in five minutes went far over the trailing hour.
learning.endedinfoThe learning window closed and is waiting for your review.
list.changedinfoSomeone changed the allow-list or a manual block.
key.rotatedwarningA project key was rotated; the previous pair is in its grace period.
key.old_still_in_usewarningThe previous key is still being used after a rotation.
quota.thresholdwarningThe organization passed 80% of its included events for the period.
quota.exceededwarningThe organization passed 100% of its included events. Traffic is never blocked.
run.failedcriticalAn analysis run failed after its last retry.
payment.failedcriticalStripe could not charge the card on file.
testinfoSent by "Send test" on a channel. Same shape, no real entity.

Severity is a default: you set it per event per channel when you route it, and you choose immediate, hourly or daily delivery there too.

Verifying the signature

Each webhook channel has a secret, shown once when you create it and rotatable at any time. Every request carries:

header
x-camada-signature: t=1788393727,v1=9f2c...c41d

v1 is the hex HMAC-SHA256 of the string <t>.<raw body> under your channel secret, where t is the Unix timestamp in seconds from the same header. Reject the request when |now − t| is more than 300 seconds, and compare the digests in constant time. Sign over the bytes you received, before any JSON parse or re-serialisation.

The header can carry more than one v1: for 24 hours after you rotate a secret, camada signs with both the old and the new one (t=…,v1=…,v1=…), so you can deploy the new secret without dropping deliveries. The order is not promised. Accept the request if any one v1 matches, as the sample does.

Node 20+ / Express
import { createHmac, timingSafeEqual } from 'node:crypto';
import express from 'express';

const app = express();
const SECRET = process.env.CAMADA_WEBHOOK_SECRET;

// Verify over the RAW body, before any JSON parsing.
app.post('/camada', express.raw({ type: 'application/json' }), (req, res) => {
  const header = req.get('x-camada-signature') ?? '';
  const pairs = header.split(',').map((kv) => kv.split('='));
  const t = Number(pairs.find(([k]) => k === 't')?.[1]);
  if (!Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > 300) return res.sendStatus(400);

  const expected = createHmac('sha256', SECRET).update(t + '.' + req.body).digest('hex');
  const want = Buffer.from(expected, 'hex');
  // The header can carry more than one v1 (see below): accept the request if any one matches.
  const ok = pairs
    .filter(([k]) => k === 'v1')
    .some(([, v]) => {
      const got = Buffer.from(v ?? '', 'hex');
      return got.length === want.length && timingSafeEqual(got, want);
    });
  if (!ok) return res.sendStatus(401);

  const event = JSON.parse(req.body.toString('utf8'));
  console.log(event.event, event.delivery_id);
  res.sendStatus(200); // any 2xx counts as delivered
});

Retries

A non-2xx response, a timeout or a transport error is retried up to five times with a growing delay: 30 seconds, 1 minute, 5 minutes, 15 minutes, 60 minutes. Deliveries are idempotent per event and channel, but a retry can still arrive after your first response was lost — deduplicate on delivery_id. A channel that has been failing for 24 hours is disabled automatically and the organization's admins get an email.

Noise control

  • The same event for the same entity is sent once per 60 minutes; repeats are recorded without a delivery.
  • Each channel takes a limited number of immediate deliveries an hour, 20 by default and editable per channel between 1 and 1,000 under Alerts → Channels; anything over the cap rolls into that channel's digest instead of being dropped.
  • A route can be hourly or daily instead of immediate: one summary per channel per window, with counts by event and the top entities.
  • Mute silences a project until a time you pick. Critical events still deliver.

Slack and email channels

A Slack channel takes an incoming webhook URL and receives Block Kit, not this JSON: a header, a section with the entity, verdict, confidence and project as fields, a context line, and Allow / Block buttons linking into the app. An email channel goes to named addresses or to every member of the organization, sent through Postmark from alerts@camada.app, subject [camada] <severity> <event>. Both carry the same facts as the JSON above.

Webhook channels are on Growth and above, Slack from Starter, email on every plan — see pricing. The Channels tab has a "Send test" button that pushes the test event through the real path and shows you the response code.