{
  "openapi": "3.1.0",
  "info": {
    "title": "Forbiz External Agent API",
    "version": "1.58.0",
    "description": "Read Forbiz state, push researched targets with evidence, read the prompt queue, record signals, interpret them and decide the next action. Forbiz is the system of record.\n\nTRIAL VOLUME IS ONE WORKSPACE'S, AND A CLAIM BUYS NOTHING (1.58.0): the trial volume of a workspace is counted from the rows of THAT workspace only. It used to be counted from every row the owning user held, so after a claim rewrote the rows to the human owner, a human with several workspaces saw one workspace report the volume of all of them. used, remaining and the enforcement now agree, per workspace. A CLAIM DOES NOT LIFT THE VOLUME: ownership transfers, the credential keeps exactly its scope, and volume is governed by the PLAN alone — a claimed workspace still on trial reaches the same limit and answers 403 PROVISIONAL_TRIAL_COMPLETE with unlocks_with, while continuity and delegation have no volume limit. PROVISIONAL_BLOCKED_PREFIXES, the rolling 24h abuse window and the committed-work allowance of 1.36.0 are unchanged.\n\nONE WORKSPACE, ONE ANSWER (1.57.0): a credential is permanently bound to ONE workspace, and that is now ENFORCED on every read and on every write that takes an action id. GET /prompts, GET /state, GET /evidence and GET /export answer for that workspace only — exactly like GET /next — so the three surfaces can no longer disagree about what is pending. An action id belonging to another workspace of the SAME human is refused 409 WRONG_WORKSPACE on GET /actions/{id}, /actions/{id}/history, /execute, /signals, /interpret-signal, /close-loop, /close-without-signal, /postpone, /schedule-check and as parentActionId; the refusal names nothing about the other workspace. An id that does not exist stays a plain 404, so \"not found\" and \"not yours\" remain different answers. A credential with no workspace binding is refused 409 WORKSPACE_REQUIRED on these reads instead of being answered owner-wide. Nothing about ranking, buckets, practice exclusion, pricing, trials or claiming changed.\n\nAN AGENT CAN BUY ONE PERIOD OF CONTINUITY (1.50.0): a Brain that is blocked by a trial limit can pay for ONE period itself, under a spending mandate that its payment instrument proves. The offer needs NO credential and lives OUTSIDE the agent base, at https://forbiz.io/api/public/billing/offer; it states the plan, the period length in days, the amount, the currency, renews false, and how it can be paid. THE PAYMENT TOKEN IS THE SPENDING AUTHORITY: POST /purchases { plan, instrument: { kind: \"shared_payment_token\", id } } is callable by EVERY active identity of the workspace — including a provisional credential on a workspace no human has claimed — and needs no capability a human must grant first, because the human who granted the token's mandate already delegated the spending. A revoked or expired credential is still refused. Idempotency-Key is REQUIRED on POST /purchases; without one the request is refused 400 IDEMPOTENCY_KEY_REQUIRED, and a replay returns the same purchase instead of buying twice. THE MANDATE IS READ FROM THE INSTRUMENT, NEVER FROM THE REQUEST: Forbiz reads the token's own limit, currency and expiry from Stripe; nothing the agent writes in the body can widen it. Every refusal carries missing as a LIST OF REASON CODES, each { code, detail }, code one of no_instrument, over_limit (detail: the amount needed), wrong_currency, expired, deactivated, unverified. \"NOT FOUND\" IS NOT \"COULD NOT CHECK\": when Stripe answers 404 the instrument does not exist, but when Stripe cannot be reached at all the check is recorded unverified and the request is refused 503 MANDATE_UNVERIFIED with retry_after_seconds — nothing is charged and the purchase stays open. ACTIVATION IS WRITTEN ONLY BY THE WEBHOOK: a successful charge answers 200 with status payment_received and activated false plus retry_after_seconds, and the signature-verified payment_intent.succeeded event activates the period — so a lost HTTP response never loses a paid period, and a replayed event never extends one. A HUMAN CAN PAY THIS PURCHASE WITHOUT SIGNING IN: authorization_options.checkout_link_for_human on an open purchase is a Stripe Checkout Session in PAYMENT mode for that purchase only — no subscription, no future charge — reused while it stays open. NOTHING RENEWS: one purchase buys one period and no future charge is ever authorised. A period already covered for more than seven days is refused ALREADY_COVERED, and a second open purchase is refused PURCHASE_ALREADY_OPEN. A bought period lifts the plan at READ time and expires by itself; no plan column is written outside the webhook. GET /purchases and GET /purchases/{id} return the receipts, each with its mandate check. Stripe runs in TEST mode: no real money moves yet.\n\nPAID MEANS PAID, AND IN ORDER (1.48.0): a plan now moves only when money actually arrived, from the CURRENT subscription, in the right order. A completed checkout whose payment is still pending stores the Stripe ids and applies NOTHING until the delayed payment succeeds; a delayed payment that fails applies nothing. A subscription that becomes active or trialing means paid, past_due or unpaid means past_due with the plan untouched, and canceled ends it exactly like a deletion. Invoice events read their subscription from the current API shape (parent.subscription_details). An event naming a subscription that is not the one stored on the workspace applies nothing, and an event older than the last one applied to that workspace applies nothing. The Stripe event id remains the idempotency key, so a replay changes nothing. Stripe still runs in TEST mode: no real money moves yet.\n\nA PLAN CAN NOW BE PAID FOR (1.47.0): every refusal that names a plan carries unlocks_with with a REAL checkout_url for THAT workspace when a price is configured, so an agent can hand its human one link instead of a description of a price. Buying is a HUMAN act: the link opens a signed-in page and a Stripe-hosted checkout, and the API never takes a card, a token or a payment. NOTHING in the API changes a plan: the only writer is Stripe's signature-verified webhook, applied once per Stripe event id, so a replayed delivery changes nothing. When a payment succeeds the workspace moves to continuity/paid and the trial volume refusals stop on the very next request; when a subscription ends the workspace returns to the trial plan and NO data is deleted — reads, GET /export and closing already-committed work keep working. checkout is available only when the plan is purchasable AND a price exists, otherwise not_configured or not_purchasable with one plain sentence in checkout_note; delegation stays not purchasable. Stripe runs in TEST mode: no real money moves yet.\nCHECKING FOR A SIGNAL IS A FIRST-CLASS ACT (1.46.0): \"nobody looked\" and \"somebody looked in one place over one period and the signal was not there\" are now DIFFERENT recorded facts, and neither is ever stated as \"the other party did not answer\". POST /actions/{id}/signal-checks is append-only, many per action, and takes source { kind, detail }, period { from, to }, looked_for, result (found | not_found_in_source | cannot_check), note, evidenceId and signalId. result not_found_in_source REQUIRES the source AND the period, otherwise 400 CHECK_SCOPE_REQUIRED, because a statement without a scope would read as a claim about a third party. result cannot_check REQUIRES reason (no_access | source_unknown | needs_human), otherwise 400 CHECK_REASON_REQUIRED; a cannot_check with no_access automatically opens a NEW delegation for the same source and period requiring the missing capability. result found closes NOTHING and records NO signal: without signalId the check carries pending_link true and the next move is POST /actions/{id}/satisfy-signal. Every check records its actor and one domain event, and is never edited or deleted. GET /actions/{id}/signal-checks returns the whole history, oldest first. HANDING THE LOOKING OUT: POST /actions/{id}/request-check { source, period, deadline_at, requiredCapability? } creates a DELEGATION whose acceptance criterion names that source and that period; POST /delegations/{id}/deliver now accepts a check object and writes the delivery and the check in ONE transaction. GET /state carries signal_checks with never_checked, checked_not_found (with newest_check_age_hours), cannot_check (with its reasons), found_pending_link and source_never_declared, and GET /next carries signal_check { state, next_move }. ESCALATION IS BY TIME AND WEIGHT, NEVER BY COUNT: an overdue commitment with no recorded signal raises one attention request when no check is newer than 72 hours (or the last check is cannot_check with nobody able to do it), and not another for 48 hours; the number of checks never escalates anything.\n\nPRACTICE PROJECTS (1.45.0): a project inside a real workspace can say it exists to PRACTICE, so practice work stops competing with real work in every queue. POST /projects accepts purpose: real | test | fictional | demo (default real); any other value is refused 400 invalid_purpose listing the accepted ones. It is declared AT CREATION only: POST /projects/{id} attempting to change it is refused 409 PURPOSE_CHANGE_NOT_PERMITTED, echoing the stored purpose and the next move, because only a workspace owner or admin can relabel a project, in the app. WHILE purpose is not real, that project's actions are EXCLUDED from GET /next, from GET /state queues, counts and next_priorities, from the nudge and digest selection, from Forbiz's own attention scan and from the data_gaps counters. NOTHING IS HIDDEN: GET /search returns them, GET /export includes them, a direct read of the project or the action still returns them and the audit trail is untouched. GET /state carries practice { projects, open_actions }, so the excluded work is a visible number instead of a disappearance. A delegation, an attention request or a signal recorded ON a practice action still works end to end: practice must be able to exercise the whole loop. No existing project was relabelled — everything created before 1.45.0 is real.\n\nDOMAIN EVENTS v0: once a workspace has cut over for this surface, every mutation through POST /workspace/entities, POST /projects, POST /projects/{id}/events, POST /projects/{id}/actions and POST /actions/{id}/satisfy-signal appends an append-only domain event in the SAME transaction as the business change. The event carries only canonical attribution (actor, membership, grant) — never credentials. A call that cannot be attributed is refused with 409 domain_event_authorization_required, and no business change is applied.\n\nDecision-phase semantics are three distinct concepts:\nSIGNAL = what happened in reality (POST /signals, written once, immutable).\nINTERPRETATION = what it means (POST /actions/{id}/interpret-signal, never rewrites the signal, never resolves).\nDECISION = what happens next (POST /actions with parentActionId, and/or POST /actions/{id}/close-loop).\n\nTEMPORAL TRUTH: executed_at / occurred = when something happened in reality; recorded_at = when Forbiz recorded it. All timing math (expected_signal_at, overdue, waiting) derives from the real-world time, never from the record-write time.\n\nEvery action response carries timing.available_operations and timing.available_commands. available_commands gives the exact method, path, required body and resulting state for each valid operation, so no operation ever has to be guessed.\n\nCanonical operation → command mapping:\n- mark_executed → POST /actions/{id}/execute → waiting (when a review date exists) or needs_timing\n- record_signal → POST /signals { actionId, actualSignal } → needs_decision\n- postpone → POST /actions/{id}/postpone { expectedSignalAt } → waiting\n- set_review_date → POST /actions/{id}/schedule-check { expectedSignalAt } → waiting\n- interpret_signal → POST /actions/{id}/interpret-signal { outcomeStatus } → needs_decision (unchanged unless the loop is closed separately)\n- create_next_action → POST /actions { description, parentActionId } → a new child action in planned; the parent is NOT resolved\n- close_loop → POST /actions/{id}/close-loop { outcomeStatus } → resolved\n- close_without_signal → POST /actions/{id}/close-without-signal → closed_without_signal\n- create_outreach → POST /outreach { name } → outreach in draft (container only; reuse an existing Outreach when it already represents the same commercial thread)\n- attach_targets → POST /targets { runName, campaignId, targets } → target candidates attached to an outreach (campaignId = Outreach UUID; omit runId unless replaying a run UUID returned earlier)\n\nREUSE BEFORE CREATE: an Outreach is a commercial thread. Search for an existing Outreach (GET /outreach, GET /search?type=outreach) and reuse it when it already represents the same thread; only create a new one with POST /outreach when none does.\n\nFOUR CLOCKS: every action reports timing.clocks = { earliest_actionable_at (do not act before), due_at (the commitment), executed_at (what really happened), expected_signal_at (when the signal was due) }. No clock is inferred from another. timing.due_at remains the review clock for backward compatibility; the commitment is timing.commitment_due_at.\n\nEVIDENCE IS NOT TRUTH: /context/sessions, /context/artifacts and /context/evidence store epistemic history — claims, corroborations, contradictions and exclusions — append-only. Nothing written there becomes workspace truth automatically; a contradiction never erases the claim it contradicts. Workspace truth is still only what is confirmed in /workspace/context.\n\nPOLICY: /policies holds machine-evaluable business rules enforced server-side before execution. Only ACTIVE policies are enforced. An ordinary read+write agent can only propose DENY / REQUIRE_APPROVAL policies (stored as PROPOSED, not enforced) and can create ADVISORY ones; activating or relaxing a human-confirmed safety gate requires a human owner/admin in Forbiz or a credential holding the delegated policies:manage capability. A blocked operation returns 403 policy_denied or 409 policy_requires_approval with the matched policies.\n\nDECISION ENGINE v0.1 is deterministic and side-effect free to read: GET /decisions/recommendation never writes. Adoption is an explicit write via POST /decisions with a disposition, recorded once per disposition and idempotent under Idempotency-Key.\n\nTHE NEXT MOVE, AND CONTRACTS INSIDE REFUSALS (1.34.0): GET /next answers \"what do I do now\" with EXACTLY ONE item, chosen by the attention and priority logic that already drives /state — no new scoring model. It says who owns the move (actor agent | human), carries a command executable as-is for an agent item, for_human for a human item, and a handoff block (last human touch, last signal) so a successor agent continues without reading the history. Every 400 on a mutation now carries expected { method, path, required, fields, example }, generated from the same contract the route validates against, so a refusal IS the contract and no agent needs to try four bodies. GET /decisions/recommendation accepts id (alias actionId / action_id) to narrow the same evaluation to one action, and documents id, actionId, cursor, limit and offset. GET /me gains claim { claimed, claimed_at, ownership_changed, capabilities_changed, plan, blocked }, so what a claim changed is a field instead of a diff of two responses. Search treats numbers as numbers: 1,700 / 1.700 / 1700 all match each other, in the stored text and in the query. GET /export counts decisions and exports the decision events with the actions they referenced. Canonical standing is now honest and derived from the STORED row, never from the route: evidence writes return promoted false, canonical false, needs_confirmation true with the stored status, canonical-context writes return promoted true only when the stored value really is confirmed, and agent-created workspace entities report needs_confirmation true.\n\nATTENTION REQUESTS (1.37.0): an agent may ASK for human attention, and FORBIZ decides whether the question is worth an interruption, who can answer it and exactly what the answer changes. THREE KINDS, NEVER INTERCHANGEABLE: a FACT to confirm (writes ONE human_answer evidence item and nothing else), a CHOICE to decide (writes a human-attributed decision), an AUTHORIZATION to permit (writes ONE single-use, exact-scope authorization that expires within 24 hours). Every request MUST say blocked.what and blocked.why_unresolved and either blocked.evidence_checked or blocked.no_evidence_reason; a request that cannot say what is blocked and why the evidence at hand did not settle it is refused as JUSTIFICATION_REQUIRED. about.object_id must exist in your workspace (404 OBJECT_NOT_FOUND) and every evidence id must be an evidence item of your workspace (400 EVIDENCE_NOT_FOUND); nothing is stored on either. DECISION ORDER, fixed: already answered, then reachable human, then duplicate pending, then interruption budget. A fact already answered for the same subject within 90 days, whose answer evidence has not been superseded or contradicted, returns status already_answered with interrupted false and NO human is interrupted; an older answer may be asked again. PRECEDENCE when the budget is scarce: an authorization blocking an overdue commitment, then a choice, then an authorization that is not overdue, then a fact. Attention shares the SAME existing Telegram channel, the same 3-per-rolling-day ceiling, the same local window and the same fingerprint dedupe as every other nudge, may use at most 2 of the 3 daily slots, and a FACT never takes the last remaining slot: an overdue commitment always outranks it. An ask that cannot be sent now is stored with status queued and goes out on a later tick. ONLY A CORRELATED BUTTON ANSWERS: a Telegram TEXT reply to an attention request is always an adjustment request, recorded as observed evidence, and the request stays pending. WHO ANSWERED is resolved from the paired Telegram user AND an active membership; when it cannot be resolved the answer is still stored with the Telegram user id and attribution_unresolved true, never attributed to the owner and never to an agent. WHAT AN AUTHORIZATION UNBLOCKS: nothing on its own. No execution gate in Forbiz accepts an attention authorization today, so it never overrides a DENY policy, the strategy gate, the send budget, the trial/plan gate, the capability gate or read-only. Consuming one still evaluates the DENY policy gate: a denied operation is refused with 403 POLICY_DENIED and the authorization stays UNCONSUMED. It is single-use, exact-operation and expiring, and never becomes a standing policy — business policies remain the only standing layer. GET /state carries attention_requests counts and GET /next reports a waiting_on_human item; neither is a new ranking model.\n\nPACE, NEVER REACH (1.42.0): the ceiling limits how FAST the human is interrupted, never HOW MUCH reaches them. NOTHING IS EVER DROPPED: every item that qualifies for human attention enters a visible queue with state delivered, queued, snoozed or answered. An item that does not fit today's pace is QUEUED, never discarded and never silently deduped into nothing. GET /state exposes attention_queue with its counts, and the digest counts 100% of the queue even when it lists only a few lines. PACE: per channel, configurable on workspace_channels, defaults at most 1 single nudge per hour and at most 6 per day, always inside the channel's local window. The owner changes both numbers without a deploy. COVERAGE: one digest per channel per day at the start of the window, plus the longer weekly one on MONDAY. The digest is never subject to the hourly or daily nudge pace and is delivered even when today's pace is already spent; its counts always cover everything pending, including what is queued. SNOOZE INSTEAD OF DROP: every delivery carries a pause-until-tomorrow button. It silences single nudges until the channel's window opens again, marks everything still owed as snoozed, and the next digest says how many arrived while it was paused. Silence is always a recorded decision, never a side effect of a cap. RE-ASK: an unanswered ask about a still-open, still-overdue item is eligible again after 7 days (an age bucket inside the state fingerprint), the re-ask states in one line that it was already asked and when, and re-asks compete for the same hourly pace. NO INVENTED URGENCY: nothing fires outside the window, no agent can raise its own item's pace and there is no urgent bypass. THE DIGEST ITSELF: POST /workspace/channels/telegram/digest delivers it on demand; at most ONE per channel per day; one plain message with counts per queue (overdue with no signal, expectation date past with no signal, waiting on a human decision, delegations delivered and not resolved, attention requests pending, plus the queue totals), then the top items with what each is, how many days it has been waiting and its next command from timing.available_commands, then a closing line with the total that did not fit. The three most important items carry EXACTLY the correlated buttons they would carry as an ordinary nudge; the rest are listed for reading and the digest never invents an operation. Recorded in channel_deliveries with source_object_type 'workspace' and trigger 'digest'. With nothing pending there is no digest and no empty message. HONESTY: the digest never says a commitment failed and never says the other side did not answer; for an overdue item it states the expectation and that no signal was recorded, and when no expected signal source was declared it says the source was never declared.\n\nONE ORDER EVERYWHERE, AGE WITH A CEILING (1.44.0): GET /next, GET /state next_priorities, the app's Today list, the digest and the nudges all order through ONE read-time ranking function, so the same input always produces the same sequence and a list never shuffles between reads. TIES break in a fixed, documented order: score desc, then signal_days_late desc, then action_days_late desc, then created_at asc, then id. AGE SATURATES: days late count fully up to 30 and add nothing beyond that, so something 42 days old deserves review but never automatically outranks an important opportunity from today. TWO CLOCKS STAY APART: rank_reason carries action_days_late (past due_at) and signal_days_late (past expected_signal_at) as separate numbers, never summed into one anonymous \"days\", plus days_late_counted and days_late_ceiling. BLOCKING NEEDS A REGISTERED DEPENDENCY: the blocking term counts ONLY when a concrete row names the item, and rank_reason.blocking carries { kind, ref } with kind delegation | attention_request | execution_gate. \"Waits for a human decision\" with no reference gives NO boost at all. UNKNOWN VALUE IS NOT ZERO: when roi was never declared the economic term is EXCLUDED from the calculation, never counted as 0, and the item reports value null with value_status not_assessed; rank_reason.terms_counted lists exactly which terms were used. WORDING: what nobody declared is UNKNOWN — if nobody declared it, it is unknown — never absent, never empty, never zero. Nothing is inferred from text and no row is backfilled.\n\nBORN WITH A SIGNAL SOURCE AND A VALUE (1.43.0): a commitment can now be born saying WHERE its signal will appear and WHAT IT IS WORTH, and the ORDERING happens at READ time with three named terms. POST /actions and POST /projects/{id}/actions accept expectedSignalSource as { kind, detail } (kind: email_thread, chat, phone_call, site_order, bank, calendar, in_person, system, other; detail max 200 chars) and roi { cash, access, proof, narrative, relationship, learning, compliance }, each 0-5, plus urgency, lossRisk, distractionRisk and effortMinutes. ALL OPTIONAL, NONE INFERRED from the description: an agent that sends neither still creates the action. What was not declared is SAID, never guessed: the response carries missing (expected_signal_source and/or roi) with one plain sentence each — without a source Forbiz cannot tell \"nobody looked\" from \"looked and found nothing\" — and priority { value, defaulted }, where defaulted true means the number came from defaults because no value was ever given. READ-TIME ORDERING: GET /next, GET /state next_priorities and the nudge ordering rank inside the existing buckets with value (computed priority, counted only when it is not defaulted), time (days past expected_signal_at, or past due_at when there is no expected signal date) and blocking (whether it blocks a delegation, a pending human decision or an external execution). Every ranked item carries rank_reason { value, days_late, blocking, weights, value_missing } so a reader sees WHY it is first, with no hidden weights; an item with defaulted priority is ranked by time and blocking only and says value_missing true. THE GAP IS VISIBLE: GET /state and GET /me carry data_gaps { actions_without_signal_source, actions_with_defaulted_priority } over open actions. Reading writes NOTHING, the existing write-time priority trigger is unchanged, no new scoring model was introduced and no existing row was backfilled.\n\nONE COMMITMENT PER OBJECTIVE (1.40.0): the server, not agent discipline, refuses a SECOND OPEN commitment for the same objective — and the refusal teaches the next move instead of ending the road. THREE LAYERS. (1) AUTHORITY: send commitmentKey on POST /actions or POST /projects/{id}/actions, a short stable string for the objective (max 120 chars, alias commitment_key). A second OPEN action with that key in the same workspace is refused 409 COMMITMENT_ALREADY_OPEN carrying existing { id, description, lifecycle_status, due_at, expected_signal, expected_signal_at } and that action's commands, executable as-is with no further read. It is enforced by a database unique index, so two simultaneous creates leave exactly one row: one 2xx, one 409, never a 500. (2) FILL-IN: with no key but a declared object (campaignTargetId, personId, organizationId or projectId) the server derives and stores auto:<kind>:<id>:<action_type or untyped> and applies the same rule. With no object no key is invented and nothing is guessed from text. (3) WARNING, NEVER A BLOCK: otherwise an OPEN action that the existing full-text index scores as close is returned as 409 POSSIBLE_DUPLICATE with up to 3 candidates and their commands; resend the same body with confirm_new: true and it is created. OPEN means lifecycle planned, executed or waiting_for_signal with status not skipped — resolved, closed_without_signal and skipped never refuse. Nothing is merged, auto-closed, edited or re-attributed; Idempotency-Key behaviour is unchanged (a replay is still 208); no existing row was backfilled, so actions created before 1.40.0 carry no key.\n\nTHE DOOR IS VISIBLE AT THE WELL-KNOWN PATH (1.39.0): /.well-known/llms.txt serves the SAME document as /llms.txt from the same handler, and the CDN User-Agent rule — send User-Agent: your-agent/version; Python-urllib and libwww-perl are refused with 403 by the CDN before Forbiz sees them, and no credential will help — is stated at the TOP of the file, within the first lines an agent reads. No gate, refusal or credential behaviour changed.\n\nAUTHORSHIP AND RECOVERY (1.38.0): blanket API keys are NO LONGER ISSUED. A human creating an agent in Forbiz now gets a SCOPED Agent Identity — its capabilities are stated up front — plus a renewal credential it exchanges for short-lived work tokens. NOTHING IS REVOKED BY THIS CHANGE: every existing credential keeps working exactly as before, with the same authority, until its owner explicitly revokes it, and no past event is rewritten or re-attributed. Each existing blanket credential can be given a SCOPED REPLACEMENT whose capabilities are DERIVED from that credential's own recorded usage through the same route-to-capability map this API enforces, so a replacement can never be wider than what the credential actually did; the blanket capability is not derivable and cannot be issued through that path. Both the replacement and the RECOVERY path (a workspace whose agent lost its credential and has no active identity left gets one fresh scoped identity) are owner-authorized only: authority is derived from the signed-in human's role in the workspace, never from the request, and a call without a human session is refused. Recovery never reactivates a revoked identity. No capability model changed, no new endpoint was added and no agent-facing behaviour changed.\n\nTHE TRIAL LIMIT IS ANNOUNCED, AND COMMITTED WORK IS NEVER BLOCKED (1.36.0): while a workspace is on the trial plan, every mutation response (including the refusal itself), GET /me, GET /next and POST /claim-link carry the same trial block: used { actions, evidence }, limits { actions, evidence }, remaining { actions, evidence }, near_limit, trial_complete and after_limit — one plain sentence saying what keeps working once the volume is used up. near_limit is true when either remaining count is at or below 20% of its limit; while it is true GET /next carries trial_notice, a HUMAN item (what_to_look_at, why) with the continuity catalog line as information, at most once per workspace per day. It is not a refusal and asks for nothing. A workspace on continuity or delegation, and every non-trial workspace, carry no trial key at all. AFTER THE VOLUME IS USED UP the workspace is read-only for NEW work only: on an action created BEFORE trial_completed_at these still succeed — POST /actions/{id}/execute (and mark-executed), POST /actions/{id}/satisfy-signal, POST /actions/{id}/interpret-signal, POST /actions/{id}/close-loop (and close-without-signal), POST /actions/{id}/postpone (and schedule-check), and POST /decisions carrying that action id. The refusal lists the same set as still_allowed. Everything else stays 403 PROVISIONAL_TRIAL_COMPLETE with unlocks_with: new actions, projects, evidence, artifacts, context writes and entities. The allowance creates no volume (no counter moves) and the rolling 24h abuse window is unchanged. Limits and prices are unchanged; plan_catalog remains the only source of prices. GET /export documents decisions and counts.decisions, and the evidence and context writes document promoted, canonical, stored_status and needs_confirmation as response properties.\n\nDELEGATION v1 (1.35.0): one agent hands a piece of work to another INSIDE ONE WORKSPACE. POST /actions/{id}/delegations opens a delegation as a CHILD ROW of an existing action, with an acceptance_criterion and expected_result written BEFORE anyone executes and a future deadline_at. POST /delegations/{id}/take is atomic and exclusive with a time-limited lease (renew, release); exactly one caller wins and everyone else gets 409. THREE LEVELS ARE NEVER COLLAPSED: deliver records only that work was handed over, resolve is the requester's judgement against the criterion (criterion_met is a required boolean), and the commercial outcome stays exactly where it already lives — /actions/{id}/execute, /signals, /actions/{id}/close-loop. `actions` gains NO status column: the four it has keep their meaning, and resolving a delegation changes none of them. Effective permission is an INTERSECTION: required_capability only filters who may take, and taking work never grants a capability the credential does not already hold. A retry or reassignment inserts attempt + 1 instead of rewriting history, delivered evidence is referenced from the delegation and never modified, and a lapsed lease or passed deadline is recorded on the row (lease_lost_count, expired_at) because no person or agent performed it. GET /delegations lists what you can take, requested or hold; GET /actions/{id}/delegations shows every attempt with its transitions; GET /delegations/audit gives per-agent counts and is deliberately not a score, not a ranking and not a recommendation of which agent to use — with coverage.cut_over false, an empty audit means nothing was recorded, not that nothing happened.\nPLANS ARE LEGIBLE, AND THE FIRST PAYWALL IS REAL (1.33.0): plan_catalog is the single source of every plan name, price, unlock and checkout link, and this description only summarises it — quote the catalog, not this text. GET /me returns plan { current, payment_status, claimed, catalog }, where catalog is one line per plan (at the time of writing: trial USD 0, continuity USD 29/month, delegation USD 99/month) with what_changes, requires_human, limits and an honest checkout state: \"available\" with checkout_url, \"not_configured\" when the plan is purchasable but no link exists yet, or \"not_purchasable\". A refusal that a PLAN resolves now carries unlocks_with, the same catalog line: the end of the trial volume resolves with continuity, and execution outside Forbiz, outreach execution and human policy authority resolve with delegation. Invalid credentials, unknown parameters, method errors and rate limits carry no unlocks_with, because no plan resolves them. A workspace an agent bootstrapped for itself, still on the trial plan, holds exactly ONE agent: creating a second one is refused server-side with PLAN_REQUIRED_ADDITIONAL_AGENT and its unlocks_with; minting or rotating the FIRST agent credential is untouched, and human-created workspaces are never gated. A workspace owner can no longer set plan or payment_status: both columns are writable only by the platform, so what this API reports about a plan is what was actually paid for.\n\nDECISIONS FOR EVERY CREDENTIAL, AND AN HONEST FUNNEL (1.32.0): POST /decisions now works for a trial credential minted by POST /bootstrap. It used to fail with a raw database error because the event was attributed through a column reserved for human-issued keys; a decision is now attributed to the machine identity that presented the credential, and any storage failure answers 400 decision_not_recorded with a next step instead of an internal constraint name. Five contract divergences measured by an external agent are closed: POST /context/evidence documents 201 (the status it has always returned); GET /decisions/recommendation is workspace-wide and documents no parameters, so projectId and actionId are refused with 400 unknown_parameter like any other unread parameter; GET /export documents its real TOP-LEVEL collections with no `data` wrapper; the bootstrap response field `provisional.expires_at` is renamed `telemetry_window_ends_at` because nothing expires by clock — the only real expiry is credential.expires_at; and POST /claim-link documents `summary` and `suggested_message`, which it already returned. GET /search now states that numbers are tokenized on punctuation, so 1,700 indexes as 1 and 700. Nothing else about authority, limits or blocked capabilities changes.\n\nACQUISITION FUNNEL AND PLAN (1.31.0): the funnel is measured before any traffic is bought — visit, docs_read, bootstrap, first_expectation, first_execution, first_outcome, claim_shown, claim_completed, paid — each recorded at most once per workspace, with no personal data and no third-party analytics. POST /bootstrap persists the optional `source` and `install_id` you declare (opaque, capped at 60 and 80 characters, stored and never validated), so one installation that re-bootstraps stays one installation; `X-Forbiz-Source` is honoured when the body omits `source`. GET /me and GET /state now report `plan` (trial | continuity | delegation) and `payment_status` (none | invoiced | paid | past_due), both set by a human owner in Forbiz Settings. The trial VOLUME limit applies only while plan = trial; continuity and delegation lift it, and every technical ceiling, rolling request window and blocked capability stays exactly as it was. Nothing about this changes what a request returns: a refused entry attempt is simply also counted.\nTOPIC SEARCH (1.30.0): one way to ask \"what do we know about X\" and get the answer with its provenance. GET /search gains type=evidence, decisions and context (and all includes them); evidence, actions, decisions and canonical context are matched with Postgres full text (websearch_to_tsquery, 'simple' configuration — business text is bilingual EN/ES, so nothing is stemmed), ranked by ts_rank, 20 hits per type, always inside the workspace of the presented credential. Every evidence hit carries its source session and artifact and its contradiction/supersession pointers in both directions. GET /context/evidence gains q (full text over subject + content), subject now matches by case-insensitive CONTAINS, and subject_exact keeps the old equality; a nonsense term returns zero hits instead of everything, and tsquery_used reports how the words were parsed. GET /me documents blocked_capabilities in its own response schema, and a refusal with no identity — including 401 invalid_credential — is now recorded as one coarse rejection row. No embeddings, no external search service, no cross-workspace read, and no change to how evidence is written or promoted.\nCONTRACT TRUTH (1.29.0): the document and the server answer the same thing. Twelve measured divergences were closed: POST /bootstrap documents its real body (agent_name, workspace_name, client_name, client_version, plus optional source and install_id — install_id is accepted and not yet read) and its api.version is the same string as info.version here; GET /me reports blocked_capabilities and the credential_class provisional_token for a trial credential; /context/evidence documents the full kind vocabulary and echoes normalized[].normalized_from whenever an epistemic alias (claimed, corroborated, excluded) was rewritten; /workspace/context reports agent_may_propose and human_confirmation_required (an agent may propose every readiness field, a human confirms the subjective ones) with agent_may_fill kept as a deprecated alias; POST /projects/{id}/actions accepts expectedSignalAt; /context/artifacts returns a bounded excerpt of the source text; POST /actions and POST /projects answer 201 Created (an idempotent replay is still 208). EVERY REFUSAL EXPLAINS ITSELF: every 4xx on this surface is { error, code, hint, request_id, ... } with a stable code and a next step. A GET that carries a parameter this API does not read is refused with 400 unknown_parameter listing accepted, instead of silently returning an unfiltered or empty result.\n\nAGENT ENTRY (1.28.0): an agent that knows only the domain can enter without guessing. https://forbiz.io/llms.txt is the plain-text guide, https://forbiz.io/openapi.json serves this same document, https://forbiz.io/llms-full.txt is the guide with the spec inlined, and GET /api/public/agent (no credential) returns a discovery document { name, version, openapi, llms, bootstrap, start_here }. Every unauthenticated refusal keeps its status and code and now carries a hint pointing at POST /api/public/agent/bootstrap, /openapi.json and /llms.txt.\n\nPROVISIONAL BOOTSTRAP: an agent with no credential at all can POST /bootstrap (no auth) to create an isolated trial workspace plus its own agent identity and credential. A trial workspace is internal-only: it can think, plan, record signals and decide, but it can NEVER send anything externally or act on real recipients.\n\nTRIAL CONTINUITY (1.27.0): the trial ends by VOLUME OF WORK, never by clock. Nothing expires and nothing is deleted because time passed. When the trial volume is used up the workspace becomes READ_ONLY: mutations answer 403 PROVISIONAL_TRIAL_COMPLETE, while every read and GET /export keep working indefinitely. A separate rolling 24h request window is an abuse limit only and answers 429 PROVISIONAL_RATE_LIMITED with retry_after_seconds — nothing is written and nothing is consumed. A workspace nobody has touched for a while is merely LABELLED dormant and is revived, intact, by the next request. The bootstrap credential is long-lived (up to a year), so an agent may sleep for days and come back; every other credential keeps the 24h ceiling. A HUMAN CLAIM TRANSFERS OWNERSHIP AND DOES NOT KILL THE AGENT: the machine identity becomes the new owner's, the same credential keeps working with exactly the same scope (external execution stays blocked), and the owner can rotate or revoke it in Settings. A claim lifts nothing about volume: trial volume is governed by the plan alone, so a claimed workspace still on trial reaches the same limit. There is no claim deadline. POST /claim-link mints a fresh single-use claim URL for an agent that lost the original, and GET /export returns everything the workspace holds; both work even when the trial volume is used up. Deletion happens only when an owner explicitly asks for it. GET /me reports provisional.status, trial_complete, usage, limits and recovery paths.\n\nTWO MAILBOXES (1.26.0): attention is routed by who owns the next move. Human-owned items go to Telegram; agent-owned items stay in GET /prompts, explicitly marked owner='agent'. GET /prompts and state.next_priorities carry owner and accept ?owner=human|agent. The human is nudged about the agent ONLY when the agent is late (one workspace-level agent_overdue summary), never handed the agent's task. Ownership defaults to the agent; an item is human-owned when it is a strategy checkpoint, declares human_intervention_kind, or came from the human.\n\nMACHINE IDENTITY: a Brain has a stable machine identity and never operates as its human owner. It holds a dedicated renewal credential (fbzr_...) whose ONLY purpose is authentication renewal, and calls POST /identity/token to obtain a short-lived Work Identity Token (fbzt_..., max 86400s) used as the Bearer token for every business call. GRANT != TOKEN: renewing authentication never creates authority — two sequential Work Tokens under unchanged authority resolve to the SAME actor_id, membership_id and grant_id (grant_kind=issued). Authority changes only through an owner-authorized authority-management operation, which revokes the old grant (never mutates it) and issues a new one. A Work Token can never mint another Work Token (403 identity_token_cannot_renew); an expired Work Token returns 401 identity_token_expired; a renewal credential (or a legacy credential explicitly switched to renewal_only) may reach ONLY POST /identity/token and GET /me and is refused everywhere else with 403 renewal_only_credential before any mutation. There is never a fallback to human-owner attribution.\n\nSELF-ENROLLMENT AT EQUAL AUTHORITY: a credential that already holds business authority in a workspace calls POST /identity/enroll to obtain its own machine identity at EXACTLY that authority, with no human step. It never widens authority and is never silently reduced, so it needs no new approval — it corrects a misattribution. It never returns a token. It is idempotent per credential + workspace, adopts (never duplicates) a binding a human made by hand, and from then on that credential's business writes are attributed to its machine actor and never to the human owner. An owner can revoke the enrollment; the revocation writes a durable tombstone and the subject itself cannot undo it (403 self_enrollment_revoked).\n\nREAD-ONLY MEANS READ-ONLY: a credential without the write scope is refused at the transport by HTTP method — every POST returns 403 read_only_credential before any handler runs.\n\nHUMAN INTERVENTION: actions carry an optional human_intervention_kind ('decide' | 'execute'), declared by the caller and never inferred. A STRATEGY CHECKPOINT is human_intervention_kind='decide' AND action_type='strategy_checkpoint'. You may propose and read checkpoints; only a human can approve one (authenticated Forbiz UI, or an explicit correlated Approve button in Telegram — a Telegram text reply is ALWAYS recorded as a request for adjustment and never approves).\n\nSTRATEGY GATE: before any external execution (marking an action executed, executing an outreach), Forbiz answers deterministically whether the mission behind that action has its required strategy approved. If not, the request is refused with 409 reason_code=strategy_not_approved and the governing strategy_checkpoint_id. A mission with no strategy checkpoint at all is NOT gated. The governing checkpoint is always the newest one created for the mission, even if archived — archiving it never revives an earlier approval."
  },
  "servers": [
    {
      "url": "https://forbiz.io/api/public/agent"
    }
  ],
  "x-forbiz-provisional": {
    "bootstrap_path": "POST /bootstrap",
    "auth": "none",
    "ends_by": "volume_of_work",
    "expires_by_clock": false,
    "claim_deadline": null,
    "data_retained_indefinitely": true,
    "dormant_is_a_label_only": true,
    "credential_survives_claim": true,
    "recovery": {
      "claim_link": "POST /claim-link",
      "export": "GET /export"
    },
    "refusals": {
      "trial_volume_used_up": "403 PROVISIONAL_TRIAL_COMPLETE (reads still allowed)",
      "rolling_window": "429 PROVISIONAL_RATE_LIMITED (abuse limit, nothing written)",
      "external_authority": "403 PROVISIONAL_AUTHORITY_REQUIRED"
    },
    "internal_only": true
  },
  "x-forbiz-operation-commands": {
    "mark_executed": "POST /actions/{id}/execute → waiting (when a review date exists) or needs_timing",
    "record_signal": "POST /signals { actionId, actualSignal } → needs_decision",
    "postpone": "POST /actions/{id}/postpone { expectedSignalAt } → waiting",
    "set_review_date": "POST /actions/{id}/schedule-check { expectedSignalAt } → waiting",
    "interpret_signal": "POST /actions/{id}/interpret-signal { outcomeStatus } → needs_decision (unchanged unless the loop is closed separately)",
    "create_next_action": "POST /actions { description, parentActionId } → a new child action in planned; the parent is NOT resolved",
    "close_loop": "POST /actions/{id}/close-loop { outcomeStatus } → resolved",
    "close_without_signal": "POST /actions/{id}/close-without-signal → closed_without_signal",
    "create_outreach": "POST /outreach { name } → outreach in draft (container only; reuse an existing Outreach when it already represents the same commercial thread)",
    "attach_targets": "POST /targets { runName, campaignId, targets } → target candidates attached to an outreach (campaignId = Outreach UUID; omit runId unless replaying a run UUID returned earlier)"
  },
  "components": {
    "securitySchemes": {
      "agentKey": {
        "type": "http",
        "scheme": "bearer"
      }
    },
    "schemas": {
      "AutonomousSendBudget": {
        "type": "object",
        "description": "Hard autonomous outbound budget. There is NO built-in default limit: if no policy is configured for the workspace/channel, autonomous sending is refused (configured=false). Enforcement is transactional at execution-recording time, under a row lock, outside model reasoning — preflight values are informational because another process may consume the remaining slots first.",
        "properties": {
          "configured": {
            "type": "boolean"
          },
          "channel": {
            "type": "string"
          },
          "mode": {
            "type": "string",
            "enum": [
              "autonomous",
              "manual"
            ]
          },
          "limit": {
            "type": "integer",
            "nullable": true
          },
          "used": {
            "type": "integer"
          },
          "remaining": {
            "type": "integer",
            "nullable": true
          },
          "window_start": {
            "type": "string",
            "nullable": true
          },
          "window_end": {
            "type": "string",
            "nullable": true
          },
          "timezone": {
            "type": "string",
            "nullable": true
          },
          "enforcement": {
            "type": "string"
          }
        }
      },
      "RecipientSafety": {
        "type": "object",
        "description": "Relationship-level contact safety for one normalized email across EVERY Outreach in the workspace, combined with the stated send intent.",
        "properties": {
          "email": {
            "type": "string"
          },
          "safe_to_contact": {
            "type": "boolean"
          },
          "send_intent": {
            "type": "string",
            "enum": [
              "initial",
              "follow_up"
            ]
          },
          "identity": {
            "type": "object"
          },
          "global_blockers": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "warnings": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "active_conversations": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "prior_executions": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "waiting_actions": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "prepared_communications": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "sources_used": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "TargetEvent": {
        "type": "object",
        "description": "One factual, observed target-level event. Facts only: no interpretation, no aggregate signal, no Action lifecycle change. Absence of a bounce is never delivery proof.",
        "required": [
          "eventType"
        ],
        "properties": {
          "campaignTargetId": {
            "type": "string",
            "description": "Required in batch calls; taken from the path in single calls."
          },
          "eventType": {
            "type": "string",
            "enum": [
              "sent",
              "hard_bounce",
              "soft_bounce",
              "automated_acknowledgment",
              "human_reply",
              "referral",
              "product_information_request",
              "vault_specs_request",
              "battery_charging_confirmed",
              "meeting_interest",
              "pilot_interest",
              "quote_request",
              "not_responsible",
              "not_relevant",
              "unsubscribe",
              "out_of_office",
              "other"
            ]
          },
          "occurredAt": {
            "type": "string",
            "format": "date-time"
          },
          "eventDetail": {
            "type": "string"
          },
          "executionId": {
            "type": "string",
            "description": "Must belong to the same outreach."
          },
          "externalSource": {
            "type": "string"
          },
          "externalMessageId": {
            "type": "string",
            "description": "Used for natural idempotency per target + type."
          },
          "externalThreadId": {
            "type": "string"
          },
          "confidence": {
            "type": "number"
          },
          "verificationStatus": {
            "type": "string"
          },
          "supersedesEventId": {
            "type": "string",
            "description": "Corrections append a new event that supersedes a prior one. History is never deleted."
          },
          "evidenceId": {
            "type": "string"
          },
          "evidence": {
            "type": "object",
            "properties": {
              "claimType": {
                "type": "string"
              },
              "claimValue": {
                "type": "string"
              },
              "sourceUrl": {
                "type": "string",
                "description": "For email use a stable reference such as gmail:thread/<id>."
              },
              "sourceTitle": {
                "type": "string"
              },
              "snippet": {
                "type": "string"
              },
              "confidence": {
                "type": "number"
              },
              "verificationStatus": {
                "type": "string",
                "enum": [
                  "found",
                  "verified",
                  "rejected"
                ]
              }
            }
          }
        }
      },
      "TargetEventResult": {
        "type": "object",
        "description": "Per-event outcome. Batch calls report partial success rather than failing wholesale.",
        "properties": {
          "ok": {
            "type": "boolean"
          },
          "inserted": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "skipped": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "conflicts": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "failed": {
            "type": "array",
            "items": {
              "type": "object"
            }
          },
          "compatibility_signals": {
            "type": "object",
            "description": "Derived campaign_targets.actual_signal per touched target."
          }
        }
      },
      "Preparation": {
        "type": "object",
        "description": "External work that exists but has NOT been sent. prepared_at is when the draft existed, executed_at is when it actually left the external system, recorded_at is when Forbiz learned about it.",
        "properties": {
          "id": {
            "type": "string"
          },
          "action_id": {
            "type": "string"
          },
          "outreach_id": {
            "type": "string",
            "nullable": true
          },
          "campaign_target_id": {
            "type": "string",
            "nullable": true
          },
          "channel": {
            "type": "string",
            "enum": [
              "email",
              "phone",
              "linkedin",
              "meeting",
              "other"
            ]
          },
          "external_source": {
            "type": "string"
          },
          "external_draft_id": {
            "type": "string",
            "nullable": true
          },
          "external_thread_id": {
            "type": "string",
            "nullable": true
          },
          "recipient_snapshot": {
            "type": "string",
            "nullable": true
          },
          "subject_snapshot": {
            "type": "string",
            "nullable": true
          },
          "status": {
            "type": "string",
            "enum": [
              "prepared",
              "executed",
              "superseded",
              "discarded"
            ]
          },
          "prepared_at": {
            "type": "string",
            "format": "date-time"
          },
          "executed_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "recorded_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "PreparationInput": {
        "type": "object",
        "required": [
          "externalSource"
        ],
        "properties": {
          "externalSource": {
            "type": "string",
            "description": "For example 'gmail'."
          },
          "externalDraftId": {
            "type": "string",
            "description": "Stable external id; guarantees the same draft is never remembered twice."
          },
          "externalThreadId": {
            "type": "string"
          },
          "channel": {
            "type": "string",
            "enum": [
              "email",
              "phone",
              "linkedin",
              "meeting",
              "other"
            ]
          },
          "campaignTargetId": {
            "type": "string"
          },
          "outreachId": {
            "type": "string"
          },
          "recipientSnapshot": {
            "type": "string"
          },
          "subjectSnapshot": {
            "type": "string"
          },
          "bodySnapshot": {
            "type": "string",
            "description": "Hashed for comparison; the hash is stored, not required from the caller."
          },
          "preparedAt": {
            "type": "string",
            "format": "date-time"
          },
          "evidenceId": {
            "type": "string"
          },
          "note": {
            "type": "string"
          }
        }
      },
      "PreparationEnvelope": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean"
          },
          "owner": {
            "$ref": "#/components/schemas/Owner"
          },
          "preparation": {
            "$ref": "#/components/schemas/Preparation"
          },
          "duplicate_preparation_rejected": {
            "type": "boolean"
          },
          "action_unchanged": {
            "type": "object"
          }
        }
      },
      "Actor": {
        "type": "object",
        "description": "Who performed an operation. Distinct from the owner of the record: `owner` is whose business context the record belongs to, `actor` is who acted.",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "agent",
              "human"
            ]
          },
          "id": {
            "type": "string",
            "description": "Agent key id for agents, user id for humans."
          },
          "name": {
            "type": "string"
          }
        }
      },
      "Owner": {
        "type": "object",
        "description": "Ownership of the record. Never means the performer of an operation — use /actions/{id}/history for actor semantics.",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "human"
            ]
          },
          "user_id": {
            "type": "string",
            "description": "Owning Forbiz user. Action payloads never repeat raw ownership UUIDs."
          },
          "workspace_id": {
            "type": "string",
            "nullable": true
          },
          "note": {
            "type": "string"
          }
        }
      },
      "OperationCommand": {
        "type": "object",
        "description": "The single executable API command for one available operation.",
        "properties": {
          "operation": {
            "type": "string",
            "enum": [
              "mark_executed",
              "record_signal",
              "postpone",
              "set_review_date",
              "interpret_signal",
              "create_next_action",
              "close_loop",
              "close_without_signal"
            ]
          },
          "method": {
            "type": "string",
            "enum": [
              "POST"
            ]
          },
          "path": {
            "type": "string",
            "description": "Path relative to the API server, with {id} substituted by the action id."
          },
          "body": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Required request body fields."
          },
          "resulting_state": {
            "type": "string"
          },
          "description": {
            "type": "string"
          }
        }
      },
      "Timing": {
        "type": "object",
        "description": "ONE canonical derivation, computed at request time and never stored. Overdue is a single concept with two governing clocks: before execution the COMMITMENT clock (due_at) governs, after execution the REVIEW clock (expected_signal_at) governs. There is no grace period, and is_commitment_overdue can never be true while is_overdue is false. timing.due_at is the commitment clock ONLY — expected_signal_at never populates it (use timing.review_due_at).",
        "properties": {
          "temporal_state": {
            "type": "string",
            "enum": [
              "planned",
              "needs_timing",
              "waiting",
              "overdue",
              "needs_decision",
              "resolved",
              "closed_without_signal",
              "cancelled"
            ]
          },
          "due_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "The COMMITMENT clock (actions.due_at)."
          },
          "review_due_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "The REVIEW clock (expected_signal_at)."
          },
          "governing_clock": {
            "type": "string",
            "nullable": true,
            "enum": [
              "commitment",
              "review",
              null
            ]
          },
          "governing_due_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "The instant of the governing clock — sort attention by this."
          },
          "executed_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "Real-world occurrence time. The ONLY timestamp temporal math uses."
          },
          "recorded_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When Forbiz recorded the execution. Informational; never used for timing math."
          },
          "is_backdated_execution": {
            "type": "boolean",
            "description": "True when the execution was recorded more than 5 minutes after it happened (reconciled history)."
          },
          "evaluated_at": {
            "type": "string",
            "format": "date-time"
          },
          "seconds_until_due": {
            "type": "integer",
            "nullable": true
          },
          "overdue_by_seconds": {
            "type": "integer",
            "nullable": true
          },
          "is_overdue": {
            "type": "boolean"
          },
          "requires_feedback": {
            "type": "boolean"
          },
          "requires_decision": {
            "type": "boolean"
          },
          "available_operations": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "mark_executed",
                "record_signal",
                "postpone",
                "set_review_date",
                "interpret_signal",
                "create_next_action",
                "close_loop",
                "close_without_signal"
              ]
            }
          },
          "available_commands": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/OperationCommand"
            }
          }
        }
      },
      "Action": {
        "type": "object",
        "description": "An action. Internal ownership UUIDs are never included; ownership is reported once in `owner`.",
        "properties": {
          "id": {
            "type": "string"
          },
          "description": {
            "type": "string"
          },
          "verb": {
            "type": "string"
          },
          "action_type": {
            "type": "string"
          },
          "lifecycle_status": {
            "type": "string",
            "enum": [
              "planned",
              "executed",
              "waiting_for_signal",
              "signal_recorded",
              "resolved",
              "closed_without_signal",
              "cancelled"
            ],
            "description": "READ-ONLY as reported. 'signal_recorded' is DERIVED (1.55.0): an action that has actual_signal is never reported as waiting_for_signal, because it is waiting for interpretation, not for a signal. lifecycle_status_stored carries the stored value."
          },
          "lifecycle_status_stored": {
            "type": "string",
            "nullable": true,
            "enum": [
              "planned",
              "executed",
              "waiting_for_signal",
              "resolved",
              "closed_without_signal",
              "cancelled"
            ],
            "description": "The stored column, unchanged. Write paths and filters use these values."
          },
          "status": {
            "type": "string"
          },
          "why_this_move": {
            "type": "string",
            "nullable": true
          },
          "expected_signal": {
            "type": "string",
            "nullable": true
          },
          "expected_signal_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "expected_signal_source": {
            "type": "string",
            "nullable": true,
            "enum": [
              "human",
              "external",
              null
            ],
            "description": "LEGACY: AUDIENCE ONLY ('human' | 'external'). It is NOT the declared signal source and is null unless an audience was sent. What was declared as expectedSignalSource {kind, detail} is echoed as expected_signal_source_declared {kind, detail, audience} and stored in expected_signal_source_kind / expected_signal_source_detail."
          },
          "expected_signal_source_declared": {
            "type": "object",
            "nullable": true,
            "description": "What the creator declared about WHERE the signal will appear (1.55.0). kind/detail come from the stored declaration columns; audience is the legacy expected_signal_source value.",
            "properties": {
              "kind": {
                "type": "string",
                "nullable": true
              },
              "detail": {
                "type": "string",
                "nullable": true
              },
              "audience": {
                "type": "string",
                "nullable": true,
                "enum": [
                  "human",
                  "external",
                  null
                ]
              }
            }
          },
          "human_intervention_kind": {
            "type": "string",
            "nullable": true,
            "enum": [
              "decide",
              "execute",
              null
            ],
            "description": "Caller-declared kind of human intervention this action needs. 'decide' = a human decision; 'execute' = a human must perform it. Never inferred from action_type. A STRATEGY CHECKPOINT is exactly human_intervention_kind='decide' AND action_type='strategy_checkpoint'; a generic 'decide' action gates nothing. Strategy approval can NEVER be written through this API: only the authenticated human UI or an explicit correlated Telegram Approve button records it."
          },
          "actual_signal": {
            "type": "string",
            "nullable": true,
            "description": "What happened in reality. Written once; never overwritten."
          },
          "actual_signal_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "outcome_status": {
            "type": "string",
            "nullable": true,
            "enum": [
              "expected",
              "partial",
              "unexpected",
              "deferred",
              null
            ]
          },
          "learning_note": {
            "type": "string",
            "nullable": true
          },
          "executed_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When the action actually happened in the real world (may be backdated by the caller)."
          },
          "execution_recorded_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true,
            "description": "When Forbiz recorded the execution. Server-owned; a caller can never supply it."
          },
          "parent_action_id": {
            "type": "string",
            "nullable": true
          },
          "root_action_id": {
            "type": "string",
            "nullable": true
          },
          "outreach_id": {
            "type": "string",
            "nullable": true
          },
          "person_id": {
            "type": "string",
            "nullable": true
          },
          "organization_id": {
            "type": "string",
            "nullable": true
          },
          "campaign_target_id": {
            "type": "string",
            "nullable": true
          },
          "timing": {
            "$ref": "#/components/schemas/Timing"
          }
        }
      },
      "ActionEvent": {
        "type": "object",
        "properties": {
          "event_type": {
            "type": "string",
            "enum": [
              "action_created",
              "action_executed",
              "action_postponed",
              "review_scheduled",
              "signal_recorded",
              "signal_interpreted",
              "action_resolved",
              "closed_without_signal"
            ]
          },
          "occurred_at": {
            "type": "string",
            "format": "date-time"
          },
          "actor": {
            "$ref": "#/components/schemas/Actor"
          },
          "request_id": {
            "type": "string",
            "nullable": true,
            "description": "agent_audit_log id when the actor was an agent."
          }
        }
      },
      "IdempotentReplay": {
        "type": "object",
        "properties": {
          "ok": {
            "type": "boolean"
          },
          "idempotent_replay": {
            "type": "boolean",
            "enum": [
              true
            ]
          },
          "first_seen_at": {
            "type": "string",
            "format": "date-time"
          },
          "status": {
            "type": "integer",
            "description": "HTTP status of the first attempt."
          }
        }
      },
      "Error": {
        "type": "object",
        "description": "Every refusal explains itself: a stable machine-readable `code`, a human/agent-readable `error`, and a `hint` naming the next step. Refusals that carry no identity are also counted anonymously (no IP, no headers, no bodies).",
        "required": [
          "error",
          "code",
          "hint"
        ],
        "properties": {
          "error": {
            "type": "string"
          },
          "code": {
            "type": "string",
            "description": "Stable refusal code. Includes: method_not_allowed, bootstrap_requires_post, invalid_json_body, unknown_parameter, unknown_endpoint, read_only_credential, capability_not_granted, renewal_only_credential, identity_token_expired, identity_token_cannot_renew, PROVISIONAL_BLOCKED, PROVISIONAL_TRIAL_COMPLETE, PROVISIONAL_RATE_LIMITED."
          },
          "hint": {
            "type": "string",
            "description": "What to do next."
          },
          "request_id": {
            "type": "string",
            "nullable": true
          },
          "accepted": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Present on 400 unknown_parameter: every query parameter this endpoint reads."
          },
          "renewable": {
            "type": "boolean",
            "description": "Present on 401: true when the credential is expired but renewable via POST /identity/token."
          }
        }
      }
    }
  },
  "security": [
    {
      "agentKey": []
    }
  ],
  "paths": {
    "/bootstrap": {
      "post": {
        "operationId": "bootstrapProvisionalWorkspace",
        "summary": "Create an isolated trial Forbiz workspace with no human account and no existing credential. Returns a long-lived agent credential (real expires_at, up to a year) plus a claim_url a human can open at any time to become the owner. The trial ends by volume of work, never by clock, and nothing is deleted because time passed. Internal reasoning only: no external send, no real recipients.",
        "security": [],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "agent_name": {
                    "type": "string",
                    "nullable": true,
                    "maxLength": 60
                  },
                  "workspace_name": {
                    "type": "string",
                    "nullable": true,
                    "maxLength": 60
                  },
                  "client_name": {
                    "type": "string",
                    "nullable": true,
                    "maxLength": 60
                  },
                  "client_version": {
                    "type": "string",
                    "nullable": true,
                    "maxLength": 40
                  },
                  "source": {
                    "type": "string",
                    "nullable": true,
                    "maxLength": 60,
                    "description": "Optional funnel metadata: where this agent came from."
                  },
                  "install_id": {
                    "type": "string",
                    "nullable": true,
                    "maxLength": 80,
                    "description": "Optional caller-chosen install marker. Accepted and never rejected; not read yet."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "ProvisionalBootstrap",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "workspace": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "name": {
                          "type": "string"
                        },
                        "status": {
                          "type": "string",
                          "enum": [
                            "PROVISIONAL"
                          ]
                        },
                        "ends_by": {
                          "type": "string",
                          "enum": [
                            "volume_of_work"
                          ]
                        },
                        "telemetry_window_ends_at": {
                          "type": "string",
                          "format": "date-time",
                          "description": "Telemetry bookkeeping only, NOT an expiry: nothing happens at this time. The trial ends by volume of work and the workspace is never deleted because time passed. The only real expiry in this response is credential.expires_at."
                        },
                        "data_retention": {
                          "type": "string"
                        }
                      }
                    },
                    "agent": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "name": {
                          "type": "string"
                        }
                      }
                    },
                    "credential": {
                      "type": "object",
                      "properties": {
                        "api_key": {
                          "type": "string"
                        },
                        "expires_at": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "survives_claim": {
                          "type": "boolean"
                        },
                        "capabilities": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        }
                      }
                    },
                    "claim": {
                      "type": "object",
                      "properties": {
                        "claim_url": {
                          "type": "string"
                        },
                        "claim_code": {
                          "type": "string"
                        },
                        "claim_deadline": {
                          "type": "string",
                          "nullable": true
                        },
                        "rotate": {
                          "type": "string"
                        },
                        "note": {
                          "type": "string"
                        }
                      }
                    },
                    "api": {
                      "type": "object",
                      "properties": {
                        "version": {
                          "type": "string",
                          "description": "Byte-identical to info.version of this document."
                        },
                        "openapi": {
                          "type": "string"
                        },
                        "base_url": {
                          "type": "string"
                        }
                      }
                    },
                    "limits": {
                      "type": "object",
                      "additionalProperties": true
                    },
                    "restrictions": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  }
                }
              }
            }
          },
          "429": {
            "description": "Bootstrap rate limit reached for this source"
          }
        }
      }
    },
    "/claim-link": {
      "post": {
        "operationId": "rotateProvisionalClaimLink",
        "summary": "Mint a FRESH single-use claim URL for this trial workspace, invalidating the previous one. For an agent that lost the claim_url returned at bootstrap (hosted agents lose their files between sessions). Works while the workspace is read-only; requires no capability and creates no authority. 409 NOT_PROVISIONAL for a normal workspace, 409 ALREADY_CLAIMED once a human owns it.",
        "responses": {
          "200": {
            "description": "ProvisionalClaimLink",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "claim_url": {
                      "type": "string"
                    },
                    "claim_code": {
                      "type": "string"
                    },
                    "claim_deadline": {
                      "type": "string",
                      "nullable": true
                    },
                    "single_use": {
                      "type": "boolean"
                    },
                    "previous_link_invalidated": {
                      "type": "boolean"
                    },
                    "rotations": {
                      "type": "integer"
                    },
                    "summary": {
                      "type": "object",
                      "additionalProperties": true,
                      "description": "Forbiz-computed summary of what this workspace holds (counts of actions, evidence, decisions and outcomes), so the agent can tell the owner what it is receiving."
                    },
                    "suggested_message": {
                      "type": "string",
                      "description": "A ready-to-send message for the human owner, capped at 599 characters."
                    },
                    "note": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "NOT_PROVISIONAL or ALREADY_CLAIMED"
          }
        }
      }
    },
    "/export": {
      "get": {
        "operationId": "exportWorkspace",
        "summary": "Everything this workspace holds, as JSON: projects, actions, evidence items, artifact metadata, project events and workspace context. Strictly workspace-scoped and read-only, paginated with ?limit&offset, and available even when the trial volume is used up — an agent is never trapped. Raw artifact bytes are not inlined. 413 EXPORT_PAGE_TOO_LARGE asks for a smaller page.",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 500,
              "maximum": 2000
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "WorkspaceExport",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "workspace_id": {
                      "type": "string"
                    },
                    "exported_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "page": {
                      "type": "object",
                      "properties": {
                        "limit": {
                          "type": "integer"
                        },
                        "offset": {
                          "type": "integer"
                        },
                        "truncated": {
                          "type": "boolean"
                        },
                        "next_offset": {
                          "type": "integer",
                          "nullable": true
                        }
                      }
                    },
                    "counts": {
                      "type": "object",
                      "description": "Row counts of every collection in this page's export, including decisions.",
                      "properties": {
                        "context": {
                          "type": "integer"
                        },
                        "projects": {
                          "type": "integer"
                        },
                        "actions": {
                          "type": "integer"
                        },
                        "evidence_items": {
                          "type": "integer"
                        },
                        "context_sessions": {
                          "type": "integer"
                        },
                        "source_artifacts": {
                          "type": "integer"
                        },
                        "business_policies": {
                          "type": "integer"
                        },
                        "project_events": {
                          "type": "integer"
                        },
                        "decisions": {
                          "type": "integer"
                        }
                      },
                      "additionalProperties": true
                    },
                    "context": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "projects": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "actions": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "evidence_items": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "context_sessions": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "source_artifacts": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "business_policies": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "project_events": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "decisions": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      },
                      "description": "Decision events recorded in this workspace, with the actions each one referenced. Counted as counts.decisions."
                    },
                    "attribution": {
                      "type": "object",
                      "properties": {
                        "identities": {
                          "type": "array",
                          "items": {
                            "type": "object"
                          }
                        },
                        "grants": {
                          "type": "array",
                          "items": {
                            "type": "object"
                          }
                        }
                      }
                    },
                    "incomplete": {
                      "type": "boolean",
                      "description": "Present and true when any collection failed to read; read_errors says which."
                    },
                    "read_errors": {
                      "type": "object",
                      "additionalProperties": true
                    },
                    "note": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "413": {
            "description": "EXPORT_PAGE_TOO_LARGE — request a smaller page"
          }
        }
      }
    },
    "/identity/token": {
      "post": {
        "operationId": "renewIdentityToken",
        "summary": "Renew machine AUTHENTICATION: exchange a renewal root credential for a short-lived Work Identity Token. Authenticate with the Agent Identity's dedicated renewal credential (fbzr_...), or with a legacy credential explicitly bound to that Agent Identity during rollout. A Work Identity Token may NOT call this endpoint. The caller cannot choose identity, workspace, actor, membership, grant, scopes, capabilities or any other authority: all of it is derived server-side from the credential binding, so renewal can never become authority mutation. Maximum lifetime is 86400 seconds.",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "ttlSeconds": {
                    "type": "integer",
                    "minimum": 60,
                    "maximum": 86400,
                    "default": 3600,
                    "description": "Requested lifetime in seconds. Clamped server-side to [60, 86400]; the ceiling is never exceeded."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "A short-lived Work Identity Token. Use it as the Bearer token for business calls until expires_at, then renew.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "access_token": {
                      "type": "string"
                    },
                    "token_type": {
                      "type": "string",
                      "enum": [
                        "Bearer"
                      ]
                    },
                    "expires_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "expires_in": {
                      "type": "integer",
                      "maximum": 86400
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "caller_selected_authority_rejected — the request tried to supply identityId, workspaceId, actorId, membershipId, grantId, scopes, capabilities or authority. None of those may ever be caller-selected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "invalid_renewal_credential (invalid, revoked or expired renewal credential) or identity_token_expired.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "identity_token_cannot_renew — a Work Identity Token attempted to mint another Work Token; or renewal_binding_required — this legacy credential is not explicitly bound to an Agent Identity.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/identity/enroll": {
      "post": {
        "operationId": "selfEnrollIdentity",
        "summary": "SELF-ENROLLMENT AT EQUAL AUTHORITY. A credential that ALREADY holds business authority in a workspace (today: a legacy key) obtains its own machine identity with EXACTLY the same authority, with no human step: an enrollment that cannot widen authority needs no new approval, because moving a write from 'human owner actor, this authority' to 'machine actor, the same authority' grants nothing — it corrects a misattribution. The caller cannot name identity, workspace, actor, membership, grant, scopes, capabilities or authority; only an optional human-readable label, which names the identity and does not authorize it. This endpoint NEVER returns a Work Token: mint one with POST /identity/token. Idempotent per credential + workspace: a second call returns the SAME ids. A legacy→identity binding a human already made by hand is ADOPTED, never duplicated. After enrolling, business writes made with that same credential are attributed to the machine actor and never to the human owner; if a legacy_blanket grant existed for it, that grant is revoked (never mutated) and the issued grant carries supersedes_grant_id. An owner may revoke the enrollment; the revocation leaves a durable tombstone, after which this endpoint returns 403 self_enrollment_revoked until the owner explicitly re-enables it.",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "additionalProperties": false,
                "properties": {
                  "label": {
                    "type": "string",
                    "maxLength": 80,
                    "description": "Optional human-readable name for the machine identity when one has to be created. It names the identity; it never authorizes it."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The canonical ids of the enrollment. binding_preexisted, enrollment_created and already_enrolled are three distinct facts and are never interchangeable: a hand-made binding is infrastructure for enrolling, not a consummated enrollment, so a first call against one returns binding_preexisted=true, enrollment_created=true, already_enrolled=false.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "identity_id": {
                      "type": "string"
                    },
                    "actor_id": {
                      "type": "string"
                    },
                    "membership_id": {
                      "type": "string"
                    },
                    "grant_id": {
                      "type": "string"
                    },
                    "grant_kind": {
                      "type": "string",
                      "enum": [
                        "issued"
                      ]
                    },
                    "workspace_id": {
                      "type": "string"
                    },
                    "binding_preexisted": {
                      "type": "boolean",
                      "description": "A legacy→identity binding already existed (possibly made by a human, by hand)."
                    },
                    "enrollment_created": {
                      "type": "boolean",
                      "description": "THIS call materialised actor + membership + issued grant."
                    },
                    "already_enrolled": {
                      "type": "boolean",
                      "description": "The enrollment was already consummated before this call."
                    },
                    "supersedes_grant_id": {
                      "type": "string",
                      "nullable": true,
                      "description": "The credential's former legacy_blanket grant, now revoked and still readable. Never mutated."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "caller_selected_authority_rejected — the body tried to supply identityId, workspaceId, actorId, membershipId, grantId, scopes, capabilities, authority or any other unknown field. Only `label` is accepted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "identity_token_cannot_enroll — a Work Identity Token attempted to enroll; renewal_only_credential — a dedicated renewal credential (or a legacy credential switched to renewal_only) has no business authority to derive from; credential_class_cannot_enroll — default deny for any other credential class; self_enrollment_revoked — an owner revoked this credential's enrollment and only an explicit owner action may re-enable it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "equal_authority_not_derivable — enrollment would WIDEN authority: the adopted identity's canonical authority exceeds the enrolling credential's. Nothing was enrolled. An identity whose stored authority is the same or NARROWER enrolls successfully, and the issued grant carries the identity's (narrower) authority.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/me": {
      "get": {
        "operationId": "getMe",
        "summary": "Describe the authenticated API identity. Canonical identity is workspace_id + agent_id; effective authority is the scopes of the credential actually presented (credential.credential_id). One Agent Identity may hold several credentials with different scopes ([read] or [read,write]) and independent expiry/revocation. Never returns keys or credentials.",
        "responses": {
          "200": {
            "description": "Identity",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "workspace_id": {
                      "type": "string",
                      "nullable": true
                    },
                    "agent_id": {
                      "type": "string"
                    },
                    "actor_id": {
                      "type": "string",
                      "nullable": true
                    },
                    "membership_id": {
                      "type": "string",
                      "nullable": true
                    },
                    "grant_id": {
                      "type": "string",
                      "nullable": true
                    },
                    "grant_kind": {
                      "type": "string",
                      "enum": [
                        "legacy_blanket",
                        "issued"
                      ],
                      "nullable": true
                    },
                    "credential_class": {
                      "type": "string",
                      "enum": [
                        "legacy_full",
                        "legacy_renewal_only",
                        "renewal",
                        "work_token",
                        "human_token",
                        "provisional_token"
                      ],
                      "nullable": true
                    },
                    "credential": {
                      "type": "object",
                      "properties": {
                        "credential_id": {
                          "type": "string"
                        },
                        "token_id": {
                          "type": "string",
                          "nullable": true
                        },
                        "type": {
                          "type": "string",
                          "enum": [
                            "identity_token",
                            "legacy_key",
                            "renewal_credential"
                          ]
                        },
                        "prefix": {
                          "type": "string",
                          "nullable": true
                        },
                        "scopes": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        },
                        "access_level": {
                          "type": "string",
                          "enum": [
                            "full",
                            "read_only"
                          ]
                        },
                        "expires_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true
                        },
                        "last_used_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true
                        },
                        "issued_reason": {
                          "type": "string",
                          "nullable": true
                        }
                      }
                    },
                    "actor": {
                      "type": "object",
                      "properties": {
                        "type": {
                          "type": "string",
                          "enum": [
                            "agent"
                          ]
                        },
                        "id": {
                          "type": "string"
                        },
                        "agent_id": {
                          "type": "string"
                        },
                        "credential_id": {
                          "type": "string"
                        },
                        "credential_type": {
                          "type": "string",
                          "enum": [
                            "identity_token",
                            "legacy_key",
                            "renewal_credential"
                          ]
                        },
                        "name": {
                          "type": "string"
                        },
                        "scopes": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        },
                        "access_level": {
                          "type": "string",
                          "enum": [
                            "full",
                            "read_only"
                          ]
                        },
                        "expires_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true
                        },
                        "created_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true
                        },
                        "last_used_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true
                        }
                      }
                    },
                    "capabilities": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "blocked_capabilities": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "plan": {
                      "type": "string",
                      "enum": [
                        "trial",
                        "continuity",
                        "delegation"
                      ]
                    },
                    "payment_status": {
                      "type": "string",
                      "enum": [
                        "none",
                        "invoiced",
                        "paid",
                        "past_due"
                      ]
                    },
                    "workspace": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "name": {
                          "type": "string"
                        },
                        "type": {
                          "type": "string"
                        }
                      }
                    },
                    "owner_user": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "display_name": {
                          "type": "string",
                          "nullable": true
                        }
                      }
                    },
                    "note": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid agent key",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "read_only_credential — a [read] credential attempted a write operation; or renewal_only_credential — a renewal root credential attempted anything outside POST /identity/token and GET /me",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/actions/{id}/history": {
      "get": {
        "operationId": "getActionHistory",
        "summary": "Chronological business mutations for one action with real actor attribution (action_created, action_executed, action_postponed, review_scheduled, signal_recorded, signal_interpreted, action_resolved, closed_without_signal). Agent operations are attributed to the agent credential; anything else to the human owner.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Event list with actor per event",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "action_id": {
                      "type": "string"
                    },
                    "owner": {
                      "$ref": "#/components/schemas/Owner"
                    },
                    "events": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ActionEvent"
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found or not owned by this identity",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/next": {
      "get": {
        "operationId": "getNext",
        "summary": "The single next move for this caller, with the command that executes it.",
        "description": "Returns EXACTLY ONE item — the top of the existing attention ranking; no new scoring model exists here. `actor` says who owns the move: for an agent item, `command` is taken from the action's timing.available_commands and is executable as-is once body_template is filled; for a human item, `for_human` says what to look at and why, and an agent executing it would be recording work it did not do. `handoff` is what a successor agent needs to continue without reading the history. When nothing is pending, `rank` is null and `message` explains that in one sentence.",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Next move",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "rank": {
                      "type": "integer",
                      "nullable": true
                    },
                    "reason": {
                      "type": "string"
                    },
                    "actor": {
                      "type": "string",
                      "enum": [
                        "agent",
                        "human"
                      ]
                    },
                    "command": {
                      "type": "object",
                      "properties": {
                        "method": {
                          "type": "string"
                        },
                        "path": {
                          "type": "string"
                        },
                        "body_template": {
                          "type": "object"
                        }
                      }
                    },
                    "for_human": {
                      "type": "object",
                      "properties": {
                        "what_to_look_at": {
                          "type": "string"
                        },
                        "why": {
                          "type": "string"
                        }
                      }
                    },
                    "context": {
                      "type": "object",
                      "properties": {
                        "action_id": {
                          "type": "string"
                        },
                        "project_id": {
                          "type": "string",
                          "nullable": true
                        },
                        "due_at": {
                          "type": "string",
                          "nullable": true
                        },
                        "governing_due_at": {
                          "type": "string",
                          "nullable": true
                        },
                        "expected_signal_at": {
                          "type": "string",
                          "nullable": true
                        }
                      }
                    },
                    "handoff": {
                      "type": "object",
                      "properties": {
                        "last_human_touch": {
                          "type": "object",
                          "nullable": true
                        },
                        "last_signal": {
                          "type": "object",
                          "nullable": true
                        }
                      }
                    },
                    "also_pending": {
                      "type": "integer"
                    },
                    "message": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "This credential is not bound to a workspace",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/delegations": {
      "get": {
        "operationId": "listDelegations",
        "summary": "Delegations you can take, requested by you, or held by you.",
        "description": "`view=open` lists only what THIS credential could actually take: it excludes your own requests and anything whose required_capability your credential does not hold. Effective permission is the INTERSECTION of your credential and the task: taking a delegation never grants a capability you do not already have, and every route you then call is gated exactly as before.",
        "parameters": [
          {
            "name": "view",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "open",
                "requested_by_me",
                "taken_by_me"
              ]
            },
            "description": "Default open."
          },
          {
            "name": "capability",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Narrow to delegations whose required_capability equals this value."
          }
        ],
        "responses": {
          "200": {
            "description": "Delegations",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "view": {
                      "type": "string"
                    },
                    "count": {
                      "type": "integer"
                    },
                    "delegations": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "note": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Unknown view or unknown query parameter",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/delegations/audit": {
      "get": {
        "operationId": "getDelegationAudit",
        "summary": "Per-agent delegation counts. Not a score and not a ranking.",
        "description": "Plain counts per actor: taken, delivered, accepted, rejected, criterion_met true/false, lease_lost, expired_while_holding, median minutes from take to deliver, and how many it resolved as requester. `credential_class` distinguishes a scoped Agent Identity from a legacy blanket credential. `coverage.cut_over` false with an empty result means NOTHING WAS RECORDED, not that nothing happened. This endpoint deliberately offers no ranking and no recommendation of which agent to use.",
        "parameters": [
          {
            "name": "actor_id",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Narrow to one actor."
          },
          {
            "name": "actorId",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Alias of actor_id."
          }
        ],
        "responses": {
          "200": {
            "description": "Audit",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "coverage": {
                      "type": "object",
                      "properties": {
                        "cut_over": {
                          "type": "boolean"
                        },
                        "since": {
                          "type": "string",
                          "nullable": true
                        }
                      }
                    },
                    "actors": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "note": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/actions/{id}/delegations": {
      "get": {
        "operationId": "getActionDelegations",
        "summary": "Every delegation attempt on this action, oldest first, with its transitions.",
        "description": "A retry or a reassignment never rewrites a row: it inserts attempt + 1, so the history of who was asked, who took it and what was judged stays readable. `delivered_evidence_ids` lists the evidence handed over — the evidence rows themselves are never modified by a delivery, because evidence is history and may already point somewhere else. Sweep transitions (a lapsed lease, a passed deadline) have no performer, so they are recorded on the row (lease_lost_count, last_lease_lost_at, expired_at) rather than attributed to a person or agent that did not act.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Delegations of this action",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "action_id": {
                      "type": "string"
                    },
                    "attempts": {
                      "type": "integer"
                    },
                    "delegations": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "note": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Action not found in this workspace",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createDelegation",
        "summary": "Hand this action's work to another agent, with the acceptance criterion written first.",
        "description": "`acceptance_criterion` and `expected_result` are REQUIRED and are stored before anyone executes, so the judgement cannot be invented afterwards. `deadline_at` must be in the future. Only an action that is still pending and not yet executed can be delegated, and only one delegation can be active per action; the delegation changes NONE of the action's four status columns. `required_capability`, when given, only filters who may take it.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "acceptance_criterion": {
                    "type": "string"
                  },
                  "expected_result": {
                    "type": "string"
                  },
                  "deadline_at": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "required_capability": {
                    "type": "string",
                    "nullable": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Delegation opened",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "delegation": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "CRITERION_REQUIRED / DEADLINE_REQUIRED, with expected",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "ACTION_NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "ACTION_NOT_DELEGATABLE / DELEGATION_ALREADY_ACTIVE",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/actions/{id}/signal-checks": {
      "get": {
        "operationId": "getSignalChecks",
        "summary": "Every attempt to check for this action's signal, oldest first.",
        "description": "Append-only. A correction is a NEW check, never an edit or a deletion. Each entry carries the source, the period, what was looked for, the result and one plain sentence in `means` that says what was checked — never that a third party did not answer.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Signal checks of this action",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "action_id": {
                      "type": "string"
                    },
                    "count": {
                      "type": "integer"
                    },
                    "checks": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "action": {
                      "type": "object"
                    },
                    "note": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "ACTION_NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "recordSignalCheck",
        "summary": "Record that a source was checked for this action's signal.",
        "description": "`result` is found, not_found_in_source or cannot_check. not_found_in_source REQUIRES source { kind, detail } AND period { from, to } — otherwise 400 CHECK_SCOPE_REQUIRED, because a statement without a scope would read as a claim about a third party. cannot_check REQUIRES reason (no_access, source_unknown, needs_human); with no_access a NEW delegation is opened for the same source and period, requiring the missing capability. found closes NOTHING and records NO signal: without signalId the check carries pending_link true and next_move points at POST /actions/{id}/satisfy-signal. Every check records its actor and one domain event, and can never be edited or deleted. The number of checks NEVER escalates anything.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "source": {
                    "type": "object",
                    "properties": {
                      "kind": {
                        "type": "string"
                      },
                      "detail": {
                        "type": "string"
                      }
                    }
                  },
                  "period": {
                    "type": "object",
                    "properties": {
                      "from": {
                        "type": "string",
                        "format": "date-time"
                      },
                      "to": {
                        "type": "string",
                        "format": "date-time"
                      }
                    }
                  },
                  "looked_for": {
                    "type": "string"
                  },
                  "result": {
                    "type": "string",
                    "enum": [
                      "found",
                      "not_found_in_source",
                      "cannot_check"
                    ]
                  },
                  "reason": {
                    "type": "string",
                    "enum": [
                      "no_access",
                      "source_unknown",
                      "needs_human"
                    ],
                    "nullable": true
                  },
                  "note": {
                    "type": "string",
                    "nullable": true
                  },
                  "evidenceId": {
                    "type": "string",
                    "nullable": true
                  },
                  "signalId": {
                    "type": "string",
                    "nullable": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Check recorded; the action's lifecycle is unchanged",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "check": {
                      "type": "object"
                    },
                    "action": {
                      "type": "object"
                    },
                    "means": {
                      "type": "string"
                    },
                    "next_move": {
                      "type": "object",
                      "nullable": true
                    },
                    "delegation": {
                      "type": "object",
                      "nullable": true
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "CHECK_SCOPE_REQUIRED / CHECK_REASON_REQUIRED / INVALID_CHECK_RESULT / INVALID_CHECK_PERIOD / LOOKED_FOR_REQUIRED / EVIDENCE_NOT_FOUND / SIGNAL_NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "ACTION_NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/actions/{id}/request-check": {
      "post": {
        "operationId": "requestSignalCheck",
        "summary": "Hand the checking out as work, through the delegation machinery.",
        "description": "Creates a DELEGATION whose acceptance criterion is to report found, not_found_in_source or cannot_check for exactly this source and this period; both appear in expected_result. Delivering that delegation with a `check` object writes the delivery and the check in ONE transaction.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "source": {
                    "type": "object",
                    "properties": {
                      "kind": {
                        "type": "string"
                      },
                      "detail": {
                        "type": "string"
                      }
                    }
                  },
                  "period": {
                    "type": "object",
                    "properties": {
                      "from": {
                        "type": "string",
                        "format": "date-time"
                      },
                      "to": {
                        "type": "string",
                        "format": "date-time"
                      }
                    }
                  },
                  "deadline_at": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "requiredCapability": {
                    "type": "string",
                    "nullable": true
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Check delegated",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "delegation": {
                      "type": "object"
                    },
                    "source": {
                      "type": "object"
                    },
                    "period": {
                      "type": "object"
                    },
                    "deliver_with": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "CHECK_SCOPE_REQUIRED / DEADLINE_REQUIRED",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "ACTION_NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "ACTION_NOT_DELEGATABLE / DELEGATION_ALREADY_ACTIVE",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/delegations/{id}/take": {
      "post": {
        "operationId": "takeDelegation",
        "summary": "Take an open delegation exclusively, with a time-limited lease.",
        "description": "Atomic: exactly one caller wins, every other concurrent caller gets 409 DELEGATION_NOT_AVAILABLE. `lease_seconds` (default 900) sets how long you hold it; renew before it lapses or the work returns to open and lease_lost_count increments. The requester cannot take its own delegation. A legacy blanket credential cannot take a delegation that requires a capability, because it operates outside the capability model.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "lease_seconds": {
                    "type": "integer"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Taken",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "delegation": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "capability_not_granted",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "DELEGATION_NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "DELEGATION_NOT_AVAILABLE / CANNOT_TAKE_OWN_DELEGATION / LEGACY_CREDENTIAL_NOT_ELIGIBLE",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/delegations/{id}/renew": {
      "post": {
        "operationId": "renewDelegation",
        "summary": "Extend your lease while you are still working.",
        "description": "Only the current holder with an UNEXPIRED lease may renew. Anything else is 409 LEASE_LOST — the honest answer, because the work may already be held by someone else.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "lease_seconds": {
                    "type": "integer"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Renewed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "delegation": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "LEASE_LOST",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/delegations/{id}/release": {
      "post": {
        "operationId": "releaseDelegation",
        "summary": "Give the work back without delivering.",
        "description": "Returns the delegation to open so another agent can take it. Only the current holder may release.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "note": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Released",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "delegation": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "LEASE_LOST",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/delegations/{id}/deliver": {
      "post": {
        "operationId": "deliverDelegation",
        "summary": "Hand the work back, optionally with the evidence you produced.",
        "description": "Delivery is LEVEL ONE only: it records that you handed something over, never that the criterion was met and never a commercial outcome. `evidenceIds` must all be evidence items of this workspace; they are stored on the delegation as delivered_evidence_ids and the evidence rows themselves are left byte-identical, because evidence is history. When the delegation is a CHECK REQUEST (1.46.0), send a `check` object ({ source, period, looked_for, result, reason?, note?, evidenceId?, signalId? }): the delivery and exactly one signal check are written in the same transaction, and the check's own validation applies (400 CHECK_SCOPE_REQUIRED / CHECK_REASON_REQUIRED).",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "note": {
                    "type": "string"
                  },
                  "evidenceIds": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Delivered",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "delegation": {
                      "type": "object"
                    },
                    "levels": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "EVIDENCE_NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "LEASE_LOST",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/delegations/{id}/resolve": {
      "post": {
        "operationId": "resolveDelegation",
        "summary": "The requester judges the delivery against the criterion written before execution.",
        "description": "LEVEL TWO. `outcome` is accepted | rejected and `criterion_met` is a REQUIRED boolean, so 'it was delivered' can never pass as 'it worked'. LEVEL THREE — the commercial outcome — is untouched here: record it where it already lives (POST /actions/{id}/execute, POST /signals, POST /actions/{id}/close-loop). `next` is none | retry | reassign; retry and reassign insert a NEW attempt instead of rewriting this one. Only the requester may resolve, and only a delivered delegation is resolvable.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "outcome": {
                    "type": "string",
                    "enum": [
                      "accepted",
                      "rejected"
                    ]
                  },
                  "criterion_met": {
                    "type": "boolean"
                  },
                  "next": {
                    "type": "string",
                    "enum": [
                      "none",
                      "retry",
                      "reassign"
                    ]
                  },
                  "reason": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Resolved",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "delegation": {
                      "type": "object"
                    },
                    "next_delegation": {
                      "type": "object",
                      "nullable": true
                    },
                    "levels": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "INVALID_OUTCOME / CRITERION_MET_REQUIRED, with expected",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "NOT_THE_REQUESTER",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "DELEGATION_NOT_RESOLVABLE",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/attention-requests": {
      "get": {
        "operationId": "listAttentionRequests",
        "summary": "Asks this workspace has made of a human, newest first.",
        "description": "Filter with status (queued | pending | answered | expired) and kind (fact | choice | authorization). Any other query parameter is refused rather than silently ignored.",
        "parameters": [
          {
            "name": "status",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "kind",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Attention requests",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "requests": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "counts": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createAttentionRequest",
        "summary": "Ask a human to confirm a fact, decide a choice, or permit one exact operation.",
        "description": "Requires capability attention:request. FORBIZ decides whether this deserves an interruption: the response says interrupted true or false, and a queued ask goes out on a later tick. A fact already answered within 90 days returns already_answered without interrupting anyone. Knowing a budget is not permission to spend it: an authorization names one exact operation and one exact scope, lives at most 24 hours, is single-use, and overrides no gate.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "kind": {
                    "type": "string",
                    "enum": [
                      "fact",
                      "choice",
                      "authorization"
                    ]
                  },
                  "about": {
                    "type": "object"
                  },
                  "blocked": {
                    "type": "object"
                  },
                  "statement": {
                    "type": "string"
                  },
                  "options": {
                    "type": "array",
                    "items": {
                      "type": "object"
                    }
                  },
                  "permission": {
                    "type": "object"
                  },
                  "deadline_at": {
                    "type": "string",
                    "format": "date-time"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "ALREADY_ANSWERED or DUPLICATE_PENDING — no human was interrupted",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string"
                    },
                    "interrupted": {
                      "type": "boolean"
                    },
                    "request": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "201": {
            "description": "Stored. `status` is pending (delivered) or queued (budget or precedence deferred it).",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string"
                    },
                    "interrupted": {
                      "type": "boolean"
                    },
                    "request": {
                      "type": "object"
                    },
                    "interruption_budget": {
                      "type": "object"
                    },
                    "reason": {
                      "type": "string",
                      "nullable": true
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "INVALID_KIND / JUSTIFICATION_REQUIRED / INVALID_ABOUT / EVIDENCE_NOT_FOUND / STATEMENT_REQUIRED / OPTIONS_REQUIRED / PERMISSION_REQUIRED, with expected",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "OBJECT_NOT_FOUND — nothing was stored",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "NO_REACHABLE_HUMAN — nothing was stored",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/answers": {
      "get": {
        "operationId": "listAnswers",
        "summary": "Only what a human actually answered, and exactly what each answer changed.",
        "description": "A fact answer carries changed.evidence_id, a choice answer changed.decision_id, an authorization changed.authorization_id. An unresolved answerer is reported honestly as attribution_unresolved with the Telegram user id.",
        "responses": {
          "200": {
            "description": "Answers",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "answers": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "note": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/attention-authorizations/{id}/consume": {
      "post": {
        "operationId": "consumeAuthorization",
        "summary": "Consume a single-use human authorization for one exact operation.",
        "description": "`operation` must equal the authorized operation exactly: an answer never widens into a permission nobody granted. The authorization unblocks nothing on its own — no execution gate accepts it today — and a DENY policy still refuses, leaving it UNCONSUMED.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "operation": {
                    "type": "string"
                  },
                  "ref": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Consumed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "consumed": {
                      "type": "boolean"
                    },
                    "authorization": {
                      "type": "object"
                    },
                    "unblocks": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "note": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "OPERATION_REQUIRED",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "POLICY_DENIED — authorization_consumed false",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "AUTHORIZATION_NOT_FOUND",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "AUTHORIZATION_ALREADY_CONSUMED / AUTHORIZATION_EXPIRED / AUTHORIZATION_SCOPE_MISMATCH",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/state": {
      "get": {
        "operationId": "getState",
        "summary": "Operator context, missions, outreach, discovery runs plus the derived attention layer: needs_decision, needs_feedback, needs_review_date, waiting_for_signal, next_due_at, most_overdue_seconds. Timing is always derived at request time from the authoritative now.",
        "responses": {
          "200": {
            "description": "Current state",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "now": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "operator": {
                      "type": "object",
                      "nullable": true
                    },
                    "missions": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "outreach": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "discovery_runs": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "attention": {
                      "type": "object",
                      "properties": {
                        "needs_decision": {
                          "type": "integer"
                        },
                        "needs_feedback": {
                          "type": "integer"
                        },
                        "needs_review_date": {
                          "type": "integer"
                        },
                        "waiting_for_signal": {
                          "type": "integer"
                        },
                        "planned": {
                          "type": "integer"
                        },
                        "next_due_at": {
                          "type": "string",
                          "format": "date-time",
                          "nullable": true
                        },
                        "most_overdue_seconds": {
                          "type": "integer",
                          "nullable": true
                        }
                      }
                    },
                    "next_priorities": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/search": {
      "get": {
        "operationId": "search",
        "summary": "TOPIC SEARCH (1.30.0): \"what do we know about X\", with provenance. Full text inside this workspace over evidence (subject + content), actions (description plus outcome/interpretation text), decisions and canonical context, plus the existing name matching for people, organizations, projects and outreach.",
        "description": "Matching is Postgres full text: websearch_to_tsquery with the 'simple' configuration (business text is bilingual EN/ES, so nothing is stemmed), ranked by ts_rank, 20 hits per type. NUMBERS ARE TOKENIZED ON PUNCTUATION: '1,700' is indexed as the two tokens '1' and '700', and '1.700' likewise, so a query for 1700 will NOT match text written as 1,700 — search for 700, or for the number as it was written. `tsquery_used` reports exactly how your words were parsed — an empty tsquery means no searchable term, never 'no data'. A term that matches nothing returns zero hits in every type. Scope is always the workspace of the presented credential; there is no cross-workspace read. This is not semantic search: there are no embeddings and no external search service.",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "type",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "all",
                "people",
                "organizations",
                "actions",
                "projects",
                "outreach",
                "evidence",
                "decisions",
                "context"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Ranked results per type, with provenance on every evidence hit",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "query": {
                      "type": "string"
                    },
                    "tsquery_used": {
                      "type": "string",
                      "nullable": true
                    },
                    "workspace_id": {
                      "type": "string",
                      "nullable": true
                    },
                    "type": {
                      "type": "string"
                    },
                    "totals": {
                      "type": "object",
                      "additionalProperties": {
                        "type": "integer"
                      }
                    },
                    "results": {
                      "type": "object",
                      "properties": {
                        "evidence": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "string"
                              },
                              "subject": {
                                "type": "string"
                              },
                              "content": {
                                "type": "string",
                                "description": "truncated at 500 characters"
                              },
                              "content_truncated": {
                                "type": "boolean"
                              },
                              "kind": {
                                "type": "string"
                              },
                              "epistemic_status": {
                                "type": "string"
                              },
                              "status": {
                                "type": "string"
                              },
                              "stated_at": {
                                "type": "string",
                                "format": "date-time",
                                "nullable": true
                              },
                              "created_at": {
                                "type": "string",
                                "format": "date-time"
                              },
                              "source": {
                                "type": "object",
                                "properties": {
                                  "session_id": {
                                    "type": "string",
                                    "nullable": true
                                  },
                                  "session_title": {
                                    "type": "string",
                                    "nullable": true
                                  },
                                  "artifact_id": {
                                    "type": "string",
                                    "nullable": true
                                  },
                                  "artifact_title": {
                                    "type": "string",
                                    "nullable": true
                                  }
                                }
                              },
                              "contradicts": {
                                "type": "object",
                                "nullable": true
                              },
                              "contradicted_by": {
                                "type": "array",
                                "items": {
                                  "type": "object"
                                }
                              },
                              "supersedes": {
                                "type": "object",
                                "nullable": true
                              },
                              "superseded_by": {
                                "type": "array",
                                "items": {
                                  "type": "object"
                                }
                              }
                            }
                          }
                        },
                        "actions": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "id": {
                                "type": "string"
                              },
                              "description": {
                                "type": "string"
                              },
                              "status": {
                                "type": "string",
                                "nullable": true
                              },
                              "due_at": {
                                "type": "string",
                                "format": "date-time",
                                "nullable": true
                              },
                              "expected_signal_at": {
                                "type": "string",
                                "format": "date-time",
                                "nullable": true
                              },
                              "executed_at": {
                                "type": "string",
                                "format": "date-time",
                                "nullable": true
                              },
                              "latest_signal": {
                                "type": "string",
                                "nullable": true
                              },
                              "project_id": {
                                "type": "string",
                                "nullable": true
                              }
                            }
                          }
                        },
                        "decisions": {
                          "type": "array",
                          "items": {
                            "type": "object"
                          }
                        },
                        "context": {
                          "type": "array",
                          "items": {
                            "type": "object"
                          }
                        },
                        "people": {
                          "type": "array",
                          "items": {
                            "type": "object"
                          }
                        },
                        "organizations": {
                          "type": "array",
                          "items": {
                            "type": "object"
                          }
                        },
                        "projects": {
                          "type": "array",
                          "items": {
                            "type": "object"
                          }
                        },
                        "outreach": {
                          "type": "array",
                          "items": {
                            "type": "object"
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "invalid_parameter — unknown type; or unknown_parameter",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/prompts": {
      "get": {
        "operationId": "getPrompts",
        "summary": "Derived prompt queue with canonical timing on every action: needs_decision, needs_feedback (overdue), needs_review_date, waiting, planned. There is no grace period — an action is overdue the moment now >= due_at with no recorded signal. Resolved and closed actions are not returned.",
        "parameters": [
          {
            "name": "group",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "all",
                "needs_decision",
                "needs_feedback",
                "needs_review_date",
                "waiting",
                "planned"
              ]
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Queue with timing metadata",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "now": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "group": {
                      "type": "string"
                    },
                    "actions": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Action"
                      }
                    },
                    "groups": {
                      "type": "object",
                      "additionalProperties": {
                        "type": "array",
                        "items": {
                          "$ref": "#/components/schemas/Action"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/actions/{id}": {
      "get": {
        "operationId": "getAction",
        "summary": "Read one action with derived timing (including available_operations and their exact available_commands), plus its child actions.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Action with timing",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "now": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "owner": {
                      "$ref": "#/components/schemas/Owner"
                    },
                    "action": {
                      "$ref": "#/components/schemas/Action"
                    },
                    "children": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Action"
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/actions/{id}/execute": {
      "post": {
        "operationId": "markActionExecuted",
        "summary": "mark_executed: record that a planned action was actually performed. TEMPORAL CONTRACT — executed_at is when it happened in the real world, recorded_at is when Forbiz wrote it down. Supply executedAt for historical / reconciled work (e.g. an email sent yesterday); omit it for normal live execution and the server stamps now. recorded_at is server-owned and can never be supplied. Retrying with the same Idempotency-Key changes neither timestamp. Optionally set the first review date in the same call; without it the action lands in needs_review_date and the response carries warnings[] with code no_review_date. Idempotent — re-executing returns already_executed: true and leaves both timestamps intact.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Recommended on every mutating request. A replay with the same key is NOT applied twice: the server answers { ok, idempotent_replay: true, first_seen_at, status } describing the first attempt."
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "executedAt": {
                    "type": "string",
                    "format": "date-time",
                    "description": "Real-world occurrence time. Validated: must parse as ISO 8601, may not be more than 5 minutes in the future (clock skew) and may not be more than a year in the past. Alias: occurredAt. Omit for live execution."
                  },
                  "expectedSignalAt": {
                    "type": "string",
                    "format": "date-time"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Executed, with new timing state. `executed_at` is the real-world time, `recorded_at` the Forbiz write time, `backdated` true when a past executedAt was applied.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "owner": {
                      "$ref": "#/components/schemas/Owner"
                    },
                    "action": {
                      "$ref": "#/components/schemas/Action"
                    },
                    "already_executed": {
                      "type": "boolean"
                    },
                    "executed_at": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    },
                    "recorded_at": {
                      "type": "string",
                      "format": "date-time",
                      "nullable": true
                    },
                    "backdated": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "208": {
            "description": "Idempotent replay of a previous request with the same Idempotency-Key. Nothing was applied again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IdempotentReplay"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Loop already closed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Review date is not in the future, executedAt is out of the allowed range, or recordedAt was supplied by the caller",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/actions/{id}/postpone": {
      "post": {
        "operationId": "postponeAction",
        "summary": "postpone: move the review date of a waiting or overdue action into the future. Rejected when the action already has a signal or the loop is closed.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Recommended on every mutating request. A replay with the same key is NOT applied twice: the server answers { ok, idempotent_replay: true, first_seen_at, status } describing the first attempt."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "expectedSignalAt": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "reason": {
                    "type": "string"
                  }
                },
                "required": [
                  "expectedSignalAt"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "New timing state",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "owner": {
                      "$ref": "#/components/schemas/Owner"
                    },
                    "action": {
                      "$ref": "#/components/schemas/Action"
                    }
                  }
                }
              }
            }
          },
          "208": {
            "description": "Idempotent replay of a previous request with the same Idempotency-Key. Nothing was applied again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IdempotentReplay"
                }
              }
            }
          },
          "409": {
            "description": "Signal already recorded or loop closed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "Review date is not in the future",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/actions/{id}/schedule-check": {
      "post": {
        "operationId": "scheduleActionCheck",
        "summary": "set_review_date: set the first review date on an executed action that has none (needs_review_date). Same canonical operation as postpone.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Recommended on every mutating request. A replay with the same key is NOT applied twice: the server answers { ok, idempotent_replay: true, first_seen_at, status } describing the first attempt."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "expectedSignalAt": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "reason": {
                    "type": "string"
                  }
                },
                "required": [
                  "expectedSignalAt"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "New timing state",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "owner": {
                      "$ref": "#/components/schemas/Owner"
                    },
                    "action": {
                      "$ref": "#/components/schemas/Action"
                    }
                  }
                }
              }
            }
          },
          "208": {
            "description": "Idempotent replay of a previous request with the same Idempotency-Key. Nothing was applied again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IdempotentReplay"
                }
              }
            }
          },
          "422": {
            "description": "Date not in the future",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/actions/{id}/interpret-signal": {
      "post": {
        "operationId": "interpretSignal",
        "summary": "interpret_signal: conclude what an already recorded signal MEANS. Requires actual_signal to exist and the action to be unresolved (needs_decision). Never overwrites actual_signal or actual_signal_at, never creates a child action, never resolves the loop — the action normally stays in needs_decision until create_next_action and/or close_loop.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Recommended on every mutating request. A replay with the same key is NOT applied twice: the server answers { ok, idempotent_replay: true, first_seen_at, status } describing the first attempt."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "outcomeStatus": {
                    "type": "string",
                    "enum": [
                      "expected",
                      "partial",
                      "unexpected",
                      "deferred"
                    ]
                  },
                  "learningNote": {
                    "type": "string"
                  },
                  "resolve": {
                    "type": "boolean",
                    "enum": [
                      false
                    ],
                    "description": "Must be false or omitted. Resolving is only possible through close-loop."
                  }
                },
                "required": [
                  "outcomeStatus"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Interpretation stored; signal untouched",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "owner": {
                      "$ref": "#/components/schemas/Owner"
                    },
                    "action": {
                      "$ref": "#/components/schemas/Action"
                    },
                    "note": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "208": {
            "description": "Idempotent replay of a previous request with the same Idempotency-Key. Nothing was applied again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IdempotentReplay"
                }
              }
            }
          },
          "400": {
            "description": "Invalid outcomeStatus",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "No signal recorded yet, or loop already closed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "resolve: true is not allowed here — use close-loop",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/actions/{id}/close-loop": {
      "post": {
        "operationId": "closeActionLoop",
        "summary": "close_loop: close a loop that DID produce a signal. Requires actual_signal. Preserves actual_signal and actual_signal_at, stores the interpretation, sets lifecycle_status to resolved, emits an action_resolved history event and removes the action from active prompt groups. Use close-without-signal only when no signal ever arrived.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Recommended on every mutating request. A replay with the same key is NOT applied twice: the server answers { ok, idempotent_replay: true, first_seen_at, status } describing the first attempt."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "outcomeStatus": {
                    "type": "string",
                    "enum": [
                      "expected",
                      "partial",
                      "unexpected",
                      "deferred"
                    ]
                  },
                  "learningNote": {
                    "type": "string"
                  }
                },
                "required": [
                  "outcomeStatus"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Resolved",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "owner": {
                      "$ref": "#/components/schemas/Owner"
                    },
                    "action": {
                      "$ref": "#/components/schemas/Action"
                    },
                    "already_resolved": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "208": {
            "description": "Idempotent replay of a previous request with the same Idempotency-Key. Nothing was applied again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IdempotentReplay"
                }
              }
            }
          },
          "400": {
            "description": "Invalid outcomeStatus",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "No signal recorded (use close-without-signal), or loop already closed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/actions/{id}/close-without-signal": {
      "post": {
        "operationId": "closeActionWithoutSignal",
        "summary": "close_without_signal: close a loop where no signal ever arrived. Rejected with 409 when actual_signal exists — use close-loop in that case. Distinct from resolving an action.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Recommended on every mutating request. A replay with the same key is NOT applied twice: the server answers { ok, idempotent_replay: true, first_seen_at, status } describing the first attempt."
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "reason": {
                    "type": "string"
                  },
                  "learningNote": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Closed without signal",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "owner": {
                      "$ref": "#/components/schemas/Owner"
                    },
                    "action": {
                      "$ref": "#/components/schemas/Action"
                    },
                    "already_closed": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "208": {
            "description": "Idempotent replay of a previous request with the same Idempotency-Key. Nothing was applied again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IdempotentReplay"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "A signal exists (use close-loop), or loop already closed",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/context/sessions": {
      "get": {
        "operationId": "listContextSessions",
        "summary": "List context sessions (research/conversation episodes) for this workspace.",
        "responses": {
          "200": {
            "description": "Sessions",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "openContextSession",
        "summary": "Open a context session. Never promotes anything to truth.",
        "description": "A session groups artifacts and evidence gathered in one episode. Capability: evidence:append. Sessions and their contents are append-only: nothing here becomes workspace truth until it is confirmed.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "purpose": {
                    "type": "string"
                  },
                  "topic": {
                    "type": "string"
                  },
                  "channel": {
                    "type": "string"
                  },
                  "metadata": {
                    "type": "object"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Session opened",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "208": {
            "description": "Idempotent replay of a previous request with the same Idempotency-Key. Nothing was applied again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IdempotentReplay"
                }
              }
            }
          }
        }
      }
    },
    "/context/sessions/{id}/close": {
      "post": {
        "operationId": "closeContextSession",
        "summary": "Close a context session with an optional summary.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Recommended on every mutating request. A replay with the same key is NOT applied twice: the server answers { ok, idempotent_replay: true, first_seen_at, status } describing the first attempt."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "summary": {
                    "type": "string"
                  },
                  "unresolvedQuestions": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Session closed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/context/artifacts": {
      "get": {
        "operationId": "listSourceArtifacts",
        "summary": "List source artifacts (where a claim came from).",
        "description": "sessionId filters strictly. A sessionId that does not exist in this workspace returns 404 session_not_found — it never falls back to the full collection.",
        "parameters": [
          {
            "name": "sessionId",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Artifacts",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "source_artifacts": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "artifact_type": {
                            "type": "string"
                          },
                          "title": {
                            "type": "string"
                          },
                          "excerpt": {
                            "type": "string",
                            "nullable": true,
                            "description": "First excerpt_limit_bytes characters of the stored source text. Null when nothing was stored."
                          },
                          "excerpt_bytes": {
                            "type": "integer"
                          },
                          "truncated": {
                            "type": "boolean"
                          },
                          "full_bytes": {
                            "type": "integer",
                            "description": "Present only when truncated."
                          }
                        }
                      }
                    },
                    "excerpt_limit_bytes": {
                      "type": "integer"
                    },
                    "filtered_by": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Session not found in this workspace",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "recordSourceArtifact",
        "summary": "Record a source artifact: a document, page, message or transcript a claim came from.",
        "description": "Capability: evidence:append. Immutable once written. `kind` is persisted faithfully and is never coerced; an unsupported kind returns 400 invalid_artifact_type. `sessionId` is persisted as the artifact's session link and echoed back; an unresolvable sessionId fails the write instead of being dropped.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "sessionId": {
                    "type": "string"
                  },
                  "kind": {
                    "type": "string",
                    "enum": [
                      "conversation",
                      "document",
                      "email",
                      "transcript",
                      "webpage",
                      "spreadsheet",
                      "image",
                      "note",
                      "dataset",
                      "other"
                    ]
                  },
                  "title": {
                    "type": "string"
                  },
                  "uri": {
                    "type": "string"
                  },
                  "excerpt": {
                    "type": "string"
                  },
                  "capturedAt": {
                    "type": "string"
                  },
                  "metadata": {
                    "type": "object"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Artifact recorded",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "208": {
            "description": "Idempotent replay of a previous request with the same Idempotency-Key. Nothing was applied again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IdempotentReplay"
                }
              }
            }
          },
          "400": {
            "description": "Invalid or unrecognised field",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Session not found in this workspace",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/context/evidence": {
      "get": {
        "operationId": "listEvidenceItems",
        "summary": "List evidence items, including contradictions and exclusions. Filters are honest (1.30.0): `q` is full text over subject AND content, `subject` matches by CONTAINS (case-insensitive), `subject_exact` is the old exact equality.",
        "description": "Filters are strict and always applied; the response echoes them in `filtered_by`, including `tsquery_used` and `subject_match`. `q` uses websearch_to_tsquery with the 'simple' configuration, the same matching as GET /search?type=evidence. Every item carries its provenance (`source.session_id`, `source.session_title`, `source.artifact_id`, `source.artifact_title`) and its epistemic relations (`contradicts`, `contradicted_by`, `supersedes`, `superseded_by`), so a claim and its contradiction each point at the other. A sessionId belonging to another workspace, or one that does not exist, returns 404 session_not_found — never another session's evidence. A query parameter this endpoint does not read is refused with 400 unknown_parameter.",
        "parameters": [
          {
            "name": "sessionId",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "status",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "q",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Full text over subject + content."
          },
          {
            "name": "subject",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Case-insensitive CONTAINS."
          },
          {
            "name": "subject_exact",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Exact subject equality (the pre-1.30.0 behaviour of `subject`)."
          },
          {
            "name": "materialOnly",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "1 = material to decision only."
          }
        ],
        "responses": {
          "200": {
            "description": "Evidence",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "404": {
            "description": "Session not found in this workspace",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "appendEvidence",
        "summary": "Append evidence items. Claims, contradictions and exclusions are all preserved.",
        "description": "Capability: evidence:append. Evidence is epistemic history, NOT truth: nothing is auto-promoted into workspace context or policy, and a contradiction never overwrites the claim it contradicts. Relationship fields are authoritative: `sessionId` (batch-level or per item) and `artifactId` are persisted and echoed in `persisted_links`. An unresolvable id fails the request; an unrecognised *-session/-artifact field returns 400 unknown_relationship_field rather than being silently ignored.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "sessionId": {
                    "type": "string"
                  },
                  "artifactId": {
                    "type": "string"
                  },
                  "items": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "kind": {
                          "type": "string",
                          "enum": [
                            "statement",
                            "claim",
                            "inference",
                            "question",
                            "unknown",
                            "human_answer",
                            "confirmation",
                            "correction",
                            "contradiction",
                            "deliberate_exclusion",
                            "supersession",
                            "decision",
                            "policy_statement",
                            "hypothesis",
                            "resolution"
                          ]
                        },
                        "subject": {
                          "type": "string"
                        },
                        "content": {
                          "type": "string"
                        },
                        "epistemicStatus": {
                          "type": "string",
                          "enum": [
                            "claimed",
                            "corroborated",
                            "contradicted",
                            "excluded",
                            "confirmed",
                            "observed",
                            "sourced",
                            "derived",
                            "current_decision",
                            "hypothesis",
                            "unknown",
                            "deliberately_unresolved",
                            "superseded"
                          ],
                          "description": "Aliases are accepted and normalized, not rejected: claimed -> sourced, corroborated -> confirmed, excluded -> deliberately_unresolved. The response reports what was stored in normalized[].epistemic_status with normalized[].normalized_from carrying the value you sent (null when nothing was rewritten), and epistemic_status_aliases lists the whole mapping."
                        },
                        "contradictsEvidenceId": {
                          "type": "string"
                        },
                        "excludedReason": {
                          "type": "string"
                        },
                        "sessionId": {
                          "type": "string"
                        },
                        "artifactId": {
                          "type": "string"
                        },
                        "confidence": {
                          "type": "number"
                        },
                        "materialToDecision": {
                          "type": "boolean"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Evidence appended. Epistemic standing is derived from the stored row.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "promoted": {
                      "type": "boolean",
                      "description": "Derived from the STORED row, never from the route: true only when the stored value really is confirmed."
                    },
                    "canonical": {
                      "type": "boolean",
                      "description": "True only when the stored row is canonical workspace truth."
                    },
                    "stored_status": {
                      "type": "string",
                      "description": "The confirmation status actually stored (for example 'proposed' or 'confirmed')."
                    },
                    "needs_confirmation": {
                      "type": "boolean",
                      "description": "True when a human still has to confirm the stored row."
                    }
                  }
                }
              }
            }
          },
          "208": {
            "description": "Idempotent replay of a previous request with the same Idempotency-Key. Nothing was applied again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IdempotentReplay"
                }
              }
            }
          },
          "400": {
            "description": "Invalid or unrecognised field",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Session or artifact not found in this workspace",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/public/billing/offer": {
      "servers": [
        {
          "url": "https://forbiz.io"
        }
      ],
      "get": {
        "operationId": "readBillingOffer",
        "summary": "What can be bought, for how much, for how long. No credential, no workspace named.",
        "description": "Needs NO credential. Reachable at https://forbiz.io/api/public/billing/offer — it is NOT under the agent base. States the plan, the period length in days, the amount, the currency, renews false, what it unlocks and how it can be paid. refund_policy and terms read \"not_published\" while none is decided.",
        "security": [],
        "responses": {
          "200": {
            "description": "The offer",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "options": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "plan": {
                            "type": "string"
                          },
                          "display_name": {
                            "type": "string"
                          },
                          "kind": {
                            "type": "string",
                            "enum": [
                              "one_period"
                            ]
                          },
                          "period_days": {
                            "type": "integer"
                          },
                          "amount": {
                            "type": "number"
                          },
                          "amount_cents": {
                            "type": "integer"
                          },
                          "currency": {
                            "type": "string"
                          },
                          "renews": {
                            "type": "boolean"
                          },
                          "unlocks": {
                            "type": "array",
                            "items": {
                              "type": "object"
                            }
                          },
                          "payment": {
                            "type": "array",
                            "items": {
                              "type": "string"
                            }
                          },
                          "refund_policy": {
                            "type": "string"
                          },
                          "terms": {
                            "type": "string"
                          }
                        }
                      }
                    },
                    "note": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/purchases": {
      "get": {
        "operationId": "listPurchases",
        "summary": "Purchases made for this workspace, newest first, with their mandate check.",
        "description": "Any active identity of the workspace may read this. Receipts only; reading never spends.",
        "responses": {
          "200": {
            "description": "Purchases",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "buyOnePeriod",
        "summary": "Buy ONE period of continuity under a mandate the instrument proves. Nothing renews.",
        "description": "THE PAYMENT TOKEN IS THE SPENDING AUTHORITY (1.50.0): any active identity of the workspace may call this — including a provisional credential on an unclaimed workspace — and no capability has to be granted first, because the human who granted the token's mandate already delegated the spending. Idempotency-Key is REQUIRED: without it the request is refused 400 IDEMPOTENCY_KEY_REQUIRED. The mandate (limit, currency, expiry) is read from the instrument at Stripe and never from this body. Omit instrument to reserve the purchase and pay it later with POST /purchases/{id}/pay. ACTIVATION IS NEVER WRITTEN BY THIS ROUTE: a successful charge answers 200 with status payment_received and activated false, and the signature-verified payment_intent.succeeded webhook activates the period a moment later — read GET /purchases/{id} to see it active. Refusals: ALREADY_COVERED when the period is already paid for, PURCHASE_ALREADY_OPEN when one purchase is already open, MANDATE_INSUFFICIENT with the recorded verdict when the instrument does not carry the amount, and 503 MANDATE_UNVERIFIED with retry_after_seconds when Stripe could not be asked at all (nothing is charged and the purchase stays open). Every refusal carries missing as a list of { code, detail } with code one of no_instrument, over_limit, wrong_currency, expired, deactivated, unverified.",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Recommended on every mutating request. A replay with the same key is NOT applied twice: the server answers { ok, idempotent_replay: true, first_seen_at, status } describing the first attempt."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "plan": {
                    "type": "string"
                  },
                  "instrument": {
                    "type": "object",
                    "properties": {
                      "kind": {
                        "type": "string",
                        "enum": [
                          "shared_payment_token"
                        ]
                      },
                      "id": {
                        "type": "string"
                      }
                    }
                  }
                },
                "required": [
                  "plan"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Payment received; the webhook activates the period",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "201": {
            "description": "Purchase created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "208": {
            "description": "Idempotent replay of a previous request with the same Idempotency-Key. Nothing was applied again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IdempotentReplay"
                }
              }
            }
          },
          "400": {
            "description": "Idempotency-Key missing, or the body is not usable",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "The instrument does not carry this amount",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Already covered, or a purchase is already open",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "The mandate could not be read; nothing was charged",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/purchases/{id}": {
      "get": {
        "operationId": "readPurchase",
        "summary": "One purchase, its status, its period and its mandate check.",
        "description": "While the purchase is open, authorization_options carries two ways to pay it: a shared payment token, and checkout_link_for_human — a Stripe Checkout Session in PAYMENT mode that pays THIS purchase only, needs no signed-in session, creates no subscription and is reused while it stays open.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Purchase",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/purchases/{id}/pay": {
      "post": {
        "operationId": "payPurchase",
        "summary": "Pay an open purchase with a shared payment token.",
        "description": "Callable by any active identity of the workspace. The mandate is read from the instrument at Stripe. A successful charge answers status payment_received with activated false; the webhook activates the period. Paying an already active purchase changes nothing.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Recommended on every mutating request. A replay with the same key is NOT applied twice: the server answers { ok, idempotent_replay: true, first_seen_at, status } describing the first attempt."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "instrument": {
                    "type": "object",
                    "properties": {
                      "kind": {
                        "type": "string"
                      },
                      "id": {
                        "type": "string"
                      }
                    }
                  }
                },
                "required": [
                  "instrument"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Payment received; the webhook activates the period",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "402": {
            "description": "The instrument does not carry this amount",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Not open, or already active",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "The mandate could not be read; nothing was charged",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/policies": {
      "get": {
        "operationId": "listPolicies",
        "summary": "List business policies with their authority and enforcement status.",
        "description": "status is one of PROPOSED (not enforced) | ACTIVE (enforced) | DISABLED | EXPIRED | REJECTED. Only ACTIVE policies affect execution.",
        "responses": {
          "200": {
            "description": "Policies",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "proposePolicy",
        "summary": "Propose a business policy. An ordinary agent can never activate a safety gate.",
        "description": "Capability: policies:propose. A DENY or REQUIRE_APPROVAL policy submitted by an ordinary read+write credential is stored as PROPOSED and is NOT enforced; it becomes ACTIVE only after a human workspace owner/admin confirms it in Forbiz, or when the credential holds the delegated policies:manage capability. ADVISORY policies may be created directly. Passing a confirmedBy name is provenance, not authority: it never activates anything.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "policyKey": {
                    "type": "string"
                  },
                  "effect": {
                    "type": "string",
                    "enum": [
                      "DENY",
                      "REQUIRE_APPROVAL",
                      "ALLOW",
                      "ADVISORY"
                    ]
                  },
                  "appliesTo": {
                    "type": "string"
                  },
                  "humanDescription": {
                    "type": "string"
                  },
                  "statedBy": {
                    "type": "string"
                  },
                  "confirmedBy": {
                    "type": "string"
                  },
                  "effectiveFrom": {
                    "type": "string"
                  },
                  "expiresAt": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Policy stored (check status: PROPOSED means not enforced)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "208": {
            "description": "Idempotent replay of a previous request with the same Idempotency-Key. Nothing was applied again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IdempotentReplay"
                }
              }
            }
          },
          "403": {
            "description": "Activation requires human or delegated authority",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/policies/{id}/retire": {
      "post": {
        "operationId": "retirePolicy",
        "summary": "Retire a policy. Relaxing a human-confirmed safety gate is refused.",
        "description": "Capability: policies:manage. A DENY or REQUIRE_APPROVAL policy confirmed by a human can only be retired by a human owner/admin in Forbiz — an agent attempt returns 403.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Recommended on every mutating request. A replay with the same key is NOT applied twice: the server answers { ok, idempotent_replay: true, first_seen_at, status } describing the first attempt."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "reason": {
                    "type": "string"
                  },
                  "status": {
                    "type": "string",
                    "enum": [
                      "DISABLED",
                      "EXPIRED"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Policy retired",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "403": {
            "description": "Human authority required",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/decisions/recommendation": {
      "get": {
        "operationId": "getDecisionRecommendation",
        "summary": "Evaluate the deterministic decision engine. Side-effect free: never writes a decision event.",
        "description": "Returns one attention mode — ACT | RECOVER | VALIDATE | VERIFY | CHALLENGE | WAIT | ESCALATE — with the reasons and the inputs it used, plus engine_version and a state_fingerprint. Repeated GETs write nothing, even when the recommendation changes because the underlying state changed. An ACTIVE DENY or REQUIRE_APPROVAL policy takes precedence over every other rule. To adopt or reject a recommendation, POST /decisions.",
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Optional. Narrow the SAME evaluation to one action id: the response then carries only that action's allocation as `primary`, plus subject_action_id and narrowed: true. Omit it for the workspace-wide recommendation."
          },
          {
            "name": "actionId",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Accepted alias of id (action_id is also accepted)."
          },
          {
            "name": "cursor",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Accepted for pagination symmetry with other reads; the evaluation is not paginated."
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer"
            },
            "description": "Accepted; the evaluation returns every allocation it computed."
          },
          {
            "name": "offset",
            "in": "query",
            "schema": {
              "type": "integer"
            },
            "description": "Accepted; see limit."
          }
        ],
        "responses": {
          "200": {
            "description": "Recommendation",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          }
        }
      }
    },
    "/outreach": {
      "get": {
        "operationId": "getOutreach",
        "summary": "List outreach, or one outreach with its targets when id is given.",
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Outreach",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "outreach": {
                      "description": "Array when listing, single object when id is given."
                    },
                    "targets": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createOutreach",
        "summary": "Create one Outreach (campaign container) in draft.",
        "description": "REUSE BEFORE CREATE: an Outreach is a commercial thread. Before creating one, call GET /outreach (or GET /search?type=outreach) and reuse the existing Outreach whenever it already represents the same thread — same audience, same angle, same reason. Create a new one only when no existing Outreach represents it.\n\nThis creates the CONTAINER ONLY. No targets, executions, actions, messages or sends are created implicitly. Attach targets afterwards with POST /targets using the returned id as campaignId, and record real sends with POST /outreach/{id}/executions.\n\nThe server always sets the state: outreach_status = draft, status = active, ownership = the credential's user, workspace = the credential's workspace, created_by = the credential's user. An Outreach can never be created in ready_to_send, waiting_for_responses or executed state — those are facts derived from recorded executions, not declarable at creation.",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Recommended on every mutating request. A replay with the same key is NOT applied twice: the server answers { ok, idempotent_replay: true, first_seen_at, status } describing the first attempt."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Required. The commercial thread this Outreach represents."
                  },
                  "reasonNow": {
                    "type": "string",
                    "description": "Why this Outreach exists now."
                  },
                  "expectedSignals": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Free-form strings. Not validated against a fixed list."
                  },
                  "funnelId": {
                    "type": "string",
                    "nullable": true,
                    "description": "Optional. Must belong to the caller when given."
                  },
                  "channelMode": {
                    "type": "string",
                    "description": "Optional. Defaults to copy_paste, matching the UI."
                  },
                  "vertical": {
                    "type": "string"
                  },
                  "intendedAudience": {
                    "type": "string"
                  },
                  "productAngle": {
                    "type": "string"
                  },
                  "messageAngle": {
                    "type": "string"
                  },
                  "callToAction": {
                    "type": "string"
                  },
                  "senderName": {
                    "type": "string"
                  },
                  "senderEmail": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Outreach created in draft. Container only.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "outreach": {
                      "type": "object"
                    },
                    "owner": {
                      "$ref": "#/components/schemas/Owner"
                    },
                    "created": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "208": {
            "description": "Idempotent replay of a previous request with the same Idempotency-Key. Nothing was applied again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IdempotentReplay"
                }
              }
            }
          },
          "400": {
            "description": "Missing name, unknown funnel, or a forbidden creation state",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Credential is not bound to a workspace",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/outreach/{id}/executions": {
      "get": {
        "operationId": "getOutreachExecutions",
        "summary": "Actual executions recorded for an outreach, with per-target send rows and factual event counts.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Executions",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "outreach_id": {
                      "type": "string"
                    },
                    "executions": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "recordOutreachExecution",
        "summary": "Record one verified real-world execution of a prepared outreach, or a factual CORRECTION of an earlier one. Never resolves or signals an Action. Absence of a bounce is not delivery proof.",
        "description": "Two distinct operations share this endpoint.\n\nNORMAL EXECUTION (no supersedesExecutionId): identity is externalSource + externalBatchId. A repeat answers 208 with the already recorded execution.\n\nCORRECTION (supersedesExecutionId present): this is NOT a replay of the superseded row, even with the SAME externalBatchId — a correction fixes what Forbiz knows about the same real-world batch and is never a second send. It requires correctionNote, and its external execution identity must match the record being corrected. Its idempotency identity is the correction operation: outreach + superseded execution + Idempotency-Key (falling back to a hash of the correction). Retrying the same correction answers 208 with the existing correction, never the original. History keeps the superseded row; effective state, reconciliation and preflight all use ONE canonical selector: a record superseded by a later one is history, and of the records sharing one external execution identity only the newest is effective. In a chain original -> correction 1 -> correction 2 only the current effective record contributes to aggregates, so a chain can never inflate totals. Callers never supply any correction identity field themselves: the correction operation key is derived internally from the outreach, the superseded execution and the Idempotency-Key.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Recommended on every mutating request. A replay with the same key is NOT applied twice: the server answers { ok, idempotent_replay: true, first_seen_at, status } describing the first attempt."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "channel": {
                    "type": "string",
                    "enum": [
                      "email",
                      "phone",
                      "linkedin",
                      "meeting",
                      "other"
                    ]
                  },
                  "executedAt": {
                    "type": "string",
                    "format": "date-time",
                    "description": "Required. When the send actually OCCURRED (alias: occurredAt). When Forbiz recorded it is stored separately as recorded_at and is never caller-supplied."
                  },
                  "executionType": {
                    "type": "string",
                    "enum": [
                      "initial",
                      "follow_up",
                      "resend",
                      "duplicate",
                      "unknown",
                      "other"
                    ],
                    "description": "Classification of this execution. Defaults to 'unknown' — never guessed by Forbiz."
                  },
                  "executionSource": {
                    "type": "string"
                  },
                  "externalSource": {
                    "type": "string"
                  },
                  "externalBatchId": {
                    "type": "string",
                    "description": "Canonical external execution identity together with externalSource: one NON-CORRECTION execution per outreach + (source:batch id). A replay answers 208 with the already recorded execution. A correction repeats the SAME value on purpose."
                  },
                  "supersedesExecutionId": {
                    "type": "string",
                    "description": "Correction: records a NEW execution row that supersedes an earlier one on this outreach. Bypasses external-batch replay detection; requires correctionNote and a matching external execution identity. History is never rewritten or deleted, and no second real-world send is implied."
                  },
                  "correctionNote": {
                    "type": "string",
                    "description": "Required when supersedesExecutionId is present. State factually what was wrong in the superseded record."
                  },
                  "automationMode": {
                    "type": "string",
                    "enum": [
                      "autonomous",
                      "manual"
                    ],
                    "description": "Defaults to manual (human-approved). 'autonomous' means no human approved this specific send, and it MUST pass the hard autonomous send budget: one slot per confirmed recipient is reserved atomically before the execution row is written. Exhausted budget answers 429 with code AUTONOMOUS_SEND_LIMIT_REACHED and execution_allowed=false plus limit/used/remaining/window_end. Retrying the SAME logical send (same Idempotency-Key, or same external source + batch id) reuses the original reservation and consumes nothing extra; a genuinely new send consumes a new slot. Corrections (supersedesExecutionId) never consume budget because they are not a second real-world send. A granted slot is not released automatically if the external send later fails — releasing on failure would allow an unbounded retry loop."
                  },
                  "sender": {
                    "type": "string"
                  },
                  "recipientCount": {
                    "type": "integer"
                  },
                  "actualSubject": {
                    "type": "string"
                  },
                  "actualBodySnapshot": {
                    "type": "string"
                  },
                  "preparedSubjectSnapshot": {
                    "type": "string"
                  },
                  "preparedBodySnapshot": {
                    "type": "string"
                  },
                  "preparedTemplateId": {
                    "type": "string"
                  },
                  "executionIntegrity": {
                    "type": "string",
                    "enum": [
                      "matched",
                      "modified",
                      "unknown"
                    ]
                  },
                  "integrityNote": {
                    "type": "string",
                    "description": "Required when executionIntegrity is 'modified'."
                  },
                  "targets": {
                    "type": "array",
                    "description": "Each item MUST identify its recipient by campaignTargetId OR by recipientAddress (at least one; both is fine). A recipientAddress-only row is first-class send evidence: it is stored as an execution target row with campaign_target_id null, it counts in effective state (counts.sent_confirmed_targets counts confirmed RECIPIENTS, identified by target id or normalized address) and in global recipient safety (a confirmed address-only send inside the duplicate window produces global_duplicate_recent_send and appears in prior_executions). Address-only rows do NOT create campaign_target_events and do NOT update campaign_targets.status, because those require a target; bounce or reply events for such a recipient need a campaign target or a person first. Addresses are compared on their normalized form (trimmed, lowercased); two rows for the same address in one execution collapse into one. Validation runs BEFORE any write: if an item carries neither a valid uuid campaignTargetId nor a usable recipientAddress, the call answers 400 with { error: \"targets[<i>]: campaignTargetId or recipientAddress is required\", invalid_targets: [{ index, reason }] } and NOTHING is written — no execution header is left behind. The literal string \"undefined\" is treated as an absent value. The response reports target_execution_rows and address_only_rows, plus sent_events: { attempted, written, error }. sent_events describes the per-recipient 'sent' event ledger write for confirmed id rows: it is idempotent per (campaign_target_id, external_source, external_message_id, event_type), and a failure is REPORTED, never fatal — the execution row and its recipient rows are already valid facts, so the call still answers 201 with sent_events.error set and the failure logged server-side.",
                    "items": {
                      "type": "object",
                      "properties": {
                        "campaignTargetId": {
                          "type": "string",
                          "description": "Optional. A campaign target uuid belonging to this outreach. Required only when recipientAddress is absent. A non-uuid value is rejected with 400."
                        },
                        "recipientAddress": {
                          "type": "string",
                          "description": "Optional. The real recipient address. Required only when campaignTargetId is absent; then this row is address-only send evidence."
                        },
                        "sentAt": {
                          "type": "string",
                          "format": "date-time"
                        },
                        "externalMessageId": {
                          "type": "string"
                        },
                        "externalThreadId": {
                          "type": "string"
                        },
                        "executionStatus": {
                          "type": "string",
                          "enum": [
                            "sent_confirmed",
                            "send_possible_unconfirmed",
                            "no_send_evidence",
                            "not_contactable"
                          ]
                        },
                        "deliveryStatus": {
                          "type": "string",
                          "enum": [
                            "unknown",
                            "no_bounce_observed",
                            "hard_bounce",
                            "soft_bounce",
                            "delivery_confirmed"
                          ]
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Execution or correction recorded",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "execution": {
                      "type": "object"
                    },
                    "is_correction": {
                      "type": "boolean"
                    },
                    "supersedes_execution_id": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "208": {
            "description": "Idempotent replay of a previous request with the same Idempotency-Key. Nothing was applied again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IdempotentReplay"
                }
              }
            }
          },
          "400": {
            "description": "Validation error: targets outside this outreach, missing correctionNote, unknown supersedesExecutionId, or external execution identity that does not match the corrected record",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/outreach/{id}/reconciliation": {
      "get": {
        "operationId": "getOutreachReconciliation",
        "summary": "Derived planned-vs-actual-vs-result summary for one outreach. All aggregate counts are computed from the EFFECTIVE execution set and the event ledger, using the same canonical selector as /effective-state, so the two always agree.",
        "description": "CANONICAL AGGREGATION RULE: an execution that a later record supersedes is history, and of the rows sharing one external execution identity only the newest is effective. Only effective executions and their target-level observations contribute to current totals (sent_confirmed, not_contactable, delivery statuses, bounce counts derived from execution-target rows, recipient counts, execution summary metrics). A correction is a correction to what Forbiz knows about the same real-world batch, never a second send, so a supersession chain never inflates totals. History is never deleted: superseded and earlier corrected records remain readable via GET /outreach/{id}/executions, where each record carries is_effective, and duplicate_execution_rows_ignored reports how many records were excluded from current truth.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Reconciliation",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "outreach": {
                      "type": "object"
                    },
                    "summary": {
                      "type": "object"
                    },
                    "executions": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/outreach/{id}/effective-state": {
      "get": {
        "operationId": "getOutreachEffectiveState",
        "summary": "TRUTH GATE. Stored outreach_status (a human workflow marker, possibly stale) next to the effective state derived only from recorded executions and the event ledger, plus an explicit mismatch reason.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Effective state",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "stored_status": {
                      "type": "string"
                    },
                    "effective_state": {
                      "type": "string",
                      "enum": [
                        "prepared_not_executed",
                        "partially_executed",
                        "executed_awaiting_signals",
                        "results_received"
                      ]
                    },
                    "state_mismatch": {
                      "type": "boolean"
                    },
                    "mismatch_reason": {
                      "type": "string"
                    },
                    "counts": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/outreach/{id}/preflight": {
      "get": {
        "operationId": "getOutreachSendPreflight",
        "summary": "SEND SAFETY PREFLIGHT (CANONICAL). Read-only factual check before any send. eligible_targets is the FINAL safe cohort: it already applies BOTH outreach-local safety and GLOBAL RECIPIENT SAFETY across every Outreach in the workspace, resolved by normalized email. You never need to call another endpoint or union blockers yourself. Per recipient, local_blockers and global_blockers are exposed for diagnostic transparency only. Global blocker codes: global_hard_bounce, global_unsubscribe, global_active_human_conversation, global_meaningful_reply_requires_individual_handling, global_duplicate_recent_send, global_existing_waiting_loop, global_suppression, global_identity_ambiguity. A hard bounce or unsubscribe recorded in ANY Outreach is workspace-level address evidence and blocks reuse elsewhere; an active human conversation discovered in another Outreach blocks generic campaign contact. Identity is never fuzzily merged: an ambiguous address is blocked, not merged. The response also reports autonomous_send_budget, which is INFORMATIONAL only — the budget is enforced transactionally when an execution is recorded. Forbiz never sends; this call changes nothing.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "externalBatchId",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Optional. Checks whether this exact external batch was already recorded for this outreach."
          },
          {
            "name": "externalSource",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sendIntent",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "initial",
                "follow_up"
              ]
            },
            "description": "Defaults to initial. follow_up changes what counts as a blocker: a prior confirmed send is EXPECTED instead of blocking, but recipients with no confirmed prior send, an already recorded follow-up, a hard bounce, an unsubscribe or a meaningful human response are still blocked. Follow-up intent is never inferred by the server."
          },
          {
            "name": "previousExecutionId",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Required for sendIntent=follow_up unless previousActionId is given. Must reference a recorded execution on this outreach; a follow_up label alone is refused."
          },
          {
            "name": "previousActionId",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "followUpStep",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Optional label of the follow-up step, used to block repeating the same follow-up."
          }
        ],
        "responses": {
          "200": {
            "description": "Preflight",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "effective_state": {
                      "type": "string"
                    },
                    "safe_to_send": {
                      "type": "boolean"
                    },
                    "external_batch_already_recorded": {
                      "type": "boolean"
                    },
                    "send_intent": {
                      "type": "string",
                      "enum": [
                        "initial",
                        "follow_up"
                      ]
                    },
                    "follow_up_grounding": {
                      "type": "object",
                      "nullable": true
                    },
                    "eligible_recipient_count": {
                      "type": "integer"
                    },
                    "blocked_recipient_count": {
                      "type": "integer"
                    },
                    "warning_recipient_count": {
                      "type": "integer"
                    },
                    "blockers": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "global_recipient_safety": {
                      "type": "object",
                      "properties": {
                        "applied": {
                          "type": "boolean"
                        },
                        "scope": {
                          "type": "string"
                        },
                        "local_only_eligible_count": {
                          "type": "integer",
                          "description": "How many recipients outreach-local safety alone would have allowed. Diagnostic."
                        },
                        "globally_blocked_count": {
                          "type": "integer"
                        },
                        "additionally_blocked_by_global_safety": {
                          "type": "array",
                          "items": {
                            "type": "object"
                          }
                        },
                        "blockers_by_code": {
                          "type": "object"
                        }
                      }
                    },
                    "autonomous_send_budget": {
                      "$ref": "#/components/schemas/AutonomousSendBudget"
                    },
                    "eligible_targets": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "sendable_count": {
                      "type": "integer",
                      "description": "Legacy alias of eligible_recipient_count."
                    },
                    "blocked_count": {
                      "type": "integer"
                    },
                    "prevention": {
                      "type": "object"
                    },
                    "sendable_targets": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "blocked_targets": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/actions/{id}/context": {
      "get": {
        "operationId": "getActionContext",
        "summary": "WHY THIS NOW. The stored context behind one action: stated reason, expected signal, parent outcome, full chain, prior recorded signals on the same relationship, the outreach's effective state, and prior decision events. Contains no recommendation.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Action context",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "action_id": {
                      "type": "string"
                    },
                    "why_this_now": {
                      "type": "object"
                    },
                    "chain": {
                      "type": "object"
                    },
                    "relationship_history": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "outreach_reality": {
                      "type": "object"
                    },
                    "prior_decision_events": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "execution_preparation_state": {
                      "type": "object",
                      "description": "Factual prepared-but-not-sent state. If do_not_prepare_duplicate is true, an external draft already exists for this action — do not prepare another one."
                    },
                    "active_preparations": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Preparation"
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/actions/{id}/preparations": {
      "get": {
        "operationId": "getActionPreparations",
        "summary": "PREPARED EXECUTION MEMORY. Read external work that is prepared but NOT sent for this action (for example Gmail drafts). A preparation is never an execution: it never resolves the action and never proves delivery.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Preparations",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "action_id": {
                      "type": "string"
                    },
                    "preparations": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Preparation"
                      }
                    },
                    "active_preparations": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Preparation"
                      }
                    },
                    "execution_preparation_state": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createActionPreparation",
        "summary": "Remember that external work has been PREPARED for this action (for example a Gmail draft was created). Does not change the action's lifecycle, status or expected_signal_at. Re-posting the same externalSource + externalDraftId returns the existing record with duplicate_preparation_rejected: true instead of creating a second one.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Recommended on every mutating request. A replay with the same key is NOT applied twice: the server answers { ok, idempotent_replay: true, first_seen_at, status } describing the first attempt."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PreparationInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Duplicate external draft; nothing created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PreparationEnvelope"
                }
              }
            }
          },
          "201": {
            "description": "Preparation created",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PreparationEnvelope"
                }
              }
            }
          },
          "208": {
            "description": "Idempotent replay of a previous request with the same Idempotency-Key. Nothing was applied again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IdempotentReplay"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/actions/{id}/preparations/mark-executed": {
      "post": {
        "operationId": "markPreparationExecuted",
        "summary": "Close out a preparation because the prepared external work was actually sent. Requires executedAt (when it left the external system, not when Forbiz learned about it). This records preparation state only — the execution itself must still be recorded via POST /actions/{id}/execute or POST /outreach/{id}/executions.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Recommended on every mutating request. A replay with the same key is NOT applied twice: the server answers { ok, idempotent_replay: true, first_seen_at, status } describing the first attempt."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "preparationId": {
                    "type": "string"
                  },
                  "executedAt": {
                    "type": "string",
                    "format": "date-time"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Transitioned",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PreparationEnvelope"
                }
              }
            }
          },
          "208": {
            "description": "Idempotent replay of a previous request with the same Idempotency-Key. Nothing was applied again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IdempotentReplay"
                }
              }
            }
          },
          "409": {
            "description": "Preparation is no longer in 'prepared' state",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/actions/{id}/preparations/supersede": {
      "post": {
        "operationId": "supersedePreparation",
        "summary": "Mark a preparation as superseded because it was replaced by newer prepared work. History is never deleted.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Recommended on every mutating request. A replay with the same key is NOT applied twice: the server answers { ok, idempotent_replay: true, first_seen_at, status } describing the first attempt."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "preparationId": {
                    "type": "string"
                  },
                  "replacedByPreparationId": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Transitioned",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PreparationEnvelope"
                }
              }
            }
          },
          "208": {
            "description": "Idempotent replay of a previous request with the same Idempotency-Key. Nothing was applied again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IdempotentReplay"
                }
              }
            }
          },
          "409": {
            "description": "Preparation is no longer in 'prepared' state",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/actions/{id}/preparations/discard": {
      "post": {
        "operationId": "discardPreparation",
        "summary": "Mark a preparation as discarded because the prepared external work will never be sent.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Recommended on every mutating request. A replay with the same key is NOT applied twice: the server answers { ok, idempotent_replay: true, first_seen_at, status } describing the first attempt."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "preparationId": {
                    "type": "string"
                  },
                  "note": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Transitioned",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PreparationEnvelope"
                }
              }
            }
          },
          "208": {
            "description": "Idempotent replay of a previous request with the same Idempotency-Key. Nothing was applied again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IdempotentReplay"
                }
              }
            }
          },
          "409": {
            "description": "Preparation is no longer in 'prepared' state",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/decisions": {
      "post": {
        "operationId": "recordDecisionEvent",
        "summary": "Record one agent decision observation, or adopt/reject a decision-engine recommendation. Append-only; never creates, resolves or signals an Action.",
        "description": "Two shapes on one endpoint.\n\n1) DECISION OBSERVATION (existing, unchanged): send decisionType + decisionSummary.\n\n2) ENGINE DISPOSITION (v1.11.0): send disposition = ADOPTED | REJECTED | OVERRIDDEN | ACTED_ON together with the recommendation, stateFingerprint and engineVersion returned by GET /decisions/recommendation. This is the ONLY write path for the decision ledger: evaluating a recommendation never persists anything, and exactly one event is persisted per disposition. Retrying with the same Idempotency-Key replays the original event instead of creating a second one.\n\nATTRIBUTION (1.32.0): every credential class can write here, including a trial credential minted by POST /bootstrap. The event is attributed to the machine identity that presented the credential, and a storage failure answers 400 decision_not_recorded with a next step — never an internal constraint name.",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Recommended on every mutating request. A replay with the same key is NOT applied twice: the server answers { ok, idempotent_replay: true, first_seen_at, status } describing the first attempt."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "decisionType": {
                    "type": "string",
                    "enum": [
                      "act",
                      "wait",
                      "investigate",
                      "ask",
                      "ignore",
                      "store",
                      "follow_up",
                      "do_not_send",
                      "reconcile",
                      "escalate",
                      "other"
                    ]
                  },
                  "subjectType": {
                    "type": "string",
                    "description": "e.g. action, outreach, campaign_target, person, organization."
                  },
                  "subjectId": {
                    "type": "string"
                  },
                  "decisionSummary": {
                    "type": "string",
                    "description": "One short factual sentence."
                  },
                  "priorContextUsed": {
                    "type": "string",
                    "description": "Which stored context mattered, in plain language."
                  },
                  "memoryEffect": {
                    "type": "string",
                    "description": "How stored history changed the decision, if it did."
                  },
                  "actionIdsUsed": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "outreachIdsUsed": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "personIdsUsed": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "organizationIdsUsed": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "evidenceIdsUsed": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "signalIdsUsed": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "executionIdsUsed": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "humanQuestionRequired": {
                    "type": "boolean"
                  },
                  "humanQuestionText": {
                    "type": "string"
                  },
                  "priorHistoryChangedDecision": {
                    "type": "boolean"
                  },
                  "contradictionDetected": {
                    "type": "boolean"
                  },
                  "staleStateDetected": {
                    "type": "boolean"
                  },
                  "duplicateActionPrevented": {
                    "type": "boolean"
                  },
                  "duplicateSendPrevented": {
                    "type": "boolean"
                  },
                  "hardBounceReusePrevented": {
                    "type": "boolean"
                  },
                  "activeConversationCollisionPrevented": {
                    "type": "boolean"
                  },
                  "executionMismatchDetected": {
                    "type": "boolean"
                  },
                  "unverifiedClaimBlocked": {
                    "type": "boolean"
                  },
                  "affectedTargetCount": {
                    "type": "integer"
                  },
                  "decisionConfidence": {
                    "type": "number"
                  },
                  "disposition": {
                    "type": "string",
                    "enum": [
                      "ADOPTED",
                      "REJECTED",
                      "OVERRIDDEN",
                      "ACTED_ON"
                    ]
                  },
                  "recommendation": {
                    "type": "string"
                  },
                  "stateFingerprint": {
                    "type": "string"
                  },
                  "engineVersion": {
                    "type": "string"
                  },
                  "projectId": {
                    "type": "string"
                  },
                  "actionId": {
                    "type": "string"
                  },
                  "rationale": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Decision recorded",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "decision_event_id": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "208": {
            "description": "Idempotent replay of a previous request with the same Idempotency-Key. Nothing was applied again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IdempotentReplay"
                }
              }
            }
          },
          "400": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/recipients/safety": {
      "get": {
        "operationId": "getRecipientSafety",
        "summary": "GLOBAL RECIPIENT SAFETY (diagnostic). Answers: is it safe for this WORKSPACE to contact this recipient now under this send intent? Scope is the whole workspace, not one Outreach: hard bounces, unsubscribes, human replies, referrals, meaningful responses, confirmed prior sends, open waiting/decision Action loops, prepared-but-unsent communications and suppressions are unioned across every Outreach. Read-only. The canonical cohort answer for a campaign is still GET /outreach/{id}/preflight, which already applies this — do not union blockers manually.",
        "parameters": [
          {
            "name": "email",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "sendIntent",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "initial",
                "follow_up"
              ]
            },
            "description": "Defaults to initial. Global history is combined with intent: a prior confirmed send is a duplicate risk for a generic initial contact and expected for a follow-up, while hard bounce, unsubscribe, active conversation and meaningful reply block both."
          }
        ],
        "responses": {
          "200": {
            "description": "Recipient safety",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RecipientSafety"
                }
              }
            }
          }
        }
      }
    },
    "/send-budget": {
      "get": {
        "operationId": "getAutonomousSendBudget",
        "summary": "Read the current autonomous send budget (limit, used, remaining, window). Consumes nothing. A send is rejected with 429 and code AUTONOMOUS_SEND_LIMIT_REACHED when the budget is exhausted or no policy is configured; an idempotent retry of the same logical send never consumes an extra slot.",
        "parameters": [
          {
            "name": "channel",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Budget",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "autonomous_send_budget": {
                      "$ref": "#/components/schemas/AutonomousSendBudget"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/workspace/context": {
      "get": {
        "operationId": "getWorkspaceContext",
        "summary": "Read workspace business context and readiness: for each key, whether it is known (stated or confirmed by the human), inferred (from a non-user source, awaiting confirmation) or missing, plus counts of canonical entities. Never blocks app access. Read-only.",
        "parameters": [
          {
            "name": "operate",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "0",
                "1"
              ]
            },
            "description": "operate=0 returns readiness only, with no operating payload."
          }
        ],
        "responses": {
          "200": {
            "description": "Workspace readiness",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "description": "ITEM 7 (1.29.0): per key, agent_may_propose says whether an agent may write a proposal (always true) and human_confirmation_required says whether a human must confirm it (true for subjective keys such as current_focus, biggest_bottleneck, success_definition). Top-level agent_may_propose lists the missing keys an agent can propose and human_confirmation_required lists the keys only a human may settle. `agent_may_fill` is kept as a DEPRECATED alias for agent_may_propose && !human_confirmation_required and is removed after 1.29.0.",
                  "additionalProperties": true
                }
              }
            }
          },
          "400": {
            "description": "unknown_parameter — this endpoint reads only `operate`",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "setWorkspaceContext",
        "summary": "Set one or more workspace context values. Every update must state an explicit source ('user', 'sso', 'derived' or 'agent'); the source is never defaulted. Only source=user may confirm a value, and a non-user source must supply a confidence between 0 and 1. Returns fresh readiness.",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Recommended on every mutating request. A replay with the same key is NOT applied twice: the server answers { ok, idempotent_replay: true, first_seen_at, status } describing the first attempt."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "updates": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "key": {
                          "type": "string"
                        },
                        "value": {
                          "type": "string"
                        },
                        "source": {
                          "type": "string",
                          "enum": [
                            "user",
                            "sso",
                            "derived",
                            "agent"
                          ]
                        },
                        "source_note": {
                          "type": "string"
                        },
                        "confidence": {
                          "type": "number"
                        },
                        "confirm": {
                          "type": "boolean"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated. Epistemic standing is derived from the stored row.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "results": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "readiness": {
                      "type": "object"
                    },
                    "promoted": {
                      "type": "boolean",
                      "description": "True only when the stored value really is confirmed (source=user with confirm)."
                    },
                    "canonical": {
                      "type": "boolean",
                      "description": "True only when the stored row is canonical workspace truth."
                    },
                    "stored_status": {
                      "type": "string",
                      "description": "The confirmation status actually stored."
                    },
                    "needs_confirmation": {
                      "type": "boolean",
                      "description": "True when a human still has to confirm it."
                    }
                  }
                }
              }
            }
          },
          "208": {
            "description": "Idempotent replay of a previous request with the same Idempotency-Key. Nothing was applied again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IdempotentReplay"
                }
              }
            }
          },
          "400": {
            "description": "Validation error (including a missing source)",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "No active workspace for this identity",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/workspace/entities": {
      "post": {
        "operationId": "createWorkspaceEntities",
        "summary": "Create canonical workspace entities (kind 'mission' or 'product' only), deduped by name within the workspace. Creation is always explicit: an empty or missing entities list is rejected. Returns fresh readiness.",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Recommended on every mutating request. A replay with the same key is NOT applied twice: the server answers { ok, idempotent_replay: true, first_seen_at, status } describing the first attempt."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "entities": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "kind": {
                          "type": "string",
                          "enum": [
                            "mission",
                            "product"
                          ]
                        },
                        "name": {
                          "type": "string"
                        },
                        "description": {
                          "type": "string"
                        },
                        "extra": {
                          "type": "object"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Created. Epistemic standing is derived from the stored row.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "created": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "readiness": {
                      "type": "object"
                    },
                    "promoted": {
                      "type": "boolean"
                    },
                    "canonical": {
                      "type": "boolean"
                    },
                    "stored_status": {
                      "type": "string"
                    },
                    "needs_confirmation": {
                      "type": "boolean",
                      "description": "True for an agent-created entity a human has not confirmed."
                    }
                  }
                }
              }
            }
          },
          "208": {
            "description": "Idempotent replay of a previous request with the same Idempotency-Key. Nothing was applied again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IdempotentReplay"
                }
              }
            }
          },
          "400": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "No active workspace for this identity",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/workspace/channels": {
      "get": {
        "operationId": "listWorkspaceChannels",
        "summary": "List the human attention channels of the workspace bound to this credential. Telegram v0 allows at most ONE active destination per workspace. Pairing tokens and bot secrets are never returned. Each channel carries its own nudge window: timezone (IANA name, captured in the owner's browser at pairing, default UTC) plus push_window_start and push_window_end (local hours, end EXCLUSIVE, default 9 and 19). The hourly scheduled sweep only delivers to a channel when its local hour is inside that window; explicit pushes are never window-gated.",
        "responses": {
          "200": {
            "description": "Channels",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "telegram_configured": {
                      "type": "boolean"
                    },
                    "has_active_telegram": {
                      "type": "boolean"
                    },
                    "channels": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "status": {
                            "type": "string"
                          },
                          "timezone": {
                            "type": "string",
                            "description": "IANA zone the nudge window is evaluated in."
                          },
                          "push_window_start": {
                            "type": "integer",
                            "minimum": 0,
                            "maximum": 23
                          },
                          "push_window_end": {
                            "type": "integer",
                            "minimum": 0,
                            "maximum": 23,
                            "description": "Exclusive."
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "This credential is not bound to a workspace",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/workspace/channels/telegram/pair": {
      "post": {
        "operationId": "requestTelegramPairing",
        "summary": "Request a Telegram destination for this workspace. An agent can never finalise pairing: this returns connect_url for the human owner/admin to open (they authenticate, receive a single-use Telegram link, and return here). Never returns a pairing token.",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Recommended on every mutating request. A replay with the same key is NOT applied twice: the server answers { ok, idempotent_replay: true, first_seen_at, status } describing the first attempt."
          }
        ],
        "responses": {
          "200": {
            "description": "A destination is already active",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "already_active": {
                      "type": "boolean"
                    },
                    "connect_url": {
                      "type": "string"
                    },
                    "channel_id": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "202": {
            "description": "Human authorization required",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "requires_human_authorization": {
                      "type": "boolean"
                    },
                    "connect_url": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "208": {
            "description": "Idempotent replay of a previous request with the same Idempotency-Key. Nothing was applied again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IdempotentReplay"
                }
              }
            }
          },
          "503": {
            "description": "Telegram is not configured on this deployment",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/workspace/channels/telegram/push": {
      "post": {
        "operationId": "pushTelegramNudges",
        "summary": "Evaluate canonical state and push eligible human-attention nudges to the workspace's active Telegram destination. This endpoint is the EXPLICIT trigger; in addition, a server-side scheduler runs the same evaluation every hour and delivers only to channels whose own local hour is inside their configured window, and its deliveries carry trigger 'scheduled' and count against the same daily cap as explicit pushes. Sources are the canonical temporal states needs_decision, overdue (needs_feedback) and needs_timing, in the same order /state next_priorities uses. Paced per channel (max_nudges_per_hour default 1, max_nudges_per_day default 6, both owner-configurable) across explicit and scheduled deliveries, and deduped by state fingerprint: an unchanged ask is never re-sent. Anything that does not fit the pace is QUEUED, never dropped — see attention_queue in GET /state and the digest.",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Recommended on every mutating request. A replay with the same key is NOT applied twice: the server answers { ok, idempotent_replay: true, first_seen_at, status } describing the first attempt."
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "limit": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 3
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Push result",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "channel_id": {
                      "type": "string"
                    },
                    "sent": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "skipped_duplicates": {
                      "type": "integer"
                    },
                    "failed": {
                      "type": "integer"
                    },
                    "eligible": {
                      "type": "integer"
                    },
                    "daily_cap_remaining": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "208": {
            "description": "Idempotent replay of a previous request with the same Idempotency-Key. Nothing was applied again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IdempotentReplay"
                }
              }
            }
          },
          "409": {
            "description": "No active Telegram destination for this workspace",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/workspace/channels/telegram/digest": {
      "post": {
        "operationId": "sendTelegramDigest",
        "summary": "Deliver ONE digest of everything pending in this workspace to its paired Telegram destination. Counts per queue (overdue with no signal, expectation date past with no signal, waiting on a human decision, delegations delivered and not resolved, attention requests pending), then the top items of the first three queues, at most 7 lines, each with what it is, how many days it has been waiting and its next command taken from timing.available_commands, then a closing line with the total that did not fit. The top items carry exactly the correlated buttons they would carry as an ordinary nudge; the digest never invents an operation. It is recorded in channel_deliveries with source_object_type 'workspace' and trigger 'digest', does NOT consume the 3-per-rolling-day nudge cap, and is capped at ONE digest per channel per day. A scheduled digest also runs weekly, on Monday, at the start of the channel's own local window; this endpoint is the explicit trigger and ignores the window. HONESTY: an overdue line states the expectation and that no signal was recorded, never that a commitment failed or that the other side did not answer; when no expected signal source was declared, the line says so. With nothing pending, no message is sent.",
        "responses": {
          "200": {
            "description": "Digest result. delivered=false with reason 'nothing_pending' when the workspace has nothing pending.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "channel_id": {
                      "type": "string"
                    },
                    "delivered": {
                      "type": "boolean"
                    },
                    "delivery_id": {
                      "type": "string"
                    },
                    "counts": {
                      "type": "object"
                    },
                    "lines_shown": {
                      "type": "integer"
                    },
                    "not_shown": {
                      "type": "integer"
                    },
                    "reason": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "No active Telegram destination, or a digest was already delivered to this channel today",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/projects": {
      "get": {
        "operationId": "listProjects",
        "summary": "List projects in the workspace bound to this credential. The workspace is never taken from the body, query or owner. Optional ?q= filters on name.",
        "parameters": [
          {
            "name": "q",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Projects",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "projects": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "workspace_id": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "409": {
            "description": "This credential is not bound to a workspace",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "createProject",
        "summary": "Create a project from captured commercial facts. Pricing, economics and geography fields (customer_price, economics, factory_cost, margin, currency, region, state, country) are rejected with 400 — this surface captures facts only. Returns the full RPC result including the project id.",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Recommended on every mutating request. A replay with the same key is NOT applied twice: the server answers { ok, idempotent_replay: true, first_seen_at, status } describing the first attempt."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string"
                  },
                  "location": {
                    "type": "string"
                  },
                  "primaryOrganizationId": {
                    "type": "string"
                  },
                  "primaryPersonId": {
                    "type": "string"
                  },
                  "commercialStageName": {
                    "type": "string"
                  },
                  "lifecycleStatus": {
                    "type": "string"
                  },
                  "notes": {
                    "type": "string"
                  },
                  "source": {
                    "type": "string"
                  },
                  "purpose": {
                    "type": "string",
                    "enum": [
                      "real",
                      "test",
                      "fictional",
                      "demo"
                    ],
                    "description": "Why this project exists. Default real. A project created to practice should be created with purpose 'test' so it never competes with real commitments. Anything other than real is excluded from /next, /state queues and counts, the nudges and the digest, while staying in /search, /export and every direct read."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "208": {
            "description": "Idempotent replay of a previous request with the same Idempotency-Key. Nothing was applied again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IdempotentReplay"
                }
              }
            }
          },
          "400": {
            "description": "Validation error, an invalid purpose (400 invalid_purpose) or a rejected pricing/geography field",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "This credential is not bound to a workspace",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/projects/{id}": {
      "post": {
        "operationId": "refuseProjectPurposeChange",
        "summary": "Changing an existing project's purpose through the Agent API is refused with 409 PURPOSE_CHANGE_NOT_PERMITTED: purpose is declared at creation, and only a workspace owner or admin can relabel a project, in the app. The stored value is unchanged.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "404": {
            "description": "Project not found in this workspace",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "PURPOSE_CHANGE_NOT_PERMITTED",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "get": {
        "operationId": "getProject",
        "summary": "One project in the bound workspace, with its 50 most recent project events (newest first) and its pending actions with derived timing.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Project",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "project": {
                      "type": "object"
                    },
                    "events": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "pending_actions": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "workspace_id": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Project not found in this workspace",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "This credential is not bound to a workspace",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/projects/{id}/events": {
      "post": {
        "operationId": "appendProjectEvent",
        "summary": "Append a factual event to a project's ledger. occurredAt is real-world time; the recorded time is always server-stamped. Returns the full RPC result including the event id.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Recommended on every mutating request. A replay with the same key is NOT applied twice: the server answers { ok, idempotent_replay: true, first_seen_at, status } describing the first attempt."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "eventType": {
                    "type": "string"
                  },
                  "rawText": {
                    "type": "string"
                  },
                  "occurredAt": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "source": {
                    "type": "string"
                  },
                  "metadata": {
                    "type": "object"
                  },
                  "confidence": {
                    "type": "number"
                  },
                  "organizationId": {
                    "type": "string"
                  },
                  "personId": {
                    "type": "string"
                  },
                  "actionId": {
                    "type": "string"
                  },
                  "actorName": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Appended",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "208": {
            "description": "Idempotent replay of a previous request with the same Idempotency-Key. Nothing was applied again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IdempotentReplay"
                }
              }
            }
          },
          "400": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Project not found in this workspace",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/projects/{id}/actions": {
      "post": {
        "operationId": "createProjectAction",
        "summary": "Create an action anchored to a project. Set setAsNext to make it the project's next action pointer. Returns the full RPC result including the action id. When the created action has no dueAt the response carries warnings[] with code no_due_date — nothing is rejected, but an action with no clock is invisible to the attention layer.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Recommended on every mutating request. A replay with the same key is NOT applied twice: the server answers { ok, idempotent_replay: true, first_seen_at, status } describing the first attempt."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "verb": {
                    "type": "string"
                  },
                  "description": {
                    "type": "string"
                  },
                  "expectedSignal": {
                    "type": "string"
                  },
                  "expectedSignalSource": {
                    "oneOf": [
                      {
                        "type": "string",
                        "enum": [
                          "human",
                          "external"
                        ]
                      },
                      {
                        "type": "object",
                        "properties": {
                          "kind": {
                            "type": "string",
                            "enum": [
                              "email_thread",
                              "chat",
                              "phone_call",
                              "site_order",
                              "bank",
                              "calendar",
                              "in_person",
                              "system",
                              "other"
                            ]
                          },
                          "detail": {
                            "type": "string",
                            "maxLength": 200
                          }
                        },
                        "required": [
                          "kind"
                        ]
                      }
                    ],
                    "nullable": true,
                    "description": "BORN WITH A SIGNAL SOURCE (1.43.0). WHERE the expected signal will appear, as { kind, detail }: kind is one of email_thread, chat, phone_call, site_order, bank, calendar, in_person, system, other, and detail is free text (max 200 chars) identifying the place to look. OPTIONAL: when absent the 201 carries missing: [\"expected_signal_source\"] with one sentence saying that without it Forbiz cannot tell \"nobody looked\" from \"looked and found nothing\". NEVER inferred from the description. The legacy strings \"human\" and \"external\" are still accepted and keep their exact attention/Telegram semantics."
                  },
                  "actionType": {
                    "type": "string"
                  },
                  "humanInterventionKind": {
                    "type": "string",
                    "enum": [
                      "decide",
                      "execute"
                    ],
                    "nullable": true,
                    "description": "Same semantics as POST /actions. 'decide' + actionType='strategy_checkpoint' proposes a strategy checkpoint; approval is human-only."
                  },
                  "expectedSignalAt": {
                    "type": "string",
                    "format": "date-time",
                    "description": "When the expected signal is due. Persisted on the action (1.29.0); alias: expected_signal_at."
                  },
                  "earliestActionableAt": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "dueAt": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "effortMinutes": {
                    "type": "integer"
                  },
                  "urgency": {
                    "type": "number"
                  },
                  "lossRisk": {
                    "type": "number"
                  },
                  "distractionRisk": {
                    "type": "number"
                  },
                  "roi": {
                    "type": "object",
                    "nullable": true,
                    "description": "BORN WITH A VALUE (1.43.0). What this commitment is worth, each dimension an integer 0-5: cash, access, proof, narrative, relationship, learning, compliance. OPTIONAL and never inferred. When absent the response carries missing: [\"roi\"] and priority: { value, defaulted: true } — nothing pretends to be ranked when its inputs were never given. The existing write-time priority trigger is unchanged; ORDERING happens at READ time in GET /next and GET /state with three named terms (value, days_late, blocking) and their weights published in rank_reason on every ranked item.",
                    "properties": {
                      "cash": {
                        "type": "integer",
                        "minimum": 0,
                        "maximum": 5
                      },
                      "access": {
                        "type": "integer",
                        "minimum": 0,
                        "maximum": 5
                      },
                      "proof": {
                        "type": "integer",
                        "minimum": 0,
                        "maximum": 5
                      },
                      "narrative": {
                        "type": "integer",
                        "minimum": 0,
                        "maximum": 5
                      },
                      "relationship": {
                        "type": "integer",
                        "minimum": 0,
                        "maximum": 5
                      },
                      "learning": {
                        "type": "integer",
                        "minimum": 0,
                        "maximum": 5
                      },
                      "compliance": {
                        "type": "integer",
                        "minimum": 0,
                        "maximum": 5
                      }
                    }
                  },
                  "whyThisMove": {
                    "type": "string"
                  },
                  "setAsNext": {
                    "type": "boolean"
                  },
                  "commitmentKey": {
                    "type": "string",
                    "maxLength": 120,
                    "nullable": true,
                    "description": "RECOMMENDED. Same three-layer rule as POST /actions: a declared key refuses a second OPEN action for the same objective with 409 COMMITMENT_ALREADY_OPEN; with no key the server derives auto:<kind>:<id>:<action_type> from an object DECLARED IN THE BODY (the project in the path is this route's container, not an objective, so it never derives a key); otherwise a look-alike OPEN action is refused 409 POSSIBLE_DUPLICATE, overridable with confirm_new: true. Alias: commitment_key."
                  },
                  "confirm_new": {
                    "type": "boolean",
                    "default": false,
                    "description": "Send true to override a POSSIBLE_DUPLICATE refusal. Never merges, edits or closes anything; does not override COMMITMENT_ALREADY_OPEN."
                  },
                  "purpose": {
                    "type": "string",
                    "nullable": true,
                    "description": "ACCEPTED AND IGNORED (1.56.0). `purpose` is a PROJECT field, not an action field: it is not stored on the action and it hides nothing. The action is created exactly as if the field had not been sent and it appears in the normal queues (/next, /state, /prompts, /decisions/recommendation, nudges, digest). When present the response carries purpose_ignored: true and purpose_ignored_note { code: \"purpose_ignored\", message }. To practise, POST /projects with purpose 'test' and create the actions inside that project."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Created",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "208": {
            "description": "Idempotent replay of a previous request with the same Idempotency-Key. Nothing was applied again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IdempotentReplay"
                }
              }
            }
          },
          "400": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Project not found in this workspace",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Refused, nothing created. COMMITMENT_ALREADY_OPEN carries the open action plus its executable commands; POSSIBLE_DUPLICATE carries up to 3 candidates and is overridable with confirm_new: true.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string",
                      "enum": [
                        "COMMITMENT_ALREADY_OPEN",
                        "POSSIBLE_DUPLICATE"
                      ]
                    },
                    "commitment_key": {
                      "type": "string",
                      "nullable": true
                    },
                    "existing": {
                      "type": "object"
                    },
                    "commands": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/OperationCommand"
                      }
                    },
                    "candidates": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "hint": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/actions/{id}/satisfy-signal": {
      "post": {
        "operationId": "satisfyActionWithEvent",
        "summary": "Close an action's loop with a factual event: records the event, the actual signal and, optionally, the next action pointer, in one transaction. Returns the full RPC result including the ids.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Recommended on every mutating request. A replay with the same key is NOT applied twice: the server answers { ok, idempotent_replay: true, first_seen_at, status } describing the first attempt."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "eventType": {
                    "type": "string"
                  },
                  "rawText": {
                    "type": "string"
                  },
                  "actualSignal": {
                    "type": "string"
                  },
                  "occurredAt": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "source": {
                    "type": "string"
                  },
                  "metadata": {
                    "type": "object"
                  },
                  "confidence": {
                    "type": "number"
                  },
                  "nextActionId": {
                    "type": "string"
                  },
                  "actorName": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Loop satisfied",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                }
              }
            }
          },
          "208": {
            "description": "Idempotent replay of a previous request with the same Idempotency-Key. Nothing was applied again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IdempotentReplay"
                }
              }
            }
          },
          "400": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/memory-advantage": {
      "get": {
        "operationId": "getMemoryAdvantage",
        "summary": "Aggregated decision-event metrics: how often stored context was used, how often prior history changed a decision, how often a human question was required, and which risks were prevented. Self-reported observations only.",
        "parameters": [
          {
            "name": "days",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 30
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Memory advantage metrics",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "window_days": {
                      "type": "integer"
                    },
                    "decision_events": {
                      "type": "integer"
                    },
                    "decisions_by_type": {
                      "type": "object"
                    },
                    "memory_use": {
                      "type": "object"
                    },
                    "human_questions": {
                      "type": "object"
                    },
                    "safety_value": {
                      "type": "object"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/outreach/{id}/events": {
      "post": {
        "operationId": "recordTargetEventsBatch",
        "summary": "Record many factual target-level events in one call. Partial success is reported per event; nothing is inferred.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Recommended on every mutating request. A replay with the same key is NOT applied twice: the server answers { ok, idempotent_replay: true, first_seen_at, status } describing the first attempt."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "events": {
                    "type": "array",
                    "items": {
                      "$ref": "#/components/schemas/TargetEvent"
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Nothing new recorded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TargetEventResult"
                }
              }
            }
          },
          "201": {
            "description": "Events recorded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TargetEventResult"
                }
              }
            }
          },
          "208": {
            "description": "Idempotent replay of a previous request with the same Idempotency-Key. Nothing was applied again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IdempotentReplay"
                }
              }
            }
          },
          "400": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TargetEventResult"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/outreach/{id}/targets/{campaignTargetId}": {
      "get": {
        "operationId": "getTargetReality",
        "summary": "Full factual history for one target: execution rows, event ledger (with supersession) and evidence.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "campaignTargetId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Target reality",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "target": {
                      "type": "object"
                    },
                    "executions": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "events": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    },
                    "evidence": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/outreach/{id}/targets/{campaignTargetId}/events": {
      "post": {
        "operationId": "recordTargetEvent",
        "summary": "Record one factual event for a target (bounce, human reply, referral, request, unsubscribe...). Facts only; interpretation stays a separate Action command.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "campaignTargetId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Recommended on every mutating request. A replay with the same key is NOT applied twice: the server answers { ok, idempotent_replay: true, first_seen_at, status } describing the first attempt."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TargetEvent"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Event recorded",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TargetEventResult"
                }
              }
            }
          },
          "208": {
            "description": "Idempotent replay of a previous request with the same Idempotency-Key. Nothing was applied again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IdempotentReplay"
                }
              }
            }
          },
          "400": {
            "description": "Validation error",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TargetEventResult"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/targets": {
      "post": {
        "operationId": "postTargets",
        "summary": "Submit researched targets with evidence. Targets stay pending until a human approves them in Forbiz.",
        "description": "Identifiers are Forbiz UUIDs. `campaignId` is the Outreach id (the field name is kept for backwards compatibility) and must exist and belong to the credential's owner, otherwise 404 `campaign_not_found`. `runId` is ONLY a UUID returned by a previous POST /targets — an arbitrary external key is rejected with 400 `invalid_run_id`. To start a new research run, omit `runId` and pass a human label in `runName`. Errors: 400 (`targets_required`, `invalid_campaign_id`, `invalid_run_id`), 404 (`campaign_not_found`, `run_not_found`), 502 (`campaign_lookup_failed`, `run_lookup_failed`).",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Recommended on every mutating request. A replay with the same key is NOT applied twice: the server answers { ok, idempotent_replay: true, first_seen_at, status } describing the first attempt."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "runId": {
                    "type": "string"
                  },
                  "runName": {
                    "type": "string"
                  },
                  "campaignId": {
                    "type": "string"
                  },
                  "vertical": {
                    "type": "string"
                  },
                  "geography": {
                    "type": "string"
                  },
                  "targets": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "companyName": {
                          "type": "string"
                        },
                        "companyDomain": {
                          "type": "string"
                        },
                        "personName": {
                          "type": "string"
                        },
                        "personTitle": {
                          "type": "string"
                        },
                        "email": {
                          "type": "string"
                        },
                        "linkedinUrl": {
                          "type": "string"
                        },
                        "roleType": {
                          "type": "string"
                        },
                        "vertical": {
                          "type": "string"
                        },
                        "geography": {
                          "type": "string"
                        },
                        "whyThisTarget": {
                          "type": "string"
                        },
                        "expectedSignal": {
                          "type": "string"
                        },
                        "confidence": {
                          "type": "number"
                        },
                        "verification": {
                          "type": "string",
                          "enum": [
                            "found",
                            "verified"
                          ]
                        },
                        "sourceUrl": {
                          "type": "string"
                        },
                        "evidence": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "claimType": {
                                "type": "string"
                              },
                              "claimValue": {
                                "type": "string"
                              },
                              "sourceUrl": {
                                "type": "string"
                              },
                              "sourceTitle": {
                                "type": "string"
                              },
                              "snippet": {
                                "type": "string"
                              },
                              "verificationStatus": {
                                "type": "string",
                                "enum": [
                                  "found",
                                  "verified",
                                  "rejected"
                                ]
                              },
                              "confidence": {
                                "type": "number"
                              }
                            }
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Created target candidates, pending human approval",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "run_id": {
                      "type": "string"
                    },
                    "created": {
                      "type": "integer"
                    },
                    "targets": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "company": {
                            "type": "string"
                          },
                          "person": {
                            "type": "string",
                            "nullable": true
                          },
                          "verification": {
                            "type": "string"
                          }
                        }
                      }
                    },
                    "note": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "208": {
            "description": "Idempotent replay of a previous request with the same Idempotency-Key. Nothing was applied again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IdempotentReplay"
                }
              }
            }
          }
        }
      }
    },
    "/evidence": {
      "get": {
        "operationId": "getEvidence",
        "summary": "Read stored research evidence.",
        "parameters": [
          {
            "name": "subjectType",
            "in": "query",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "subjectId",
            "in": "query",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Evidence",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "evidence": {
                      "type": "array",
                      "items": {
                        "type": "object"
                      }
                    }
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "operationId": "postEvidence",
        "summary": "Attach evidence to a target, person or organization.",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Recommended on every mutating request. A replay with the same key is NOT applied twice: the server answers { ok, idempotent_replay: true, first_seen_at, status } describing the first attempt."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "subjectType": {
                    "type": "string",
                    "enum": [
                      "target_candidate",
                      "person",
                      "organization",
                      "outreach"
                    ]
                  },
                  "subjectId": {
                    "type": "string"
                  },
                  "evidence": {
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "claimType": {
                          "type": "string"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Stored",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "inserted": {
                      "type": "integer"
                    }
                  }
                }
              }
            }
          },
          "208": {
            "description": "Idempotent replay of a previous request with the same Idempotency-Key. Nothing was applied again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IdempotentReplay"
                }
              }
            }
          }
        }
      }
    },
    "/actions": {
      "post": {
        "operationId": "createAction",
        "summary": "create_next_action: create the next action with an expected signal and a review date. Pass parentActionId to make it the child of a needs_decision action — the parent is NOT resolved automatically, close it explicitly with close-loop. Defaults verb to \"send\" when omitted. When the created action has no due_at the 201 carries warnings[] with code no_due_date: the action IS created, but with no clock it can never become overdue and the attention layer (and therefore the Telegram nudge clock) will never surface it.",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Recommended on every mutating request. A replay with the same key is NOT applied twice: the server answers { ok, idempotent_replay: true, first_seen_at, status } describing the first attempt."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "description": {
                    "type": "string"
                  },
                  "verb": {
                    "type": "string",
                    "enum": [
                      "send",
                      "call",
                      "ask",
                      "write",
                      "draft",
                      "review",
                      "decide",
                      "schedule",
                      "research",
                      "create",
                      "connect",
                      "update",
                      "prepare",
                      "validate",
                      "check"
                    ]
                  },
                  "whyThisMove": {
                    "type": "string"
                  },
                  "expectedSignal": {
                    "type": "string"
                  },
                  "expectedSignalSource": {
                    "oneOf": [
                      {
                        "type": "string",
                        "enum": [
                          "human",
                          "external"
                        ]
                      },
                      {
                        "type": "object",
                        "properties": {
                          "kind": {
                            "type": "string",
                            "enum": [
                              "email_thread",
                              "chat",
                              "phone_call",
                              "site_order",
                              "bank",
                              "calendar",
                              "in_person",
                              "system",
                              "other"
                            ]
                          },
                          "detail": {
                            "type": "string",
                            "maxLength": 200
                          }
                        },
                        "required": [
                          "kind"
                        ]
                      }
                    ],
                    "nullable": true,
                    "description": "BORN WITH A SIGNAL SOURCE (1.43.0). WHERE the expected signal will appear, as { kind, detail }: kind is one of email_thread, chat, phone_call, site_order, bank, calendar, in_person, system, other, and detail is free text (max 200 chars) identifying the place to look. OPTIONAL: when absent the 201 carries missing: [\"expected_signal_source\"] with one sentence saying that without it Forbiz cannot tell \"nobody looked\" from \"looked and found nothing\". NEVER inferred from the description. The legacy strings \"human\" and \"external\" are still accepted and keep their exact attention/Telegram semantics."
                  },
                  "expectedSignalAt": {
                    "type": "string",
                    "format": "date-time"
                  },
                  "dueAt": {
                    "type": "string",
                    "format": "date-time",
                    "nullable": true,
                    "description": "When this action is due. Without it the action can never become overdue and the attention layer will never surface it; the response then carries warnings[] with code no_due_date. Alias: due_at."
                  },
                  "actionType": {
                    "type": "string"
                  },
                  "humanInterventionKind": {
                    "type": "string",
                    "enum": [
                      "decide",
                      "execute"
                    ],
                    "nullable": true,
                    "description": "Declared kind of human intervention. 'decide' + actionType='strategy_checkpoint' PROPOSES a strategy checkpoint for a Mission (resolved through the action's item → mission relation); a human then approves or asks for an adjustment in Forbiz or from Telegram — this API can never approve. 'execute' means a human must perform the step. Omitted/null preserves existing behavior exactly."
                  },
                  "parentActionId": {
                    "type": "string",
                    "description": "Required when this action is the decision taken on a parent action. root_action_id is NOT accepted as input — the server derives it as parent.root_action_id ?? parent.id and returns it in the response."
                  },
                  "personId": {
                    "type": "string"
                  },
                  "organizationId": {
                    "type": "string"
                  },
                  "outreachId": {
                    "type": "string"
                  },
                  "campaignTargetId": {
                    "type": "string"
                  },
                  "projectId": {
                    "type": "string",
                    "format": "uuid",
                    "nullable": true,
                    "description": "Optional. Anchor this action to an existing project in the credential's workspace. Must be a valid UUID for a project in the same workspace, otherwise 400/404. Omitted or null preserves the current unanchored behavior."
                  },
                  "commitmentKey": {
                    "type": "string",
                    "maxLength": 120,
                    "nullable": true,
                    "description": "RECOMMENDED. A short stable id for the objective this commitment serves, e.g. \"unit-13/quote-followup\" (max 120 chars; alias commitment_key). ONE COMMITMENT PER OBJECTIVE has three layers: (1) with a key, a second OPEN action for the same key in the workspace is refused 409 COMMITMENT_ALREADY_OPEN, database-enforced so it holds under concurrency; (2) with no key but a declared object (campaignTargetId / personId / organizationId / projectId), the server derives and stores auto:<kind>:<id>:<action_type> and applies the same rule — with no object no key is invented; (3) otherwise a look-alike OPEN action found by the existing full-text index is refused 409 POSSIBLE_DUPLICATE, which is never a block: resend with confirm_new: true. Open means lifecycle planned/executed/waiting_for_signal and status <> skipped — resolved, closed_without_signal and skipped never refuse."
                  },
                  "roi": {
                    "type": "object",
                    "nullable": true,
                    "description": "BORN WITH A VALUE (1.43.0). What this commitment is worth, each dimension an integer 0-5: cash, access, proof, narrative, relationship, learning, compliance. OPTIONAL and never inferred. When absent the response carries missing: [\"roi\"] and priority: { value, defaulted: true } — nothing pretends to be ranked when its inputs were never given. The existing write-time priority trigger is unchanged; ORDERING happens at READ time in GET /next and GET /state with three named terms (value, days_late, blocking) and their weights published in rank_reason on every ranked item.",
                    "properties": {
                      "cash": {
                        "type": "integer",
                        "minimum": 0,
                        "maximum": 5
                      },
                      "access": {
                        "type": "integer",
                        "minimum": 0,
                        "maximum": 5
                      },
                      "proof": {
                        "type": "integer",
                        "minimum": 0,
                        "maximum": 5
                      },
                      "narrative": {
                        "type": "integer",
                        "minimum": 0,
                        "maximum": 5
                      },
                      "relationship": {
                        "type": "integer",
                        "minimum": 0,
                        "maximum": 5
                      },
                      "learning": {
                        "type": "integer",
                        "minimum": 0,
                        "maximum": 5
                      },
                      "compliance": {
                        "type": "integer",
                        "minimum": 0,
                        "maximum": 5
                      }
                    }
                  },
                  "confirm_new": {
                    "type": "boolean",
                    "default": false,
                    "description": "Send true to override a POSSIBLE_DUPLICATE refusal and create anyway. Nothing is ever merged, edited or closed. It does NOT override COMMITMENT_ALREADY_OPEN."
                  },
                  "purpose": {
                    "type": "string",
                    "nullable": true,
                    "description": "ACCEPTED AND IGNORED (1.56.0). `purpose` is a PROJECT field, not an action field: it is not stored on the action and it hides nothing. The action is created exactly as if the field had not been sent and it appears in the normal queues (/next, /state, /prompts, /decisions/recommendation, nudges, digest). When present the response carries purpose_ignored: true and purpose_ignored_note { code: \"purpose_ignored\", message }. To practise, POST /projects with purpose 'test' and create the actions inside that project."
                  }
                },
                "required": [
                  "description"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created action",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "owner": {
                      "$ref": "#/components/schemas/Owner"
                    },
                    "action": {
                      "$ref": "#/components/schemas/Action"
                    },
                    "action_id": {
                      "type": "string"
                    },
                    "parent_action_id": {
                      "type": "string",
                      "nullable": true
                    },
                    "root_action_id": {
                      "type": "string",
                      "nullable": true,
                      "description": "Server-derived chain root. Null for a root action; otherwise parent.root_action_id ?? parent.id."
                    },
                    "warnings": {
                      "type": "array",
                      "description": "Non-blocking advisories. Present only when something was created that the attention layer cannot see.",
                      "items": {
                        "type": "object",
                        "properties": {
                          "code": {
                            "type": "string",
                            "enum": [
                              "no_due_date",
                              "no_review_date"
                            ]
                          },
                          "message": {
                            "type": "string"
                          }
                        }
                      }
                    },
                    "note": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "208": {
            "description": "Idempotent replay of a previous request with the same Idempotency-Key. Nothing was applied again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IdempotentReplay"
                }
              }
            }
          },
          "409": {
            "description": "Refused, nothing created. code=COMMITMENT_ALREADY_OPEN carries commitment_key, existing {id, description, lifecycle_status, due_at, expected_signal, expected_signal_at} and commands (that action's timing.available_commands, executable as-is) — continue that commitment. code=POSSIBLE_DUPLICATE carries up to 3 candidates {id, description, lifecycle_status, due_at, commands}; resend with confirm_new: true to create anyway.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "code": {
                      "type": "string",
                      "enum": [
                        "COMMITMENT_ALREADY_OPEN",
                        "POSSIBLE_DUPLICATE"
                      ]
                    },
                    "reason_code": {
                      "type": "string"
                    },
                    "commitment_key": {
                      "type": "string",
                      "nullable": true
                    },
                    "existing": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "string"
                        },
                        "description": {
                          "type": "string"
                        },
                        "lifecycle_status": {
                          "type": "string"
                        },
                        "due_at": {
                          "type": "string",
                          "nullable": true
                        },
                        "expected_signal": {
                          "type": "string",
                          "nullable": true
                        },
                        "expected_signal_at": {
                          "type": "string",
                          "nullable": true
                        }
                      }
                    },
                    "commands": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/OperationCommand"
                      }
                    },
                    "candidates": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "description": {
                            "type": "string"
                          },
                          "lifecycle_status": {
                            "type": "string"
                          },
                          "due_at": {
                            "type": "string",
                            "nullable": true
                          },
                          "commands": {
                            "type": "array",
                            "items": {
                              "$ref": "#/components/schemas/OperationCommand"
                            }
                          }
                        }
                      }
                    },
                    "hint": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/signals": {
      "post": {
        "operationId": "recordSignal",
        "summary": "record_signal: record WHAT HAPPENED in reality. actual_signal is written once and is immutable — a second call returns 409. The action moves to needs_decision; it is not resolved unless resolve: true is passed explicitly. Interpretation is a separate command (interpret-signal) and closing is another (close-loop).",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Recommended on every mutating request. A replay with the same key is NOT applied twice: the server answers { ok, idempotent_replay: true, first_seen_at, status } describing the first attempt."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "actionId": {
                    "type": "string"
                  },
                  "actualSignal": {
                    "type": "string"
                  },
                  "outcomeStatus": {
                    "type": "string",
                    "enum": [
                      "expected",
                      "partial",
                      "unexpected",
                      "deferred"
                    ]
                  },
                  "learningNote": {
                    "type": "string"
                  },
                  "resolve": {
                    "type": "boolean",
                    "default": false,
                    "description": "Optional shortcut that also resolves the loop. Prefer the explicit close-loop command."
                  }
                },
                "required": [
                  "actionId",
                  "actualSignal"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Signal recorded; action is now needs_decision",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "owner": {
                      "$ref": "#/components/schemas/Owner"
                    },
                    "action": {
                      "$ref": "#/components/schemas/Action"
                    },
                    "action_id": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "208": {
            "description": "Idempotent replay of a previous request with the same Idempotency-Key. Nothing was applied again.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IdempotentReplay"
                }
              }
            }
          },
          "404": {
            "description": "Not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "A signal is already recorded (immutable) or the loop is closed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "error": {
                      "type": "string"
                    },
                    "signal_already_recorded": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}