Closing the loop
Verdict gets better for you when it learns what happened after each decision. That only works if every outcome can be traced to the decision that caused it. This page is the tagging convention that makes the trace work: one id, carried by every send, reported back with one call.
The decision id
Section titled “The decision id”Every decision response has an id, for example dec_01JC3Z4Y8R9K2M5N7P1Q3S5T7V (id). It is dec_ followed by a ULID, it is unique, and it never changes. Keep it next to whatever your agent does with the answer: the lead you add to a campaign, the email you send, the meeting link you share, the approval you ask a human for.
You can read the decision again at any time with GET /v1/decisions/{id}, free.
Tag the send
Section titled “Tag the send”Put the id where the tool you send with can carry it, so that tool’s events come back tagged.
| Where the send goes | Carry the id as |
|---|---|
| Your own SMTP server or an inbox API | the email header X-Verdict-Decision: dec_... |
| Instantly, Smartlead or another sequencer | a custom field named verdict_decision_id on the lead |
| Cal.com, Calendly or another booking link | a metadata or UTM field named verdict_decision_id |
| A human approval step (a queue, a Slack button) | keep the id with the item and report approved or skipped |
Use exactly these names: X-Verdict-Decision for the header and verdict_decision_id for custom fields and metadata. The inbound receivers (POST /v1/webhooks/instantly/{receiver_token} and the others on Outcomes) look for them. One id per send: when a message rests on several decisions (for example qualify_firm, then pick_person, then should_send), tag it with the last one, the decision that let it go out.
The header is only for your sends. Verdict’s own endpoints do not read or return X-Verdict-Decision; the id of a decision is always the id in its response.
The SDK helpers
Section titled “The SDK helpers”The SDKs build the tags for you. withDecision(decisionId) (TypeScript, @verdict/sdk) and with_decision(decision_id) (Python, askverdict) check that the id is a decision id and return the three shapes:
import { Verdict, withDecision } from "@verdict/sdk";
const verdict = new Verdict(); // reads VERDICT_API_KEYconst decision = await verdict.decisions.shouldSend(request);const tag = withDecision(decision.id);// tag.headers -> { "X-Verdict-Decision": "dec_..." } for your own SMTP or inbox API// tag.customFields -> { verdict_decision_id: "dec_..." } Instantly custom_variables, Smartlead custom_fields// tag.metadata -> { verdict_decision_id: "dec_..." } Cal.com metadata, Calendly fieldsThe names are also exported as constants: DECISION_HEADER (X-Verdict-Decision) and DECISION_FIELD (verdict_decision_id). isDecisionId(value) and assertDecisionId(value) validate an id you stored yourself. Python has the same names in snake case.
For your own approval step, verdict.approve(decisionId) and verdict.skip(decisionId) report approved and skipped, and verdict.outcomes.report({ decisionId, outcome, observedAt }) reports any other outcome (observedAt defaults to now).
Report the outcome
Section titled “Report the outcome”When something happens, report it with POST /v1/outcomes. Free, unlimited, scope outcomes:write.
curl -X POST 'https://api.askverdict.io/v1/outcomes' \ -H "Authorization: Bearer $VERDICT_API_KEY" \ -H "Idempotency-Key: $(uuidgen)" \ -H 'Content-Type: application/json' \ -d '{ "decision_id": "dec_01JC3Z4Y8R9K2M5N7P1Q3S5T7V", "outcome": "meeting_booked", "observed_at": "2026-11-09T16:30:00Z", "meta": { "channel": "email" }}'{ "id": "out_01JC3Z4Y8R9K2M5N7P1Q3S5T7V", "decision_id": "dec_01JC3Z4Y8R9K2M5N7P1Q3S5T7V", "outcome": "meeting_booked", "observed_at": "2026-11-09T16:30:00Z", "meta": { "channel": "email" }, "created_at": "2026-11-09T16:30:02Z"}decision_idis the id you tagged the send with.outcomeis a built-in outcome orcustom:<slug>for your own.observed_atis when it happened, not when you report it.
The call is idempotent on the three together: the first report answers 201, the same report again answers 200 with the outcome already stored and changes nothing. So a webhook retried by a vendor, or a job that reports twice, never double counts. An id from another organization is answered 404, exactly like an id that does not exist.
What happens next
Section titled “What happens next”Once your organization has 50 labeled decisions for a decision type and question-set version, Verdict refits a calibration map for your org right away, and again at every further 50. A decision is labeled by its first outcome that says whether it was right (approved, replied or meeting_booked say yes; skipped, not_interested or reopened say no). Further outcomes on the same decision, and outcomes that say neither (such as delivered for most decisions, or custom: ones), do not add to the count. From then on calibration says basis: org and p is calibrated on your own results. Maps are also refit nightly. Outcomes explains the bases and what is pooled.
Your thresholds are separate from calibration: set them per ICP profile with PATCH /v1/icps/{id}/thresholds to trade precision for recall. A threshold changes answer and nothing else.