{"openapi":"3.1.0","info":{"title":"AfterLaunch public API","version":"1.0.0","description":"AfterLaunch is the agentic growth engine: continuous measurement of how a product gets found, kept as a record and ranked into briefed growth moves. This API is access to one AfterLaunch product run: read the ranked Feed of prepared growth moves, the automated loops and their autonomy, and drafted outputs; act on moves (ship, skip, edit the draft) and approve a drafted output for dispatch. Administrators may run loops on demand and configure loop autonomy, cadence and scans. Authenticate every request with \"Authorization: Bearer al_...\" (an API key bound to one enrollment). Reads need the read scope; acting on a move and minting a checkout need act; the metered pulls need write; administrator configuration changes need config; tracked questions retain their existing config scope and customer entitlement. A key carrying write satisfies act. Metered actions (run, approve) honour per-tenant daily and monthly spend caps and, for runs, a REQUIRED Idempotency-Key header: reusing a key replays the original result instead of spending again."},"servers":[{"url":"https://afterlaunch.io"}],"components":{"schemas":{"Error":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}},"securitySchemes":{"apiKey":{"type":"http","scheme":"bearer","description":"AfterLaunch API key, format al_<48 hex chars>."}}},"security":[{"apiKey":[]}],"paths":{"/api/v1/content/calendar":{"get":{"operationId":"getContentCalendar","summary":"Read this product run's calendar, up to 100 saved source drafts and their current canonical Feed state. Requires read. Honour queue_position and needs_data. channel_state is a stored projection; check calendar_sync and linkage before acting. x_production_enabled is the existing twitter_thread switch. Read coverage before planning: unknown or incomplete history is not an empty review pile. No generation or spend.","responses":{"200":{"description":"Up to 200 calendar pieces, ordered by queue position then recency, and up to 100 saved records ordered by source output update time.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","required":["enrollment_id","timezone","calendar","drafts","x_production_enabled","can_record_drafts","coverage"],"properties":{"enrollment_id":{"type":"string","format":"uuid"},"timezone":{"type":"string"},"calendar":{"type":"array","maxItems":200,"items":{"type":"object","required":["id","title","angle","kind","channels","channel_state","evidence","status","queue_position","link","notes","needs_data","updated_at"],"properties":{"id":{"type":"string","format":"uuid"},"title":{"type":"string"},"angle":{"type":["string","null"]},"kind":{"type":"string","enum":["opinion","educational","amplify"]},"channels":{"type":"array","items":{"type":"string"}},"channel_state":{"type":"object","description":"Stored per-channel projection, including status and output_id when present. For linked content, the canonical move and calendar_sync expose lifecycle drift. Other channels are preserved."},"evidence":{"description":"The existing JSON evidence for this piece, preserved without rewriting."},"status":{"type":"string","enum":["pool","queued","done","dismissed"]},"queue_position":{"type":["integer","null"]},"link":{"type":["string","null"]},"notes":{"type":["string","null"]},"needs_data":{"type":"boolean"},"updated_at":{"type":"string","format":"date-time"}}}},"coverage":{"type":"object","required":["ideas","drafts"],"properties":{"ideas":{"type":"object","required":["returned","total","complete"],"properties":{"returned":{"type":"integer","minimum":0},"total":{"type":["integer","null"],"minimum":0},"complete":{"type":"boolean","description":"Exact response count equals all returned, decoded records. False for unknown counts or truncation. This is a point-in-time read, not a reservation."}}},"drafts":{"type":"object","required":["returned","total","complete"],"properties":{"returned":{"type":"integer","minimum":0},"total":{"type":["integer","null"],"minimum":0},"complete":{"type":"boolean","description":"Exact response count equals all returned, decoded records. False for unknown counts or truncation. This is a point-in-time read, not a reservation."}}}}},"drafts":{"type":"array","maxItems":100,"items":{"type":"object","required":["output_id","idea_id","run_key","channel","title","source_url","why","claim_ids","claims","output_status","status","source_draft","move","prepared_result","legacy_publication_report","drafts_withheld","drafts_match","linkage","linkage_reason","calendar_sync","updated_at"],"properties":{"output_id":{"type":"string","format":"uuid"},"idea_id":{"type":"string","format":"uuid"},"run_key":{"type":"string"},"channel":{"const":"x"},"title":{"type":"string"},"source_url":{"type":["string","null"],"description":"Source evidence; also the exact destination for a classified legacy reply."},"why":{"type":"string"},"claim_ids":{"type":"array","items":{"type":"string","format":"uuid"}},"claims":{"type":"array","items":{"type":"object","required":["id","text","number","caveat"],"properties":{"id":{"type":"string","format":"uuid"},"text":{"type":"string"},"number":{"type":"string"},"caveat":{"type":"string"}}}},"output_status":{"type":"string","description":"Raw source output status. posted alone proves neither founder confirmation nor published words."},"status":{"type":"string","description":"Current canonical move status when linked, otherwise raw output status. Prepared attempt content never implies shipped."},"source_draft":{"type":["object","null"],"properties":{"storage":{"enum":["candidate","output"]},"candidate_id":{"type":["string","null"]},"text":{"type":"string"}},"description":"Current source candidate, or output fallback. Null when drafts are withheld. Not published words."},"move":{"type":["object","null"],"properties":{"id":{"type":"string","format":"uuid"},"identity_key":{"type":["string","null"]},"source_key":{"type":"string"},"source_output_id":{"type":"string","format":"uuid"},"action_kind":{"type":["string","null"]},"target":{"type":["string","null"]},"status":{"type":"string"},"updated_at":{"type":"string","format":"date-time"},"draft_content":{"type":["string","null"]},"draft_kind":{"type":["string","null"],"enum":["full_draft","brief",null]}},"description":"Ownership-bound Feed read, carrying the actual move id. A collection app_url does not open the move in focus."},"prepared_result":{"type":["object","null"],"description":"Current attempt from readPreparedMoveResult, including job/attempt identity, state, prepared_content and recorded published_url. Preparation is not publication; prepared_content is not reported final copy. Private material is withheld at the shared tier boundary."},"legacy_publication_report":{"type":["object","null"],"properties":{"basis":{"const":"human_reported"},"text":{"type":"string"},"url":{"type":"string"}},"description":"Only the old pilot explicit human_reported confirmation and its stored words/link. Never reconstructed from output status or a draft."},"drafts_withheld":{"type":"boolean"},"drafts_match":{"type":["boolean","null"]},"linkage":{"enum":["linked","incomplete","legacy_terminal"]},"linkage_reason":{"type":["string","null"]},"calendar_sync":{"enum":["linked","missing","different_output","lifecycle_differs"],"description":"Comparison with stored channel_state. The move lifecycle is authoritative; this reader does not silently repair completion projections."},"updated_at":{"type":"string","format":"date-time","description":"Source output update time. The move and attempt carry their own timestamps."}}}},"x_production_enabled":{"type":"boolean"},"can_record_drafts":{"type":"boolean","description":"True when access permits filing and X production is disabled or has not been configured. The first draft write can create disabled storage; this read creates nothing. This is not an OAuth connection check."}}},"meta":{"type":"object","properties":{"enrollment_id":{"type":"string","format":"uuid"},"generated_at":{"type":"string","format":"date-time"}},"required":["enrollment_id","generated_at"]}},"required":["data","meta"]}}}},"401":{"description":"Missing, invalid or revoked API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key does not carry the scope this endpoint requires.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The key is not bound to the current product run.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Per-key rate limit exceeded. Honour Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"operationId":"recordContent","summary":"File a research idea, original X post or append-only research outcome. Research records preserve the original question, evidence, hold and lifecycle. The caller supplies the conclusion; it is not automatic verification or permission to draft. Same-key replays preserve receipts; changed results or stale new writes conflict. Research is visible in Memory at /app/knowledge-base?open=research (requires act and current draft access). No generation, spend, posting or lifecycle decision. Stable run_key fingerprints preserve the original request. New drafts require action_kind social_post, one existing idea_id or source_idea, and disabled product X production. If no X loop exists, the first authorised draft write creates a disabled storage row without activating generation or posting. Canonical claim caveats travel with the draft. Filing creates an output, materialises a candidate only if absent, and claims a pending Feed move under the output UUID. Replays preserve edits and terminal history. New drafts are original posts. Explicit adopt_legacy recovers exact saved requests as original posts or replies with their saved X status destination. It refuses changed or unclassified actioned work and retains candidate edits. Review and completion are not part of this endpoint.","requestBody":{"required":true,"content":{"application/json":{"schema":{"oneOf":[{"type":"object","additionalProperties":false,"required":["operation","run_key","source_idea"],"properties":{"operation":{"const":"idea"},"run_key":{"type":"string","minLength":1,"maxLength":160,"pattern":"^[a-zA-Z0-9:_./-]+$"},"source_idea":{"type":"object","additionalProperties":false,"required":["title","needs_data"],"properties":{"title":{"type":"string","minLength":1,"maxLength":300,"pattern":"^(?!\\s*$)[^\\u2014]*$","description":"Non-empty prose after trimming. Em dashes are rejected."},"angle":{"type":"string","minLength":1,"maxLength":3000,"pattern":"^(?!\\s*$)[^\\u2014]*$","description":"Non-empty prose after trimming. Em dashes are rejected."},"link":{"type":"string","format":"uri","maxLength":2048,"pattern":"^https?://","description":"An HTTP or HTTPS address without embedded credentials."},"needs_data":{"type":"boolean","description":"True when this idea needs evidence before publication copy can be prepared."},"channels":{"type":"array","minItems":1,"maxItems":4,"uniqueItems":true,"items":{"enum":["x","linkedin","article","seo"]},"description":"Where the piece should go out. Omitted means x."},"kind":{"enum":["opinion","educational","amplify"],"description":"Omitted means the calendar default, educational."},"score":{"type":"number","minimum":0,"maximum":10,"description":"Priority from 0 to 10, at most two decimals. Omitted means unscored."},"final_text":{"type":"object","additionalProperties":false,"required":["channel","text"],"properties":{"channel":{"enum":["x","linkedin"],"description":"One of this idea's channels."},"text":{"type":"string","minLength":1,"maxLength":20000,"pattern":"^[^\\u2014]*\\S[^\\u2014]*$","description":"The finished post, kept byte for byte (not trimmed). Em dashes are rejected."}},"description":"A post the owner has already finished. The morning slot delivers it exactly as written, with no rewrite. Text AfterLaunch would alter or block is refused."},"desk":{"type":"object","additionalProperties":false,"required":["kind"],"properties":{"kind":{"enum":["post","reply","task","decision","read"],"description":"A post or reply needs final_text and takes its channel from it. A task has no channel; its words are the title. A decision or a read is a thing that needs the owner and is not a post: its words are the title, it takes no slot, and it stays open until ticked."},"slot_at":{"type":"string","format":"date-time","description":"The slot to file it into. Present means the item is filed queued; omitted means the backlog."},"image":{"type":"string","minLength":1,"maxLength":2000,"description":"One line: which image to attach, or its address."},"first_reply":{"type":"string","minLength":1,"maxLength":2000,"description":"X: the reply that carries the link, posted under the original."},"target_url":{"type":"string","format":"uri","maxLength":2048,"pattern":"^https?://","description":"A reply: the thread it answers. A decision or a read: the link it opens."},"due_on":{"type":"string","format":"date","description":"A decision or a read only: the local date it is due on. Omitted means undated."},"why":{"type":"string","minLength":1,"maxLength":200,"description":"A decision or a read only: one line on why it matters."},"step":{"type":"string","minLength":1,"maxLength":200,"description":"A decision or a read only: the step that is the owner's."},"source":{"type":"string","minLength":1,"maxLength":80,"description":"A decision or a read only: who raised it."},"note":{"type":"string","minLength":1,"maxLength":200,"description":"Any kind: one line the owner reads before posting, a \"Your call: ...\" included. Shown under the row on the desk."},"venue":{"enum":["hn","producthunt","reddit","email"],"description":"A task or a decision only: where it goes out when it is not X or LinkedIn. It picks the row's mark; a task or a decision without one is not drawn on the desk."}},"description":"An item for the Posting Desk, which the owner posts by hand. A desk item is kept out of the morning slot, so it goes out once."},"series":{"type":"object","additionalProperties":false,"required":["name"],"properties":{"name":{"type":"string","minLength":1,"maxLength":120,"description":"Matched among running series, case and spacing aside."},"kind":{"enum":["major_drop","minor_drop","posts","free_tool","launch","announcement","experiment_writeup"],"description":"Used only when the series is created. Omitted means posts."},"starts_on":{"type":"string","format":"date","description":"Used only when created. Omitted means the desk slot's date, else today."},"ends_on":{"type":"string","format":"date","description":"Used only when created. Omitted means open-ended."},"why":{"type":"string","minLength":1,"maxLength":200,"description":"One line on why the series exists. Required to create one."}},"description":"The run of items this one belongs to (a drop, a run of posts). Matched by name, else created; the idea is filed into it."}}}}},{"type":"object","additionalProperties":false,"required":["operation","run_key","title","text","channel","why"],"properties":{"operation":{"const":"draft"},"run_key":{"type":"string","minLength":1,"maxLength":160,"pattern":"^[a-zA-Z0-9:_./-]+$"},"idea_id":{"type":"string","format":"uuid"},"source_idea":{"type":"object","additionalProperties":false,"required":["title","needs_data"],"properties":{"title":{"type":"string","minLength":1,"maxLength":300,"pattern":"^(?!\\s*$)[^\\u2014]*$","description":"Non-empty prose after trimming. Em dashes are rejected."},"angle":{"type":"string","minLength":1,"maxLength":3000,"pattern":"^(?!\\s*$)[^\\u2014]*$","description":"Non-empty prose after trimming. Em dashes are rejected."},"link":{"type":"string","format":"uri","maxLength":2048,"pattern":"^https?://","description":"An HTTP or HTTPS address without embedded credentials."},"needs_data":{"type":"boolean","description":"True when this idea needs evidence before publication copy can be prepared."}}},"title":{"type":"string","minLength":1,"maxLength":300,"pattern":"^(?!\\s*$)[^\\u2014]*$","description":"Non-empty prose after trimming. Em dashes are rejected."},"text":{"type":"string","minLength":1,"maxLength":20000,"pattern":"^(?!\\s*$)[^\\u2014]*$","description":"Non-empty prose after trimming. Em dashes are rejected."},"channel":{"const":"x"},"action_kind":{"const":"social_post","description":"Required for new drafts. Omission is accepted only to recognise an unchanged old-pilot replay; unclassified legacy pending records are not adopted as posts."},"source_url":{"type":"string","format":"uri","maxLength":2048,"pattern":"^https?://","description":"Source evidence for new original posts. An explicit legacy reply adoption retains this exact X status URL as its destination."},"adopt_legacy":{"type":"object","additionalProperties":false,"required":["output_id","action_kind"],"properties":{"output_id":{"type":"string","format":"uuid"},"action_kind":{"enum":["social_post","community_reply"]}},"description":"Explicit recovery of an existing exact replay, never new content. Omit top-level action_kind and restate the original request. Classification preserves output, idea, original words and any existing move."},"why":{"type":"string","minLength":1,"maxLength":5000,"pattern":"^(?!\\s*$)[^\\u2014]*$","description":"Non-empty prose after trimming. Em dashes are rejected."},"claim_ids":{"type":"array","maxItems":20,"items":{"type":"string","format":"uuid"},"default":[],"description":"Canonical usable claim IDs from get_claims. Missing, held or malformed claims are refused. Their stored caveats travel with the draft."}},"oneOf":[{"required":["idea_id"],"not":{"required":["source_idea"]}},{"required":["source_idea"],"not":{"required":["idea_id"]}}]},{"type":"object","additionalProperties":false,"required":["operation","run_key","idea_id","expected_updated_at","research"],"properties":{"operation":{"const":"research"},"run_key":{"type":"string","minLength":1,"maxLength":160,"pattern":"^[a-zA-Z0-9:_./-]+$"},"idea_id":{"type":"string","format":"uuid"},"expected_updated_at":{"type":"string","format":"date-time","description":"Pass the calendar value unchanged for a new result; exact replays do not need a fresh timestamp."},"research":{"type":"object","additionalProperties":false,"required":["outcome","summary","method","limitations","sources"],"properties":{"outcome":{"type":"string","enum":["supported","not_supported","inconclusive"]},"summary":{"type":"string","minLength":1,"maxLength":5000,"pattern":"^(?!\\s*$)[^\\u2014]*$","description":"Non-empty prose after trimming. Em dashes are rejected."},"method":{"type":"string","minLength":1,"maxLength":5000,"pattern":"^(?!\\s*$)[^\\u2014]*$","description":"Non-empty prose after trimming. Em dashes are rejected."},"limitations":{"type":"string","minLength":1,"maxLength":5000,"pattern":"^(?!\\s*$)[^\\u2014]*$","description":"Non-empty prose after trimming. Em dashes are rejected."},"sources":{"type":"array","maxItems":50,"items":{"type":"object","additionalProperties":false,"required":["title","url","observed_at"],"properties":{"title":{"type":"string","minLength":1,"maxLength":300,"pattern":"^(?!\\s*$)[^\\u2014]*$","description":"Non-empty prose after trimming. Em dashes are rejected."},"url":{"type":"string","format":"uri","maxLength":2048,"pattern":"^https?://","description":"An HTTP or HTTPS address without embedded credentials."},"observed_at":{"type":"string","format":"date-time"}}}}}}}}]}}}},"responses":{"200":{"description":"The current linked record or idea receipt. A legacy terminal replay returns its history without inventing a pending move. No receipt implies posting or focused browser visibility.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"oneOf":[{"type":"object","required":["output_id","idea_id","run_key","channel","title","source_url","why","claim_ids","claims","output_status","status","source_draft","move","prepared_result","legacy_publication_report","drafts_withheld","drafts_match","linkage","linkage_reason","calendar_sync","updated_at"],"properties":{"output_id":{"type":"string","format":"uuid"},"idea_id":{"type":"string","format":"uuid"},"run_key":{"type":"string"},"channel":{"const":"x"},"title":{"type":"string"},"source_url":{"type":["string","null"],"description":"Source evidence; also the exact destination for a classified legacy reply."},"why":{"type":"string"},"claim_ids":{"type":"array","items":{"type":"string","format":"uuid"}},"claims":{"type":"array","items":{"type":"object","required":["id","text","number","caveat"],"properties":{"id":{"type":"string","format":"uuid"},"text":{"type":"string"},"number":{"type":"string"},"caveat":{"type":"string"}}}},"output_status":{"type":"string","description":"Raw source output status. posted alone proves neither founder confirmation nor published words."},"status":{"type":"string","description":"Current canonical move status when linked, otherwise raw output status. Prepared attempt content never implies shipped."},"source_draft":{"type":["object","null"],"properties":{"storage":{"enum":["candidate","output"]},"candidate_id":{"type":["string","null"]},"text":{"type":"string"}},"description":"Current source candidate, or output fallback. Null when drafts are withheld. Not published words."},"move":{"type":["object","null"],"properties":{"id":{"type":"string","format":"uuid"},"identity_key":{"type":["string","null"]},"source_key":{"type":"string"},"source_output_id":{"type":"string","format":"uuid"},"action_kind":{"type":["string","null"]},"target":{"type":["string","null"]},"status":{"type":"string"},"updated_at":{"type":"string","format":"date-time"},"draft_content":{"type":["string","null"]},"draft_kind":{"type":["string","null"],"enum":["full_draft","brief",null]}},"description":"Ownership-bound Feed read, carrying the actual move id. A collection app_url does not open the move in focus."},"prepared_result":{"type":["object","null"],"description":"Current attempt from readPreparedMoveResult, including job/attempt identity, state, prepared_content and recorded published_url. Preparation is not publication; prepared_content is not reported final copy. Private material is withheld at the shared tier boundary."},"legacy_publication_report":{"type":["object","null"],"properties":{"basis":{"const":"human_reported"},"text":{"type":"string"},"url":{"type":"string"}},"description":"Only the old pilot explicit human_reported confirmation and its stored words/link. Never reconstructed from output status or a draft."},"drafts_withheld":{"type":"boolean"},"drafts_match":{"type":["boolean","null"]},"linkage":{"enum":["linked","incomplete","legacy_terminal"]},"linkage_reason":{"type":["string","null"]},"calendar_sync":{"enum":["linked","missing","different_output","lifecycle_differs"],"description":"Comparison with stored channel_state. The move lifecycle is authoritative; this reader does not silently repair completion projections."},"updated_at":{"type":"string","format":"date-time","description":"Source output update time. The move and attempt carry their own timestamps."}}},{"type":"object","required":["idea_id","title","angle","link","needs_data","channels","kind","score","status"],"properties":{"idea_id":{"type":"string","format":"uuid"},"title":{"type":"string"},"angle":{"type":["string","null"]},"link":{"type":["string","null"]},"needs_data":{"type":"boolean"},"channels":{"type":"array","items":{"type":"string"}},"kind":{"type":"string","enum":["opinion","educational","amplify"]},"score":{"type":["number","null"]},"status":{"type":"string","enum":["pool","queued","done","dismissed"],"description":"A new idea is pool. An idempotent replay returns its current state without reopening it."}}},{"type":"object","required":["idea_id","research","needs_data","updated_at","app_url"],"properties":{"idea_id":{"type":"string","format":"uuid"},"research":{"type":"object","additionalProperties":false,"required":["outcome","summary","method","limitations","sources","run_key","request_hash","recorded_at"],"properties":{"outcome":{"type":"string","enum":["supported","not_supported","inconclusive"]},"summary":{"type":"string","minLength":1,"maxLength":5000,"pattern":"^(?!\\s*$)[^\\u2014]*$","description":"Non-empty prose after trimming. Em dashes are rejected."},"method":{"type":"string","minLength":1,"maxLength":5000,"pattern":"^(?!\\s*$)[^\\u2014]*$","description":"Non-empty prose after trimming. Em dashes are rejected."},"limitations":{"type":"string","minLength":1,"maxLength":5000,"pattern":"^(?!\\s*$)[^\\u2014]*$","description":"Non-empty prose after trimming. Em dashes are rejected."},"sources":{"type":"array","maxItems":50,"items":{"type":"object","additionalProperties":false,"required":["title","url","observed_at"],"properties":{"title":{"type":"string","minLength":1,"maxLength":300,"pattern":"^(?!\\s*$)[^\\u2014]*$","description":"Non-empty prose after trimming. Em dashes are rejected."},"url":{"type":"string","format":"uri","maxLength":2048,"pattern":"^https?://","description":"An HTTP or HTTPS address without embedded credentials."},"observed_at":{"type":"string","format":"date-time"}}}},"run_key":{"type":"string","minLength":1,"maxLength":160,"pattern":"^[a-zA-Z0-9:_./-]+$"},"request_hash":{"type":"string","pattern":"^[a-f0-9]{64}$"},"recorded_at":{"type":"string","format":"date-time"}}},"needs_data":{"type":"boolean"},"updated_at":{"type":"string","format":"date-time"},"app_url":{"type":"string"}}}]},"meta":{"type":"object","properties":{"enrollment_id":{"type":"string","format":"uuid"},"generated_at":{"type":"string","format":"date-time"}},"required":["enrollment_id","generated_at"]}},"required":["data","meta"]}}}},"400":{"description":"Invalid operation, unknown field, value outside its bounds, wrong idea selection, or an absent, held or malformed canonical claim.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, invalid or revoked API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key does not carry the scope this endpoint requires.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The key is not bound to the current product run, or the requested idea/output does not belong to it.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Changed replay, ineligible idea, enabled product X production, or incomplete candidate/move/calendar linkage. When already saved, the error includes retained_output_id. Read the calendar before retrying the same run_key; no retained output is deleted.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Per-key rate limit exceeded. Honour Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"The X production state could not be confirmed. Missing storage is created disabled only by an authorised draft write. Idea and research recording do not require that storage.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/content/experiments":{"get":{"operationId":"getExperiments","summary":"Read the growth content experiments already run for this product run, newest first by ran_at. Requires read. Each result is one observation with its stated scope, not a rate. Read-only: this never runs, claims or pays for an experiment. available is false while the experiment records cannot be read yet, and the empty list then means unread, not none.","parameters":[{"name":"idea_id","in":"query","required":false,"schema":{"type":"string","format":"uuid"},"description":"Only experiments run for this calendar idea."},{"name":"experiment_id","in":"query","required":false,"schema":{"type":"string","format":"uuid"},"description":"Only this experiment."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":50,"default":20}}],"responses":{"200":{"description":"Up to limit experiments for the bound product run.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","required":["available","experiments"],"properties":{"available":{"type":"boolean"},"experiments":{"type":"array","maxItems":50,"items":{"type":"object","required":["id","idea_id","run_id","kind","question","method","prediction","evidence","claim_ids","figure","run_date","spend_usd","ran_at"],"properties":{"id":{"type":"string","format":"uuid"},"idea_id":{"type":["string","null"],"format":"uuid"},"run_id":{"type":["string","null"],"format":"uuid"},"kind":{"type":"string","enum":["claim_lookup","engine_probe","corpus_query"]},"question":{"type":"string"},"method":{"type":["string","null"]},"prediction":{"type":["string","null"]},"evidence":{"type":["string","null"],"description":"The stored evidence prose; every line carries its scope."},"claim_ids":{"type":"array","items":{"type":"string"}},"figure":{"description":"The stored figure specification, or null when the result was not numeric."},"run_date":{"type":["string","null"],"description":"The run local date the result was stored under."},"spend_usd":{"type":"number","minimum":0},"ran_at":{"type":"string","format":"date-time"}}}}}},"meta":{"type":"object","properties":{"enrollment_id":{"type":"string","format":"uuid"},"generated_at":{"type":"string","format":"date-time"}},"required":["enrollment_id","generated_at"]}},"required":["data","meta"]}}}},"400":{"description":"An idea_id or experiment_id that is not a UUID, or a limit outside 1 to 50.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, invalid or revoked API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key does not carry the scope this endpoint requires.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The key is not bound to the current product run.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Per-key rate limit exceeded. Honour Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/whoami":{"get":{"operationId":"getWhoami","summary":"Confirm the key works and see what it is bound to, in one call: enrollment_id, key_name, key_prefix and scopes, the product_url this run is about, tier and access_state, trial_ends_at, and onboarding { snapshot_status, current_phase, feed_ready, scanned, message } (feed_ready is false while the Growth Snapshot deep scan is still building). credits is present only when metering is live. The same payload the whoami MCP tool returns, without its text screen. Read-only, zero spend.","responses":{"200":{"description":"What this key is bound to.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"onboarding":{"type":"object","properties":{"snapshot_status":{"type":"string","enum":["building","ready"],"description":"'building' while the deep scan is still running, 'ready' once it has finished."},"current_phase":{"type":["string","null"],"description":"A short honest label of what the deep scan is doing right now while building; null when ready."},"feed_ready":{"type":"boolean","description":"True once the deep scan (Stage B) has finished and the moves list holds the stronger stage-B moves."}}}}},"meta":{"type":"object","properties":{"enrollment_id":{"type":"string","format":"uuid"},"generated_at":{"type":"string","format":"date-time"}},"required":["enrollment_id","generated_at"]}},"required":["data","meta"]}}}},"401":{"description":"Missing, invalid or revoked API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key does not carry the scope this endpoint requires.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Per-key rate limit exceeded. Honour Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/feed":{"get":{"operationId":"listFeed","summary":"The ranked backlog of prepared growth moves. Live (pending) moves only by default; resolved moves (shipped, skipped, archived, expired) keep stale ranks, so they are excluded unless ?include=resolved is passed.","parameters":[{"name":"include","in":"query","required":false,"schema":{"type":"string","enum":["resolved"]},"description":"Pass 'resolved' to include resolved moves (shipped, skipped, archived, expired) alongside the live ones."},{"name":"since","in":"query","required":false,"schema":{"type":"string","format":"date-time"},"description":"Optional ISO-8601 timestamp. When set, return only moves first seen after it, the incremental \"what is new\" pull. Pass the newest first_seen_at you have already read so the same move is never returned twice. An invalid value returns 400."},{"name":"verbosity","in":"query","required":false,"schema":{"type":"string","enum":["compact","full"]},"description":"How much of each move to return. Default 'compact': every move carries has_draft and draft_kind but no material body, so a long list stays cheap to read. Pass 'full' to include draft_content on every move. For the body of a single move, prefer GET /api/v1/moves/{id}. An invalid value returns 400."}],"responses":{"200":{"description":"The ranked moves, best first. total_live counts the live (pending) backlog regardless of the include or since filters. snapshot_status, current_phase, deep_scan_eta_seconds and poll_after_seconds are always present and mirror the MCP list_feed tool.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"moves":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"title":{"type":"string"},"why":{"type":["string","null"]},"area":{"type":"string"},"dimension":{"type":["string","null"]},"action_kind":{"type":["string","null"],"description":"The move shape. Open text (not an enum) so the vocabulary can grow without a migration. Values include the brief taxonomy (html_edit, social_post, long_form_content, ...) and the synthesised-move kinds (community_answer, community_reply, community_insight, seo_fix, keyword_gap, ai_answer, competitor_response, gsc_capture, backlink_outreach, ...). community_reply / community_insight are the Community Radar moves (dark behind COMMUNITY_RADAR_ENABLED)."},"target":{"type":["string","null"]},"status":{"type":"string"},"rank":{"type":"integer"},"opportunity_score":{"type":"number"},"is_today_move":{"type":"boolean"},"estimated_minutes":{"type":["integer","null"]},"first_seen_at":{"type":"string","format":"date-time"},"posture":{"type":["string","null"],"enum":["act","draft_only","founder_voice",null],"description":"Who may do this move. 'act': the connected agent may complete it itself (a listing, a change to the founder's own site). 'draft_only': the agent drafts, a person posts (every community reply; Reddit always). 'founder_voice': the founder only, because the value is that it is them or the step binds their identity (a post from their own account, outreach from their own address, an OAuth grant, a profile claim). Always present; null for a move with no actor. Use it to split the moves into the agent's jobs and the founder's without a get_move per row."},"has_draft":{"type":"boolean","description":"Whether readable full-draft material is waiting on this move. Always present. False when the material is a brief, absent, or unavailable behind the post-trial access wall. Read draft_kind for the first two cases and the response-level drafts_withheld flag for the last."},"draft_kind":{"type":["string","null"],"enum":["full_draft","brief",null],"description":"What the readable move material is: 'full_draft' is an inspectable artefact, 'brief' is an assignment to complete, and null means absent or withheld. This describes the material; it does not certify quality or publication."},"draft_content":{"type":["string","null"],"description":"The complete, trimmed move material. It may be a full draft or a brief as declared by draft_kind. Present ONLY when ?verbosity=full is passed: the default list is compact because bodies were most of its weight. GET /api/v1/moves/{id} serves the body of one move."}}}},"total_live":{"type":"integer","description":"How many moves are live (status pending) on this product."},"snapshot_status":{"type":"string","enum":["building","ready"],"description":"The onboarding / deep-scan status, always present: 'building' while the deep scan is still running, 'ready' once it has finished."},"current_phase":{"type":["string","null"],"description":"A short honest label of what the deep scan is doing right now while building; null when ready."},"deep_scan_eta_seconds":{"type":["integer","null"],"description":"While building, the rough number of seconds until the deep scan is expected to finish (floored so it stays positive); null when ready."},"poll_after_seconds":{"type":["integer","null"],"description":"While building, how long to wait before polling the feed again; null when ready."},"more_moves_generating":{"type":"boolean","description":"Present and true ONLY while the deep scan is still running (stronger stage-B moves are still being generated and will land on a later read). Absent once the scan has finished. When present, re-read the feed after suggested_recheck_after_seconds before treating this list as final."},"suggested_recheck_after_seconds":{"type":"integer","description":"How long to wait before re-reading the feed when more_moves_generating is present. Absent otherwise."}}},"meta":{"type":"object","properties":{"enrollment_id":{"type":"string","format":"uuid"},"generated_at":{"type":"string","format":"date-time"}},"required":["enrollment_id","generated_at"]}},"required":["data","meta"]}}}},"400":{"description":"An invalid since value (not a parseable timestamp).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"401":{"description":"Missing, invalid or revoked API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key does not carry the scope this endpoint requires.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Per-key rate limit exceeded. Honour Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/loops":{"get":{"operationId":"listLoops","summary":"Every loop PROVISIONED on the enrollment with its autonomy, plus runnable_loop_types: the loop types POST /loops/{loopType}/run accepts. The sets differ: a runnable type must also be provisioned and enabled here before it can run.","responses":{"200":{"description":"The provisioned loops and the runnable loop types.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"loops":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"loop_type":{"type":"string"},"enabled":{"type":"boolean"},"autonomy":{"type":"string","description":"Resolved autonomy rung: review, one_click or autonomous."},"created_at":{"type":"string","format":"date-time"}}}},"runnable_loop_types":{"type":"array","items":{"type":"string"},"description":"The loop types run_loop accepts (types with an on-demand generate event)."}}},"meta":{"type":"object","properties":{"enrollment_id":{"type":"string","format":"uuid"},"generated_at":{"type":"string","format":"date-time"}},"required":["enrollment_id","generated_at"]}},"required":["data","meta"]}}}},"401":{"description":"Missing, invalid or revoked API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key does not carry the scope this endpoint requires.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Per-key rate limit exceeded. Honour Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/loops/{loopType}":{"get":{"operationId":"getLoop","summary":"One loop by type.","parameters":[{"name":"loopType","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The loop.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"loop":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"loop_type":{"type":"string"},"enabled":{"type":"boolean"},"autonomy":{"type":"string","description":"Resolved autonomy rung: review, one_click or autonomous."},"created_at":{"type":"string","format":"date-time"}}}}},"meta":{"type":"object","properties":{"enrollment_id":{"type":"string","format":"uuid"},"generated_at":{"type":"string","format":"date-time"}},"required":["enrollment_id","generated_at"]}},"required":["data","meta"]}}}},"401":{"description":"Missing, invalid or revoked API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key does not carry the scope this endpoint requires.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No loop of that type on this enrollment.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"429":{"description":"Per-key rate limit exceeded. Honour Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/enrollment":{"get":{"operationId":"getEnrollment","summary":"The product-run summary and delivery settings, including the product identity: product_url (the site this run is about), tier (free, founder, growth or scale) and trial_ends_at. Also carries onboarding { snapshot_status, current_phase, feed_ready } (the same block the whoami MCP tool returns): if this product just onboarded, feed_ready is false while the Growth Snapshot deep scan is still building. When credit metering is live the response also carries credits { included, topup, total, pct } (pct is the fuel-gauge fill 0-100, or null when there is no allowance to gauge against); the field is absent while metering is off.","responses":{"200":{"description":"The enrollment.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"onboarding":{"type":"object","properties":{"snapshot_status":{"type":"string","enum":["building","ready"],"description":"'building' while the deep scan is still running, 'ready' once it has finished."},"current_phase":{"type":["string","null"],"description":"A short honest label of what the deep scan is doing right now while building; null when ready."},"feed_ready":{"type":"boolean","description":"True once the deep scan (Stage B) has finished and the moves list holds the stronger stage-B moves."}}}}},"meta":{"type":"object","properties":{"enrollment_id":{"type":"string","format":"uuid"},"generated_at":{"type":"string","format":"date-time"}},"required":["enrollment_id","generated_at"]}},"required":["data","meta"]}}}},"401":{"description":"Missing, invalid or revoked API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key does not carry the scope this endpoint requires.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Enrollment not found.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"429":{"description":"Per-key rate limit exceeded. Honour Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/outputs":{"get":{"operationId":"listOutputs","summary":"The enrollment's drafted outputs (summary; newest first).","responses":{"200":{"description":"The drafted outputs (id, loop_type, kind, status, title).","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"outputs":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"loop_type":{"type":"string"},"kind":{"type":"string"},"status":{"type":"string"},"title":{"type":["string","null"]},"created_at":{"type":"string","format":"date-time"}}}}}},"meta":{"type":"object","properties":{"enrollment_id":{"type":"string","format":"uuid"},"generated_at":{"type":"string","format":"date-time"}},"required":["enrollment_id","generated_at"]}},"required":["data","meta"]}}}},"401":{"description":"Missing, invalid or revoked API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key does not carry the scope this endpoint requires.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Per-key rate limit exceeded. Honour Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/outputs/{id}":{"get":{"operationId":"getOutput","summary":"A single drafted output.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The output.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object"},"meta":{"type":"object","properties":{"enrollment_id":{"type":"string","format":"uuid"},"generated_at":{"type":"string","format":"date-time"}},"required":["enrollment_id","generated_at"]}},"required":["data","meta"]}}}},"401":{"description":"Missing, invalid or revoked API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key does not carry the scope this endpoint requires.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found (including cross-tenant ids).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"429":{"description":"Per-key rate limit exceeded. Honour Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/visibility":{"get":{"operationId":"getVisibility","summary":"The measured AI visibility (GEO) results: share of voice, per-engine coverage, tracked questions with per-engine presence, cited sources with their outreach play, competitor gaps, trend, crawlability, plus the off-site reach signals (community mentions, threads worth answering, listings presence). discovered_at advances as a new scan lands. Full engine answer text is opt-in per question via ?prompt (tier-gated exactly as in the app).","parameters":[{"name":"prompt","in":"query","required":false,"schema":{"type":"string"},"description":"Drill into one tracked question by its id (from the prompts list): returns that prompt with the full engine answer text and citations where your plan includes it."}],"responses":{"200":{"description":"The visibility payload (or, with ?prompt, the single-question drill).","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"discovered_at":{"type":["string","null"],"format":"date-time"},"finding_source":{"type":["string","null"],"enum":["finding","snapshot",null],"description":"Whether this is a live scan ('finding') or the day-0 snapshot baseline ('snapshot')."},"brand_name":{"type":"string"},"own_domain":{"type":["string","null"]},"tier":{"type":"string"},"site_readability":{"type":"string"},"summary":{"type":["object","null"]},"engines":{"type":"array","items":{"type":"object"}},"share_of_voice":{"type":"array","items":{"type":"object"}},"prompts":{"type":"array","items":{"type":"object"}},"competitor_gaps":{"type":"array","items":{"type":"object"}},"top_cited_sites":{"type":"array","items":{"type":"object"}},"trusted_sources":{"type":"array","items":{"type":"object"}},"crawlability":{"type":["object","null"]},"trend":{"type":"array","items":{"type":"object"}},"movement":{"type":["object","null"]},"scan_profile":{"type":["object","null"]},"tracked_prompts":{"type":["array","null"],"items":{"type":"string"}},"custom_prompts":{"type":"array","items":{"type":"string"}},"community_mentions":{"type":["object","null"]},"community_threads":{"type":"array","items":{"type":"object"}},"listings_presence":{"type":["object","null"]}}},"meta":{"type":"object","properties":{"enrollment_id":{"type":"string","format":"uuid"},"generated_at":{"type":"string","format":"date-time"}},"required":["enrollment_id","generated_at"]}},"required":["data","meta"]}}}},"401":{"description":"Missing, invalid or revoked API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key does not carry the scope this endpoint requires.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No visibility data for this enrollment, an unknown prompt id, or a key bound to a superseded product run.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"429":{"description":"Per-key rate limit exceeded. Honour Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/seo":{"get":{"operationId":"getSeo","summary":"The site-wide SEO audit: issue groups with severity, affected pages and recommendations (recommendations blank on the free tier, as in the app), the weekly score trend, page performance (Core Web Vitals), links and domain authority with movement, and the day-0 search ranks. ?include=pages adds the per-page audit detail.","parameters":[{"name":"include","in":"query","required":false,"schema":{"type":"string","enum":["pages"]},"description":"Pass 'pages' for the per-page audit detail."}],"responses":{"200":{"description":"The SEO audit payload.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"discovered_at":{"type":["string","null"],"format":"date-time"},"finding_source":{"type":["string","null"]},"tier":{"type":"string"},"site_url":{"type":["string","null"]},"total_issues":{"type":"integer"},"counts":{"type":["object","null"]},"pages_audited":{"type":"integer"},"discovery_source":{"type":["string","null"]},"summary":{"type":["string","null"]},"issue_groups":{"type":"array","items":{"type":"object"}},"score_trend":{"type":["array","null"],"items":{"type":"object"}},"performance":{"type":["object","null"]},"links":{"type":["object","null"]},"serp":{"type":["object","null"]},"outreach_opportunity_count":{"type":"integer"},"pages":{"type":"array","items":{"type":"object"},"description":"Present only with ?include=pages."}}},"meta":{"type":"object","properties":{"enrollment_id":{"type":"string","format":"uuid"},"generated_at":{"type":"string","format":"date-time"}},"required":["enrollment_id","generated_at"]}},"required":["data","meta"]}}}},"401":{"description":"Missing, invalid or revoked API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key does not carry the scope this endpoint requires.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No enrollment data, or a key bound to a superseded product run.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"429":{"description":"Per-key rate limit exceeded. Honour Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/competitors":{"get":{"operationId":"getCompetitors","summary":"The competitor roster with the measured stat battery for you and each rival (performance, SEO, Core Web Vitals, domain age and authority, G2 and Product Hunt, content freshness), including the founder's own list edits. Read before outreach so a rival is never a pitch target. Nulls are honest not-measured values, never fabricated zeros.","responses":{"200":{"description":"The roster and batteries.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object"},"meta":{"type":"object","properties":{"enrollment_id":{"type":"string","format":"uuid"},"generated_at":{"type":"string","format":"date-time"}},"required":["enrollment_id","generated_at"]}},"required":["data","meta"]}}}},"401":{"description":"Missing, invalid or revoked API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key does not carry the scope this endpoint requires.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No enrollment data, or a key bound to a superseded product run.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"429":{"description":"Per-key rate limit exceeded. Honour Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/outcomes":{"get":{"operationId":"getOutcomes","summary":"What the shipped growth moves earned: tracked-link clicks per move, shipped-move counters, the weekly movement scoreboard, and the GA4 / Search Console connection state. The per-move rows and the scoreboard are Founder-tier (stripped server-side for a free key, exactly as in the app); the aggregate headline survives for every tier.","responses":{"200":{"description":"The outcomes payload.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object"},"meta":{"type":"object","properties":{"enrollment_id":{"type":"string","format":"uuid"},"generated_at":{"type":"string","format":"date-time"}},"required":["enrollment_id","generated_at"]}},"required":["data","meta"]}}}},"401":{"description":"Missing, invalid or revoked API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key does not carry the scope this endpoint requires.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No enrollment data, or a key bound to a superseded product run.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"429":{"description":"Per-key rate limit exceeded. Honour Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/snapshot":{"get":{"operationId":"getSnapshot","summary":"The scan this product run started from: positioning, the prioritised actions with impact and effort, the competitor set, and the day-0 SEO and AI visibility baselines. The stable business-context read; for live signal data use /api/v1/visibility and /api/v1/seo. An enrollment with no snapshot yet answers 200 with has_snapshot false.","responses":{"200":{"description":"The snapshot payload (has_snapshot false when none exists yet).","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object"},"meta":{"type":"object","properties":{"enrollment_id":{"type":"string","format":"uuid"},"generated_at":{"type":"string","format":"date-time"}},"required":["enrollment_id","generated_at"]}},"required":["data","meta"]}}}},"401":{"description":"Missing, invalid or revoked API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key does not carry the scope this endpoint requires.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No enrollment data, or a key bound to a superseded product run.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"429":{"description":"Per-key rate limit exceeded. Honour Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/activity":{"get":{"operationId":"getActivity","summary":"The product timeline: what the agent and the founder already did and why (drafts, ships, skips, scans, observations, decisions with rationale), newest first. Read before acting so recent work is never redone or contradicted.","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":200,"default":50},"description":"How many entries to return (clamped to 1..200)."}],"responses":{"200":{"description":"The timeline entries, newest first.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"window_days":{"type":"integer"},"truncated":{"type":"boolean"},"entries":{"type":"array","items":{"type":"object"}}}},"meta":{"type":"object","properties":{"enrollment_id":{"type":"string","format":"uuid"},"generated_at":{"type":"string","format":"date-time"}},"required":["enrollment_id","generated_at"]}},"required":["data","meta"]}}}},"401":{"description":"Missing, invalid or revoked API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key does not carry the scope this endpoint requires.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No enrollment data, or a key bound to a superseded product run.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"429":{"description":"Per-key rate limit exceeded. Honour Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/kb/pages":{"get":{"operationId":"listKbPages","summary":"The Memory pages (the shared record the growth engine drafts from): summaries with a content-size hint, no bodies. Fetch one body via /api/v1/kb/pages/{slug}. Read-only exposure; uploads are deliberately not on the machine surface, and the Memory chat is retired product-wide.","responses":{"200":{"description":"The page summaries.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object"},"meta":{"type":"object","properties":{"enrollment_id":{"type":"string","format":"uuid"},"generated_at":{"type":"string","format":"date-time"}},"required":["enrollment_id","generated_at"]}},"required":["data","meta"]}}}},"401":{"description":"Missing, invalid or revoked API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key does not carry the scope this endpoint requires.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No enrollment data, or a key bound to a superseded product run.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"429":{"description":"Per-key rate limit exceeded. Honour Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/kb/pages/{slug}":{"get":{"operationId":"getKbPage","summary":"One Memory page by slug, with its full markdown body (capped at 20000 characters; truncated flags a cut tail). Slugs come from /api/v1/kb/pages.","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The page.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object"},"meta":{"type":"object","properties":{"enrollment_id":{"type":"string","format":"uuid"},"generated_at":{"type":"string","format":"date-time"}},"required":["enrollment_id","generated_at"]}},"required":["data","meta"]}}}},"401":{"description":"Missing, invalid or revoked API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key does not carry the scope this endpoint requires.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Unknown slug (including cross-tenant slugs), or a key bound to a superseded product run.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"429":{"description":"Per-key rate limit exceeded. Honour Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/kb/voice-profile":{"get":{"operationId":"getVoiceProfile","summary":"The structured voice the engine drafts in: tone, characteristic phrases, vocabulary, sentence style and what it avoids, capped exactly as the drafting context caps them. `register` names which voice this is and the registers do not sound alike: 'brand' is the voice of the product's own website, 'personal' is the founder's own extracted from writing samples they supplied, 'imported' came from an imported brief, and null is a producer that has not been classified. `source` is the same fact in the older producer vocabulary ('site', 'uploaded_samples', 'imported'). Pass ?register= to ask for one register rather than the current row; has_profile is false, with a null profile, when that voice has not been extracted yet.","parameters":[{"name":"register","in":"query","required":false,"schema":{"type":"string","enum":["brand","personal","imported"]}}],"responses":{"200":{"description":"The requested voice profile, or has_profile false.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object"},"meta":{"type":"object","properties":{"enrollment_id":{"type":"string","format":"uuid"},"generated_at":{"type":"string","format":"date-time"}},"required":["enrollment_id","generated_at"]}},"required":["data","meta"]}}}},"401":{"description":"Missing, invalid or revoked API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key does not carry the scope this endpoint requires.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No enrollment data, or a key bound to a superseded product run.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"429":{"description":"Per-key rate limit exceeded. Honour Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/kb/business-context":{"get":{"operationId":"getBusinessContext","summary":"The structured positioning facts the engine drafts from: strategic_positioning (one-liner, primary value prop, target audience, notable differentiators), category, positioning_language, icp, pricing_model, feature_taxonomy, each with the updated_at of the row it came from, plus the competitor set with how each rival is similar and how it differs. A fact that has not been extracted is null rather than an error, and the competitor roster is empty rather than absent when no snapshot has run.","responses":{"200":{"description":"The positioning facts, each nullable.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object"},"meta":{"type":"object","properties":{"enrollment_id":{"type":"string","format":"uuid"},"generated_at":{"type":"string","format":"date-time"}},"required":["enrollment_id","generated_at"]}},"required":["data","meta"]}}}},"401":{"description":"Missing, invalid or revoked API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key does not carry the scope this endpoint requires.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No enrollment data, or a key bound to a superseded product run.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"429":{"description":"Per-key rate limit exceeded. Honour Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/kb/claims":{"get":{"operationId":"getClaims","summary":"The claim library on this product run: the measured numbers the founder will stand behind in public, newest first. Each carries text (the claim in one sentence), number (the measurement with its unit and comparison), trigger (keywords saying when it is the relevant one), caveat (stated against the founder's own interest) and held. A claim marked held is on record and NOT for use. Never cite a claim without its caveat. total is how many are on record and usable is how many are not held; an empty library answers 200 with an empty list, never an error. Read-only, zero spend.","responses":{"200":{"description":"claims carries the library, newest first; total and usable count it.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object"},"meta":{"type":"object","properties":{"enrollment_id":{"type":"string","format":"uuid"},"generated_at":{"type":"string","format":"date-time"}},"required":["enrollment_id","generated_at"]}},"required":["data","meta"]}}}},"401":{"description":"Missing, invalid or revoked API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key does not carry the scope this endpoint requires.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No enrollment data, or a key bound to a superseded product run.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"429":{"description":"Per-key rate limit exceeded. Honour Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/kb/claims/record":{"post":{"operationId":"recordClaim","summary":"Put one measured number the founder will stand behind in public, with the caveat that travels with it, into this product run's claim library. Scope 'act'; free, unmetered, and available on every plan. Keyed on the claim text, so recording the same claim twice supersedes the earlier wording rather than duplicating it, and a claim the founder has pinned is left untouched and reported as already recorded. Bounded to a fixed number of claims per product run per day, claimed atomically before the write.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"text":{"type":"string","minLength":10,"maxLength":300,"description":"The claim in one sentence, as it would be said out loud."},"trigger":{"type":"array","items":{"type":"string","minLength":2,"maxLength":60},"minItems":1,"maxItems":10,"description":"Keywords saying when this claim is the relevant one."},"number":{"type":"string","minLength":1,"maxLength":200,"description":"The measurement, with its unit and what it is measured against."},"caveat":{"type":"string","minLength":10,"maxLength":500,"description":"What the number does not prove, stated against the founder's own interest."},"held":{"type":"boolean","description":"True to record it and block its use. Defaults to false."}},"required":["text","trigger","number","caveat"],"additionalProperties":false}}}},"responses":{"200":{"description":"Recorded. recorded says whether a row was written; superseded_previous says whether it replaced an earlier wording of the same claim; board_now carries the top moves as they stand, and is absent when that read failed.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object"},"meta":{"type":"object","properties":{"enrollment_id":{"type":"string","format":"uuid"},"generated_at":{"type":"string","format":"date-time"}},"required":["enrollment_id","generated_at"]}},"required":["data","meta"]}}}},"400":{"description":"A field is outside its bounds, or trigger is empty.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"401":{"description":"Missing, invalid or revoked API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key does not carry the scope this endpoint requires.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"This product run has reached today's claim limit. Honour Retry-After.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}}}}},"/api/v1/kb/voice-axes":{"post":{"operationId":"setVoiceAxes","summary":"Write the calibrated leans from a voice round into the FOUNDER'S OWN voice profile, the personal register. It can never touch the brand register: the brand voice came from the website, is what pages and listings are written in, and this endpoint takes no parameter that could point at it. Scope 'act'; free, unmetered, every plan. Merges per axis, so an axis you do not send keeps what it had and the descriptive voice fields (tone, characteristic phrases, vocabulary, sentence style, avoid) are untouched. A personal profile the founder has pinned is left as it is and reported as already recorded. An unknown axis id is refused rather than ignored, because a lean silently dropped is a calibration lost. Bounded to a fixed number of writes per product run per day, claimed atomically before the write.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"axes":{"type":"object","description":"Keyed by axis id, each { lean, strength }. lean is 'a' or 'b', the pole this voice sits nearer; strength is the share of that axis's observations that fell on the leaning side, so 0.5 is an even split and 1 is every observation one way.","additionalProperties":{"type":"object","properties":{"lean":{"type":"string","enum":["a","b"]},"strength":{"type":"number","minimum":0.5,"maximum":1}},"required":["lean","strength"],"additionalProperties":false}}},"required":["axes"],"additionalProperties":false}}}},"responses":{"200":{"description":"Recorded. register is always personal; axes carries every axis now on record, read back off the written row; created_profile says whether this founder had no personal voice before now.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object"},"meta":{"type":"object","properties":{"enrollment_id":{"type":"string","format":"uuid"},"generated_at":{"type":"string","format":"date-time"}},"required":["enrollment_id","generated_at"]}},"required":["data","meta"]}}}},"400":{"description":"axes is missing, empty, names an axis id that does not exist, or carries a lean or strength outside its range.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"401":{"description":"Missing, invalid or revoked API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key does not carry the scope this endpoint requires.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"This product run has reached today's voice-axes limit. Honour Retry-After.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}}}}},"/api/v1/kb/voice-rules/record":{"post":{"operationId":"recordVoiceRule","summary":"Save one writing rule the person approved into their OWN voice profile, the personal register, never the brand's. The morning posts drafted for them follow it, get_move and get_voice_profile carry it, and they see and remove it on their Memory page. Record a rule only after the person approved it in words; never one you inferred. Scope 'act'; free, unmetered, every plan. The voice keeps at most 12 rules and a 13th is refused rather than replacing one. The same rule twice is recognised and left as it is. A pinned voice profile still takes the rule: the pin protects the descriptive voice, which this write leaves untouched. Validation, a repeat and a full voice are all answered before the daily write allowance is claimed; a write that passes them claims it atomically first.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"text":{"type":"string","minLength":10,"maxLength":200,"description":"The rule as they approved it, in one sentence."}},"required":["text"],"additionalProperties":false}}}},"responses":{"200":{"description":"Recorded, or the same rule was already on record (recorded false, reason already_approved). recorded says whether a row was written; rule is the rule; rules carries every approved rule now on record, read back off the written row; created_profile says whether this person had no voice of their own before now.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object"},"meta":{"type":"object","properties":{"enrollment_id":{"type":"string","format":"uuid"},"generated_at":{"type":"string","format":"date-time"}},"required":["enrollment_id","generated_at"]}},"required":["data","meta"]}}}},"400":{"description":"text is missing or outside its bounds.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"401":{"description":"Missing, invalid or revoked API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key does not carry the scope this endpoint requires.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Not saved. The voice already holds its 12 approved rules (the person removes one on their Memory page first), or the profile changed while the rule was being written (send it again).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"429":{"description":"This product run has reached today's writing-rule limit. Honour Retry-After.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}}}}},"/api/v1/profiles/record":{"post":{"operationId":"recordProfile","summary":"File one public profile or listing the person already has on a directory site (Settings, Your profiles), so AfterLaunch counts it as a verified presence and a weekly check confirms it is still live. Scope 'act'; free, unmetered, every plan. The same validation the Settings form runs: surface is one of the directory site ids, and url must sit on that site's own host with a profile-shaped path. One address per site: a new address replaces the old one and clears its earlier confirmation, and the same address again is answered without a write. The save runs one free check that the page is live, at most one per product run per 30 seconds.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"surface":{"type":"string","enum":["g2","capterra","getapp","software-advice","trustradius","clutch","trustpilot","product-hunt","alternativeto","saashub","betalist","sourceforge","uneed","theres-an-ai-for-that","openalternative","awesome-selfhosted","railway-templates","vercel-templates","futurepedia","hacker-news","indie-hackers","reddit","quora","wikidata","crunchbase","linkedin-company","google-business-profile","github","wikipedia","youtube-channel","stackshare"],"description":"The directory site id."},"url":{"type":"string","minLength":1,"maxLength":500,"description":"The full https address of their profile on that site."}},"required":["surface","url"],"additionalProperties":false}}}},"responses":{"200":{"description":"Recorded, or the same address was already saved (recorded false, reason already_saved). replaced_url is the address this one replaced on the same site, or null; verified says whether the free check confirmed the page live just now (false is not a failure: the weekly check confirms it later); profiles carries every profile on record.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object"},"meta":{"type":"object","properties":{"enrollment_id":{"type":"string","format":"uuid"},"generated_at":{"type":"string","format":"date-time"}},"required":["enrollment_id","generated_at"]}},"required":["data","meta"]}}}},"400":{"description":"surface is not a directory site id, or url is missing, too long, or not a profile address on that site.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"401":{"description":"Missing, invalid or revoked API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key does not carry the scope this endpoint requires.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No product run is bound to this key, or it is superseded.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"429":{"description":"Per-key rate limit exceeded. Honour Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Not saved: the profiles could not be read or written. Send it again.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}}}}},"/api/v1/competitors/add":{"post":{"operationId":"addCompetitor","summary":"Add one company the person confirmed they compete with to their rival roster (Memory, Who you're up against), which the next AI visibility scan measures them against and every competitor read carries. Scope 'act'; free, unmetered, every plan. The same hand edit the Memory roster editor makes: it never runs the paid research the in-app add does. domain may be a bare domain or a full address; the rival is stored as its https home page. A rival already on the roster, by domain or by name ignoring case, is answered without a write. At most 10 rivals: past that the add is refused until one is removed in Memory.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"domain":{"type":"string","minLength":1,"maxLength":2000,"description":"Their website, e.g. example.com."},"name":{"type":"string","maxLength":120,"description":"Optional. The company name; defaults to the domain."}},"required":["domain"],"additionalProperties":false}}}},"responses":{"200":{"description":"Added, or the rival was already on the roster (added false, reason already_tracked). competitor is the rival added or matched; the rest is the roster as the competitors read serves it, read back after the write, and is absent when that read failed.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object"},"meta":{"type":"object","properties":{"enrollment_id":{"type":"string","format":"uuid"},"generated_at":{"type":"string","format":"date-time"}},"required":["enrollment_id","generated_at"]}},"required":["data","meta"]}}}},"400":{"description":"domain is not a public website address, or name is too long.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"401":{"description":"Missing, invalid or revoked API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key does not carry the scope this endpoint requires.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No product run is bound to this key, or it is superseded.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"409":{"description":"Not added: the roster already holds 10 rivals. The person removes one in Memory first.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"429":{"description":"Per-key rate limit exceeded. Honour Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Not added: the roster could not be read or written. Send it again.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}}}}},"/api/v1/standup":{"get":{"operationId":"getStandup","summary":"The daily growth standup in one call: receipts (recently shipped moves with what was verified live and the clicks they earned), ready (the top ranked moves that can be shipped now, each with its deliverable line), while_you_were_gone (what the engine did on its own), in_flight (deep-scan status, phase and ETA) and next_scan_at. There is ONE list of work and it is the moves list: a needs_you list sat here until 2026-08-21 and every item on it was a move ready already carried, so it was deleted rather than filtered. Composed from the same loaders the app renders from; it never advances the \"what is new\" anchor in Growth moves.","responses":{"200":{"description":"The standup payload. as_of is when it was composed; message is a ready-to-relay narration; drafts_withheld mirrors the list_feed paywall flag.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object"},"meta":{"type":"object","properties":{"enrollment_id":{"type":"string","format":"uuid"},"generated_at":{"type":"string","format":"date-time"}},"required":["enrollment_id","generated_at"]}},"required":["data","meta"]}}}},"401":{"description":"Missing, invalid or revoked API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key does not carry the scope this endpoint requires.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No enrollment data, or a key bound to a superseded product run.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"429":{"description":"Per-key rate limit exceeded. Honour Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/moves/{id}":{"get":{"operationId":"getMove","summary":"One growth move in full. Source move material remains distinct from prepared_result, the durable worker result for this exact move and attempt. A ready result awaits founder review and is not published. lifecycle_move and prepared_result carry the exact timestamps, nullable attempt and legacy state needed for stale-safe acceptance and review. The existing access rule gates both bodies; prepared_result_withheld never claims whether a hidden result exists. Read-only; free.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The move brief.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object"},"meta":{"type":"object","properties":{"enrollment_id":{"type":"string","format":"uuid"},"generated_at":{"type":"string","format":"date-time"}},"required":["enrollment_id","generated_at"]}},"required":["data","meta"]}}}},"401":{"description":"Missing, invalid or revoked API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key does not carry the scope this endpoint requires.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Unknown move id (including cross-tenant ids), or a key bound to a superseded product run.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"429":{"description":"Per-key rate limit exceeded. Honour Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/moves/{id}/accept":{"post":{"operationId":"acceptMove","summary":"Accept a move after the founder says yes. Atomically records or reuses one durable preparation attempt. A configured worker starts only when launch_won is true; an unchanged response never starts another worker. No completion or publication is claimed. Scope 'act'; free.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"operation":{"type":"string","enum":["accept","retry"],"default":"accept","description":"Retry requires expected_attempt_id; use null only with the exact legacy state and updated_at."},"target":{"type":"string","enum":["agent_pull","share","open","claude_cloud","cursor","codex"]},"expected_attempt_id":{"type":["string","null"],"format":"uuid","description":"Required for retry. Pass the exact managed attempt, or null for a legacy job."},"expected_legacy_state":{"type":["string","null"],"description":"get_move: prepared_result.legacy_state ?? state."},"expected_legacy_updated_at":{"type":["string","null"],"format":"date-time"},"note":{"type":["string","null"],"maxLength":2000}},"required":["target"]}}}},"responses":{"200":{"description":"Committed acceptance or unchanged replay. The job carries its exact attempt_id.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object"},"meta":{"type":"object","properties":{"enrollment_id":{"type":"string","format":"uuid"},"generated_at":{"type":"string","format":"date-time"}},"required":["enrollment_id","generated_at"]}},"required":["data","meta"]}}}},"401":{"description":"Missing, invalid or revoked API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key does not carry the scope this endpoint requires.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Unknown or cross-tenant move.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"409":{"description":"Stale attempt, illegal transition, or lifecycle migration unavailable.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"429":{"description":"Per-key rate limit exceeded. Honour Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/moves/{id}/ship":{"post":{"operationId":"shipMove","summary":"Record explicit founder-confirmed completion. prepared_result confirms the exact reviewed attempt; independent_work records work the founder performed separately, including manual tasks with no public URL. With no published_url, the completion takes the address the move already holds (a planned page on the founder's own site, the page a site edit changed, the thread a reply went into) and ships with it. A change to the founder's own site with no address sent and none held, or one sent with planned_url, is recorded as prepared (status 'prepared', lifecycle_status 'taken_on'), not shipped. A planned_url is the AI saying the change is merged and deploying: AfterLaunch checks that page, marks the move shipped once the text is live, and after two days of checks ships it as not verified. Nothing is refused for want of an address. planned_url may be given with the prepared result or on a later call: a prepared result saved with no link takes it, the same link again changes nothing, and only a different saved link is refused. Every 200 carries message, one line for a connected AI to relay, in the same words the MCP screen leads with. This endpoint records an attestation and never publishes. A prepared asset URL is not a published URL. Scope 'act'; free.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"completion_kind":{"type":"string","enum":["prepared_result","independent_work"]},"job_id":{"type":["string","null"],"format":"uuid"},"attempt_id":{"type":["string","null"],"format":"uuid"},"expected_move_state":{"type":["string","null"],"enum":["pending","taken_on","shipped","skipped","archived","expired",null],"description":"Required with expected_move_updated_at for independent_work."},"expected_move_updated_at":{"type":["string","null"],"format":"date-time","description":"Required with expected_move_state for independent_work."},"expected_legacy_state":{"type":["string","null"],"description":"get_move: prepared_result.legacy_state ?? state."},"expected_legacy_updated_at":{"type":["string","null"],"format":"date-time"},"founder_confirmed":{"type":"boolean","const":true},"published_url":{"type":["string","null"],"description":"Actual public URL when one exists. Required for a change to the founder's own site to count as shipped."},"planned_url":{"type":["string","null"],"description":"The page a merged site change lives at, while it deploys. Must be on the founder's own site. Recorded with the prepared result, including one saved earlier with no link or with a pull request; never counted as publication: AfterLaunch checks the page and ships the move once the text is live."},"note":{"type":["string","null"],"maxLength":2000},"final_text":{"type":["string","null"],"maxLength":20000,"description":"Optional. Only the words the founder actually posted, exactly as they went out; never your rewrite or the draft. Used only to learn how they write, for a post or reply they published; it marks nothing published. Omit it if unknown."}},"required":["completion_kind","job_id","attempt_id","founder_confirmed"]}}}},"responses":{"200":{"description":"Shipped, or prepared when a change to the founder's own site arrived with no published_url (idempotent on repeat).","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object"},"meta":{"type":"object","properties":{"enrollment_id":{"type":"string","format":"uuid"},"generated_at":{"type":"string","format":"date-time"}},"required":["enrollment_id","generated_at"]}},"required":["data","meta"]}}}},"401":{"description":"Missing, invalid or revoked API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key does not carry the scope this endpoint requires.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"409":{"description":"Already skipped or otherwise resolved, a stale attempt, or a planned_url that differs from the link the prepared result already carries.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"429":{"description":"Per-key rate limit exceeded. Honour Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/moves/{id}/review":{"post":{"operationId":"reviewMove","summary":"Record an explicit founder review decision: revise, withdraw, adopt an inspected legacy result, or correct a publication record. Revision creates a new attempt only on a won transaction. This endpoint never publishes. Scope 'act'; free.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"operation":{"type":"string","enum":["revise","withdraw","adopt_legacy_result","correct_publication"],"description":"adopt_legacy_result requires a null expected attempt, exact move CAS, exact legacy job CAS when job_id is present, and inspected prepared content or asset URL."},"job_id":{"type":["string","null"],"format":"uuid"},"expected_attempt_id":{"type":["string","null"],"format":"uuid","description":"Exact managed attempt. Null identifies legacy work and requires exact move CAS plus legacy job CAS when job_id is present."},"expected_move_state":{"type":["string","null"],"enum":["pending","taken_on","shipped","skipped","archived","expired",null]},"expected_move_updated_at":{"type":["string","null"],"format":"date-time"},"expected_legacy_state":{"type":["string","null"],"description":"get_move: prepared_result.legacy_state ?? state."},"expected_legacy_updated_at":{"type":["string","null"],"format":"date-time"},"prepared_content":{"type":["string","null"],"maxLength":20000},"asset_url":{"type":["string","null"]},"note":{"type":["string","null"],"maxLength":2000}},"required":["operation","job_id","expected_attempt_id"]}}}},"responses":{"200":{"description":"Review decision committed or unchanged.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object"},"meta":{"type":"object","properties":{"enrollment_id":{"type":"string","format":"uuid"},"generated_at":{"type":"string","format":"date-time"}},"required":["enrollment_id","generated_at"]}},"required":["data","meta"]}}}},"401":{"description":"Missing, invalid or revoked API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key does not carry the scope this endpoint requires.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Unknown or cross-tenant move.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"409":{"description":"Stale attempt or illegal review transition.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"429":{"description":"Per-key rate limit exceeded. Honour Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/moves/{id}/skip":{"post":{"operationId":"skipMove","summary":"Dismiss a move (the founder's Skip action). Free; scope 'act'.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"feedback":{"type":"object","properties":{"key":{"type":"string","enum":["not_relevant","wrong_voice","wrong_angle","wrong_timing","low_quality","duplicate","other"],"description":"Why the move was dismissed. Closed vocabulary; unknown keys are ignored by the learning loop."},"note":{"type":["string","null"],"maxLength":500,"description":"Optional free-text detail alongside key."}},"required":["key"]}}}}}},"responses":{"200":{"description":"Skipped (idempotent on repeat).","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object"},"meta":{"type":"object","properties":{"enrollment_id":{"type":"string","format":"uuid"},"generated_at":{"type":"string","format":"date-time"}},"required":["enrollment_id","generated_at"]}},"required":["data","meta"]}}}},"401":{"description":"Missing, invalid or revoked API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key does not carry the scope this endpoint requires.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"409":{"description":"Already shipped or otherwise resolved.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"429":{"description":"Per-key rate limit exceeded. Honour Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/moves/{id}/draft":{"post":{"operationId":"updateMoveDraft","summary":"Replace a pending move's draft body before shipping. Free; scope 'act'.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"draft_fence":{"type":"object","description":"For a content pilot edit, return draft_fence unchanged from get_move or get_output. Refresh after a conflict.","properties":{"move_id":{"type":"string","format":"uuid"},"move_state":{"type":"string"},"move_updated_at":{"type":"string","format":"date-time"},"job_id":{"type":["string","null"],"format":"uuid"},"attempt_id":{"type":["string","null"],"format":"uuid"},"job_state":{"type":["string","null"]},"job_updated_at":{"type":["string","null"],"format":"date-time"},"candidate_id":{"type":"string","format":"uuid"},"draft":{"type":"string","maxLength":20000}},"required":["move_id","move_state","move_updated_at","job_id","attempt_id","job_state","job_updated_at","candidate_id","draft"],"additionalProperties":false},"draft_content":{"type":"string","maxLength":20000}},"required":["draft_content"]}}}},"responses":{"200":{"description":"Draft saved.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object"},"meta":{"type":"object","properties":{"enrollment_id":{"type":"string","format":"uuid"},"generated_at":{"type":"string","format":"date-time"}},"required":["enrollment_id","generated_at"]}},"required":["data","meta"]}}}},"401":{"description":"Missing, invalid or revoked API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key does not carry the scope this endpoint requires.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"409":{"description":"The move is already resolved.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"429":{"description":"Per-key rate limit exceeded. Honour Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/moves/{id}/archive":{"post":{"operationId":"archiveMove","summary":"Archive a move (a lighter dismissal that hides it from the moves list). Free; scope 'act'.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Archived (idempotent on repeat).","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object"},"meta":{"type":"object","properties":{"enrollment_id":{"type":"string","format":"uuid"},"generated_at":{"type":"string","format":"date-time"}},"required":["enrollment_id","generated_at"]}},"required":["data","meta"]}}}},"401":{"description":"Missing, invalid or revoked API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key does not carry the scope this endpoint requires.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"409":{"description":"Already shipped, skipped or otherwise resolved.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"429":{"description":"Per-key rate limit exceeded. Honour Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/moves/{id}/undo":{"post":{"operationId":"undoMove","summary":"Restore a just-shipped or just-skipped move to pending (the founder's Undo): the self-correction verb for a wrong ship or skip. Free; scope 'act'. Idempotent on a move that is already pending. Never unwinds a real channel publish; archived or expired moves cannot be restored.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Restored to pending (idempotent when already pending; restored says whether this call flipped it).","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object"},"meta":{"type":"object","properties":{"enrollment_id":{"type":"string","format":"uuid"},"generated_at":{"type":"string","format":"date-time"}},"required":["enrollment_id","generated_at"]}},"required":["data","meta"]}}}},"401":{"description":"Missing, invalid or revoked API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key does not carry the scope this endpoint requires.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"409":{"description":"The move is archived or expired and cannot be restored.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"429":{"description":"Per-key rate limit exceeded. Honour Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/outputs/{id}/status":{"post":{"operationId":"setOutputStatus","summary":"Resolve one drafted output in the review queue: ship (mark done / posted-status WITHOUT publishing to a channel) or skip. Never posts anywhere; real channel egress is approve_output. Free; scope 'act'; Founder tier. Idempotent when the output is already in the requested state.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"action":{"type":"string","enum":["ship","skip"]},"final_text":{"type":"string","maxLength":20000,"description":"Optional. Only the words the founder actually posted, exactly as they went out; never your rewrite or the draft. Used only to learn how they write, for a post or reply they published; it marks nothing published. Omit it if unknown."}},"required":["action"],"additionalProperties":false}}}},"responses":{"200":{"description":"Resolved (idempotent when already in the requested state).","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object"},"meta":{"type":"object","properties":{"enrollment_id":{"type":"string","format":"uuid"},"generated_at":{"type":"string","format":"date-time"}},"required":["enrollment_id","generated_at"]}},"required":["data","meta"]}}}},"400":{"description":"Invalid body (action must be ship or skip).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"401":{"description":"Missing, invalid or revoked API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key does not carry the scope this endpoint requires.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found (including cross-tenant ids).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"409":{"description":"The output is already resolved in another state.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"429":{"description":"Per-key rate limit exceeded. Honour Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/outputs/{id}/redraft":{"post":{"operationId":"redraftOutput","summary":"Replace a drafted output's body with an edited version before it is resolved. Manual replacement only: the instruction-driven AI redraft stays in the app. The previous draft is kept as revision history. Free; scope 'act'; Founder tier.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"draft_fence":{"type":"object","description":"For a content pilot edit, return draft_fence unchanged from get_move or get_output. Refresh after a conflict.","properties":{"move_id":{"type":"string","format":"uuid"},"move_state":{"type":"string"},"move_updated_at":{"type":"string","format":"date-time"},"job_id":{"type":["string","null"],"format":"uuid"},"attempt_id":{"type":["string","null"],"format":"uuid"},"job_state":{"type":["string","null"]},"job_updated_at":{"type":["string","null"],"format":"date-time"},"candidate_id":{"type":"string","format":"uuid"},"draft":{"type":"string","maxLength":20000}},"required":["move_id","move_state","move_updated_at","job_id","attempt_id","job_state","job_updated_at","candidate_id","draft"],"additionalProperties":false},"draft":{"type":"string","maxLength":20000}},"required":["draft"],"additionalProperties":false}}}},"responses":{"200":{"description":"Draft saved.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object"},"meta":{"type":"object","properties":{"enrollment_id":{"type":"string","format":"uuid"},"generated_at":{"type":"string","format":"date-time"}},"required":["enrollment_id","generated_at"]}},"required":["data","meta"]}}}},"400":{"description":"Invalid draft (1 to 20000 characters).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"401":{"description":"Missing, invalid or revoked API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key does not carry the scope this endpoint requires.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found (including cross-tenant ids).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"409":{"description":"The output is already resolved and can no longer be edited.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"429":{"description":"Per-key rate limit exceeded. Honour Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/scans/{scan}/refresh":{"post":{"operationId":"refreshScan","summary":"METERED: run an on-demand AI Visibility scan. Scope 'write'; Founder tier; ONE scan per scan type per product per local day (any repeat that day replays the original result, whatever the key); per-tenant daily and monthly spend caps; Idempotency-Key header REQUIRED.","parameters":[{"name":"scan","in":"path","required":true,"schema":{"type":"string","enum":["ai_visibility"]},"description":"The scan to refresh. Only 'ai_visibility' today."},{"name":"Idempotency-Key","in":"header","required":true,"schema":{"type":"string","maxLength":200},"description":"Client-minted key (required for contract stability). The scan is claimed once per scan type per product per local day; a repeat that day replays the original result at no cost, regardless of the key."}],"responses":{"200":{"description":"Dispatched (or idempotent replay).","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object"},"meta":{"type":"object","properties":{"enrollment_id":{"type":"string","format":"uuid"},"generated_at":{"type":"string","format":"date-time"}},"required":["enrollment_id","generated_at"]}},"required":["data","meta"]}}}},"400":{"description":"Unknown scan, or missing Idempotency-Key.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"401":{"description":"Missing, invalid or revoked API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"Not enough credits (once credit metering is live). The error carries an upgrade pointer (top-up + create_checkout).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"403":{"description":"The key does not carry the scope this endpoint requires.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Per-key rate limit exceeded. Honour Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Claims temporarily unavailable; nothing was started.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}}}}},"/api/v1/feed/replenish":{"post":{"operationId":"replenishFeed","summary":"METERED: generate a fresh batch of growth moves on demand (the in-app 'Find more moves'). Scope 'write'; Founder tier; once a day per product; per-tenant daily and monthly spend caps; Idempotency-Key header REQUIRED.","parameters":[{"name":"Idempotency-Key","in":"header","required":true,"schema":{"type":"string","maxLength":200},"description":"Client-minted claim key. The same key never runs twice."}],"responses":{"200":{"description":"Dispatched (or idempotent replay).","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object"},"meta":{"type":"object","properties":{"enrollment_id":{"type":"string","format":"uuid"},"generated_at":{"type":"string","format":"date-time"}},"required":["enrollment_id","generated_at"]}},"required":["data","meta"]}}}},"400":{"description":"Missing Idempotency-Key.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"401":{"description":"Missing, invalid or revoked API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"Not enough credits (once credit metering is live). The error carries an upgrade pointer (top-up + create_checkout).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"403":{"description":"The key does not carry the scope this endpoint requires.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Per-key rate limit exceeded. Honour Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Claims temporarily unavailable; nothing was started.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}}}}},"/api/v1/loops/{loopType}/run":{"post":{"operationId":"runLoop","summary":"METERED: administrator-only loop generation once, on demand. Scope 'write'; costs 1 credit per loop per cycle (a repeat run in the same cycle replays at no cost, whatever the key); a run that produces no draft (a clean skip or a failure) is refunded, so you are only charged when a draft is generated; per-tenant daily and monthly spend caps; Idempotency-Key header REQUIRED.","parameters":[{"name":"loopType","in":"path","required":true,"schema":{"type":"string"}},{"name":"Idempotency-Key","in":"header","required":true,"schema":{"type":"string","maxLength":200},"description":"Client-minted key (required for contract stability). The run is charged and claimed once per loop per product per cycle; a repeat run in the same cycle replays the original result at no cost, regardless of the key."}],"responses":{"200":{"description":"Dispatched (or idempotent replay).","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object"},"meta":{"type":"object","properties":{"enrollment_id":{"type":"string","format":"uuid"},"generated_at":{"type":"string","format":"date-time"}},"required":["enrollment_id","generated_at"]}},"required":["data","meta"]}}}},"400":{"description":"Unknown loop type or missing Idempotency-Key.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"401":{"description":"Missing, invalid or revoked API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"Not enough credits. The error carries an upgrade pointer (top-up + create_checkout). A positive credit balance is required to run; the Founder plan gate applied before credit metering went live.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"403":{"description":"The key does not carry the scope this endpoint requires.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"The loop type is runnable but not provisioned for this product. GET /api/v1/loops lists the provisioned loops; a loop must be provisioned and enabled before it can run.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"429":{"description":"Per-key rate limit exceeded. Honour Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Run claims temporarily unavailable; nothing was started.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}}}}},"/api/v1/outreach/preenrol":{"post":{"operationId":"outreachPreenrol","summary":"Administrator only: pre-enrol outreach products. Each row gets an account under the alias outreach-<slug>@guest.afterlaunch.io, a snapshot and a paused enrollment, one provisioning event for a NEW row only, and a 30-day entry link. An alias that already has an enrollment answers 'exists' with its link and nothing is written; if that moves list never started building, the provisioning event is sent once more (resent: true). A product already pre-enrolled under another address (its owner switched to their own email, or another slug) answers 'exists' with no link and an error saying so. Scope 'write'; at most 25 rows a call; dry_run reads and writes nothing.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["rows","campaign"],"additionalProperties":false,"properties":{"rows":{"type":"array","minItems":1,"maxItems":25,"items":{"type":"object","required":["url","label"],"additionalProperties":false,"properties":{"url":{"type":"string","format":"uri","maxLength":2048,"description":"The product, http(s), with a dotted host. Refused: a user or port in the address, localhost, an IP address, a platform nobody owns (github.com and the like), and afterlaunch.io or any subdomain of it."},"label":{"type":"string","minLength":1,"maxLength":200},"slug":{"type":"string","minLength":1,"maxLength":50,"description":"Optional. The alias slug; read from the host when absent (dyad.sh is dyad). \"afterlaunch\" is reserved."}}}},"campaign":{"type":"string","pattern":"^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$","description":"Stamped on each new row as the cohort outreach:<campaign>."},"dry_run":{"type":"boolean","description":"Report what a real call would do; no write, no event."}}}}}},"responses":{"200":{"description":"One result per row, in order. status is created, exists, would_create (dry run) or failed; link is null for would_create, a failure, or an existing row that is not an outreach row.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"dry_run":{"type":"boolean"},"rows":{"type":"array","items":{"type":"object","properties":{"label":{"type":"string"},"alias":{"type":["string","null"]},"url":{"type":"string"},"status":{"type":"string","enum":["created","exists","would_create","failed"]},"link":{"type":["string","null"]},"resent":{"type":"boolean","description":"On 'exists' only: true when the moves list had never started building and the provisioning event was sent again. Never true on a dry run."},"error":{"type":"string"}},"required":["label","alias","url","status","link"]}}},"required":["rows","dry_run"]},"meta":{"type":"object","properties":{"enrollment_id":{"type":"string","format":"uuid"},"generated_at":{"type":"string","format":"date-time"}},"required":["enrollment_id","generated_at"]}},"required":["data","meta"]}}}},"400":{"description":"Invalid body: a bad url, more than 25 rows, no campaign, or two rows on one slug. Nothing was written.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"401":{"description":"Missing, invalid or revoked API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key does not carry the scope this endpoint requires.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Per-key rate limit exceeded. Honour Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"The deployment has no public site url, so no link could be made. Nothing was written.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}}}}},"/api/v1/loops/{loopType}/autonomy":{"post":{"operationId":"setLoopAutonomy","summary":"Administrator-only loop autonomy configuration (review, one_click, autonomous). Scope 'config'. Social channels never accept 'autonomous'.","parameters":[{"name":"loopType","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"level":{"type":"string","enum":["review","one_click","autonomous"]}},"required":["level"]}}}},"responses":{"200":{"description":"The resolved autonomy after the write.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object"},"meta":{"type":"object","properties":{"enrollment_id":{"type":"string","format":"uuid"},"generated_at":{"type":"string","format":"date-time"}},"required":["enrollment_id","generated_at"]}},"required":["data","meta"]}}}},"401":{"description":"Missing, invalid or revoked API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key does not carry the scope this endpoint requires.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not expected in normal operation: the settings row is created on demand for a fresh product run.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"422":{"description":"Social channels are review-and-post only.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"429":{"description":"Per-key rate limit exceeded. Honour Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/loops/{loopType}/cadence":{"post":{"operationId":"setLoopCadence","summary":"Slow or pause a loop's cadence for this product. Scope 'config'; administrator access. The bounded enum can only SLOW or pause a loop, never speed it up. 'default' clears the override.","parameters":[{"name":"loopType","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"cadence":{"type":"string","enum":["default","biweekly","monthly","paused"]}},"required":["cadence"]}}}},"responses":{"200":{"description":"The resulting override map after the write.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object"},"meta":{"type":"object","properties":{"enrollment_id":{"type":"string","format":"uuid"},"generated_at":{"type":"string","format":"date-time"}},"required":["enrollment_id","generated_at"]}},"required":["data","meta"]}}}},"400":{"description":"Invalid cadence (only default | biweekly | monthly | paused; faster-than-default is not settable).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"401":{"description":"Missing, invalid or revoked API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key does not carry the scope this endpoint requires.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not expected in normal operation: the settings row is created on demand for a fresh product run.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"429":{"description":"Per-key rate limit exceeded. Honour Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/scans/ai_visibility/config":{"post":{"operationId":"setScanConfig","summary":"Set how AI visibility scans run: which engines to ask and how often. Every paid scan asks the included 50 questions, so the question count is fixed: only 50 is accepted. Scope 'config'; administrator access. EVERY FIELD IS OPTIONAL and a partial body merges over what is stored, so setting the cadence does not reset the engine set. Send an EMPTY body to READ the current config, the included config and the quote ladder without changing anything. The quote's `delta_credits_per_scan` is the only number you are charged: it prices what this config asks for ABOVE the included config of 50 questions on four engines, which nothing can exceed, so it is zero. `credits_per_scan_at_floor` is published beside it as a reference and is never billed, because the included config is the unmetered heartbeat. Narrowing (fewer engines, a slower cadence) costs nothing and refunds nothing. Cadence can only slow or pause, never run faster than weekly. Setting the config spends nothing; the scan it governs is metered separately at its own chokepoint.","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"engines":{"type":"array","items":{"type":"string","enum":["openai","gemini","google_aio","perplexity"]}},"prompt_count":{"type":"integer","minimum":1,"maximum":50},"cadence":{"type":"string","enum":["default","biweekly","monthly","paused"]}}}}}},"responses":{"200":{"description":"The current config, the included config, the quote (per scan and per month, both the reference figure and the charged delta), the quote ladder, and whether this call changed anything.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object"},"meta":{"type":"object","properties":{"enrollment_id":{"type":"string","format":"uuid"},"generated_at":{"type":"string","format":"date-time"}},"required":["enrollment_id","generated_at"]}},"required":["data","meta"]}}}},"400":{"description":"An unknown engine, a question count outside 1..50, an unknown cadence, a config that cannot be priced, or a setting that could not be saved. Nothing changed.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"401":{"description":"Missing, invalid or revoked API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key does not carry the scope this endpoint requires.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Per-key rate limit exceeded. Honour Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/scans/ai_visibility/intensity":{"post":{"operationId":"setScanIntensity","summary":"RETIRED, kept live for one release: use POST /api/v1/scans/ai_visibility/config instead. Forwards to the same verb, so an existing integration keeps working. The dial widened from one lever (passes per question) to four (engines, question count, cadence, passes) on 2026-08-30. The passes lever is not adjustable yet, so a body that asks to change `level` is refused rather than silently ignored; an empty body still READS the current config and its quote.","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"level":{"type":"string","enum":["l1","l2","l3"]}}}}}},"responses":{"200":{"description":"The current level, the full ladder with a quote per level (passes, engines, questions, credit price and whether it is available), and whether this call changed anything.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object"},"meta":{"type":"object","properties":{"enrollment_id":{"type":"string","format":"uuid"},"generated_at":{"type":"string","format":"date-time"}},"required":["enrollment_id","generated_at"]}},"required":["data","meta"]}}}},"400":{"description":"Invalid level, or a level that is not switched on yet, or the setting could not be saved. Nothing changed.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"401":{"description":"Missing, invalid or revoked API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key does not carry the scope this endpoint requires.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Per-key rate limit exceeded. Honour Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/billing/checkout":{"post":{"operationId":"createCheckout","summary":"Mint a Founder-tier checkout link for this product run. Returns a checkout_url the founder opens to pay; once payment succeeds the Dodo webhook upgrades THIS enrollment to Founder automatically (no tier flip happens here). Reachable on a free trial because it is the path TO Founder; scope 'act'. The checkout is always bound to the enrollment your key owns, never one supplied in the request, so it cannot upgrade another account. Rate limited per product. Inert until paid tiers are enabled.","requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"description":"No body is required. The enrollment is taken from your API key, never from the request."}}}},"responses":{"200":{"description":"A checkout_url to open, bound to your own enrollment.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object"},"meta":{"type":"object","properties":{"enrollment_id":{"type":"string","format":"uuid"},"generated_at":{"type":"string","format":"date-time"}},"required":["enrollment_id","generated_at"]}},"required":["data","meta"]}}}},"400":{"description":"The account has no email on file to bill.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"401":{"description":"Missing, invalid or revoked API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key does not carry the scope this endpoint requires.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Per-key rate limit exceeded. Honour Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"The checkout link could not be created; nothing was charged.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"503":{"description":"Paid tiers are not enabled yet (dark posture).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}}}}},"/api/v1/billing/topup":{"post":{"operationId":"createTopup","summary":"Mint a credit top-up checkout link for this product run. Pass pack_id ('small', 'medium' or 'large'). Returns a checkout_url the founder opens to pay; once payment succeeds the Dodo webhook adds the pack's credits to THIS enrollment automatically (no credits are granted here). Founder plan required (on a trial, create_checkout is the path there); scope 'act'. The top-up is always bound to the enrollment your key owns, never one supplied in the request, so it cannot credit another account. Rate limited per product. Inert until credit metering is enabled.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"pack_id":{"type":"string","enum":["small","medium","large"],"description":"The refill pack size. The founder sees the price on the checkout page."}},"required":["pack_id"],"additionalProperties":false}}}},"responses":{"200":{"description":"A checkout_url to open, bound to your own enrollment.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object"},"meta":{"type":"object","properties":{"enrollment_id":{"type":"string","format":"uuid"},"generated_at":{"type":"string","format":"date-time"}},"required":["enrollment_id","generated_at"]}},"required":["data","meta"]}}}},"400":{"description":"Unknown pack_id (must be 'small', 'medium' or 'large'), or the account has no email on file to bill.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"401":{"description":"Missing, invalid or revoked API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key does not carry the scope this endpoint requires.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Per-key rate limit exceeded. Honour Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"The top-up link could not be created; nothing was charged.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"503":{"description":"Credit top-ups are not enabled yet (dark posture).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}}}}},"/api/v1/outputs/{id}/approve":{"post":{"operationId":"approveOutput","summary":"METERED: approve one drafted output for dispatch (the in-app Post now). Scope 'egress'; Founder tier; credits metered with the output id as the idempotency ref; at-most-once egress by atomic claim.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Dispatched; external_ref carries the posted URL when the platform returns one.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object"},"meta":{"type":"object","properties":{"enrollment_id":{"type":"string","format":"uuid"},"generated_at":{"type":"string","format":"date-time"}},"required":["enrollment_id","generated_at"]}},"required":["data","meta"]}}}},"401":{"description":"Missing, invalid or revoked API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"402":{"description":"Not enough credits (once credit metering is live). The error carries an upgrade pointer (top-up + create_checkout).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"403":{"description":"The key does not carry the scope this endpoint requires.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Not found.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"409":{"description":"Not pending review, or the dispatch failed cleanly (no charge kept).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"429":{"description":"Per-key rate limit exceeded. Honour Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/connections":{"get":{"operationId":"listConnections","summary":"Which growth data sources and distribution channels are connected to this product run: Google Analytics 4, Search Console, Google Business Profile, and the LinkedIn / Reddit posting channels. Each row carries provider, label, an honest state (connected, not_connected, not_available), a detail (the property, site or account when connected; the reason it is shut when not_available) and connected_at.","responses":{"200":{"description":"One row per connector this product run knows about.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object"},"meta":{"type":"object","properties":{"enrollment_id":{"type":"string","format":"uuid"},"generated_at":{"type":"string","format":"date-time"}},"required":["enrollment_id","generated_at"]}},"required":["data","meta"]}}}},"401":{"description":"Missing, invalid or revoked API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key does not carry the scope this endpoint requires.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Per-key rate limit exceeded. Honour Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/connections/start":{"post":{"operationId":"startConnection","summary":"Mint a one-time link the FOUNDER opens in their own browser to connect a source. A machine credential never completes third-party OAuth consent, so this returns url (hand it over), poll_token, poll_after_seconds and expires_at, and poll_connection reports the outcome. The link lasts 15 minutes. Scope 'write', but free and unmetered: no LLM call, no paid API, no email, no external write. At most one live link per (product run, provider): a second start while one is pending answers 409.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"provider":{"type":"string","enum":["ga4","gsc","gbp","linkedin","reddit"],"description":"Which connector to connect. Providers shut on this account are refused with a written reason rather than handed a link that dead-ends."}},"required":["provider"],"additionalProperties":false}}}},"responses":{"200":{"description":"The one-time consent link, its expiry, and the poll token to follow it with.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object"},"meta":{"type":"object","properties":{"enrollment_id":{"type":"string","format":"uuid"},"generated_at":{"type":"string","format":"date-time"}},"required":["enrollment_id","generated_at"]}},"required":["data","meta"]}}}},"400":{"description":"Unknown provider.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"401":{"description":"Missing, invalid or revoked API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key does not carry the scope this endpoint requires.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"A link for this provider is already waiting for the founder. Poll the token you hold, or retry after it expires.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"429":{"description":"Per-key rate limit exceeded. Honour Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Agent-initiated connections are not enabled on this deployment yet.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}}}}},"/api/v1/connections/poll":{"post":{"operationId":"pollConnection","summary":"Did the founder finish the connection you handed them a link for? POST rather than GET because the poll token is a credential and a credential in a query string lands in logs. Answers status pending (with poll_after_seconds), connected (with the property, site or account it now reads), expired or not_found. IDEMPOTENT: nothing is consumed or spent, so a connected handoff keeps reporting connected until its row expires. Scope 'read'.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"poll_token":{"type":"string","description":"The poll_token startConnection returned."}},"required":["poll_token"],"additionalProperties":false}}}},"responses":{"200":{"description":"The handoff status. An unknown or foreign token answers 200 with status not_found rather than telegraphing another product run.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object"},"meta":{"type":"object","properties":{"enrollment_id":{"type":"string","format":"uuid"},"generated_at":{"type":"string","format":"date-time"}},"required":["enrollment_id","generated_at"]}},"required":["data","meta"]}}}},"400":{"description":"Missing or malformed poll_token.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"401":{"description":"Missing, invalid or revoked API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key does not carry the scope this endpoint requires.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Per-key rate limit exceeded. Honour Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/connections/disconnect":{"post":{"operationId":"disconnectConnection","summary":"Disconnect a source and delete the credential AfterLaunch stored for it (for Google it is also revoked at Google). Scope 'config', not act: destroying a credential is an account setting, not a move. Idempotent: disconnecting something that was never connected succeeds and changes nothing. Free.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"provider":{"type":"string","enum":["ga4","gsc","gbp","linkedin","reddit"],"description":"Which connector to disconnect."}},"required":["provider"],"additionalProperties":false}}}},"responses":{"200":{"description":"Disconnected (or already not connected).","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object"},"meta":{"type":"object","properties":{"enrollment_id":{"type":"string","format":"uuid"},"generated_at":{"type":"string","format":"date-time"}},"required":["enrollment_id","generated_at"]}},"required":["data","meta"]}}}},"400":{"description":"Unknown provider.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"401":{"description":"Missing, invalid or revoked API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key does not carry the scope this endpoint requires.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Per-key rate limit exceeded. Honour Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/visibility/prompts":{"get":{"operationId":"listTrackedPrompts","summary":"The customer questions this product run is measured on across the AI answer engines. Returns prompts (in tracking order), cap (the hard limit of 15), used, and curated (false means the set was auto-generated and every question can be replaced). Readable on every plan: seeing the questions you are measured on is a view question, while changing them is Founder-only.","responses":{"200":{"description":"The tracked question portfolio.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object"},"meta":{"type":"object","properties":{"enrollment_id":{"type":"string","format":"uuid"},"generated_at":{"type":"string","format":"date-time"}},"required":["enrollment_id","generated_at"]}},"required":["data","meta"]}}}},"401":{"description":"Missing, invalid or revoked API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key does not carry the scope this endpoint requires.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No enrollment data, or a key bound to a superseded product run.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"429":{"description":"Per-key rate limit exceeded. Honour Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/visibility/prompts/track":{"post":{"operationId":"trackPrompt","summary":"Add one customer question to the tracked AI visibility set. Scope 'config'; Founder tier; free and unmetered (no LLM call, no paid API, no email). Two consequences worth stating: the next SCHEDULED scan runs at full depth instead of probe-skipping, and the week-over-week trend re-baselines because the question set changed. 10 to 200 characters, deduplicated ignoring case and punctuation, up to the included 50.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"text":{"type":"string","minLength":10,"maxLength":200,"description":"The customer question to track."}},"required":["text"],"additionalProperties":false}}}},"responses":{"200":{"description":"The updated tracked set, with cap and used.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object"},"meta":{"type":"object","properties":{"enrollment_id":{"type":"string","format":"uuid"},"generated_at":{"type":"string","format":"date-time"}},"required":["enrollment_id","generated_at"]}},"required":["data","meta"]}}}},"400":{"description":"The question is shorter than 10 or longer than 200 characters.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"401":{"description":"Missing, invalid or revoked API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key does not carry the scope this endpoint requires.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Already tracked, the set is full at 15, or this product run has no scanned questions to curate from yet.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"429":{"description":"Too many tracked-question changes for this product run. Honour Retry-After.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}}}}},"/api/v1/visibility/prompts/untrack":{"post":{"operationId":"untrackPrompt","summary":"Remove one customer question from the tracked AI visibility set, freeing a slot against the included cap of 50. POST rather than DELETE because the question is identified by its text, and a body is the honest place for it. Scope 'config'; Founder tier; free and unmetered. Same two consequences as trackPrompt. IDEMPOTENT: removing something that is not tracked succeeds and changes nothing.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"text":{"type":"string","description":"The tracked question to remove. Matched ignoring case and punctuation."}},"required":["text"],"additionalProperties":false}}}},"responses":{"200":{"description":"The updated tracked set, with cap and used.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object"},"meta":{"type":"object","properties":{"enrollment_id":{"type":"string","format":"uuid"},"generated_at":{"type":"string","format":"date-time"}},"required":["enrollment_id","generated_at"]}},"required":["data","meta"]}}}},"401":{"description":"Missing, invalid or revoked API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key does not carry the scope this endpoint requires.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"This product run has no scanned questions to curate from yet.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"429":{"description":"Too many tracked-question changes for this product run. Honour Retry-After.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}}}}},"/api/v1/scoreboard":{"get":{"operationId":"getScoreboard","summary":"The period proof: AI visibility measured at the start and the end of the window (before / after), the moves shipped between them, the scans that ran, prepared_count (site moves prepared right now and waiting for their live URL, the same count the in-app Scoreboard reads; not windowed; null when unreadable), and the receipts with their verified state and tracked-link clicks. like_for_like is the honesty field and must be read before reporting any change: true means both scans asked the same tracked questions, false means the set changed inside the window or the answer source changed, so the numbers are a new baseline rather than a movement, and null means it cannot be proven either way. Composed from the same functions the day-14 recap and the Feed proof strip use. Defaults to 14 days; ?window_days is clamped to 7 to 90 rather than rejected.","parameters":[{"name":"window_days","in":"query","required":false,"schema":{"type":"integer","minimum":7,"maximum":90},"description":"How many days the scoreboard covers (7 to 90, default 14)."}],"responses":{"200":{"description":"The scoreboard payload.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object"},"meta":{"type":"object","properties":{"enrollment_id":{"type":"string","format":"uuid"},"generated_at":{"type":"string","format":"date-time"}},"required":["enrollment_id","generated_at"]}},"required":["data","meta"]}}}},"401":{"description":"Missing, invalid or revoked API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key does not carry the scope this endpoint requires.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No enrollment data, or a key bound to a superseded product run.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"429":{"description":"Per-key rate limit exceeded. Honour Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/visibility/gaps/dismiss":{"post":{"operationId":"dismissGap","summary":"Set aside one AI visibility gap the founder is not going to act on. Resolves it in ONE place: it stops being re-proposed on the AI Visibility surface AND on the Feed, and the reason feeds the learning loop. Scope 'act'; free, unmetered, and available on every plan. The write is an atomic claim, so a gap the founder already shipped or archived is left untouched and reported as already resolved.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"prompt_id":{"type":"string","description":"The tracked question id, from /api/v1/visibility."},"prompt_text":{"type":"string","description":"The tracked question text, from /api/v1/visibility."},"feedback":{"type":"object","properties":{"key":{"type":"string","enum":["not_relevant","wrong_voice","wrong_angle","wrong_timing","low_quality","duplicate","other"]}},"description":"Why it is being set aside. Closed vocabulary; unknown keys are ignored."}},"required":["prompt_id","prompt_text"],"additionalProperties":false}}}},"responses":{"200":{"description":"Resolved. resolution says whether the tombstone was created, applied to an existing row, or the gap was already resolved.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object"},"meta":{"type":"object","properties":{"enrollment_id":{"type":"string","format":"uuid"},"generated_at":{"type":"string","format":"date-time"}},"required":["enrollment_id","generated_at"]}},"required":["data","meta"]}}}},"400":{"description":"prompt_id or prompt_text missing or empty.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"401":{"description":"Missing, invalid or revoked API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key does not carry the scope this endpoint requires.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Too many dismissals for this product run. Honour Retry-After.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}}}}},"/api/v1/memory/insights/record":{"post":{"operationId":"recordInsight","summary":"Write one durable insight the founder just revealed (a standing preference, a rule about how they write, a fact about the product, or what a shipped move earned) into this product run's Memory, the shared record every growth and marketing draft is written from. Scope 'act'; free, unmetered, and available on every plan. Append-only into the KB substrate with inline provenance (which key wrote it, when, and the move it came out of). Recording the same insight twice supersedes the earlier note rather than duplicating it, and a note the founder has pinned is left untouched and reported as already recorded. Bounded to 500 characters and to a fixed number of insights per product run per day, claimed atomically before the write.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"insight":{"type":"string","minLength":10,"maxLength":500,"description":"The durable insight, as one plain sentence in the founder's own terms."},"kind":{"type":"string","enum":["preference","voice","fact","outcome"],"description":"preference = a standing choice about how they work. voice = a rule about how they write (one they approved goes to record_voice_rule). fact = something true about the product or business. outcome = what a shipped move actually earned."},"source_move_id":{"type":"string","description":"Optional growth move id (uuid) the insight came out of, from /api/v1/feed. Must be a move on this product run."}},"required":["insight","kind"],"additionalProperties":false}}}},"responses":{"200":{"description":"Recorded. recorded says whether a row was written; superseded_previous says whether it replaced an earlier note of the same insight; board_now carries the top moves as they stand (with move ids) so the caller can carry on without a second read, and is absent when that read failed.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object"},"meta":{"type":"object","properties":{"enrollment_id":{"type":"string","format":"uuid"},"generated_at":{"type":"string","format":"date-time"}},"required":["enrollment_id","generated_at"]}},"required":["data","meta"]}}}},"400":{"description":"insight is outside 10 to 500 characters, kind is not one of the four, or source_move_id is not a uuid.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"401":{"description":"Missing, invalid or revoked API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key does not carry the scope this endpoint requires.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"source_move_id names no move on this product run.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"429":{"description":"This product run has reached today's insight limit. Honour Retry-After.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}}}}},"/api/v1/moves/propose":{"post":{"operationId":"proposeMove","summary":"Put a thread the caller found, or a move the owner's plan calls for, onto this product run's moves list as a pending move. Scope 'act'; free, unmetered, every plan. Nothing is generated: the row carries the caller's title, reason, target if one was given, sources, the plan channel it serves if named, and the draft if one was written. It lands at the tail of the list in the shape the feed mints for its kind, so it gets the same brief, hand-off and ship rule (a long_form_content page counts as shipped only with its live URL). Kinds: community_reply and community_answer (a thread), directory_listing (a directory), citation_outreach (a page the AI engines cite), earned_media, cold_outreach and long_form_content. The first four need a target; the rest take one optionally. Deduplicated: a repeat of the same kind and target, or the same kind and normalised title when there is no target, lands on one row (reported as already listed, with the existing move id, and without using a daily slot), and a thread the radar already found is not duplicated. Posture is resolved from the kind and the venue exactly as the feed resolves it: a Reddit or Hacker News target is never one the AI acts on (draft_only, or founder_voice for outreach), and outreach goes out from the owner's own address. Bounded per product run per day, claimed atomically before the write: 20 community threads, and 10 of the other kinds.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string","minLength":10,"maxLength":140,"description":"The move, as the owner would name it."},"why":{"type":"string","minLength":20,"maxLength":600,"description":"What was read, or what the plan says, that makes it worth the owner's time."},"kind":{"type":"string","enum":["community_reply","community_answer","directory_listing","citation_outreach","earned_media","cold_outreach","long_form_content"]},"target":{"type":"string","description":"The https address of the thread, directory or cited page (required for community_reply, community_answer, directory_listing and citation_outreach); optional for the other kinds."},"draft":{"type":"string","maxLength":8000,"description":"Optional draft."},"sources":{"type":"array","items":{"type":"string"},"maxItems":5,"description":"Optional: up to five https addresses the why rests on."},"serves":{"type":"string","enum":["linkedin","site","placements","x","reddit","quora","youtube","outreach","other"],"description":"Optional: the channel in the owner's plan this move serves. Stored on the move's evidence and shown on its card in Growth moves."}},"required":["title","why","kind"],"additionalProperties":false}}}},"responses":{"200":{"description":"proposed says whether a row was added; move_id is the move either way; posture and posture_reason say who may do it.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object"},"meta":{"type":"object","properties":{"enrollment_id":{"type":"string","format":"uuid"},"generated_at":{"type":"string","format":"date-time"}},"required":["enrollment_id","generated_at"]}},"required":["data","meta"]}}}},"400":{"description":"A field is outside its bounds, kind is not proposable, target is missing for a kind that needs one or is not an https address, or serves is not one of the plan channels.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"401":{"description":"Missing, invalid or revoked API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key does not carry the scope this endpoint requires.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"This product run has reached today's proposal limit for this kind. Honour Retry-After.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}}}}},"/api/v1/queue":{"get":{"operationId":"listQueue","summary":"Unclaimed preparation jobs, oldest first. Each summary carries job_id and its nullable attempt_id but never prepared_content. Scope 'act'; free.","responses":{"200":{"description":"jobs carries the queue, oldest first; total is how many came back. An empty queue is an empty list, never an error.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object"},"meta":{"type":"object","properties":{"enrollment_id":{"type":"string","format":"uuid"},"generated_at":{"type":"string","format":"date-time"}},"required":["enrollment_id","generated_at"]}},"required":["data","meta"]}}}},"401":{"description":"Missing, invalid or revoked API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key does not carry the scope this endpoint requires.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Per-key rate limit exceeded. Honour Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/queue/{id}/pick-up":{"post":{"operationId":"pickUpJob","summary":"Try to claim one exact queued attempt. Start work only when the response carries claim_won:true. A changed:false result is a non-winner even when another session uses the same credential. Scope 'act'; free.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"attempt_id":{"type":["string","null"],"format":"uuid"},"session_url":{"type":["string","null"],"description":"Optional. The launch already gave your user the link to this session; pass it only when you opened this session yourself and can see its address."},"site_repository":{"type":["string","null"],"maxLength":200,"description":"Optional, for a routine working a site move: the repository it can write to, as owner/name, or null if it has none. Omit it to say nothing."}},"required":["attempt_id"]}}}},"responses":{"200":{"description":"Claimed. picked_up_by is the API key label the claim was recorded under.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object"},"meta":{"type":"object","properties":{"enrollment_id":{"type":"string","format":"uuid"},"generated_at":{"type":"string","format":"date-time"}},"required":["enrollment_id","generated_at"]}},"required":["data","meta"]}}}},"401":{"description":"Missing, invalid or revoked API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key does not carry the scope this endpoint requires.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No job with that id is in the queue on this product run.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"409":{"description":"Another agent claimed it first, or it has already moved on.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"429":{"description":"Per-key rate limit exceeded. Honour Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/queue/{id}/report":{"post":{"operationId":"reportJob","summary":"Report one claimed attempt as running, needs_input, ready_for_review, shipped or failed, with its exact attempt_id. needs_input requires question (one question for the user, at most 500 characters) and leaves the job running. shipped on a managed attempt freezes the result and records completion with published_url, the live https address; a merged change to the user's own site takes its page address as planned_url (the pull request as receipt_url) and stays prepared until AfterLaunch sees the text there; do not poll it. A report from any key of the same person is accepted. Ready requires prepared_content or a prepared asset_url (or the pull request address when you opened one) and remains unpublished; a listing or profile form left on its review page is reported here, with what is filled and what is left. It can be reported again with a better result until the user has used it on AfterLaunch, after which it is locked and review_move revise starts a new attempt. If you cannot read the whole thread or page (a login wall, a cloud session), say so and ask the user for a screenshot; do not draft from a snippet as if it were the thread. On a stored legacy job, attempt_id null, shipped settles its move and job in one transaction. Scope 'act'; free.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"attempt_id":{"type":["string","null"],"format":"uuid"},"state":{"type":"string","enum":["running","needs_input","ready_for_review","shipped","failed"],"description":"What has happened to the job."},"asset_url":{"type":"string","description":"URL of the prepared asset. This does not claim publication."},"prepared_content":{"type":"string","maxLength":20000,"description":"Prepared result body for ready_for_review."},"receipt_url":{"type":"string","description":"A permalink to the proof, when there is one."},"session_url":{"type":"string","description":"Optional. The launch already gave your user the link to this session; pass it only when you opened this session yourself and can see its address."},"site_repository":{"type":["string","null"],"maxLength":200,"description":"Optional, for a routine working a site move: the repository it can write to, as owner/name, or null if it has none. Omit it to say nothing."},"fail_reason":{"type":"string","maxLength":500,"description":"Required with 'failed': one sentence on what stopped the agent."},"question":{"type":"string","maxLength":500,"description":"Required with 'needs_input': the one question the agent is asking its user."},"published_url":{"type":["string","null"],"description":"With 'shipped': the live https address of the work, when it has one."},"planned_url":{"type":["string","null"],"description":"With 'shipped': the page on the user's own site a merged change lives at, while it deploys. AfterLaunch checks it."}},"required":["attempt_id","state"],"additionalProperties":false}}}},"responses":{"200":{"description":"Recorded. state is where the job now stands; a repeat of the same state comes back unchanged.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object"},"meta":{"type":"object","properties":{"enrollment_id":{"type":"string","format":"uuid"},"generated_at":{"type":"string","format":"date-time"}},"required":["enrollment_id","generated_at"]}},"required":["data","meta"]}}}},"400":{"description":"Invalid state, attempt, required result, failure reason or URL.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"401":{"description":"Missing, invalid or revoked API key.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"The key does not carry the scope this endpoint requires.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No job with that id is on this product run.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"409":{"description":"Stale attempt, a job not in progress, or illegal transition.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"}},"required":["code","message"]}},"required":["error"]}}}},"429":{"description":"Per-key rate limit exceeded. Honour Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/onboard/start":{"post":{"operationId":"onboardStart","summary":"Agent-initiated onboarding, step 1: submit the founder's site URL + email. The founder gets ONE approval email; nothing runs (and nothing is spent) until they click it. No authentication (this flow is how a key is obtained). Rate limited per IP and per email; one live request per email at a time. Different people may analyse the same public site in separate accounts.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"url":{"type":"string","description":"The founder's site, http(s)."},"email":{"type":"string","description":"The founder's email; receives the approval link."},"agent_label":{"type":"string","maxLength":120,"description":"Optional: who is asking (shown to the founder)."}},"required":["url","email"]}}}},"responses":{"201":{"description":"Pending approval. Returns device_code (poll with it), poll_interval_seconds, expires_at."},"400":{"description":"Invalid url or email, or a body that does not match the schema. The body is strict: unknown fields are rejected.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"Agent onboarding is not open.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"trial_used (email already had its free trial) or pending_exists (a live request already awaits approval).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited. Honour Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"The approval email could not be sent; nothing was created.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/onboard/poll":{"post":{"operationId":"onboardPoll","summary":"Agent-initiated onboarding, step 3: exchange the device code for the minted API key, which carries the read, act and write scopes. Delivered exactly once; store it immediately. Poll at the returned interval until status is ready.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"device_code":{"type":"string"}},"required":["device_code"]}}}},"responses":{"200":{"description":"status pending (keep polling) or ready (api_key + enrollment_id, shown exactly once)."},"404":{"description":"Unknown or expired device code, or the flow is not open.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"410":{"description":"The key was already delivered once.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Polling too fast. Honour Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/starter/redeem":{"post":{"operationId":"starterRedeem","summary":"Redeem a one-time starter token for an API key carrying the read and act scopes. The founder copied the starter from /app/connect and pasted it into this session; the key is MINTED at redemption and returned exactly once, so store it immediately. The token dies in the same moment and starters expire after 15 minutes either way. No authentication (this is how a session obtains a key). Rate limited per IP.","security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"token":{"type":"string","description":"The one-time claim token from the pasted starter text."}},"required":["token"]}}}},"responses":{"200":{"description":"Redeemed. Returns api_key (shown exactly once, read + act, 90-day expiry), enrollment_id and expires_at."},"400":{"description":"A body that does not match the schema. The body is strict: unknown fields are rejected.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"No live starter matches this token, or the door is not open.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"410":{"description":"The starter was already redeemed, or it expired. Ask the founder for a fresh one.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited. Honour Retry-After.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"The redemption could not be completed. Nothing was handed over.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"The starter was valid but the key could not be created. Nothing was handed over.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}}}