# Set up a webhook

> Build a receiver for Cevoid webhooks, create a webhook destination for it and publish a Flow that sends events there.

Breadcrumb: Webhooks › Set up a webhook · Updated: 2026-10-18
Docs index: https://docs.cevoid.com/developers/llms.txt

A webhook needs three things: a receiver on your side, a destination in Cevoid that points at it, and a Flow whose action sends events to the destination.

## Prerequisites

- You can open Settings → Flows in your Cevoid workspace.
- You have a public HTTPS URL for your receiver. Cevoid cannot reach `localhost`; during development, expose your machine through a tunnel service.

## Build the receiver

Your receiver accepts a `POST`, verifies the signature against the exact raw body, records the event and answers with a `2xx` status within 10 seconds. Do slow work after you respond, for example in a job queue.

These receivers use the [Standard Webhooks](https://www.standardwebhooks.com/) library (`npm install standardwebhooks`) to check the signature:

```ts title="Next.js"
// app/webhooks/cevoid/route.ts
import { Webhook } from "standardwebhooks"

const webhook = new Webhook(process.env.CEVOID_WEBHOOK_SECRET ?? "")

export async function POST(request: Request) {
  // The raw body: parsing and re-serializing JSON breaks the signature.
  const body = await request.text()
  try {
    webhook.verify(body, Object.fromEntries(request.headers))
  } catch {
    return new Response("Invalid signature", { status: 401 })
  }

  const event = JSON.parse(body)
  // Your own storage: skip an event ID you have already processed.
  if (await alreadyProcessed(event.id)) return new Response(null, { status: 204 })

  switch (event.type) {
    case "order.fulfilled":
      await queueFulfilledOrder(event.data.order)
      break
  }
  return new Response(null, { status: 204 })
}
```

```js title="Express"
import express from "express"
import { Webhook } from "standardwebhooks"

const app = express()
const webhook = new Webhook(process.env.CEVOID_WEBHOOK_SECRET)

// express.raw keeps the body as bytes for the signature check.
app.post("/webhooks/cevoid", express.raw({ type: "application/json" }), async (req, res) => {
  const body = req.body.toString("utf8")
  try {
    webhook.verify(body, req.headers)
  } catch {
    return res.status(401).send("Invalid signature")
  }

  const event = JSON.parse(body)
  // Your own storage: skip an event ID you have already processed.
  if (await alreadyProcessed(event.id)) return res.sendStatus(204)

  if (event.type === "order.fulfilled") await queueFulfilledOrder(event.data.order)
  res.sendStatus(204)
})
```

Answer a bad signature with `401`: Cevoid stops retrying, and once your secret is fixed you can retry the delivery from Outbox. Other `4xx` statuses stop the delivery for good; see [How Cevoid reads your response](https://docs.cevoid.com/developers/webhooks/retries-and-replays.md#how-cevoid-reads-your-response).

## Set up a webhook

1. Go to Settings → Flows and choose Create flow.
2. Choose Forward event → Webhook.
3. Choose the trigger, such as _Order fulfilled_ or _Product review submitted_. Its event is the `type` your receiver gets.
4. Choose Create destination. To reuse a destination, choose it instead and go to step 7.
5. Enter a name (up to 80 characters), your receiver's HTTPS URL and, if you like, a description (up to 500 characters), then choose Create destination. Result: the Flow opens in the editor and the signing secret is shown.
6. Copy the signing secret, store it where your receiver reads it (for example `CEVOID_WEBHOOK_SECRET`) and choose I saved the secret. Cevoid never shows it again.
7. Choose Publish flow. Result: from now on, each matching event is delivered to your receiver.

To send events from a Flow you already have, add an _Action_ step, set _Delivery target_ to Webhook and choose a destination in _Webhook destination_, or Create destination there. Preview payload on the step shows the `data` it sends.

Before you rely on it, [test the webhook](https://docs.cevoid.com/developers/webhooks/testing.md) with a real signed event.

## URL requirements

Cevoid checks the URL when you save the destination and again before every attempt:

- It must use HTTPS and resolve only to public addresses. Private, loopback and other reserved ranges are rejected, and so are host names such as `localhost` and names ending in `.local`, `.internal`, `.localhost` or `.home.arpa`.
- It must use a host name, not an IP address.
- It cannot contain credentials, a query string or a fragment.
- Ports 22, 23, 25, 110 and 143 are blocked.

Cevoid connects to the address it checked and does not follow redirects, so point the destination at the final URL.

## Manage destinations

Destinations are listed under Webhook destinations in Settings → Integrations → Cevoid API. The menu of each destination has:

- **Send test:** a signed `cevoid.test` event. See [Test a webhook](https://docs.cevoid.com/developers/webhooks/testing.md#send-a-test-event).
- **View deliveries:** the destination's deliveries in Outbox.
- **Edit:** the name, URL and description. A new URL applies to deliveries that have not made their first attempt yet; retries keep the URL of the first attempt.
- **Rotate secret** and **Finish secret rotation:** see [Rotate the signing secret](https://docs.cevoid.com/developers/webhooks/verify-signatures.md#rotate-the-signing-secret).
- **Disable** and **Enable:** Cevoid sends nothing to a disabled destination; its pending deliveries stop instead. A delivery that stopped between retries can be retried from Outbox once you enable the destination again.
- **Archive:** hides the destination for good. It is refused while any Flow, published or draft, still uses the destination.

## Related articles

- [Webhooks via Flows](https://docs.cevoid.com/flows/webhooks.md)
- [Flows](https://docs.cevoid.com/flows.md)

---

Canonical: https://docs.cevoid.com/developers/webhooks/set-up
