NOUKAI

Webhooks API

Receive HTTP callbacks when async flow runs reach a terminal state — signed payloads, retries, and signing-secret rotation.

Base URL: https://api.noukai.xyz/api/v1

Use webhooks to be notified when an async flow run finishes. You opt in per request by passing a callbackUrl on the /jobs endpoint; Noukai POSTs a signed JSON payload to that URL when the run reaches a terminal state.

When webhooks fire

Webhooks fire only on async (/jobs) executions. Sync /execute and step-through /step ignore callbackUrl.

v1 ships two terminal events:

EventFires when
flow.completedThe async run finished successfully and result is available.
flow.failedThe async run failed (runtime error, credits exhausted, or BYOK key rejected).

Subscribing per request

Pass callbackUrl (and optionally callbackEvents) on POST /seq/{org}/{project}/{flow}/jobs:

{
  "message": "summarize this",
  "callbackUrl": "https://example.com/hooks/noukai",
  "callbackEvents": ["flow.completed", "flow.failed"]
}
FieldTypeRequiredDescription
callbackUrlstringNoHTTPS URL to POST the event to. Must resolve to a public IP. Omit to disable webhooks for the run.
callbackEventsstring[]NoSubset of events to deliver. Defaults to all terminal events. Allowed values: flow.completed, flow.failed.

callbackUrl must be HTTPS and must resolve to a public IP address. Private, link-local, and loopback addresses are rejected by Noukai's SSRF guard and the delivery is recorded as failed_permanent.

Delivery request

Noukai POSTs application/json to your URL with the following headers:

HeaderDescription
Content-TypeAlways application/json.
User-AgentNoukai-Webhook/1.0.
X-Noukai-EventEvent name (flow.completed or flow.failed).
X-Noukai-DeliveryUUID for this delivery row. Stable across retries. Use as your idempotency key.
X-Noukai-TimestampUnix seconds at signing time. Same value as the t= field inside X-Noukai-Signature.
X-Noukai-SignatureHMAC-SHA256 signature (see Verifying signatures).

Redirects are disabled — a 3xx response is recorded as a permanent failure.

Payload — flow.completed

{
  "event": "flow.completed",
  "executionId": "9e8c2f7e-...-...-...",
  "flowId": "5c3b1a..-...-...-...",
  "organizationId": "01H...",
  "durationMs": 4218,
  "occurredAt": "2026-06-13T08:42:11.034Z",
  "result": { /* the flow's structured output */ }
}

Payload — flow.failed

{
  "event": "flow.failed",
  "executionId": "9e8c2f7e-...-...-...",
  "flowId": "5c3b1a..-...-...-...",
  "organizationId": "01H...",
  "durationMs": 132,
  "occurredAt": "2026-06-13T08:42:11.034Z",
  "errorMessage": "Block 'extract' returned non-JSON output",
  "failureReason": "error"
}
FieldTypeDescription
eventstringflow.completed or flow.failed. Matches X-Noukai-Event.
executionIdstring (UUID)The run's execution ID — same value /jobs returned synchronously.
flowIdstring (UUID)Flow that was executed.
organizationIdstringOrg that owns the flow.
durationMsintegerWall-clock duration of the run, in milliseconds.
occurredAtstring (ISO 8601 UTC)When the run reached the terminal state.
resultobject | nullPresent on flow.completed. The flow's structured output.
errorMessagestringPresent on flow.failed. Human-readable failure summary.
failureReasonstringPresent on flow.failed. One of error, credits_exhausted, byok_rejected.

Payload size limit

The body is capped at 256 KB. If a flow.completed payload would exceed the cap, the result field is replaced and the body is delivered as:

{
  "event": "flow.completed",
  "executionId": "...",
  "result": null,
  "truncated": true,
  "originalResultBytes": 1872411,
  "durationMs": 4218,
  "occurredAt": "..."
}

If you need full output for large runs, fetch it from GET /seq/{org}/{project}/{flow}/jobs/{executionId} using the executionId from the payload.

Verifying signatures

Every delivery is signed with HMAC-SHA256 over <timestamp>.<body> using your org's signing secret:

X-Noukai-Signature: t=1718268131,v1=4d3c2a1b...

During a 24-hour grace window after rotation, the header includes a second slot signed with the previous secret:

X-Noukai-Signature: t=1718268131,v1=<signed-with-new>,v2=<signed-with-old>

A handler that holds a single customer secret should accept the request if either v1 or v2 matches.

Node example

import crypto from "node:crypto";
 
function verify(rawBody: string, header: string, secret: string): boolean {
  const parts = Object.fromEntries(
    header.split(",").map((kv) => kv.split("=", 2))
  );
  if (!parts.t || !parts.v1) return false;
 
  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${parts.t}.${rawBody}`)
    .digest("hex");
 
  const matches = (sig: string) =>
    crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig));
 
  return matches(parts.v1) || (parts.v2 != null && matches(parts.v2));
}

Python example

import hmac, hashlib
 
def verify(raw_body: bytes, header: str, secret: bytes) -> bool:
    parts = dict(kv.split("=", 1) for kv in header.split(","))
    if "t" not in parts or "v1" not in parts:
        return False
 
    expected = hmac.new(
        secret, f"{parts['t']}.".encode() + raw_body, hashlib.sha256
    ).hexdigest()
 
    if hmac.compare_digest(expected, parts["v1"]):
        return True
    if "v2" in parts and hmac.compare_digest(expected, parts["v2"]):
        return True
    return False

Verify against the raw request bytes, before any JSON parsing or middleware that re-encodes whitespace. Re-serialized JSON will not match the signature.

Retries and at-least-once delivery

Delivery is at-least-once. Noukai makes up to 6 attempts per delivery, retrying transient failures with an exponential schedule:

AttemptWhen it fires
1Immediately after the run terminates.
21 minute after attempt 1 fails.
35 minutes after attempt 2 fails.
430 minutes after attempt 3 fails.
52 hours after attempt 4 fails.
612 hours after attempt 5 fails.

If attempt 6 fails, the delivery is marked dead_letter and is not retried again.

ResponseBehaviour
2xxSuccess.
3xxPermanent failure — redirects disabled.
408, 429Retried.
4xx (other)Permanent failure.
5xxRetried.
Network error / timeoutRetried.

Timeouts: 5 s connect, ~20 s total.

The X-Noukai-Delivery UUID is stable across retries — including the rare case where Noukai successfully posted to you but failed to record the outcome internally. Always dedupe on this header before acting on a payload.

Signing secret management

Each org has one webhook signing secret. The secret is auto-created the first time you fetch or rotate it.

Get masked secret

GET /organizations/{org_id}/webhooks/secret
Authorization: Bearer <token>

Returns a masked preview. The full plaintext is never returned by this endpoint — by design. You only see the full secret once, in the rotation response. Available to any org member.

{
  "organizationId": "01H...",
  "secretPreview": "whsec_abcd••••••••",
  "version": 2,
  "createdAt": "2026-05-01T08:00:00.000Z",
  "rotatedAt": "2026-06-13T08:00:00.000Z",
  "graceUntil": "2026-06-14T08:00:00.000Z"
}
FieldTypeDescription
secretPreviewstringFirst 10 chars plus a fixed mask. Safe to log.
versionintegerBumps on every rotation.
createdAtstringWhen the current secret was issued.
rotatedAtstring | nullWhen the most recent rotation happened, if any.
graceUntilstring | nullWhile set, deliveries include v2= signed with the previous secret.

Rotate the secret

POST /organizations/{org_id}/webhooks/secret/rotate
Authorization: Bearer <token>

Admin-only. Atomically issues a new secret and returns the full plaintext exactly once. For the next 24 hours, deliveries are signed with both the new secret (v1=) and the previous secret (v2=), so you can roll your verifier without dropping events.

{
  "organizationId": "01H...",
  "newSecret": "whsec_K9mZ4xQ1y7Lq2p8R...",
  "previousSecretPreview": "whsec_abcd••••••••",
  "version": 3,
  "graceUntil": "2026-06-14T08:00:00.000Z",
  "rotatedAt": "2026-06-13T08:00:00.000Z"
}

newSecret is shown once. Store it before you dismiss the response. If you lose it, rotate again.

Listing deliveries

GET /organizations/{org_id}/webhooks/deliveries?limit=50&before=<iso8601>
Authorization: Bearer <token>

Returns recent deliveries ordered newest-first. Available to any org member.

Query parameters

ParameterTypeDescription
limitinteger1–200, default 50.
beforestring (ISO 8601)Cursor — return deliveries created strictly before this timestamp. Pass back nextCursor to page.

Response

{
  "deliveries": [
    {
      "id": "d8e1...-...-...-...",
      "eventType": "flow.completed",
      "targetUrl": "https://example.com/hooks/noukai",
      "status": "succeeded",
      "attempt": 1,
      "responseStatus": 200,
      "lastAttemptedAt": "2026-06-13T08:42:11.512Z",
      "errorMessage": null,
      "createdAt": "2026-06-13T08:42:11.034Z"
    }
  ],
  "hasMore": true,
  "nextCursor": "2026-06-13T08:30:00.000Z"
}
StatusMeaning
pendingRow written; not yet attempted.
in_flightWorker is mid-attempt.
succeededEndpoint returned 2xx.
failed_retryRetryable failure; another attempt is scheduled.
failed_permanentNon-retryable failure (3xx, non-408/429 4xx, SSRF block).
dead_letterAll 6 attempts exhausted.

Authentication

Authorization: Bearer <token>

Both API keys and JWT tokens are accepted on these endpoints. Rotation additionally requires the user to be an organization admin or owner.

Error Responses

StatusDescription
401Invalid or missing auth token.
403Caller is not a member of the org (or not an admin, on rotation).
404Organization not found.
422Request validation error.