Authentication

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.settled

A 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.

Zapier

Verdo is a Zapier app with 10 published triggers, actions and searches.

Operation
Type
Key
Decision Settled
Trigger
decision_settled
Decision Created
Trigger
decision_created
Decision Stalled
Trigger
decision_stalled
Decision Status Changed
Trigger
decision_status_changed
Outcome Recorded
Trigger
outcome_recorded
Vote Cast
Trigger
vote_cast
Create Decision
Action
create_decision
Add Participant
Action
add_participant
Close Decision
Action
close_decision
Find Decision
Search
find_decision

Decision Settled fires when a decision reaches a result, whether at its deadline or closed early. It delivers the decision object below.

Events

Six event types. Each one delivers the same decision object.

Event
Dedupe key
Fires when
decision.created
decision id
A decision is opened.
vote.cast
event id
A participant votes. Fires once per vote.
decision.settled
decision id
A decision reaches a result — at its deadline, or when closed early.
decision.stalled
event id
A decision closes without reaching a result. Repeatable: a stalled decision can be extended and stall again.
decision.status_changed
event id
The lifecycle status moves, e.g. decided to implemented.
decision.outcome_recorded
event id
The owner answers the 30-day outcome review.

Webhook payload

Delivered 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.

Verifying the signature

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.

Polling

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.

The decision object

Field
Type
Notes
id
string
Dedupe key. For vote.cast, decision.stalled, decision.status_changed and decision.outcome_recorded this is the event's own id, so two votes on one decision stay two events. For decision.created and decision.settled it equals decision_id.
decision_id
string
The decision. Present on every event, so a Zap can map it the same way everywhere.
title
string
The question.
method
string | null
single_choice, ranked_choice, approval or dot_voting.
authority_mode
string | null
majority, consensus, owner_decides or approver_decides.
status
string | null
active, decided, implemented, stalled, superseded or reversed.
outcome
string | null
The 30-day review verdict once recorded: success, partial or failed.
outcome_option_id
string | null
The option that won, if the decision settled on one.
owner
object | null
{ name, email, slack_user_id } for the named owner. Null when no owner participant is set.
vote_breakdown
array
[{ option, votes }] in display order — aggregate counts only, never who voted for what. Empty array, never absent.
source
object | null
{ platform, channel_id, permalink } for where the decision came from.
url
string
Link to the decision record in the Verdo dashboard.
decided_at
string | null
ISO 8601 timestamp of settlement.

There 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.

Endpoints

Method
Path
Notes
GET
/api/zapier/auth/test
Verify a key. Returns workspace_id and workspace_name. A lapsed workspace returns 403.
GET
/api/zapier/triggers/{event}
Polling. Up to 25 items, newest first.
GET
/api/zapier/decisions
List decisions in the workspace.
GET
/api/zapier/decisions/search
Find a decision by title.
GET
/api/zapier/decisions/{id}
One decision.
POST
/api/zapier/decisions/{id}/close
Close a decision early and settle it.
POST
/api/zapier/decisions/{id}/participants
Add a participant.
POST
/api/zapier/hooks/subscribe
Register a webhook target for one event.
DELETE
/api/zapier/hooks/unsubscribe
Remove a webhook target.

Rate limits and retries

Webhook 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.

Build on Verdo

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.