> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cevoid.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks via Flows

> Send signed Cevoid events to your HTTPS endpoint from a Flow.

Use webhooks when you want a Flow to notify your own HTTPS service after a supported Cevoid event. This integration sends fixed `POST` requests with a versioned Cevoid payload. It does not support custom methods, customer-defined authorization headers, payload scripting, inbound webhooks, or developer topic subscriptions.

## Prerequisites

* You can access *Settings* and *Flows* in your Cevoid workspace.
* You have a public HTTPS receiver that accepts `POST` requests, preserves the exact raw request body for signature verification, does not redirect, and returns a `2xx` response after accepting an event.

## Set up a webhook

1. Go to *Settings* → *Integrations* → *Cevoid API*.
2. Create a webhook destination, or reuse an active destination.
3. Copy the signing secret when Cevoid shows it. The secret is shown once.
4. Go to *Settings* → *Flows* and create a Flow.
5. ***Choose*** `order.fulfilled`, `order.delivered`, or `review.submitted` as the trigger.
6. Add a webhook action, choose the destination, then publish the Flow.

You can also use the Flow quick-create option from a destination. Other triggers and developer event topics are not supported.

## Request contract

Cevoid sends an HTTPS `POST` with `content-type: application/json` and these [Standard Webhooks](https://www.standardwebhooks.com/) headers:

* `webhook-id`: stable public event ID; retries keep the same value
* `webhook-timestamp`: Unix seconds for this attempt
* `webhook-signature`: one or more `v1,<base64>` signatures

The JSON envelope is versioned independently from the event data:

```json theme={"system"}
{
  "apiVersion": 1,
  "context": {
    "recipientProfileId": null,
    "subject": { "id": "order-id", "type": "order" },
    "workspaceId": "workspace-id"
  },
  "createdAt": "2026-08-03T12:00:00.000Z",
  "data": {
    "action": {},
    "trigger": { "order": { "id": "order-id" } }
  },
  "flow": {
    "actionNodeId": "action-id",
    "attemptId": "attempt-id",
    "id": "flow-id",
    "runId": "run-id",
    "versionId": "version-id"
  },
  "id": "d8177c1d-39da-4c4d-93cd-5964043735e4",
  "payloadVersion": 1,
  "type": "order.fulfilled"
}
```

Test sends use the same transport and signing path with `type: "cevoid.test"`. They do not create fake Flow, run, or version records.

## Verify the exact raw body

Verify the signature before parsing JSON. Build the signed content as:

```text theme={"system"}
webhook-id + "." + webhook-timestamp + "." + exact_raw_request_body
```

Decode the base64 value after `whsec_`, calculate HMAC-SHA256, encode the digest as base64, and compare it in constant time with any `v1` signature in `webhook-signature`. Reject stale timestamps according to your replay-risk policy. During secret rotation, accept a match from either current secret while both signatures are present.

### Node.js

```js theme={"system"}
import { createHmac, timingSafeEqual } from 'node:crypto'

export function verifyCevoidWebhook({ rawBody, headers, secret }) {
  const id = headers['webhook-id']
  const timestamp = headers['webhook-timestamp']
  const signatureHeader = headers['webhook-signature']
  if (typeof signatureHeader !== 'string') return false

  const key = Buffer.from(secret.replace(/^whsec_/, ''), 'base64')
  const expected = createHmac('sha256', key)
    .update(`${id}.${timestamp}.${rawBody}`, 'utf8')
    .digest()

  return signatureHeader.split(' ').some((entry) => {
    const [version, encoded] = entry.split(',', 2)
    if (version !== 'v1' || !encoded) return false
    const received = Buffer.from(encoded, 'base64')
    return received.length === expected.length && timingSafeEqual(received, expected)
  })
}
```

Pass `rawBody` directly from your framework's raw-body middleware. Do not use `JSON.stringify(req.body)`.

### Python

```python theme={"system"}
import base64
import hashlib
import hmac

def verify_cevoid_webhook(raw_body: bytes, headers: dict[str, str], secret: str) -> bool:
    webhook_id = headers["webhook-id"]
    timestamp = headers["webhook-timestamp"]
    key = base64.b64decode(secret.removeprefix("whsec_"), validate=True)
    signed = webhook_id.encode() + b"." + timestamp.encode() + b"." + raw_body
    expected = base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest()).decode()

    signature_header = headers.get("webhook-signature")
    if not signature_header:
        return False

    for entry in signature_header.split(" "):
        version, separator, signature = entry.partition(",")
        if version == "v1" and separator and hmac.compare_digest(signature, expected):
            return True
    return False
```

### HMAC test vector

This fixture is derived from the automated signing contract:

```text theme={"system"}
secret: whsec_MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=
webhook-id: d8177c1d-39da-4c4d-93cd-5964043735e4
webhook-timestamp: 1785758400
raw body: {"apiVersion":1,"id":"d8177c1d-39da-4c4d-93cd-5964043735e4","type":"cevoid.test"}
webhook-signature: v1,sShEFW56NUXzVlNMX3CrZpBJGzDG7Nw65RbDc9EYLNs=
```

## Respond and deduplicate

Return any `2xx` response after you durably accept the event. Cevoid does not follow redirects.

Delivery is at least once. Store `webhook-id` and ignore an ID you have already processed. A timeout or lost response can mean your service accepted an event that Cevoid retries.

Cevoid automatically retries HTTP `408`, `425`, `429`, and `5xx` responses after approximately 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, and 12 hours, with jitter. HTTP `2xx` is accepted. Redirects, other `3xx`, and most `4xx` responses are terminal. `401` and `403` are terminal but can be retried manually after you fix the receiver.

Limits are 10 requests per second per destination and 50 requests per second per workspace. Requests have bounded timeouts and response capture. URLs must use HTTPS in production, cannot contain credentials, fragments, or query strings, and must resolve only to allowed public network addresses. DNS and URL safety are checked again for every attempt.

## Test, retry, and replay

* **Send test** queues a real signed `cevoid.test` request and opens its delivery record.
* **Retry** sends the same logical event again. It keeps the same Outbox item, `webhook-id`, URL snapshot, and byte-identical body. Only the attempt timestamp and signature change.
* **Replay** creates a new logical delivery and `webhook-id`. It clones the original versioned event data but uses the destination's current active revision and URL.

Use the delivery inspector to see logical status, attempts, safe payload metadata, response status, redacted response preview, retry reason, and retry/replay relationships. Secrets, signature headers, full hidden request URLs and bodies, cookies, authorization data, DNS results, and unrestricted response headers are never shown there.

## Troubleshooting

* **No request arrives:** confirm the destination and Flow are active, then inspect the delivery and attempt status.
* **Signature mismatch:** verify against the exact raw bytes, not parsed JSON, and remove `whsec_` before base64 decoding.
* **Repeated events:** deduplicate using `webhook-id`.
* **Redirect or private-network error:** use the final public HTTPS URL directly. Redirects and non-public targets are blocked.
* **Delivery remains incomplete:** fix the terminal reason, enable the destination, then choose retry or replay deliberately.
* **Secret was lost:** rotate it. Accept both signatures during the overlap, deploy the new secret, then finish rotation.

Cevoid does not currently support arbitrary HTTP methods, custom authentication headers, payload scripting, response-fed Flow branching, inbound webhooks, a developer topic-subscription engine, or additional developer event topics.
