Webhooks via Flows
Send signed Cevoid events to your HTTPS endpoint from a flow.
Webhooks let a flow notify your own service when something happens in Cevoid, such as an order being fulfilled or a review being submitted. Your service receives a signed POST with the order, profile, or review the event concerns, so you can act on it without polling for changes.
Every request is signed, so you can verify it came from Cevoid. Requests are fixed POST calls with a JSON body. Custom methods, custom authorization headers, and inbound webhooks are not supported.
This setup needs a service of your own that receives webhook requests. If you don't build it yourself, share this article with your developer.
Prerequisites
- You can access Settings and Flows in your Cevoid workspace
- You have a public HTTPS receiver that accepts
POSTrequests, preserves the exact raw request body for signature verification, does not redirect, and returns a2xxresponse after accepting an event
Set up a webhook
You create a webhook destination from the flow that sends to it. Cevoid shows the destination's signing secret once, when you create it.
- Navigate to Settings → Flows and click Create flow
- Select Forward event, then Webhook
- Select the trigger, such as Order fulfilled or Product review submitted. Every trigger can send its event to a webhook: see Triggers.
- Select Create destination. To use a destination you created before, select it instead and go to step 8: the flow opens in the editor.
- Enter a name, your receiver's HTTPS URL, and optionally a description
- Click Create destination. The flow opens in the editor, and the signing secret shows.
- Copy the signing secret, store it securely, and click I saved the secret
- Click Publish flow. Matching events start being delivered.
To send to a webhook from a flow you already have, see Create a webhook destination.
Request contract
Cevoid sends an HTTPS POST with content-type: application/json and these Standard Webhooks headers:
webhook-id: the event's ID, the same asidin the body. Automatic retries and Retry in Outbox keep it.webhook-timestamp: Unix seconds for this attemptwebhook-signature: one or morev1,<base64>signatures
Every event uses the same envelope. type tells you what happened, data holds the resources it concerns, and origin.flow names the flow that sent it, which is useful when several flows point at one destination. created_at is when the delivery was scheduled to send, after any wait in the flow. To see the data your flow sends, click Preview payload on its Action step.
{
"version": 1,
"id": "d8177c1d-39da-4c4d-93cd-5964043735e4",
"type": "order.fulfilled",
"created_at": "2026-08-03T12:00:00.000Z",
"data": {
"order": { "id": "ord_7k2m4n8p", "order_number": "#1043", "currency": "SEK" },
"profile": { "id": "prf_c4n8x2q1", "name": "Astrid Lindqvist" }
},
"origin": {
"flow": { "id": "wfl_3p9v6d2s", "name": "Post-purchase review request" }
}
}A test run of a flow's live version is signed and delivered the same way.
Verify the exact raw body
Verify the signature before parsing JSON. Build the signed content as:
webhook-id + "." + webhook-timestamp + "." + exact_raw_request_bodyDecode 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.
Node.js
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
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 FalseHMAC test vector
Use this example to check your verification code. The raw body is the example event above as Cevoid sends it: compact JSON, with its keys in alphabetical order.
secret: whsec_MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=
webhook-id: d8177c1d-39da-4c4d-93cd-5964043735e4
webhook-timestamp: 1785758400
raw body: {"created_at":"2026-08-03T12:00:00.000Z","data":{"order":{"currency":"SEK","id":"ord_7k2m4n8p","order_number":"#1043"},"profile":{"id":"prf_c4n8x2q1","name":"Astrid Lindqvist"}},"id":"d8177c1d-39da-4c4d-93cd-5964043735e4","origin":{"flow":{"id":"wfl_3p9v6d2s","name":"Post-purchase review request"}},"type":"order.fulfilled","version":1}
webhook-signature: v1,cAZHMKsBy3Bn+Ck4hAx9oiCM49JIe1BJ1mgwZ8aYP2Q=Respond and deduplicate
- Return any
2xxresponse once you have durably accepted the event. Cevoid then stops retrying it. - Store
webhook-idand skip an ID you have already processed. This way, a retry after a lost response cannot create a duplicate on your side. - Sequence your own state from
created_ator from the resource indata, not from arrival order. Events for the same order or profile can arrive in any sequence. - Do not redirect. Cevoid sends every request to the URL you configured.
Retries
Cevoid retries 408, 425, 429, and 5xx responses up to 6 more times, over about 21 hours, waiting longer each time. A Retry-After header is honored when it asks for longer. Cevoid waits 10 seconds for a response, and retries a timeout or a network error the same way. See Automatic retries.
Other 3xx and 4xx responses stop the delivery. After a 401 or 403, the delivery shows Provider authorization failed: fix your receiver, then retry it in Outbox. Any other 3xx or 4xx shows Invalid delivery payload, which can't be retried: fix your receiver, then replay the delivery as a new one.
URL requirements
Destination URLs must use HTTPS and resolve to a public address. They cannot contain credentials, fragments, or query strings. They need a hostname, not an IP address or localhost, and can't use ports 22, 23, 25, 110, or 143. Cevoid checks the URL when you create the destination, and again before each request.
Test, retry, and replay
- Trigger test run runs the flow once: see Test a flow. With the live version, it posts a real signed request to your endpoint. With the current draft, Cevoid only validates the payload and sends no request.
- Retry in Outbox sends the same event again, with the same
webhook-idand the same body, up to 3 times. Use it after fixing your receiver. - Replay as new delivery in Outbox sends the same event data as a new event, with a new
webhook-id, to the same destination. Because the ID is new, your deduplication doesn't filter it. - Retry from start and Retry with live flow on a flow run send a new delivery, with a new
webhook-id. Retry from failed keeps the samewebhook-id.
Every delivery shows in Outbox, with each attempt and its response status. Filter by Webhook destination to see one destination's deliveries. To retry or replay a delivery, see Skip, send now, retry, or replay a delivery.
Troubleshooting
- No request arrives: Confirm that the flow is live, then check the delivery in Outbox
- Signature mismatch: Verify against the exact raw bytes, not parsed JSON, and remove
whsec_before base64 decoding - Repeated events: Deduplicate on
webhook-id - Redirect or network error: Create a destination with the final public HTTPS URL, and select it in Webhook destination in the flow's Action step. Redirects and private addresses are blocked.
- Delivery stopped retrying: Fix the reason shown on the delivery in Outbox, then retry it, or replay it when Retry isn't offered
- Secret was lost: Create a new destination, which comes with a new signing secret, and select it in Webhook destination in the flow's Action step