POST /v1/decisions/should_send
| Call | POST /v1/decisions/should_send |
| Price | $0.003 per call, billed only when an answer is returned |
| Scope | decisions:write |
| Operation id | shouldSend |
What it does
Section titled “What it does”Should this prepared message go out now? Returns send as p and a hold_reason.
Call it as the last gate before a prepared message goes out. Give the message, the decision ids behind it, the inbox health you measure and the ICP. Returns send as p, answer (send or hold) and a hold_reason when holding. Verdict never sends anything.
Price: $0.003 per call (x-verdict-price-usd), billed only when an answer is returned: errors (including decision_unavailable, 503 with Retry-After) and refused calls are free. Send X-Max-Cost-USD to cap spend and Idempotency-Key (required) to retry safely; a repeated key within 24 hours returns the stored response and bills nothing. Every response carries the common envelope: id, p (always present, calibrated), answer, reason_code, reason, evidence, calibration, cost and balance_usd. Report what happened next with POST /v1/outcomes.
Scope: decisions:write.
Example request
Section titled “Example request”Copy it as curl. Set VERDICT_API_KEY first; uuidgen makes a fresh Idempotency-Key.
curl -X POST 'https://api.askverdict.io/v1/decisions/should_send' \ -H "Authorization: Bearer $VERDICT_API_KEY" \ -H "Idempotency-Key: $(uuidgen)" \ -H 'Content-Type: application/json' \ -d '{ "message": { "channel": "email", "subject": "Trade ops after your raise", "body": "Hi Dana, congratulations on the new fund. We help 10-person advisers automate trade reconciliation. Open to a short call next week?" }, "decision_ids": [ "dec_01JC3Z4Y8R9K2M5N7P1Q3S5T7V" ], "inbox_health": { "bounce_rate_7d": 0.012, "spam_complaint_rate_7d": 0.0005, "sent_today": 38, "daily_limit": 50 }, "recipient": { "touches_30d": 1 }, "icp_id": "icp_01JC3Z4Y8R9K2M5N7P1Q3S5T7V"}'Example response
Section titled “Example response”200: The should_send decision, in the common envelope.
{ "id": "dec_01JC3Z4Y8R9K2M5N7P1Q3S5T7V", "version": "2026-11-01", "question_set_version": "2026-11-01", "model": "jev", "calibration": { "basis": "org", "n_outcomes": 412, "threshold": 0.6 }, "latency_ms": 180, "created_at": "2026-11-07T14:03:00Z", "decision": "should_send", "p": 0.88, "answer": "send", "reason_code": "all_checks_passed", "reason": "Upstream decisions clear their thresholds and inbox health is within bounds.", "evidence": [ { "source": "request", "url": null, "observed_at": "2026-11-07", "pointer": "/inbox_health" } ], "send": 0.88, "hold_reason": null, "cost": { "decision_usd": 0.003, "vendor_usd": 0, "total_usd": 0.003 }, "balance_usd": 18.301}Parameters
Section titled “Parameters”| Name | In | Type | Required | Description |
|---|---|---|---|---|
idempotency-key |
header | string (uuid) | yes | A UUID you generate per logical request. Required on every POST. Repeating it within 24 hours returns the stored response and bills nothing; reusing it with a different body returns 409 idempotency_conflict. |
x-max-cost-usd |
header | string | no | Maximum you are willing to pay for this call, in US dollars (up to 6 decimals). If the call would cost more, it is refused with 402 max_cost_exceeded before spending. |
x-verdict-version |
header | string | no | API version to use, a date. Defaults to the version current when the key was created. The current version is 2026-11-01. |
Request body
Section titled “Request body”Schema ShouldSendRequest. The should_send input.
| Field | Type | Required | Description |
|---|---|---|---|
message |
object | yes | The prepared message. |
message.channel |
enum: “email”, “other” | yes | Channel the message will go out on. |
message.subject |
string | no | Subject line. |
message.body |
string | yes | Plain-text body as it will be sent. At most 8192 bytes of UTF-8. |
decision_ids |
array of string | yes | The decisions behind the message (for example qualify_firm and pick_person), 1 to 10. |
inbox_health |
InboxHealth | yes | Inbox health you measure; Verdict never reads your inbox. |
inbox_health.bounce_rate_7d |
number | no | Share of sends that bounced over the last 7 days. |
inbox_health.spam_complaint_rate_7d |
number | no | Share of sends marked as spam over the last 7 days. |
inbox_health.reply_rate_7d |
number | no | Share of sends replied to over the last 7 days. |
inbox_health.sent_today |
integer | no | Messages sent from this inbox today. |
inbox_health.daily_limit |
integer | no | Your daily send limit for this inbox. |
inbox_health.domain_age_days |
integer | no | Age of the sending domain in days. |
recipient |
object | no | Optional contact history of the recipient. |
recipient.last_contacted_at |
string (date-time) | no | When you last wrote to this recipient. |
recipient.touches_30d |
integer | no | Messages sent to this recipient in 30 days. |
icp_id |
string | yes | The ICP profile to decide against. |
Response fields (200)
Section titled “Response fields (200)”Schema ShouldSendResponse.
| Field | Type | Required | Description |
|---|---|---|---|
id |
string | yes | The decision id. Report outcomes against it. |
decision |
enum: “should_send” | yes | The decision name. |
version |
enum: “2026-11-01” | yes | API version that served the decision. |
question_set_version |
string (date) | yes | Version (a date) of the question set that answered (spec 6.1). |
model |
enum: “jev” | yes | Decision model that answered: always jev (ADR-0008). |
p |
number | yes | Calibrated probability (spec 6.4). Always present. |
answer |
enum: “send”, “hold” | yes | send or hold: p applied to your threshold. |
reason_code |
enum: “all_checks_passed”, “upstream_decision_below_threshold”, “upstream_decision_missing”, “inbox_health_degraded”, “daily_volume_limit_reached”, “recipient_recently_contacted”, “message_quality_issue”, “recipient_opted_out” | yes | Why, from the fixed should_send reason-code enum. |
reason |
string | yes | Short templated explanation of reason_code; never free text from a frontier model. |
evidence |
array of Evidence | yes | Cited sources, with URLs and dates. |
evidence[].source |
enum: “sec_edgar”, “gleif”, “companies_house”, “wikidata”, “request” | yes | The index source cited, or request when the evidence is part of your own input. |
evidence[].url |
string or null (uri) | yes | Link to the cited record at the source; null when source is request. |
evidence[].observed_at |
string (date) | yes | Date the source reported the fact. |
evidence[].label |
string | no | Short description of what the evidence shows. |
evidence[].pointer |
string | no | JSON Pointer into your request body when source is request, for example /thread/2. |
calibration |
Calibration | yes | How p was calibrated (spec 6.4). |
calibration.basis |
enum: “org”, “segment”, “global” | yes | org (your own outcomes), segment (your ICP’s segment) or global: how much to trust p. |
calibration.n_outcomes |
integer | yes | Outcomes behind the calibration map used. |
calibration.threshold |
number | yes | The threshold answer applied to p. |
cost |
Cost | yes | What this call was billed, itemized. Misses and errors bill nothing. |
cost.decision_usd |
number | yes | Price of the decision. US dollars, up to 6 decimals. |
cost.vendor_usd |
number | yes | Vendor pass-through charges (always 0 in v1). US dollars, up to 6 decimals. |
cost.total_usd |
number | yes | Total billed for this call. US dollars, up to 6 decimals. |
balance_usd |
number | yes | Balance after this call. US dollars, up to 6 decimals. |
latency_ms |
integer | yes | Server-side latency of the call, in milliseconds. |
created_at |
string (date-time) | yes | When the decision was made. Decisions are immutable. |
send |
number | yes | Probability that sending now is right; equal to p. |
hold_reason |
enum: “upstream_decision_weak”, “upstream_decision_missing”, “inbox_health_degraded”, “daily_volume_limit”, “recently_contacted”, “message_issue”, “recipient_opted_out”, “low_confidence”, null | yes | Why to hold, when answer is hold; null when answer is send. |
Errors
Section titled “Errors”Errors are RFC 9457 problem details and are never billed. Branch on type. See Errors.
| Status | Meaning |
|---|---|
400 |
Invalid request: malformed JSON, an unknown field, a failed constraint or a size limit (invalid_request). |
401 |
Missing, malformed, revoked or expired API key, or a bad webhook signature (unauthorized). |
402 |
Payment required: the balance cannot cover the call (insufficient_balance), or the call would exceed X-Max-Cost-USD (max_cost_exceeded). Nothing was spent. |
403 |
The key lacks the scope this operation needs (forbidden_scope). |
404 |
The resource does not exist in this organization (not_found). |
409 |
The Idempotency-Key was reused with a different request, or the first request is still running (idempotency_conflict). |
429 |
Rate limited (rate_limited). Wait Retry-After seconds. |
500 |
Unexpected server error (internal). Not billed; retry with the same Idempotency-Key. |
503 |
Temporarily unavailable: the decision model failed, timed out or is rate limited, or decisions are switched off (decision_unavailable), or a vendor the call depends on is unavailable (vendor_unavailable, phase 2). Not billed. Wait Retry-After seconds and retry with the same Idempotency-Key. |
Try it
Section titled “Try it”Open the interactive reference to send this request from the browser. The full document is at /openapi.json.