Docs/Integrations

Webhooks

Receive event notifications from Arcus — automation runs, anomalies, comments — at an endpoint you control.

Webhooks let your systems react to things that happen in Arcus. We POST a signed JSON payload to a URL you control whenever an event you've subscribed to fires.

When to use webhooks

  • An automation completes (success or failure) and you want to log it.
  • A finding is created and you want to file a Linear/Jira ticket.
  • A pin in Watch refreshes with a material delta and you want a Slack message.
  • A comment is added to a thread and you want to notify your customer success tool.

You control what your endpoint does with the payload. Arcus's job ends at the POST.

Setup

In Arcus, open Settings → Webhooks → New webhook. Provide:

  • URL — the endpoint we'll POST to. Must be HTTPS.
  • Events — a list of event types to subscribe to (see below).
  • Description — optional label, shown in the webhook list.

Click Save. Arcus generates a signing secret — a 32-byte random string. Copy it and store it securely. We show it once; rotation requires regenerating.

Event types

EventWhen it fires
automation.completedAn automation finishes a run (success or failure)
automation.failedAn automation run errors
pin.refreshedA Watch pin completes a refresh
pin.deltaA Watch pin's refresh shows a material change vs prior
finding.createdA new finding is logged
comment.addedA comment is added to a thread
thread.sharedA new share link is generated

You can subscribe to specific events or all events.

Payload shape

Every payload follows this envelope:

{
  "id": "evt_01H6Q3...",
  "type": "automation.completed",
  "tenant_id": "ten_abc123",
  "created_at": "2026-05-09T14:32:11Z",
  "data": {
    "automation_id": "aut_xyz789",
    "run_id": "run_def456",
    "status": "success",
    "trigger": { "kind": "schedule", "cron": "0 7 * * 1-5" },
    "action": { "kind": "run_thread", "thread_id": "thr_ghi012" },
    "delivery": [{ "kind": "slack", "channel": "#growth-daily" }],
    "duration_ms": 4218,
    "cost_usd": 0.062
  }
}

The data block varies by event type. The id, type, tenant_id, and created_at fields are always present.

Signing

Every webhook request includes two HTTP headers:

  • Arcus-Signature: t=1715269931,v1=abc123def456...
  • Arcus-Event-Id: evt_01H6Q3...

The signature is an HMAC-SHA256 of {timestamp}.{body} using your signing secret. Verify in your handler:

import { createHmac, timingSafeEqual } from "crypto";

function verifyArcusWebhook(req: Request, secret: string): boolean {
  const sig = req.headers.get("arcus-signature");
  if (!sig) return false;

  const parts = Object.fromEntries(
    sig.split(",").map((kv) => kv.split("=") as [string, string])
  );
  const t = parts.t;
  const v1 = parts.v1;
  if (!t || !v1) return false;

  // Reject anything older than 5 minutes — replay protection
  const ageSec = Math.abs(Date.now() / 1000 - parseInt(t, 10));
  if (ageSec > 300) return false;

  const body = req.body; // raw body string
  const expected = createHmac("sha256", secret)
    .update(`${t}.${body}`)
    .digest("hex");

  return timingSafeEqual(Buffer.from(v1), Buffer.from(expected));
}

If the signature doesn't verify, return 401 and don't process the payload. We'll mark the delivery as failed and retry.

Replay protection

The t value in the signature is the request's epoch-seconds timestamp. Reject any request older than 5 minutes — Arcus never retries with a stale timestamp.

You may also want to dedupe by arcus-event-id to handle the case where you ack a delivery but then we retry due to network blip. Cache event IDs for a few minutes; reject duplicates.

Retries

If your endpoint returns a 5xx or times out (>10s), Arcus retries with exponential backoff: 30s, 1min, 5min, 15min, 1h. After 5 failed deliveries, the webhook is marked failed and retries stop. You can re-enable from the webhook detail page.

2xx responses are taken as successful delivery. 4xx responses (other than 429) are treated as permanent failures and not retried.

Inspect deliveries

Every delivery is logged. Settings → Webhooks → [your webhook] → Deliveries shows the last 100 — request body, response status, response body, and timing. Useful when something looks wrong.

You can replay any past delivery from the deliveries list — Arcus re-POSTs with the original payload (and a fresh timestamp/signature).

Sample handler

A minimal Node.js + Express handler:

import express from "express";
import { createHmac, timingSafeEqual } from "crypto";

const app = express();
const SECRET = process.env.ARCUS_WEBHOOK_SECRET!;

app.post(
  "/webhooks/arcus",
  express.raw({ type: "application/json" }),
  (req, res) => {
    if (!verifyArcusWebhook(req, SECRET)) return res.sendStatus(401);

    const event = JSON.parse(req.body.toString());

    if (event.type === "automation.completed") {
      console.log(`automation ${event.data.automation_id} ran in ${event.data.duration_ms}ms`);
      // ... your logic
    }

    res.sendStatus(200);
  }
);

Related