Webhooks
cloudscode posts a JSON event to every URL you register whenever something happens in your tenant — instances spin up, BYOK keys change, subscriptions update, and more. Payloads are HMAC-SHA256 signed using a per-webhook secret so you can verify them.
Quick start
Section titled “Quick start”- Open Dashboard → Webhooks → New webhook.
- Paste your endpoint URL (must be
https://). - Select the events you want to receive.
- cloudscode shows your signing secret exactly once — save it now.
- Verify the
X-CloudsCode-Signatureheader on every incoming POST.
Event catalog
Section titled “Event catalog”| Type | When it fires |
|---|---|
instance.created | Provisioning starts for a new Claw instance. |
instance.deleted | Tenant requests teardown. |
instance.restarted | Tenant triggers /instances/:id/restart. |
byok.created | A BYOK LLM key is added. |
byok.deleted | A BYOK LLM key is removed. |
tenant.updated | Tenant profile / residency changes. |
subscription.updated | Stripe webhook lands a plan change (cycle, status, etc.). |
webhook.test | You clicked Send test on a webhook row. |
Payload envelope
Section titled “Payload envelope”Every POST has the shape:
{ "id": "evt_8c4f5a2e3b9c1d6e", "type": "instance.created", "created_at": 1745961234567, "tenant_id": "ten_…", "data": { "id": "inst_…", "subdomain": "acme", "tier": "pro", "status": "provisioning" }}id is the idempotency key — receivers should record it and drop dupes.
created_at is ms-epoch UTC. data shape is event-specific.
Headers we send
Section titled “Headers we send”| Header | Meaning |
|---|---|
X-CloudsCode-Signature | t=<unix-sec>,v1=<hex-hmac-sha256> — verify this. |
X-CloudsCode-Event-Id | Same value as id in the body — idempotency key. |
X-CloudsCode-Event-Type | Mirrors type. |
X-CloudsCode-Attempt | 1 on first delivery, increments on each retry. |
X-CloudsCode-Webhook-Id | Your webhook id — useful when one app handles many. |
User-Agent | cloudscode-webhook/1.0 |
Verifying the signature
Section titled “Verifying the signature”The signature scheme is identical to Stripe’s: HMAC-SHA256 over
${timestamp}.${rawRequestBody} with your webhook secret as the key.
Reject any request older than 5 minutes to prevent replay attacks.
Node.js
Section titled “Node.js”import crypto from 'node:crypto';
export function verifyCloudsCodeSig(rawBody, header, secret) { const parts = Object.fromEntries( header.split(',').map((p) => p.split('=').map((s) => s.trim())), ); const t = parseInt(parts.t, 10); const v1 = parts.v1; if (!t || !v1) return false;
// Replay window: 5 minutes if (Math.abs(Date.now() / 1000 - t) > 300) return false;
const expected = crypto .createHmac('sha256', secret) .update(`${t}.${rawBody}`) .digest('hex');
// Constant-time compare return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1));}
// Express example — note: req.rawBody must be set before JSON parsingapp.post('/webhooks/cloudscode', express.raw({ type: 'application/json' }), (req, res) => { const ok = verifyCloudsCodeSig( req.body.toString('utf8'), req.header('x-cloudscode-signature'), process.env.CLOUDSCODE_WEBHOOK_SECRET, ); if (!ok) return res.status(400).send('bad signature'); const event = JSON.parse(req.body.toString('utf8')); // …handle event… res.status(200).send('ok');});Python
Section titled “Python”import hmac, hashlib, time
def verify_cloudscode_sig(raw_body: bytes, header: str, secret: str) -> bool: parts = dict(p.strip().split('=', 1) for p in header.split(',')) try: t = int(parts['t']) v1 = parts['v1'] except (KeyError, ValueError): return False # 5-minute replay window if abs(time.time() - t) > 300: return False expected = hmac.new( secret.encode('utf-8'), f'{t}.{raw_body.decode("utf-8")}'.encode('utf-8'), hashlib.sha256, ).hexdigest() return hmac.compare_digest(expected, v1)package main
import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "fmt" "strconv" "strings" "time")
func VerifyCloudsCodeSig(rawBody []byte, header, secret string) bool { parts := map[string]string{} for _, p := range strings.Split(header, ",") { kv := strings.SplitN(strings.TrimSpace(p), "=", 2) if len(kv) == 2 { parts[kv[0]] = kv[1] } } t, err := strconv.ParseInt(parts["t"], 10, 64) if err != nil { return false } if abs(time.Now().Unix()-t) > 300 { return false } mac := hmac.New(sha256.New, []byte(secret)) fmt.Fprintf(mac, "%d.%s", t, rawBody) expected := hex.EncodeToString(mac.Sum(nil)) return hmac.Equal([]byte(expected), []byte(parts["v1"]))}
func abs(n int64) int64 { if n < 0 { return -n } return n}Retry schedule
Section titled “Retry schedule”cloudscode retries failed deliveries up to 5 attempts with this backoff (counted from the initial enqueue):
| Attempt | Delay |
|---|---|
| 1 | immediate |
| 2 | +1 minute |
| 3 | +5 minutes |
| 4 | +30 min |
| 5 | +2 hours |
| (giving up) | +12 hr |
A delivery counts as successful when your endpoint replies 2xx.
Anything else (including 3xx, 4xx, 5xx, transport errors, timeouts
15s) is a failure.
After 5 trailing failures we automatically disable the webhook to protect your endpoint, and surface a banner in the dashboard. Re-enable from Webhooks → Pause/Resume.
Idempotency
Section titled “Idempotency”Use X-CloudsCode-Event-Id (same as id in the body) to dedupe — when
we retry, the id stays the same so you can SELECT-then-INSERT against an
events table. Each retry increments X-CloudsCode-Attempt so you can
distinguish first-time deliveries from replays.
Test + replay
Section titled “Test + replay”The dashboard exposes two debugging primitives:
- Send test — fires a
webhook.testevent to a single webhook so you can verify your verification code without waiting for a real event. - Replay — re-fires an old delivery (uses the original payload + id, but signs fresh).
Both create a new row in Deliveries so you can compare against the original.
Security
Section titled “Security”- HTTPS only. HTTP URLs are rejected at registration time.
- Per-webhook secret. Rotate by creating a new webhook + deleting the old.
- Constant-time comparison. Use the SDK helpers above; never compare
signatures with
==. - 5-minute replay window. Reject any timestamp outside ± 5 min from your server clock.