{"openapi":"3.1.0","info":{"title":"Postbag API","version":"0.1.0","description":"A form backend that routes. Websites POST submissions to /s/{formId} (public, unauthenticated); everything under /v1 manages forms, streams, destinations, routes and deliveries and requires a bearer API key. Start at GET /v1/me, then see /llms.txt for the full agent onboarding guide."},"servers":[{"url":"https://postbag.dev"}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"pb_live_… API key","description":"An API key minted via POST /v1/api-keys (or the dashboard), sent as `Authorization: Bearer pb_live_…`. Scopes are manage ⊇ read ⊇ submit — a manage key satisfies every read- or submit-scoped call too."},"sandboxAuth":{"type":"http","scheme":"Sandbox","description":"The capability returned by POST /v1/public/sandboxes, sent as `Authorization: Sandbox <sandbox_token>`. It can read one bounded sandbox but cannot claim it without a separately authenticated organization."}},"schemas":{"ErrorEnvelope":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"hint":{"type":"string"},"docs":{"type":"string"},"details":{"type":"object","additionalProperties":{}}},"required":["code","message"]}},"required":["error"]},"PublicInvitation":{"type":"object","properties":{"organization":{"type":"object","properties":{"name":{"type":"string"}},"required":["name"]},"invited_by_name":{"type":["string","null"]},"role":{"type":"string","enum":["owner","admin","member"]},"expires_at":{"type":"string","example":"2026-08-21T09:00:00.000Z"},"email_hint":{"type":"string","description":"The invited email, masked, e.g. 'j***@example.com'."}},"required":["organization","invited_by_name","role","expires_at","email_hint"]},"OrganizationSummary":{"type":"object","properties":{"id":{"type":"string","example":"fm_8f3kq2"},"slug":{"type":"string"},"name":{"type":"string"},"role":{"type":["string","null"],"enum":["owner","admin","member",null],"description":"The caller's role in this organization. Null when the caller is an API key — keys are org-scoped, not user-scoped, so they have no membership role of their own."},"is_active":{"type":"boolean"}},"required":["id","slug","name","role","is_active"]},"ActiveOrganization":{"type":"object","properties":{"organization":{"type":"object","properties":{"id":{"type":"string","example":"fm_8f3kq2"},"slug":{"type":"string"},"name":{"type":"string"}},"required":["id","slug","name"]}},"required":["organization"]},"Member":{"type":"object","properties":{"id":{"type":"string","example":"fm_8f3kq2"},"user":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"email":{"type":"string"}},"required":["id","name","email"]},"role":{"type":"string","enum":["owner","admin","member"]},"joined_at":{"type":"string","example":"2026-08-21T09:00:00.000Z"}},"required":["id","user","role","joined_at"]},"Invitation":{"type":"object","properties":{"id":{"type":"string","example":"fm_8f3kq2"},"email":{"type":"string"},"role":{"type":"string","enum":["owner","admin","member"]},"expires_at":{"type":"string","example":"2026-08-21T09:00:00.000Z"},"invited_by":{"type":["object","null"],"properties":{"id":{"type":"string"},"name":{"type":"string"},"email":{"type":"string"}},"required":["id","name","email"],"description":"Null when the invitation was created by an API key rather than a signed-in session."},"created_at":{"type":"string","example":"2026-08-21T09:00:00.000Z"}},"required":["id","email","role","expires_at","invited_by","created_at"]},"AcceptInvitationResult":{"type":"object","properties":{"organization":{"type":"object","properties":{"id":{"type":"string","example":"fm_8f3kq2"},"slug":{"type":"string"},"name":{"type":"string"}},"required":["id","slug","name"]},"role":{"type":"string","enum":["owner","admin","member"]},"next":{"type":"array","items":{"type":"object","properties":{"why":{"type":"string"},"method":{"type":"string"},"path":{"type":"string"},"body":{"type":"object","additionalProperties":{}}}}}},"required":["organization","role","next"]},"PlanRedeemResult":{"allOf":[{"$ref":"#/components/schemas/OrganizationPlan"},{"type":"object","properties":{"next":{"type":"array","items":{"type":"object","properties":{"why":{"type":"string"},"method":{"type":"string"},"path":{"type":"string"},"body":{"type":"object","additionalProperties":{}}}}}},"required":["next"]}]},"OrganizationPlan":{"type":"object","properties":{"plan":{"type":"string","enum":["free","pro","team","selfhost"]},"plan_source":{"type":"string","enum":["free","billing","complimentary","selfhost"]},"plan_expires_at":{"type":["string","null"],"example":"2026-08-21T09:00:00.000Z"},"plan_note":{"type":["string","null"]}},"required":["plan","plan_source","plan_expires_at","plan_note"]},"PlanGrantCreated":{"allOf":[{"$ref":"#/components/schemas/PlanGrant"},{"type":"object","properties":{"code":{"type":"string","description":"Shown once — store it immediately. Only its hash is kept."}},"required":["code"]}]},"PlanGrant":{"type":"object","properties":{"id":{"type":"string","example":"fm_8f3kq2"},"plan":{"type":"string","enum":["free","pro","team","selfhost"]},"note":{"type":["string","null"]},"expires_at":{"type":["string","null"],"example":"2026-08-21T09:00:00.000Z"},"plan_duration_days":{"type":["integer","null"]},"max_redemptions":{"type":"integer"},"redeemed_count":{"type":"integer"},"created_by_user_id":{"type":"string"},"created_at":{"type":"string","example":"2026-08-21T09:00:00.000Z"},"revoked_at":{"type":["string","null"],"example":"2026-08-21T09:00:00.000Z"}},"required":["id","plan","note","expires_at","plan_duration_days","max_redemptions","redeemed_count","created_by_user_id","created_at","revoked_at"]},"GrowthMetrics":{"type":"object","properties":{"generated_at":{"type":"string","example":"2026-08-21T09:00:00.000Z","description":"ISO-8601 instant the windows (7d / 30d) are computed from. Every metric is a snapshot of rows retained in the primary database at this instant, not an append-only historical total. Aggregates only — no emails, names, payloads, or keys."},"organizations":{"type":"object","properties":{"total":{"type":"integer","minimum":0},"created_7d":{"type":"integer","minimum":0},"created_30d":{"type":"integer","minimum":0},"by_plan":{"type":"object","properties":{"free":{"type":"integer","minimum":0},"pro":{"type":"integer","minimum":0},"team":{"type":"integer","minimum":0},"selfhost":{"type":"integer","minimum":0}},"required":["free","pro","team","selfhost"],"description":"Counts from organization_settings.plan. Always includes free, pro, team and selfhost (0 when none). An organization without a settings row is counted as free."}},"required":["total","created_7d","created_30d","by_plan"]},"forms":{"type":"object","properties":{"total":{"type":"integer","minimum":0},"created_7d":{"type":"integer","minimum":0},"created_30d":{"type":"integer","minimum":0},"active":{"type":"integer","minimum":0,"description":"Forms with at least one non-test Submission in the last 30 days."}},"required":["total","created_7d","created_30d","active"]},"submissions":{"type":"object","properties":{"total":{"type":"integer","minimum":0},"last_7d":{"type":"integer","minimum":0},"last_30d":{"type":"integer","minimum":0},"real_last_30d":{"type":"integer","minimum":0,"description":"Submissions with test=false received in the last 30 days."},"test_last_30d":{"type":"integer","minimum":0,"description":"Submissions with test=true received in the last 30 days."},"orgs_with_real_delivery_30d":{"type":"integer","minimum":0,"description":"Distinct organizations with at least one Delivery status=sent of a non-test Submission whose sent_at is in the last 30 days."}},"required":["total","last_7d","last_30d","real_last_30d","test_last_30d","orgs_with_real_delivery_30d"]},"sandboxes":{"type":"object","properties":{"retained_created_30d":{"type":"integer","minimum":0,"description":"Retained anonymous_sandboxes rows inserted in the last 30 days."},"retained_claimed_30d":{"type":"integer","minimum":0,"description":"Retained sandbox rows whose claimed_at is in the last 30 days."},"retained_expired_or_blocked_30d":{"type":"integer","minimum":0,"description":"Retained rows currently status expired or blocked whose created_at or expires_at falls in the last 30 days."}},"required":["retained_created_30d","retained_claimed_30d","retained_expired_or_blocked_30d"],"description":"Current retained anonymous sandbox staging rows (ADR-008), not a historical creation/claim/conversion funnel. Housekeeping deletes every sandbox after expires_at, including claimed rows."},"destinations":{"type":"object","properties":{"by_type":{"type":"object","additionalProperties":{"type":"integer","minimum":0},"description":"Destination counts keyed by type. Always includes email, telegram, webhook, slack and discord (0 when none); any other stored type is included as well."}},"required":["by_type"]}},"required":["generated_at","organizations","forms","submissions","sandboxes","destinations"]},"Form":{"type":"object","properties":{"id":{"type":"string","example":"fm_8f3kq2"},"project_id":{"type":"string","example":"fm_8f3kq2"},"slug":{"type":"string"},"name":{"type":"string"},"tags":{"type":"array","items":{"type":"string"}},"status":{"type":"string","enum":["active","paused"]},"schema_mode":{"type":"string","enum":["observe","enforce","managed"]},"current_schema_version":{"type":["integer","null"]},"settings":{"type":"object","additionalProperties":{}},"submit_url":{"type":"string"},"streams":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","example":"fm_8f3kq2"},"slug":{"type":"string"},"mapping_status":{"type":"string","enum":["valid","incomplete"]}},"required":["id","slug","mapping_status"]}},"counts":{"type":"object","properties":{"submissions":{"type":"integer"},"last_submission_at":{"type":["string","null"],"example":"2026-08-21T09:00:00.000Z"}},"required":["submissions","last_submission_at"]},"created_at":{"type":"string","example":"2026-08-21T09:00:00.000Z"}},"required":["id","project_id","slug","name","tags","status","schema_mode","current_schema_version","settings","submit_url","streams","counts","created_at"]},"Destination":{"type":"object","properties":{"id":{"type":"string","example":"fm_8f3kq2"},"type":{"type":"string","enum":["email","telegram","webhook","slack","discord"]},"name":{"type":"string"},"config":{"type":"object","additionalProperties":{}},"health":{"type":"string","enum":["unknown","ok","failing"]},"verified":{"type":"boolean"},"created_at":{"type":"string","example":"2026-08-21T09:00:00.000Z"}},"required":["id","type","name","config","health","verified","created_at"]},"Route":{"type":"object","properties":{"id":{"type":"string","example":"fm_8f3kq2"},"form_id":{"type":["string","null"]},"stream_id":{"type":["string","null"]},"destination_id":{"type":"string","example":"fm_8f3kq2"},"enabled":{"type":"boolean"},"mode":{"type":"object","additionalProperties":{}},"window":{"type":"object","additionalProperties":{}},"quality":{"type":"object","additionalProperties":{}},"filter":{"type":["string","null"]},"transform":{"type":["string","null"]},"counts":{"type":"object","additionalProperties":{}},"created_at":{"type":"string","example":"2026-08-21T09:00:00.000Z"}},"required":["id","form_id","stream_id","destination_id","enabled","mode"]},"Embed":{"type":"object","properties":{"html":{"type":"string"},"fetch":{"type":"string"},"react":{"type":"string"},"astro":{"type":"string"},"nextjs_action":{"type":"string"}},"required":["html","fetch","react","astro","nextjs_action"]},"Verify":{"type":"object","properties":{"curl":{"type":"string"},"then":{"type":"string"}},"required":["curl","then"]},"Project":{"type":"object","properties":{"id":{"type":"string","example":"fm_8f3kq2"},"slug":{"type":"string"},"name":{"type":"string"},"tags":{"type":"array","items":{"type":"string"}},"created_at":{"type":"string","example":"2026-08-21T09:00:00.000Z"}},"required":["id","slug","name","tags","created_at"]},"FormCreated":{"allOf":[{"$ref":"#/components/schemas/Form"},{"type":"object","properties":{"embed":{"$ref":"#/components/schemas/Embed"},"verify":{"$ref":"#/components/schemas/Verify"},"next":{"type":"array","items":{"type":"object","properties":{"why":{"type":"string"},"method":{"type":"string"},"path":{"type":"string"},"body":{"type":"object","additionalProperties":{}}}}}}}]},"SchemaVersion":{"type":"object","properties":{"json_schema":{"type":"object","additionalProperties":{}},"ui":{"type":"object","additionalProperties":{}},"changelog":{"type":"string"},"version":{"type":"integer"},"inferred":{"type":"boolean"},"created_at":{"type":"string","example":"2026-08-21T09:00:00.000Z"},"created_by":{"type":"string"}},"required":["json_schema"]},"Submission":{"type":"object","properties":{"id":{"type":"string","example":"fm_8f3kq2"},"form_id":{"type":"string","example":"fm_8f3kq2"},"status":{"type":"string","enum":["received","quarantined","spam"]},"quarantine_reason":{"type":["string","null"],"description":"Machine-readable reason when status is quarantined: schema_violation, rate_limited, origin_rejected, turnstile_failed, or over_quota. Null for received or spam submissions."},"test":{"type":"boolean"},"data":{"type":"object","additionalProperties":{}},"form_schema_version":{"type":["integer","null"]},"spam":{"type":"object","properties":{"score":{"type":"number"},"reasons":{"type":"array","items":{"type":"string"}}},"required":["score","reasons"]},"meta":{"type":"object","additionalProperties":{}},"received_at":{"type":"string","example":"2026-08-21T09:00:00.000Z"}},"required":["id","form_id","status","quarantine_reason","test","data","form_schema_version","spam","meta","received_at"]},"SubmissionDetail":{"allOf":[{"$ref":"#/components/schemas/Submission"},{"type":"object","properties":{"attachments":{"type":"array","items":{"$ref":"#/components/schemas/SubmissionAttachment"}},"deliveries":{"type":"array","items":{"$ref":"#/components/schemas/Delivery"}},"drift":{"type":"array","items":{"type":"object","additionalProperties":{}}}}}]},"SubmissionAttachment":{"type":"object","properties":{"id":{"type":"string","example":"fm_8f3kq2"},"form_id":{"type":"string","example":"fm_8f3kq2"},"submission_id":{"type":"string","example":"fm_8f3kq2"},"field_name":{"type":"string"},"filename":{"type":"string"},"content_type":{"type":"string"},"size_bytes":{"type":"integer","minimum":0},"sha256":{"type":"string"},"download_url":{"type":"string"},"created_at":{"type":"string","example":"2026-08-21T09:00:00.000Z"}},"required":["id","form_id","submission_id","field_name","filename","content_type","size_bytes","sha256","download_url","created_at"]},"Delivery":{"type":"object","properties":{"id":{"type":"string","example":"fm_8f3kq2"},"submission_id":{"type":"string","example":"fm_8f3kq2"},"route_id":{"type":"string","example":"fm_8f3kq2"},"destination_id":{"type":"string","example":"fm_8f3kq2"},"status":{"type":"string","enum":["pending","sending","sent","failed","dead","skipped"]},"skip_reason":{"type":["string","null"]},"attempts":{"type":"integer"},"next_attempt_at":{"type":["string","null"],"example":"2026-08-21T09:00:00.000Z"},"schema_version":{"type":["integer","null"]},"payload":{"type":"object","additionalProperties":{}},"last_response":{"$ref":"#/components/schemas/DeliveryResult"},"last_error":{"type":["string","null"]},"created_at":{"type":"string","example":"2026-08-21T09:00:00.000Z"},"sent_at":{"type":["string","null"],"example":"2026-08-21T09:00:00.000Z"}},"required":["id","submission_id","route_id","destination_id","status","skip_reason","attempts","next_attempt_at","schema_version","payload","last_error","created_at","sent_at"]},"DeliveryResult":{"type":"object","properties":{"ok":{"type":"boolean"},"status_code":{"type":["integer","null"]},"latency_ms":{"type":"number"},"response_excerpt":{"type":"string"},"error":{"type":["string","null"]}}},"Stream":{"type":"object","properties":{"id":{"type":"string","example":"fm_8f3kq2"},"slug":{"type":"string"},"name":{"type":"string"},"current_schema_version":{"type":["integer","null"]},"counts":{"type":"object","properties":{"sources":{"type":"integer"},"routes":{"type":"integer"},"submissions_30d":{"type":"integer"}},"required":["sources","routes","submissions_30d"]},"created_at":{"type":"string","example":"2026-08-21T09:00:00.000Z"}},"required":["id","slug","name","current_schema_version","counts","created_at"]},"StreamDetail":{"allOf":[{"$ref":"#/components/schemas/Stream"},{"type":"object","properties":{"schema":{"$ref":"#/components/schemas/SchemaVersion"},"sources":{"type":"array","items":{"$ref":"#/components/schemas/StreamSource"}},"routes":{"type":"array","items":{"$ref":"#/components/schemas/Route"}},"form_template":{"$ref":"#/components/schemas/SchemaVersion"}}}]},"StreamSource":{"type":"object","properties":{"id":{"type":"string","example":"fm_8f3kq2"},"form_id":{"type":"string","example":"fm_8f3kq2"},"selector":{"type":"string"},"mapping":{"type":"object","additionalProperties":{}},"mapping_status":{"type":"string","enum":["valid","incomplete"]},"missing":{"type":"array","items":{"type":"string"}},"stream_schema_version":{"type":"integer"}},"required":["id"]},"Event":{"type":"object","properties":{"id":{"type":"string","example":"fm_8f3kq2"},"type":{"type":"string"},"subject":{"type":"object","additionalProperties":{}},"data":{"type":"object","additionalProperties":{}},"created_at":{"type":"string","example":"2026-08-21T09:00:00.000Z"}},"required":["id","type","subject","data","created_at"]},"SystemWebhook":{"type":"object","properties":{"id":{"type":"string","example":"fm_8f3kq2"},"url":{"type":"string"},"events":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"health":{"type":"string","enum":["unknown","ok","failing"]}},"required":["id","url","events"]},"SystemWebhookDelivery":{"type":"object","properties":{"id":{"type":"string","example":"fm_8f3kq2"},"webhook_id":{"type":"string","example":"fm_8f3kq2"},"event_id":{"type":"string","example":"fm_8f3kq2"},"event_type":{"type":"string"},"status":{"type":"string","enum":["pending","sending","sent","failed","dead"]},"attempts":{"type":"integer"},"next_attempt_at":{"type":["string","null"],"example":"2026-08-21T09:00:00.000Z"},"payload":{"type":"object","additionalProperties":{}},"last_response":{"$ref":"#/components/schemas/DeliveryResult"},"last_error":{"type":["string","null"]},"created_at":{"type":"string","example":"2026-08-21T09:00:00.000Z"},"sent_at":{"type":["string","null"],"example":"2026-08-21T09:00:00.000Z"}},"required":["id","webhook_id","event_id","event_type","status","attempts","next_attempt_at","payload","last_error","created_at","sent_at"]}},"parameters":{}},"paths":{"/s/{formId}":{"post":{"operationId":"submissions_submit","tags":["submissions"],"summary":"Receive a public Submission","description":"Accepts JSON, URL-encoded data, or multipart form data. Multipart file fields become fl_ references in Submission data. Anonymous sandbox Forms do not accept files.","security":[],"parameters":[{"schema":{"type":"string"},"required":true,"name":"formId","in":"path"}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":{}}},"application/x-www-form-urlencoded":{"schema":{"type":"object","additionalProperties":{}}},"multipart/form-data":{"schema":{"type":"object","additionalProperties":{}}}}},"responses":{"200":{"description":"Submission accepted","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"submission_id":{"type":"string"},"status":{"type":"string","enum":["received","quarantined","spam"]},"idempotent":{"type":"boolean"},"deliveries":{"type":"array","items":{"type":"string"}},"attachments":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"}},"required":["id"]}}},"required":["ok","submission_id","status"]}}}},"303":{"description":"Browser form redirect after acceptance"},"400":{"description":"Malformed request"},"402":{"description":"Retained attachment storage limit reached, including pending work"},"413":{"description":"Body, attachment size, or attachment count limit exceeded"},"422":{"description":"Validation failed"},"503":{"description":"Attachment storage unavailable"}}}},"/v1/auth/providers":{"get":{"operationId":"auth_providers","tags":["discovery"],"summary":"Which sign-in methods this instance has configured","description":"Public, unauthenticated. Email/password reports sign-in and sign-up separately because hosted Postbag keeps password sign-in for existing users while disabling new password registration. `social` lists only the OAuth providers this instance has both a client id and secret for (self-host parity: a self-hosted instance with neither configured returns an empty list). The dashboard calls this once, cached, to decide which social sign-in buttons to render.","security":[],"responses":{"200":{"description":"ok","content":{"application/json":{"schema":{"type":"object","properties":{"email_password":{"type":"object","properties":{"sign_in":{"type":"boolean"},"sign_up":{"type":"boolean"}},"required":["sign_in","sign_up"]},"email_code":{"type":"object","properties":{"sign_in":{"type":"boolean"},"sign_up":{"type":"boolean"}},"required":["sign_in","sign_up"]},"social":{"type":"array","items":{"type":"string","enum":["google","github"]}},"sign_in_url":{"type":"string"}},"required":["email_password","email_code","social","sign_in_url"]}}}}}}},"/v1/billing/webhook":{"post":{"operationId":"billing_webhook","tags":["billing"],"summary":"Receive a signed Polar webhook","security":[],"parameters":[{"schema":{"type":"string"},"required":true,"name":"webhook-id","in":"header"},{"schema":{"type":"string"},"required":true,"name":"webhook-timestamp","in":"header"},{"schema":{"type":"string"},"required":true,"name":"webhook-signature","in":"header"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","additionalProperties":{}}}}},"responses":{"202":{"description":"accepted"},"400":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/auth/request-code":{"post":{"operationId":"auth_request_code","tags":["discovery"],"summary":"Email a 6-digit sign-in code (step 1 of agent onboarding without a browser)","description":"Public, unauthenticated. Sends a 6-digit code good for 10 minutes to the given address. Always 200 for a well-formed email, whether or not an account exists — this endpoint never confirms or denies an account's existence. Rate limited per email and per client IP. The human reads the code out of their inbox and gives it to the agent, which calls POST /v1/auth/verify-code next.","security":[],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string","format":"email","description":"Lowercased and trimmed before use. Always 200s for a well-formed address."}},"required":["email"]}}}},"responses":{"200":{"description":"ok","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","enum":[true]},"expires_in":{"type":"integer"},"next":{"type":"string"}},"required":["ok","expires_in","next"]}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"501":{"description":"Email sending is not configured on this server","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"502":{"description":"The code email could not be sent","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/auth/verify-code":{"post":{"operationId":"auth_verify_code","tags":["discovery"],"summary":"Verify a code and mint an API key (step 2 of agent onboarding without a browser)","description":"Public, unauthenticated. Verifies the code sent by POST /v1/auth/request-code and, on success, mints an API key through the same path POST /v1/api-keys uses — default scope manage. A new email provisions a personal organization first, exactly like every other sign-up path. An existing user's key is minted against their personal organization (the one they own, oldest first, if they belong to several). This does not set a session cookie — it mints a key, it does not log a browser in.","security":[],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string","format":"email"},"code":{"type":"string","description":"The 6-digit code from the email."},"key_name":{"type":"string","minLength":1,"maxLength":32,"description":"Defaults to \"agent · <YYYY-MM-DD>\"."},"scopes":{"type":"array","items":{"type":"string","enum":["manage","read","submit"]},"minItems":1,"default":["manage"]}},"required":["email","code"]}}}},"responses":{"201":{"description":"created","content":{"application/json":{"schema":{"type":"object","properties":{"api_key":{"type":"string","description":"Shown once — store it immediately."},"key_id":{"type":"string"},"scopes":{"type":"array","items":{"type":"string","enum":["manage","read","submit"]}},"organization":{"type":"object","properties":{"id":{"type":"string"},"slug":{"type":"string"},"name":{"type":"string"}},"required":["id","slug","name"]},"user":{"type":"object","properties":{"email":{"type":"string"},"created":{"type":"boolean"}},"required":["email","created"]},"next":{"type":"array","items":{"type":"string"}}},"required":["api_key","key_id","scopes","organization","user","next"]}}}},"401":{"description":"Invalid or expired code","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/public/sandboxes":{"post":{"operationId":"public_sandboxes_create","tags":["discovery"],"summary":"Create a bounded 24-hour sandbox Form without an account","description":"Requires a canonical UUIDv4 Idempotency-Key. Creates an inert sandbox Form that can accept at most five test Submissions and cannot create Destinations, Routes, Deliveries, Events or outbound traffic.","security":[],"parameters":[{"schema":{"type":"string","description":"Canonical lowercase UUIDv4 generated with a CSPRNG."},"required":true,"description":"Canonical lowercase UUIDv4 generated with a CSPRNG.","name":"idempotency-key","in":"header"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100},"origin":{"type":"string","format":"uri","description":"The site origin allowed to submit, canonicalized before storage."},"claim_email":{"type":"string","format":"email","description":"Optional verified-email binding. Include only when the user explicitly supplied it."}},"required":["name"],"additionalProperties":false}}}},"responses":{"201":{"description":"created or idempotently replayed","content":{"application/json":{"schema":{"type":"object","properties":{"sandbox":{"type":"object","properties":{"id":{"type":"string","example":"fm_8f3kq2"},"name":{"type":"string"},"status":{"type":"string","enum":["active","claimed","expired","blocked"]},"submit_url":{"type":"string"},"expires_at":{"type":"string","example":"2026-08-21T09:00:00.000Z"},"accepted_count":{"type":"integer"},"remaining":{"type":"integer"}},"required":["id","name","status","submit_url","expires_at","accepted_count","remaining"]},"sandbox_token":{"type":"string","description":"Shown only in this response. Treat it as a secret."},"authorization":{"type":"object","properties":{"scheme":{"type":"string","enum":["Sandbox"]},"example":{"type":"string"}},"required":["scheme","example"]},"embed":{"type":"object","properties":{"html":{"type":"string"},"fetch":{"type":"string"},"react":{"type":"string"},"astro":{"type":"string"},"nextjs_action":{"type":"string"}},"required":["html","fetch","react","astro","nextjs_action"]},"verify":{"type":"object","properties":{"curl":{"type":"string"},"then":{"type":"string"}},"required":["curl","then"]},"claim_url":{"type":"string"},"next":{"type":"array","items":{"type":"object","properties":{"why":{"type":"string"},"method":{"type":"string"},"path":{"type":"string"},"headers":{"type":"object","additionalProperties":{"type":"string"}},"body":{"type":"object","additionalProperties":{}}},"required":["why","method","path"]}}},"required":["sandbox","sandbox_token","authorization","embed","verify","claim_url","next"]}}}},"409":{"description":"Idempotency conflict","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Invalid input","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"429":{"description":"Source allowance exhausted","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"503":{"description":"Disabled or globally full","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/public/sandboxes/{id}":{"get":{"operationId":"public_sandboxes_get","tags":["discovery"],"summary":"Read a sandbox and its bounded Submissions using the sandbox capability","security":[{"sandboxAuth":[]}],"parameters":[{"schema":{"type":"string","example":"fm_8f3kq2"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","description":"Sandbox <sandbox_token>"},"required":true,"description":"Sandbox <sandbox_token>","name":"authorization","in":"header"}],"responses":{"200":{"description":"ok","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","example":"fm_8f3kq2"},"name":{"type":"string"},"status":{"type":"string","enum":["active","claimed","expired","blocked"]},"submit_url":{"type":"string"},"expires_at":{"type":"string","example":"2026-08-21T09:00:00.000Z"},"accepted_count":{"type":"integer"},"remaining":{"type":"integer"},"submissions":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","example":"fm_8f3kq2"},"data":{"type":"object","additionalProperties":{}},"meta":{"type":"object","additionalProperties":{}},"received_at":{"type":"string","example":"2026-08-21T09:00:00.000Z"}},"required":["id","data","meta","received_at"]}}},"required":["id","name","status","submit_url","expires_at","accepted_count","remaining","submissions"]}}}},"401":{"description":"Invalid sandbox capability","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"410":{"description":"Expired or consumed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/invitations/{id}":{"get":{"operationId":"invitations_get","tags":["discovery"],"summary":"Look up a pending invitation (public)","description":"Public — no credentials required, so the accept page can render the org name and inviter before the visitor signs in. The invited email is masked (email_hint).","security":[],"parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"ok","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublicInvitation"}}}},"400":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}},"delete":{"operationId":"invitations_revoke","tags":["discovery"],"summary":"Revoke a pending invitation","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"204":{"description":"revoked"},"400":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/me":{"get":{"operationId":"me_get","tags":["discovery"],"summary":"Identify the caller and summarise the organization","description":"The first call an agent holding only an API key should make: who it is, which org, which scopes the key has, plan limits and usage, and what already exists (counts). Call this before anything else.","responses":{"200":{"description":"ok","content":{"application/json":{"schema":{"type":"object","properties":{"organization":{"type":"object","properties":{"id":{"type":"string"},"slug":{"type":"string"},"name":{"type":"string"},"plan":{"type":"string"},"plan_source":{"type":"string"},"plan_expires_at":{"type":["string","null"]},"plan_note":{"type":["string","null"]},"timezone":{"type":"string"}},"required":["id","slug","name","plan","plan_source","plan_expires_at","plan_note","timezone"]},"key":{"type":"object","properties":{"prefix":{"type":"string"},"scopes":{"type":"array","items":{"type":"string","enum":["manage","read","submit"]}}},"required":["scopes"]},"organizations":{"type":"array","items":{"$ref":"#/components/schemas/OrganizationSummary"}},"counts":{"type":"object","properties":{"projects":{"type":"integer"},"forms":{"type":"integer"},"streams":{"type":"integer"},"destinations":{"type":"integer"},"routes":{"type":"integer"}},"required":["projects","forms","streams","destinations","routes"]},"limits":{"type":"object","properties":{"forms":{"type":"integer"},"submissions_per_month":{"type":"integer"},"destinations":{"type":"integer"},"retention_days":{"type":"integer"},"attachment_max_bytes":{"type":"integer"},"attachments_per_submission":{"type":"integer"},"attachment_storage_bytes":{"type":"integer"},"used":{"type":"object","properties":{"forms":{"type":"integer"},"submissions_this_month":{"type":"integer"},"attachment_storage_bytes":{"type":"integer"}},"required":["forms","submissions_this_month","attachment_storage_bytes"]}},"required":["forms","submissions_per_month","destinations","retention_days","attachment_max_bytes","attachments_per_submission","attachment_storage_bytes","used"]},"next":{"type":"array","items":{"type":"object","properties":{"why":{"type":"string"},"method":{"type":"string"},"path":{"type":"string"},"body":{"type":"object","additionalProperties":{}}}}}},"required":["organization","key","organizations","counts","limits","next"]}}}}}}},"/v1/api-keys":{"post":{"operationId":"api_keys_create","tags":["discovery"],"summary":"Create an API key for the active organization (session only, owner/admin)","description":"Requires a signed-in dashboard session, not another API key — an agent cannot mint its own first key this way. Owner or admin (job L). The full key is only ever returned once, in this response; store it immediately.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":32,"description":"A label to tell keys apart, e.g. 'CI deploy key'. Maximum 32 characters."},"scopes":{"type":"array","items":{"type":"string","enum":["manage","read","submit"]},"minItems":1,"default":["manage"],"description":"manage ⊇ read ⊇ submit — a manage key satisfies every read- or submit-scoped call too."}}}}}},"responses":{"201":{"description":"created","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":["string","null"]},"prefix":{"type":["string","null"]},"scopes":{"type":"array","items":{"type":"string","enum":["manage","read","submit"]}},"key":{"type":"string"},"created_at":{"type":"string"}},"required":["id","name","prefix","scopes","key","created_at"]}}}},"400":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}},"get":{"operationId":"api_keys_list","tags":["discovery"],"summary":"List API keys for the active organization","description":"Prefixes only — full key values are never returned again after creation.","responses":{"200":{"description":"ok","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":["string","null"]},"prefix":{"type":["string","null"]},"scopes":{"type":"array","items":{"type":"string","enum":["manage","read","submit"]}},"enabled":{"type":"boolean"},"created_at":{"type":"string"}},"required":["id","name","prefix","scopes","enabled","created_at"]}}}}}}}},"/v1/api-keys/{id}":{"delete":{"operationId":"api_keys_revoke","tags":["discovery"],"summary":"Delete an API key","description":"Revokes the key immediately; any client still using it starts getting 401s.","parameters":[{"schema":{"type":"string","description":"The API key id (not the key value itself)."},"required":true,"description":"The API key id (not the key value itself).","name":"id","in":"path"}],"responses":{"204":{"description":"deleted"},"400":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/me/active-organization":{"post":{"operationId":"me_set_active_organization","tags":["discovery"],"summary":"Switch the signed-in session's active organization","description":"Session only — an API key already resolves to exactly one organization and cannot switch. The caller must already be a member of the target organization (403 forbidden otherwise). Every subsequent /v1 call on this session scopes to the new organization.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"organization_id":{"type":"string"}},"required":["organization_id"]}}}},"responses":{"200":{"description":"ok","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActiveOrganization"}}}},"400":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/organizations/active":{"patch":{"operationId":"organizations_update_active","tags":["discovery"],"summary":"Update the active organization's settings","description":"Owner or admin (a manage-scoped API key counts as admin). Today that is the workspace timezone, used by the dashboard as the default when creating a digest Route. Each digest Route stores its own timezone, so changing this setting does not alter existing Routes.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"timezone":{"type":"string","minLength":1,"description":"IANA timezone, e.g. Europe/Stockholm. The dashboard uses it as the default for new digest Routes."}},"required":["timezone"]}}}},"responses":{"200":{"description":"ok","content":{"application/json":{"schema":{"type":"object","properties":{"timezone":{"type":"string"}},"required":["timezone"]}}}},"400":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/organizations":{"post":{"operationId":"organizations_create","tags":["discovery"],"summary":"Create a new organization and switch to it (session only)","description":"Session only. Creates the organization (the caller becomes its owner), the same default settings + 'Default' project every signup gets, then makes it this session's active organization.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1},"slug":{"type":"string","minLength":1}},"required":["name"]}}}},"responses":{"201":{"description":"created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrganizationSummary"}}}},"400":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/members":{"get":{"operationId":"members_list","tags":["discovery"],"summary":"List the organization's members","responses":{"200":{"description":"ok","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Member"}}}}}}}},"/v1/members/{memberId}":{"patch":{"operationId":"members_update_role","tags":["discovery"],"summary":"Change a member's role (owner only)","description":"Owner only — an admin cannot promote or demote anyone, including themselves. Cannot demote the organization's last owner (409 last_owner); promote someone else first.","parameters":[{"schema":{"type":"string","description":"The Membership id (not the user id)."},"required":true,"description":"The Membership id (not the user id).","name":"memberId","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"role":{"type":"string","enum":["owner","admin","member"]}},"required":["role"]}}}},"responses":{"200":{"description":"ok","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Member"}}}},"400":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}},"delete":{"operationId":"members_remove","tags":["discovery"],"summary":"Remove a member, or leave the organization","description":"Owner or admin may remove any other member. A signed-in member may always remove themselves (leave) regardless of role. Either way, the organization's last owner cannot be removed (409 last_owner).","parameters":[{"schema":{"type":"string","description":"The Membership id (not the user id)."},"required":true,"description":"The Membership id (not the user id).","name":"memberId","in":"path"}],"responses":{"204":{"description":"removed"},"400":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/invitations":{"get":{"operationId":"invitations_list","tags":["discovery"],"summary":"List pending invitations","responses":{"200":{"description":"ok","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Invitation"}}}}}}},"post":{"operationId":"invitations_create","tags":["discovery"],"summary":"Invite someone to the organization","description":"Owner or admin. Sends an email via Resend when the server has RESEND_API_KEY configured; otherwise the invitation is still created and its accept link can be shared manually. Re-inviting an address with a pending invitation resends it (extends the 7-day expiry) instead of creating a duplicate.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string","format":"email"},"role":{"type":"string","enum":["admin","member"],"description":"Invite as admin or member. Promote to owner from Members after they join."}},"required":["email","role"]}}}},"responses":{"201":{"description":"created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Invitation"}}}},"400":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"502":{"description":"The invitation was created, but the email could not be sent","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/invitations/{id}/accept":{"post":{"operationId":"invitations_accept","tags":["discovery"],"summary":"Accept an invitation (session only)","description":"Session only. The signed-in user's email must match the invitation's invited email exactly (403 invitation_email_mismatch otherwise). On success the organization becomes this session's active organization.","parameters":[{"schema":{"type":"string"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"ok","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AcceptInvitationResult"}}}},"400":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/plan/redeem":{"post":{"operationId":"plan_redeem","tags":["plan"],"summary":"Redeem a complimentary-access grant code for the active organization","description":"Requires the manage scope (a session is always manage-scoped). A code minted by POST /v1/admin/plan-grants sets plan, plan_source='complimentary', plan_note and — if the grant has a plan_duration_days — plan_expires_at. A paying org (plan_source='billing') cannot redeem (409 plan_is_billing: cancel the subscription first); redeeming a lower tier than the org's current plan is refused (409 plan_not_upgrade).","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"code":{"type":"string","minLength":1,"description":"A code minted by POST /v1/admin/plan-grants."}},"required":["code"]}}}},"responses":{"200":{"description":"ok","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlanRedeemResult"}}}},"400":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"410":{"description":"The code is expired, revoked, or fully redeemed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/admin/plan-grants":{"post":{"operationId":"admin_plan_grants_create","tags":["admin"],"summary":"Mint a complimentary-access grant code (platform admin only)","description":"Platform-admin only: allowed when the caller's email (the session user's email, or — for an API key — the owner member's email of the key's organization) is in the server's PLATFORM_ADMIN_EMAILS env var (comma-separated, empty by default). Any other caller gets 404 not_found, not 403, so a self-hosted operator who never sets PLATFORM_ADMIN_EMAILS never sees this endpoint exist. Touches no tenant — the code is redeemed by an org's own owner with POST /v1/plan/redeem, which is where plan_source actually changes.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"plan":{"type":"string","enum":["free","pro","team","selfhost"],"description":"The tier this code grants on redemption."},"note":{"type":"string","maxLength":280,"description":"Shown to the redeeming org, e.g. \"Courtesy of Postbag\"."},"expires_at":{"type":"string","format":"date-time","description":"When the code itself stops being redeemable. Omit for a code that never expires on its own."},"max_redemptions":{"type":"integer","minimum":1,"default":1,"description":"How many different organizations may redeem this code."},"plan_duration_days":{"type":"integer","minimum":1,"description":"How many days after redemption the granted plan lasts. Omit for a grant with no automatic expiry."}},"required":["plan"]}}}},"responses":{"201":{"description":"created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlanGrantCreated"}}}},"400":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}},"get":{"operationId":"admin_plan_grants_list","tags":["admin"],"summary":"List plan grants (platform admin only; hashed codes never returned)","description":"Platform-admin only: allowed when the caller's email (the session user's email, or — for an API key — the owner member's email of the key's organization) is in the server's PLATFORM_ADMIN_EMAILS env var (comma-separated, empty by default). Any other caller gets 404 not_found, not 403, so a self-hosted operator who never sets PLATFORM_ADMIN_EMAILS never sees this endpoint exist.","responses":{"200":{"description":"ok","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/PlanGrant"}}}}},"400":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/admin/plan-grants/{id}/revoke":{"post":{"operationId":"admin_plan_grants_revoke","tags":["admin"],"summary":"Revoke a plan grant so it can no longer be redeemed (platform admin only)","description":"Platform-admin only: allowed when the caller's email (the session user's email, or — for an API key — the owner member's email of the key's organization) is in the server's PLATFORM_ADMIN_EMAILS env var (comma-separated, empty by default). Any other caller gets 404 not_found, not 403, so a self-hosted operator who never sets PLATFORM_ADMIN_EMAILS never sees this endpoint exist. Idempotent — revoking an already-revoked grant just returns it.","parameters":[{"schema":{"type":"string","example":"fm_8f3kq2"},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"ok","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlanGrant"}}}},"400":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/admin/growth-metrics":{"get":{"operationId":"admin_growth_metrics","tags":["admin"],"summary":"Platform-wide growth KPIs (platform admin only; aggregates only)","description":"Platform-admin only: allowed when the caller's email (the session user's email, or — for an API key — the owner member's email of the key's organization) is in the server's PLATFORM_ADMIN_EMAILS env var (comma-separated, empty by default). Any other caller gets 404 not_found, not 403, so a self-hosted operator who never sets PLATFORM_ADMIN_EMAILS never sees this endpoint exist. Returns COUNT aggregates across every organization — never emails, names, submission payloads, API keys, or per-organization dumps. Requires the read scope. This is the narrow platform aggregate exception defined by ADR-011. Every value is a current snapshot of retained database rows, so retention and user deletion can reduce totals and window counts; this is not an append-only historical funnel. Sandbox fields are explicitly prefixed `retained_` because housekeeping deletes every sandbox after expires_at, including claimed rows. `orgs_with_real_delivery_30d` is organizations with at least one successful (status=sent) Delivery of a non-test Submission in 30 days.","responses":{"200":{"description":"ok","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GrowthMetrics"}}}},"400":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/billing":{"get":{"operationId":"billing_get","tags":["billing"],"summary":"Get billing status and purchasable plans","responses":{"200":{"description":"ok","content":{"application/json":{"schema":{"type":"object","properties":{"enabled":{"type":"boolean"},"plan":{"type":"string"},"plan_source":{"type":"string"},"subscription":{"type":["object","null"],"properties":{"id":{"type":"string"},"status":{"type":"string"},"current_period_end":{"type":["string","null"]},"cancel_at_period_end":{"type":"boolean"}},"required":["id","status","current_period_end","cancel_at_period_end"]},"products":{"type":"object","properties":{"pro":{"type":"object","properties":{"month":{"type":"boolean"},"year":{"type":"boolean"}},"required":["month","year"]},"team":{"type":"object","properties":{"month":{"type":"boolean"},"year":{"type":"boolean"}},"required":["month","year"]}},"required":["pro","team"]}},"required":["enabled","plan","plan_source","subscription","products"]}}}}}}},"/v1/billing/checkout":{"post":{"operationId":"billing_checkout","tags":["billing"],"summary":"Start a Polar checkout","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"plan":{"type":"string","enum":["pro","team"]},"interval":{"type":"string","enum":["month","year"]}},"required":["plan","interval"]}}}},"responses":{"201":{"description":"checkout created","content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","format":"uri"}},"required":["url"]}}}},"400":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/billing/portal":{"post":{"operationId":"billing_portal","tags":["billing"],"summary":"Open the Polar customer portal","responses":{"201":{"description":"portal session created","content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","format":"uri"}},"required":["url"]}}}},"400":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/quickstart":{"post":{"operationId":"quickstart","tags":["discovery"],"summary":"One call to a working, routed form","description":"Creates (idempotently, by name within project) the project if missing, the form, a destination for whichever of notify_email/telegram/webhook was given, and a direct route, then returns the form, an embeddable snippet, a browser-equivalent curl command to verify delivery, and next steps. Allowed origins are compared canonically, so paths, trailing slashes, host casing, and default ports do not cause mismatches. Everything it does is also available as individual calls (POST /v1/forms, /v1/destinations, /v1/routes); this is a convenience, not a special path.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1},"project":{"type":"string","default":"default"},"origin":{"type":"string","format":"uri"},"notify_email":{"type":"string","format":"email"},"telegram":{"type":"object","properties":{"bot_token":{"type":"string"},"chat_id":{"type":"string"}},"required":["bot_token","chat_id"]},"webhook":{"type":"object","properties":{"url":{"type":"string","format":"uri"},"secret":{"type":"string"}},"required":["url"]},"tags":{"type":"array","items":{"type":"string"},"default":[]},"redirect_url":{"type":"string","format":"uri"}},"required":["name"]}}}},"responses":{"201":{"description":"created (or returned)","content":{"application/json":{"schema":{"type":"object","properties":{"form":{"$ref":"#/components/schemas/Form"},"destinations":{"type":"array","items":{"$ref":"#/components/schemas/Destination"}},"routes":{"type":"array","items":{"$ref":"#/components/schemas/Route"}},"embed":{"$ref":"#/components/schemas/Embed"},"verify":{"$ref":"#/components/schemas/Verify"},"next":{"type":"array","items":{"type":"object","properties":{"why":{"type":"string"},"method":{"type":"string"},"path":{"type":"string"},"body":{"type":"object","additionalProperties":{}}}}}},"required":["form","destinations","routes","embed","verify","next"]}}}},"400":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/sandboxes/{id}/claim":{"post":{"operationId":"sandboxes_claim","tags":["forms"],"summary":"Claim a sandbox Form into the active organization","description":"Requires both an authenticated manage-scoped actor and the sandbox capability in Postbag-Sandbox-Token. The Form id and submit URL remain unchanged.","parameters":[{"schema":{"type":"string","example":"fm_8f3kq2"},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","description":"The sandbox token returned at creation."},"required":true,"description":"The sandbox token returned at creation.","name":"postbag-sandbox-token","in":"header"}],"responses":{"200":{"description":"claimed or idempotently returned","content":{"application/json":{"schema":{"type":"object","properties":{"claimed":{"type":"boolean","enum":[true]},"idempotent":{"type":"boolean"},"form":{"type":"object","properties":{"id":{"type":"string","example":"fm_8f3kq2"},"project_id":{"type":"string","example":"fm_8f3kq2"},"slug":{"type":"string"},"name":{"type":"string"},"submit_url":{"type":"string"}},"required":["id","project_id","slug","name","submit_url"]},"copied_test_submissions":{"type":"integer"},"next":{"type":"array","items":{"type":"object","properties":{"why":{"type":"string"},"method":{"type":"string"},"path":{"type":"string"},"body":{"type":"object","additionalProperties":{}}},"required":["why","method","path"]}}},"required":["claimed","idempotent","form","copied_test_submissions","next"]}}}},"401":{"description":"Invalid sandbox capability","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"402":{"description":"Target organization Form limit reached","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Claim email mismatch","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"410":{"description":"Expired or consumed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/projects":{"get":{"operationId":"projects_list","tags":["projects"],"summary":"List projects","parameters":[{"schema":{"type":"string"},"required":false,"name":"cursor","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":200},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"ok","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Project"}},"next_cursor":{"type":["string","null"]}},"required":["data","next_cursor"]}}}}}},"post":{"operationId":"projects_create","tags":["projects"],"summary":"Create a project","description":"A project groups forms — one per site or app. Pass `if_exists: \"return\"` to make this call safe to re-run.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1},"slug":{"type":"string","pattern":"^[a-z0-9][a-z0-9-]{1,62}$/u"},"tags":{"type":"array","items":{"type":"string"},"default":[]},"if_exists":{"type":"string","enum":["error","return"],"default":"error"}}}}}},"responses":{"201":{"description":"created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Project"}}}},"400":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/projects/{projectId}":{"get":{"operationId":"projects_get","tags":["projects"],"summary":"Get a project","parameters":[{"schema":{"type":"string","example":"fm_8f3kq2"},"required":true,"name":"projectId","in":"path"}],"responses":{"200":{"description":"ok","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Project"}}}},"400":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}},"patch":{"operationId":"projects_update","tags":["projects"],"summary":"Update a project","parameters":[{"schema":{"type":"string","example":"fm_8f3kq2"},"required":true,"name":"projectId","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1},"slug":{"type":"string","pattern":"^[a-z0-9][a-z0-9-]{1,62}$/u"},"tags":{"type":"array","items":{"type":"string"},"default":[]},"if_exists":{"type":"string","enum":["error","return"],"default":"error"}}}}}},"responses":{"200":{"description":"ok","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Project"}}}},"400":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}},"delete":{"operationId":"projects_delete","tags":["projects"],"summary":"Delete an empty project","description":"Fails with 409 while the project still has forms — delete or move them first.","parameters":[{"schema":{"type":"string","example":"fm_8f3kq2"},"required":true,"name":"projectId","in":"path"}],"responses":{"204":{"description":"deleted"},"400":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/forms":{"get":{"operationId":"forms_list","tags":["forms"],"summary":"List forms","parameters":[{"schema":{"type":"string"},"required":false,"name":"cursor","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":200},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","description":"Filter to one project id."},"required":false,"description":"Filter to one project id.","name":"project","in":"query"},{"schema":{"type":"string","description":"Filter to forms carrying this tag."},"required":false,"description":"Filter to forms carrying this tag.","name":"tag","in":"query"},{"schema":{"type":"string","description":"Filter to forms attached to this stream id."},"required":false,"description":"Filter to forms attached to this stream id.","name":"stream","in":"query"}],"responses":{"200":{"description":"ok","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Form"}},"next_cursor":{"type":["string","null"]}},"required":["data","next_cursor"]}}}}}},"post":{"operationId":"forms_create","tags":["forms"],"summary":"Create a form","description":"Pass `from_template` (a stream id) to attach the new form to that stream with a schema copied 1:1 from it, so the mapping is trivially complete. Pass `if_exists: \"return\"` to make this call safe to re-run.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1},"slug":{"type":"string","pattern":"^[a-z0-9][a-z0-9-]{1,62}$/u"},"project":{"type":"string","default":"default"},"tags":{"type":"array","items":{"type":"string"},"default":[]},"status":{"type":"string","enum":["active","paused"],"default":"active"},"schema_mode":{"type":"string","enum":["observe","enforce","managed"],"default":"observe"},"settings":{"type":"object","properties":{"allowed_origins":{"type":"array","items":{"type":"string","format":"uri"},"default":[]},"redirect_url":{"type":["string","null"],"format":"uri"},"honeypot_field":{"type":"string","default":"_gotcha"},"turnstile":{"type":"object","properties":{"secret":{"type":"string"},"enabled":{"type":"boolean"}}},"rate_limit":{"type":"object","properties":{"per_minute":{"type":"integer","exclusiveMinimum":0,"default":10},"burst":{"type":"integer","exclusiveMinimum":0,"default":20}}},"reply_to_field":{"type":["string","null"]}}},"schema":{"type":"object","properties":{"json_schema":{"type":"object","additionalProperties":{},"description":"JSON Schema draft 2020-12 describing the submission data. `required` drives drift detection and mapping completeness."},"ui":{"type":"object","additionalProperties":{"type":"object","additionalProperties":{}},"description":"Per-field rendering hints keyed by field name (label, placeholder, widget, options, order) — used to render embed snippets."},"changelog":{"type":"string","description":"A short human note on what changed in this version, shown in the version history."}},"required":["json_schema"]},"from_template":{"type":"string"},"if_exists":{"type":"string","enum":["error","return"],"default":"error"}}}}}},"responses":{"201":{"description":"created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FormCreated"}}}},"400":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/forms/{formId}":{"get":{"operationId":"forms_get","tags":["forms"],"summary":"Get a form","parameters":[{"schema":{"type":"string","example":"fm_8f3kq2"},"required":true,"name":"formId","in":"path"}],"responses":{"200":{"description":"ok","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Form"}}}},"400":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}},"patch":{"operationId":"forms_update","tags":["forms"],"summary":"Update a form","parameters":[{"schema":{"type":"string","example":"fm_8f3kq2"},"required":true,"name":"formId","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1},"slug":{"type":"string","pattern":"^[a-z0-9][a-z0-9-]{1,62}$/u"},"project":{"type":"string","default":"default"},"tags":{"type":"array","items":{"type":"string"},"default":[]},"status":{"type":"string","enum":["active","paused"],"default":"active"},"schema_mode":{"type":"string","enum":["observe","enforce","managed"],"default":"observe"},"settings":{"type":"object","properties":{"allowed_origins":{"type":"array","items":{"type":"string","format":"uri"},"default":[]},"redirect_url":{"type":["string","null"],"format":"uri"},"honeypot_field":{"type":"string","default":"_gotcha"},"turnstile":{"type":"object","properties":{"secret":{"type":"string"},"enabled":{"type":"boolean"}}},"rate_limit":{"type":"object","properties":{"per_minute":{"type":"integer","exclusiveMinimum":0,"default":10},"burst":{"type":"integer","exclusiveMinimum":0,"default":20}}},"reply_to_field":{"type":["string","null"]}}},"schema":{"type":"object","properties":{"json_schema":{"type":"object","additionalProperties":{},"description":"JSON Schema draft 2020-12 describing the submission data. `required` drives drift detection and mapping completeness."},"ui":{"type":"object","additionalProperties":{"type":"object","additionalProperties":{}},"description":"Per-field rendering hints keyed by field name (label, placeholder, widget, options, order) — used to render embed snippets."},"changelog":{"type":"string","description":"A short human note on what changed in this version, shown in the version history."}},"required":["json_schema"]},"from_template":{"type":"string"},"if_exists":{"type":"string","enum":["error","return"],"default":"error"}}}}}},"responses":{"200":{"description":"ok","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Form"}}}},"400":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}},"delete":{"operationId":"forms_delete","tags":["forms"],"summary":"Delete a form","parameters":[{"schema":{"type":"string","example":"fm_8f3kq2"},"required":true,"name":"formId","in":"path"}],"responses":{"204":{"description":"deleted"},"400":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/forms/{formId}/embed":{"get":{"operationId":"forms_embed","tags":["forms"],"summary":"Embed snippets","description":"Ready-to-paste HTML, fetch, React, Astro and Next.js server-action snippets for this form's submit URL.","parameters":[{"schema":{"type":"string","example":"fm_8f3kq2"},"required":true,"name":"formId","in":"path"}],"responses":{"200":{"description":"ok","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Embed"}}}},"400":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/forms/{formId}/schema":{"get":{"operationId":"forms_schema_get","tags":["forms"],"summary":"Current schema version","description":"For observe-mode forms with no published version yet, falls back to the last inferred draft (marked `inferred: true`) if one exists; publishing is still a separate, explicit act.","parameters":[{"schema":{"type":"string","example":"fm_8f3kq2"},"required":true,"name":"formId","in":"path"}],"responses":{"200":{"description":"ok","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SchemaVersion"}}}},"400":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}},"post":{"operationId":"forms_schema_publish","tags":["forms"],"summary":"Publish a new schema version","description":"Schemas are immutable versions — this always creates version N+1, never mutates an existing one.","parameters":[{"schema":{"type":"string","example":"fm_8f3kq2"},"required":true,"name":"formId","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"json_schema":{"type":"object","additionalProperties":{},"description":"JSON Schema draft 2020-12 describing the submission data. `required` drives drift detection and mapping completeness."},"ui":{"type":"object","additionalProperties":{"type":"object","additionalProperties":{}},"description":"Per-field rendering hints keyed by field name (label, placeholder, widget, options, order) — used to render embed snippets."},"changelog":{"type":"string","description":"A short human note on what changed in this version, shown in the version history."}},"required":["json_schema"]}}}},"responses":{"201":{"description":"published","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SchemaVersion"}}}},"400":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/forms/{formId}/schema/versions":{"get":{"operationId":"forms_schema_versions","tags":["forms"],"summary":"All schema versions","parameters":[{"schema":{"type":"string","example":"fm_8f3kq2"},"required":true,"name":"formId","in":"path"}],"responses":{"200":{"description":"ok","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SchemaVersion"}}}}}}}},"/v1/forms/{formId}/schema/infer":{"post":{"operationId":"forms_schema_infer","tags":["forms"],"summary":"Infer a draft schema now, from recent submissions (observe mode, no schema yet)","description":"Only for observe-mode forms with no published schema. Looks at recent non-spam submissions and produces a draft schema; call forms_schema_publish with the result (or an edited version of it) to make it real.","parameters":[{"schema":{"type":"string","example":"fm_8f3kq2"},"required":true,"name":"formId","in":"path"}],"responses":{"200":{"description":"ok","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SchemaVersion"}}}},"400":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/forms/{formId}/drift":{"get":{"operationId":"forms_drift","tags":["forms"],"summary":"Unresolved drift events","description":"Fields submitters are sending that the published schema doesn't declare. Resolve by publishing an updated schema.","parameters":[{"schema":{"type":"string","example":"fm_8f3kq2"},"required":true,"name":"formId","in":"path"}],"responses":{"200":{"description":"ok","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","additionalProperties":{}}}}}}}}},"/v1/forms/{formId}/submissions":{"get":{"operationId":"forms_submissions_list","tags":["submissions"],"summary":"List submissions for a form","parameters":[{"schema":{"type":"string","example":"fm_8f3kq2"},"required":true,"name":"formId","in":"path"},{"schema":{"type":"string"},"required":false,"name":"cursor","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":200},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","description":"Filter by status: received, quarantined or spam."},"required":false,"description":"Filter by status: received, quarantined or spam.","name":"status","in":"query"},{"schema":{"type":"string","description":"ISO timestamp; only submissions received after this."},"required":false,"description":"ISO timestamp; only submissions received after this.","name":"since","in":"query"},{"schema":{"type":"string","description":"Free-text search over submission data."},"required":false,"description":"Free-text search over submission data.","name":"q","in":"query"}],"responses":{"200":{"description":"ok","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Submission"}},"next_cursor":{"type":["string","null"]}},"required":["data","next_cursor"]}}}}}}},"/v1/submissions":{"get":{"operationId":"submissions_list","tags":["submissions"],"summary":"Search submissions across the organization","parameters":[{"schema":{"type":"string"},"required":false,"name":"cursor","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":200},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","description":"Filter to submissions routed through this stream id."},"required":false,"description":"Filter to submissions routed through this stream id.","name":"stream","in":"query"},{"schema":{"type":"string","description":"Filter to one form id."},"required":false,"description":"Filter to one form id.","name":"form","in":"query"},{"schema":{"type":"string","description":"Filter by status: received, quarantined or spam."},"required":false,"description":"Filter by status: received, quarantined or spam.","name":"status","in":"query"},{"schema":{"type":"string","description":"Free-text search over submission data."},"required":false,"description":"Free-text search over submission data.","name":"q","in":"query"}],"responses":{"200":{"description":"ok","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Submission"}},"next_cursor":{"type":["string","null"]}},"required":["data","next_cursor"]}}}}}}},"/v1/submissions/{submissionId}":{"get":{"operationId":"submissions_get","tags":["submissions"],"summary":"Get a submission with its deliveries","parameters":[{"schema":{"type":"string"},"required":true,"name":"submissionId","in":"path"}],"responses":{"200":{"description":"ok","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubmissionDetail"}}}},"400":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}},"patch":{"operationId":"submissions_update","tags":["submissions"],"summary":"Change status (re-routes if moving to received)","description":"Moving a quarantined submission to `received` re-runs routing and queues deliveries for it. An `over_quota` submission stays quarantined until the current plan has capacity.","parameters":[{"schema":{"type":"string"},"required":true,"name":"submissionId","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["received","quarantined","spam"]}},"required":["status"]}}}},"responses":{"200":{"description":"ok","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Submission"}}}},"400":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}},"delete":{"operationId":"submissions_delete","tags":["submissions"],"summary":"Delete permanently (GDPR)","description":"Irreversible. Use for data-subject deletion requests; deliveries already sent are unaffected.","parameters":[{"schema":{"type":"string"},"required":true,"name":"submissionId","in":"path"}],"responses":{"204":{"description":"deleted"},"400":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/attachments/{attachmentId}/download":{"get":{"operationId":"attachments_download","tags":["submissions"],"summary":"Download a Submission attachment","description":"Authorizes the organization, then redirects to a private URL valid for 15 minutes.","parameters":[{"schema":{"type":"string"},"required":true,"name":"attachmentId","in":"path"}],"responses":{"302":{"description":"Temporary private download URL","headers":{"Location":{"schema":{"type":"string"}}}},"400":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"503":{"description":"Attachment storage unavailable","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/streams":{"get":{"operationId":"streams_list","tags":["streams"],"summary":"List streams","parameters":[{"schema":{"type":"string"},"required":false,"name":"cursor","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":200},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"ok","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Stream"}},"next_cursor":{"type":["string","null"]}},"required":["data","next_cursor"]}}}}}},"post":{"operationId":"streams_create","tags":["streams"],"summary":"Create a stream (optionally with its first schema version and sources)","description":"A Stream collects Submissions from many Forms into one shared shape. Pass `schema` to define that shape up front, or omit it and pass `sources` — the first source's form then provides version 1 (copied from its published schema, its inferred draft, or the fields seen in recent submissions) with an identity mapping, so `{ name, sources: [{ form_id }] }` is a complete request.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1},"slug":{"type":"string","pattern":"^[a-z0-9][a-z0-9-]{1,62}$/u"},"schema":{"type":"object","properties":{"json_schema":{"type":"object","additionalProperties":{},"description":"JSON Schema draft 2020-12 describing the submission data. `required` drives drift detection and mapping completeness."},"ui":{"type":"object","additionalProperties":{"type":"object","additionalProperties":{}},"description":"Per-field rendering hints keyed by field name (label, placeholder, widget, options, order) — used to render embed snippets."},"changelog":{"type":"string","description":"A short human note on what changed in this version, shown in the version history."}},"required":["json_schema"]},"sources":{"type":"array","items":{"type":"object","properties":{"form_id":{"type":"string","description":"One Form feeding this Stream. Mutually exclusive with `selector`."},"selector":{"type":"string","pattern":"^(?:tag|project):\\S(?:.*\\S)?$","description":"A dynamic Form source: non-empty `tag:<tag>` or `project:<project_id>` without leading or trailing whitespace after `:`. Mutually exclusive with `form_id`."},"mapping":{"type":"object","additionalProperties":{"type":"object","properties":{"from":{"type":"string","description":"Copy this field's value from the source form's submitted data."},"const":{"description":"Use this fixed value instead of anything from the source."},"expr":{"type":"string","description":"A JSONata expression evaluated against the source's data."},"default":{"description":"Fallback value when the resolved value is missing."}}},"description":"Keyed by the stream schema's field names. Each entry is exactly one of `from` (copy a field from the form's data), `const` (a fixed value) or `expr` (a JSONata expression), plus an optional `default`. Every required field in the stream's schema must be covered or this call fails 422 mapping_incomplete."}}},"default":[]},"if_exists":{"type":"string","enum":["error","return"],"default":"error"}}}}}},"responses":{"201":{"description":"created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Stream"}}}},"400":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/streams/{streamId}":{"get":{"operationId":"streams_get","tags":["streams"],"summary":"Get a stream with schema, sources, mapping status and routes","parameters":[{"schema":{"type":"string","example":"fm_8f3kq2"},"required":true,"name":"streamId","in":"path"}],"responses":{"200":{"description":"ok","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StreamDetail"}}}},"400":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}},"patch":{"operationId":"streams_update","tags":["streams"],"summary":"Update name/slug","parameters":[{"schema":{"type":"string","example":"fm_8f3kq2"},"required":true,"name":"streamId","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1},"slug":{"type":"string","pattern":"^[a-z0-9][a-z0-9-]{1,62}$/u"},"schema":{"type":"object","properties":{"json_schema":{"type":"object","additionalProperties":{},"description":"JSON Schema draft 2020-12 describing the submission data. `required` drives drift detection and mapping completeness."},"ui":{"type":"object","additionalProperties":{"type":"object","additionalProperties":{}},"description":"Per-field rendering hints keyed by field name (label, placeholder, widget, options, order) — used to render embed snippets."},"changelog":{"type":"string","description":"A short human note on what changed in this version, shown in the version history."}},"required":["json_schema"]},"sources":{"type":"array","items":{"type":"object","properties":{"form_id":{"type":"string","description":"One Form feeding this Stream. Mutually exclusive with `selector`."},"selector":{"type":"string","pattern":"^(?:tag|project):\\S(?:.*\\S)?$","description":"A dynamic Form source: non-empty `tag:<tag>` or `project:<project_id>` without leading or trailing whitespace after `:`. Mutually exclusive with `form_id`."},"mapping":{"type":"object","additionalProperties":{"type":"object","properties":{"from":{"type":"string","description":"Copy this field's value from the source form's submitted data."},"const":{"description":"Use this fixed value instead of anything from the source."},"expr":{"type":"string","description":"A JSONata expression evaluated against the source's data."},"default":{"description":"Fallback value when the resolved value is missing."}}},"description":"Keyed by the stream schema's field names. Each entry is exactly one of `from` (copy a field from the form's data), `const` (a fixed value) or `expr` (a JSONata expression), plus an optional `default`. Every required field in the stream's schema must be covered or this call fails 422 mapping_incomplete."}}},"default":[]},"if_exists":{"type":"string","enum":["error","return"],"default":"error"}}}}}},"responses":{"200":{"description":"ok","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Stream"}}}},"400":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}},"delete":{"operationId":"streams_delete","tags":["streams"],"summary":"Delete a stream","parameters":[{"schema":{"type":"string","example":"fm_8f3kq2"},"required":true,"name":"streamId","in":"path"}],"responses":{"204":{"description":"deleted"},"400":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/streams/{streamId}/schema":{"get":{"operationId":"streams_schema_get","tags":["streams"],"summary":"Current stream schema","parameters":[{"schema":{"type":"string","example":"fm_8f3kq2"},"required":true,"name":"streamId","in":"path"}],"responses":{"200":{"description":"ok","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SchemaVersion"}}}},"400":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}},"post":{"operationId":"streams_schema_publish","tags":["streams"],"summary":"Publish a new stream schema version","description":"Schemas are immutable versions — this always creates version N+1. Re-validates every attached source's mapping against the new schema and returns the updated mapping statuses alongside it.","parameters":[{"schema":{"type":"string","example":"fm_8f3kq2"},"required":true,"name":"streamId","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"json_schema":{"type":"object","additionalProperties":{},"description":"JSON Schema draft 2020-12 describing the submission data. `required` drives drift detection and mapping completeness."},"ui":{"type":"object","additionalProperties":{"type":"object","additionalProperties":{}},"description":"Per-field rendering hints keyed by field name (label, placeholder, widget, options, order) — used to render embed snippets."},"changelog":{"type":"string","description":"A short human note on what changed in this version, shown in the version history."}},"required":["json_schema"]}}}},"responses":{"201":{"description":"published","content":{"application/json":{"schema":{"type":"object","properties":{"schema":{"$ref":"#/components/schemas/SchemaVersion"},"mappings":{"type":"array","items":{"$ref":"#/components/schemas/StreamSource"}}},"required":["schema","mappings"]}}}},"400":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/streams/{streamId}/sources":{"get":{"operationId":"streams_sources_list","tags":["streams"],"summary":"List sources and their mapping status","parameters":[{"schema":{"type":"string","example":"fm_8f3kq2"},"required":true,"name":"streamId","in":"path"}],"responses":{"200":{"description":"ok","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/StreamSource"}}}}}}},"post":{"operationId":"streams_sources_add","tags":["streams"],"summary":"Attach a form or selector with a mapping","description":"If the stream has no schema yet, the attached form provides version 1 (copied from its published schema, its inferred draft, or the fields seen in recent submissions) and an identity mapping is used when `mapping` is omitted — so attaching the first form never needs a hand-written schema. Fails 422 stream_schema_missing if neither the stream nor the form has any fields to work from. Fails 422 mapping_incomplete (with the missing fields listed) if the mapping does not cover every field the stream's schema requires — a form can't join a stream it can't feed.","parameters":[{"schema":{"type":"string","example":"fm_8f3kq2"},"required":true,"name":"streamId","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"form_id":{"type":"string","description":"One Form feeding this Stream. Mutually exclusive with `selector`."},"selector":{"type":"string","pattern":"^(?:tag|project):\\S(?:.*\\S)?$","description":"A dynamic Form source: non-empty `tag:<tag>` or `project:<project_id>` without leading or trailing whitespace after `:`. Mutually exclusive with `form_id`."},"mapping":{"type":"object","additionalProperties":{"type":"object","properties":{"from":{"type":"string","description":"Copy this field's value from the source form's submitted data."},"const":{"description":"Use this fixed value instead of anything from the source."},"expr":{"type":"string","description":"A JSONata expression evaluated against the source's data."},"default":{"description":"Fallback value when the resolved value is missing."}}},"description":"Keyed by the stream schema's field names. Each entry is exactly one of `from` (copy a field from the form's data), `const` (a fixed value) or `expr` (a JSONata expression), plus an optional `default`. Every required field in the stream's schema must be covered or this call fails 422 mapping_incomplete."}}}}}},"responses":{"201":{"description":"attached","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StreamSource"}}}},"400":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/streams/{streamId}/sources/{sourceId}":{"patch":{"operationId":"streams_sources_update","tags":["streams"],"summary":"Update the mapping","parameters":[{"schema":{"type":"string","example":"fm_8f3kq2"},"required":true,"name":"streamId","in":"path"},{"schema":{"type":"string","example":"fm_8f3kq2"},"required":true,"name":"sourceId","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"form_id":{"type":"string","description":"One Form feeding this Stream. Mutually exclusive with `selector`."},"selector":{"type":"string","pattern":"^(?:tag|project):\\S(?:.*\\S)?$","description":"A dynamic Form source: non-empty `tag:<tag>` or `project:<project_id>` without leading or trailing whitespace after `:`. Mutually exclusive with `form_id`."},"mapping":{"type":"object","additionalProperties":{"type":"object","properties":{"from":{"type":"string","description":"Copy this field's value from the source form's submitted data."},"const":{"description":"Use this fixed value instead of anything from the source."},"expr":{"type":"string","description":"A JSONata expression evaluated against the source's data."},"default":{"description":"Fallback value when the resolved value is missing."}}},"description":"Keyed by the stream schema's field names. Each entry is exactly one of `from` (copy a field from the form's data), `const` (a fixed value) or `expr` (a JSONata expression), plus an optional `default`. Every required field in the stream's schema must be covered or this call fails 422 mapping_incomplete."}}}}}},"responses":{"200":{"description":"ok","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StreamSource"}}}},"400":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}},"delete":{"operationId":"streams_sources_remove","tags":["streams"],"summary":"Detach","parameters":[{"schema":{"type":"string","example":"fm_8f3kq2"},"required":true,"name":"streamId","in":"path"},{"schema":{"type":"string","example":"fm_8f3kq2"},"required":true,"name":"sourceId","in":"path"}],"responses":{"204":{"description":"detached"},"400":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/streams/{streamId}/preview":{"post":{"operationId":"streams_preview","tags":["streams"],"summary":"Run a sample submission through a source's mapping","description":"Dry-runs a form's attached mapping against sample data without storing anything — useful before publishing a schema change.","parameters":[{"schema":{"type":"string","example":"fm_8f3kq2"},"required":true,"name":"streamId","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"form_id":{"type":"string","example":"fm_8f3kq2","description":"The form whose attached mapping to run."},"data":{"type":"object","additionalProperties":{},"description":"Sample submission data to map through."}},"required":["form_id","data"]}}}},"responses":{"200":{"description":"ok","content":{"application/json":{"schema":{"type":"object","properties":{"payload":{"type":"object","additionalProperties":{}},"extras":{"type":"object","additionalProperties":{}},"problems":{"type":"array","items":{"type":"string"}}},"required":["payload","extras","problems"]}}}}}}},"/v1/destinations":{"get":{"operationId":"destinations_list","tags":["destinations"],"summary":"List destinations (secrets redacted)","parameters":[{"schema":{"type":"string"},"required":false,"name":"cursor","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":200},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"ok","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Destination"}},"next_cursor":{"type":["string","null"]}},"required":["data","next_cursor"]}}}}}},"post":{"operationId":"destinations_create","tags":["destinations"],"summary":"Create a destination","description":"type is one of email, telegram, webhook, slack, discord; config shape depends on type. Call destinations_test after creating to verify it works.","requestBody":{"content":{"application/json":{"schema":{"anyOf":[{"type":"object","properties":{"type":{"type":"string","enum":["email"]},"name":{"type":"string"},"config":{"type":"object","properties":{"to":{"type":"array","items":{"type":"string","format":"email"},"minItems":1},"cc":{"type":"array","items":{"type":"string","format":"email"},"default":[]},"subject_template":{"type":"string","default":"New submission: {{form.name}}"},"from_name":{"type":"string"}},"required":["to"]}},"required":["type","config"]},{"type":"object","properties":{"type":{"type":"string","enum":["telegram"]},"name":{"type":"string"},"config":{"type":"object","properties":{"bot_token":{"type":"string","minLength":1},"chat_id":{"type":"string","minLength":1},"template":{"type":"string"}},"required":["bot_token","chat_id"]}},"required":["type","config"]},{"type":"object","properties":{"type":{"type":"string","enum":["webhook"]},"name":{"type":"string"},"config":{"type":"object","properties":{"url":{"type":"string","format":"uri"},"secret":{"type":"string"},"headers":{"type":"object","additionalProperties":{"type":"string"},"default":{}}},"required":["url"]}},"required":["type","config"]},{"type":"object","properties":{"type":{"type":"string","enum":["slack"]},"name":{"type":"string"},"config":{"type":"object","properties":{"url":{"type":"string","format":"uri"},"template":{"type":"string"}},"required":["url"]}},"required":["type","config"]},{"type":"object","properties":{"type":{"type":"string","enum":["discord"]},"name":{"type":"string"},"config":{"type":"object","properties":{"url":{"type":"string","format":"uri"},"template":{"type":"string"}},"required":["url"]}},"required":["type","config"]}]}}}},"responses":{"201":{"description":"created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Destination"}}}},"400":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/destinations/{destinationId}":{"get":{"operationId":"destinations_get","tags":["destinations"],"summary":"Get a destination (secrets redacted)","parameters":[{"schema":{"type":"string","example":"fm_8f3kq2"},"required":true,"name":"destinationId","in":"path"}],"responses":{"200":{"description":"ok","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Destination"}}}},"400":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}},"patch":{"operationId":"destinations_update","tags":["destinations"],"summary":"Update config","parameters":[{"schema":{"type":"string","example":"fm_8f3kq2"},"required":true,"name":"destinationId","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","description":"A display label for this destination."},"config":{"type":"object","additionalProperties":{},"description":"Merged shallowly into the existing config (type-specific: to/cc for email, chat_id/bot_token for telegram, url/secret for webhook)."}}}}}},"responses":{"200":{"description":"ok","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Destination"}}}},"400":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}},"delete":{"operationId":"destinations_delete","tags":["destinations"],"summary":"Delete (fails if routes reference it)","description":"Fails with 409 while a route still points at this destination — delete or repoint those routes first.","parameters":[{"schema":{"type":"string","example":"fm_8f3kq2"},"required":true,"name":"destinationId","in":"path"}],"responses":{"204":{"description":"deleted"},"400":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/destinations/{destinationId}/test":{"post":{"operationId":"destinations_test","tags":["destinations"],"summary":"Send a sample payload now and return the provider response","description":"Verifies a destination is actually reachable and configured correctly — do this right after creating one, before wiring routes to it.","parameters":[{"schema":{"type":"string","example":"fm_8f3kq2"},"required":true,"name":"destinationId","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"sample":{"type":"object","additionalProperties":{},"description":"Override the default sample payload sent for the test."}}}}}},"responses":{"200":{"description":"result","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeliveryResult"}}}},"400":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/routes":{"get":{"operationId":"routes_list","tags":["routes"],"summary":"List routes","parameters":[{"schema":{"type":"string"},"required":false,"name":"cursor","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":200},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","description":"Filter to routes whose source is this form id."},"required":false,"description":"Filter to routes whose source is this form id.","name":"form","in":"query"},{"schema":{"type":"string","description":"Filter to routes whose source is this stream id."},"required":false,"description":"Filter to routes whose source is this stream id.","name":"stream","in":"query"},{"schema":{"type":"string","description":"Filter to routes pointing at this destination id."},"required":false,"description":"Filter to routes pointing at this destination id.","name":"destination","in":"query"}],"responses":{"200":{"description":"ok","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Route"}},"next_cursor":{"type":["string","null"]}},"required":["data","next_cursor"]}}}}}},"post":{"operationId":"routes_create","tags":["routes"],"summary":"Create a route (source is exactly one of form_id / stream_id)","description":"A route ties one source (a form or a stream) to one destination, with a mode (instant or digest) and optional filter/transform/quality rules.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"form_id":{"type":"string"},"stream_id":{"type":"string"},"destination_id":{"type":"string"},"enabled":{"type":"boolean","default":true},"mode":{"oneOf":[{"type":"object","properties":{"type":{"type":"string","enum":["instant"]}},"required":["type"]},{"type":"object","properties":{"type":{"type":"string","enum":["digest"]},"cron":{"type":"string","minLength":1},"timezone":{"type":"string","minLength":1}},"required":["type","cron","timezone"]}],"default":{"type":"instant"}},"window":{"type":"object","properties":{"from":{"type":["string","null"],"format":"date-time"},"until":{"type":["string","null"],"format":"date-time"}}},"quality":{"type":"object","properties":{"exclude_spam":{"type":"boolean","default":true},"exclude_quarantined":{"type":"boolean","default":true}}},"filter":{"type":"string"},"transform":{"type":"string"}},"required":["destination_id"]}}}},"responses":{"201":{"description":"created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Route"}}}},"400":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/routes/{routeId}":{"get":{"operationId":"routes_get","tags":["routes"],"summary":"Get a route","parameters":[{"schema":{"type":"string","example":"fm_8f3kq2"},"required":true,"name":"routeId","in":"path"}],"responses":{"200":{"description":"ok","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Route"}}}},"400":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}},"patch":{"operationId":"routes_update","tags":["routes"],"summary":"Update rules / enable / disable","parameters":[{"schema":{"type":"string","example":"fm_8f3kq2"},"required":true,"name":"routeId","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"enabled":{"type":"boolean","description":"Disable a route without deleting it."},"mode":{"oneOf":[{"type":"object","properties":{"type":{"type":"string","enum":["instant"]}},"required":["type"]},{"type":"object","properties":{"type":{"type":"string","enum":["digest"]},"cron":{"type":"string","minLength":1,"description":"Cron expression for digest mode, e.g. '0 9 * * *'."},"timezone":{"type":"string","minLength":1,"description":"IANA timezone the cron runs in."}},"required":["type","cron","timezone"]}],"description":"instant delivers as submissions arrive; digest batches into one payload per period."},"window":{"type":"object","properties":{"from":{"type":["string","null"],"format":"date-time"},"until":{"type":["string","null"],"format":"date-time"}},"description":"Only deliver submissions received in this ISO datetime range."},"quality":{"type":"object","properties":{"exclude_spam":{"type":"boolean"},"exclude_quarantined":{"type":"boolean"}},"description":"Defaults to excluding spam and quarantined submissions from delivery."},"filter":{"type":"string","description":"A JSONata boolean expression; only matching submissions are delivered."},"transform":{"type":"string","description":"A JSONata expression applied to the payload before delivery."}}}}}},"responses":{"200":{"description":"ok","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Route"}}}},"400":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}},"delete":{"operationId":"routes_delete","tags":["routes"],"summary":"Delete a route","parameters":[{"schema":{"type":"string","example":"fm_8f3kq2"},"required":true,"name":"routeId","in":"path"}],"responses":{"204":{"description":"deleted"},"400":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/deliveries":{"get":{"operationId":"deliveries_list","tags":["deliveries"],"summary":"List deliveries (the outbox)","description":"A submission is a row before it is anything else; delivery is this outbox drained by a worker. Poll here to confirm a submission actually arrived at its destination.","parameters":[{"schema":{"type":"string"},"required":false,"name":"cursor","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":200},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","description":"Filter by status: pending, sending, sent, failed, dead or skipped."},"required":false,"description":"Filter by status: pending, sending, sent, failed, dead or skipped.","name":"status","in":"query"},{"schema":{"type":"string","description":"Filter to one route id."},"required":false,"description":"Filter to one route id.","name":"route","in":"query"},{"schema":{"type":"string","description":"Filter to one destination id."},"required":false,"description":"Filter to one destination id.","name":"destination","in":"query"},{"schema":{"type":"string","description":"Filter to one submission id."},"required":false,"description":"Filter to one submission id.","name":"submission","in":"query"}],"responses":{"200":{"description":"ok","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Delivery"}},"next_cursor":{"type":["string","null"]}},"required":["data","next_cursor"]}}}}}}},"/v1/deliveries/{deliveryId}":{"get":{"operationId":"deliveries_get","tags":["deliveries"],"summary":"Get a delivery with payload snapshot and last response","parameters":[{"schema":{"type":"string","example":"fm_8f3kq2"},"required":true,"name":"deliveryId","in":"path"}],"responses":{"200":{"description":"ok","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Delivery"}}}},"400":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/deliveries/{deliveryId}/retry":{"post":{"operationId":"deliveries_retry","tags":["deliveries"],"summary":"Re-snapshot the payload against current mapping/transform and queue again","description":"Use after fixing a destination's config or a stream's mapping so a `failed` or `dead` delivery gets another chance.","parameters":[{"schema":{"type":"string","example":"fm_8f3kq2"},"required":true,"name":"deliveryId","in":"path"}],"responses":{"202":{"description":"queued","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Delivery"}}}},"400":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/events":{"get":{"operationId":"events_list","tags":["events"],"summary":"Organization event log","description":"Every notable thing that happened in this org — form.created, form.schema.changed, stream.schema.changed, submission.received, and more. System webhooks (below) are just subscriptions to this same log.","parameters":[{"schema":{"type":"string"},"required":false,"name":"cursor","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":200},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","description":"Filter to one event type, e.g. 'submission.received'."},"required":false,"description":"Filter to one event type, e.g. 'submission.received'.","name":"type","in":"query"},{"schema":{"type":"string","description":"ISO timestamp; only events after this."},"required":false,"description":"ISO timestamp; only events after this.","name":"since","in":"query"}],"responses":{"200":{"description":"ok","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Event"}},"next_cursor":{"type":["string","null"]}},"required":["data","next_cursor"]}}}}}}},"/v1/webhooks":{"get":{"operationId":"webhooks_list","tags":["events"],"summary":"List system webhooks","description":"System webhooks notify an external system when org events happen (not to be confused with a webhook destination, which delivers submissions).","responses":{"200":{"description":"ok","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/SystemWebhook"}}}}}}},"post":{"operationId":"webhooks_create","tags":["events"],"summary":"Subscribe a URL to event types","description":"Every delivery is signed with the returned (or supplied) secret so the receiver can verify authenticity.","requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","format":"uri"},"events":{"type":"array","items":{"type":"string","enum":["submission.received","submission.quarantined","submission.spam","delivery.sent","delivery.failed","delivery.dead","digest.ready","form.created","form.schema.changed","stream.schema.changed","drift.detected","drift.resolved","destination.failing","destination.recovered"]},"minItems":1},"secret":{"type":"string"},"enabled":{"type":"boolean","default":true}},"required":["url","events"]}}}},"responses":{"201":{"description":"created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SystemWebhook"}}}},"400":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/webhooks/{webhookId}":{"patch":{"operationId":"webhooks_update","tags":["events"],"summary":"Update a system webhook","parameters":[{"schema":{"type":"string","example":"fm_8f3kq2"},"required":true,"name":"webhookId","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","format":"uri"},"events":{"type":"array","items":{"type":"string","enum":["submission.received","submission.quarantined","submission.spam","delivery.sent","delivery.failed","delivery.dead","digest.ready","form.created","form.schema.changed","stream.schema.changed","drift.detected","drift.resolved","destination.failing","destination.recovered"]},"minItems":1},"secret":{"type":"string"},"enabled":{"type":"boolean","default":true}}}}}},"responses":{"200":{"description":"ok","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SystemWebhook"}}}},"400":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}},"delete":{"operationId":"webhooks_delete","tags":["events"],"summary":"Delete a system webhook","parameters":[{"schema":{"type":"string","example":"fm_8f3kq2"},"required":true,"name":"webhookId","in":"path"}],"responses":{"204":{"description":"deleted"},"400":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/v1/webhooks/{webhookId}/deliveries":{"get":{"operationId":"webhooks_deliveries","tags":["events"],"summary":"Delivery history for a system webhook","parameters":[{"schema":{"type":"string","example":"fm_8f3kq2"},"required":true,"name":"webhookId","in":"path"},{"schema":{"type":"string"},"required":false,"name":"cursor","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":200},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"ok","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/SystemWebhookDelivery"}},"next_cursor":{"type":["string","null"]}},"required":["data","next_cursor"]}}}},"400":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"401":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"403":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"409":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}},"422":{"description":"Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ErrorEnvelope"}}}}}}},"/health":{"get":{"operationId":"health_get","tags":["discovery"],"summary":"Liveness and readiness","description":"Public, unauthenticated. Reports database connectivity, worker heartbeat and the oldest pending delivery age.","security":[],"responses":{"200":{"description":"ok","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"db":{"type":"string","enum":["up","down"]},"worker":{"type":"object","properties":{"heartbeat_at":{"type":["string","null"]},"alive":{"type":"boolean"}},"required":["heartbeat_at","alive"]},"oldest_pending_delivery_s":{"type":["number","null"]},"version":{"type":"string"}},"required":["ok","db","worker","oldest_pending_delivery_s","version"]}}}},"503":{"description":"db down","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"db":{"type":"string","enum":["up","down"]},"worker":{"type":"object","properties":{"heartbeat_at":{"type":["string","null"]},"alive":{"type":"boolean"}},"required":["heartbeat_at","alive"]},"oldest_pending_delivery_s":{"type":["number","null"]},"version":{"type":"string"}},"required":["ok","db","worker","oldest_pending_delivery_s","version"]}}}}}}}},"webhooks":{}}