Verdo sends a signed webhook the moment a decision settles, and exposes the same data over a REST API. Everything below is what the product does today — nothing here is planned or partial unless it says so.
Every request carries a workspace API key. Create one in Settings; the full key is shown once, at creation, and cannot be retrieved afterwards — if you lose it, revoke it and issue another. Keys begin vdok_ and are scoped to a single workspace.
Send it as X-Api-Key, or as Authorization: Bearer. Either works on every endpoint.
curl -H "X-Api-Key: vdok_..." \
https://verdo.ink/api/zapier/triggers/decision.settledA key for a workspace whose plan has lapsed still reads, but the connection test returns 403 and write actions are refused until the plan is reactivated.
Verdo is a Zapier app with 10 published triggers, actions and searches.
decision_settleddecision_createddecision_stalleddecision_status_changedoutcome_recordedvote_castcreate_decisionadd_participantclose_decisionfind_decisionDecision Settled fires when a decision reaches a result, whether at its deadline or closed early. It delivers the decision object below.
Six event types. Each one delivers the same decision object.
decision.createddecision idvote.castevent iddecision.settleddecision iddecision.stalledevent iddecision.status_changedevent iddecision.outcome_recordedevent idDelivered as POST with a JSON body. The envelope carries the event and the workspace; the decision object is identical on every event and on every delivery path.
{
"event": "decision.settled",
"product": "verdo",
"workspace_id": "c93449e8-762b-49fd-880e-f7b1cbe8ca30",
"decision": {
"id": "9f1c2b44-0e7a-4c11-9a1e-2d5f8b3c7a10",
"decision_id": "9f1c2b44-0e7a-4c11-9a1e-2d5f8b3c7a10",
"title": "Which CRM do we move to?",
"method": "single_choice",
"authority_mode": "majority",
"status": "decided",
"outcome": null,
"outcome_option_id": "3a7d1e90-5c22-4f08-b6e3-11a49c8f2d55",
"owner": { "name": "Gerald", "email": "gerald@acme.com", "slack_user_id": "U0C5HKVRDPW" },
"vote_breakdown": [
{ "option": "HubSpot", "votes": 5 },
{ "option": "Pipedrive", "votes": 2 }
],
"source": { "platform": "slack", "channel_id": "C0C5J2QT4MB", "permalink": null },
"url": "https://verdo.ink/dashboard/d/9f1c2b44-0e7a-4c11-9a1e-2d5f8b3c7a10",
"decided_at": "2026-09-30T09:00:00.000Z"
}
}Every field is always present. Absent values are null (or []), never omitted, so a field mapping does not change shape between events.
Every webhook carries X-Verdo-Signature, formatted sha256=<hex>. The value is an HMAC-SHA256 of the exact request body, keyed with your workspace’s webhook signing secret.
The signing secret is issued to you when direct webhooks are enabled for your workspace, and is never sent in a request, a response or a payload. Compare signatures in constant time, and reject any request whose signature does not match rather than processing it and logging a warning.
import { createHmac, timingSafeEqual } from "node:crypto";
// Verify against the RAW body, before any JSON parsing. Re-serialising the
// parsed object changes key order and whitespace, and the signature will not match.
export function verify(rawBody: string, header: string, secret: string): boolean {
const expected = "sha256=" + createHmac("sha256", secret).update(rawBody).digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(header ?? "");
return a.length === b.length && timingSafeEqual(a, b);
}If you are consuming Verdo through Zapier you do not need to do any of this — Zapier verifies delivery for you.
A fallback for consumers that cannot receive webhooks, and the source of the sample data you map fields against in the Zap editor.
GET /api/zapier/triggers/{event} returns a bare JSON array — up to 25 items, newest first — of the same decision objects the webhook delivers. There is no wrapper object and no pagination cursor.
Deduplicate on the top-level id. For the four events a decision can emit more than once — vote.cast, decision.stalled, decision.status_changed and decision.outcome_recorded — that is the event’s own id, so repeats stay distinct. A decision can stall, be extended, and stall again. For decision.created and decision.settled it is the decision id, because one decision really is created once and settles once.
idstringdecision_idstringtitlestringmethodstring | nullauthority_modestring | nullstatusstring | nulloutcomestring | nulloutcome_option_idstring | nullownerobject | nullvote_breakdownarraysourceobject | nullurlstringdecided_atstring | nullThere is no voter field on any event. An anonymous decision hides who voted for what, and vote_breakdown is aggregate only — the same counts the dashboard shows.
GET/api/zapier/auth/testGET/api/zapier/triggers/{event}GET/api/zapier/decisionsGET/api/zapier/decisions/searchGET/api/zapier/decisions/{id}POST/api/zapier/decisions/{id}/closePOST/api/zapier/decisions/{id}/participantsPOST/api/zapier/hooks/subscribeDELETE/api/zapier/hooks/unsubscribeWebhook delivery is attempted once per subscription per event. A target that fails is recorded against the subscription; repeated failures are visible in Settings. Polling is capped at 25 items per request, which is also the maximum a single Zap run will see.
Create an API key in Settings and point a Zap, a webhook or your own service at it.
Zapier and signed webhooks are included on Pro. Audit log export is planned for Business.