{"components":{"schemas":{"AIDraftingStruct":{"additionalProperties":false,"properties":{"enabled":{"description":"True when the server is started with SPARROW_AI_ENABLED=true, whether or not the provider settings are valid. Drafting (POST /v1/subscriptions:draftTemplate) is usable only when enabled is true and problem is absent.","type":"boolean"},"model":{"description":"Model id used for drafting; absent when drafting is off or not usable.","type":"string"},"problem":{"description":"Why drafting is enabled but not usable: a missing or invalid provider setting. POST /v1/subscriptions:draftTemplate answers 503 with this text.","type":"string"},"prompt_only":{"description":"Always the opposite of enabled: true when SPARROW_AI_ENABLED is not true, so clients offer only the copy-and-paste prompt. POST /v1/subscriptions:draftTemplatePrompt works whatever this says.","type":"boolean"},"provider":{"description":"Chat API behind drafting: anthropic, or openai for any OpenAI-compatible server (Ollama, vLLM, OpenRouter, OpenAI). Absent when drafting is off or not usable.","enum":["anthropic","openai"],"type":"string"}},"required":["enabled","prompt_only"],"type":"object"},"AlertConfigItem":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/AlertConfigItem.json"],"format":"uri","readOnly":true,"type":"string"},"consumer":{"description":"Consumer this alert config belongs to.","type":"string"},"created_at":{"description":"Creation timestamp, RFC3339.","format":"date-time","type":"string"},"email":{"description":"Recipient email address.","type":"string"},"event_types":{"description":"Sparrow system event types this recipient is alerted on.","items":{"type":"string"},"type":"array"},"id":{"description":"Alert config id (UUID).","type":"string"},"webhook_id":{"description":"Webhook this alert is scoped to; omitted for consumer-wide alerts.","type":"string"}},"required":["id","consumer","email","event_types","created_at"],"type":"object"},"AlertDeliveryStruct":{"additionalProperties":false,"properties":{"configured":{"description":"True when an active webhook under the _sparrow consumer has an unpaused subscription to Sparrow's system events, so alert configs actually send email. When false, alert configs are stored but nothing is sent.","type":"boolean"}},"required":["configured"],"type":"object"},"AttemptItem":{"additionalProperties":false,"properties":{"error_category":{"description":"Failure classification for this attempt.","enum":["success","client_error","server_error","timeout","dns_error","tls_error","connection_refused","network_error","rate_limited","unexpected_status","template_error","unknown"],"type":"string"},"error_message":{"description":"Human-readable failure reason for this attempt.","type":"string"},"response_code":{"description":"HTTP status code returned by the endpoint on this attempt; 0 when no response was received.","format":"int64","type":"integer"},"response_time":{"description":"Round-trip time of this attempt, in milliseconds.","format":"int64","type":"integer"},"success":{"description":"Whether this attempt succeeded (got one of the webhook's expected_status_codes).","type":"boolean"},"timestamp":{"description":"When this attempt was made, RFC3339.","format":"date-time","type":"string"}},"required":["success","response_time","response_code","timestamp"],"type":"object"},"AttemptsOutputBody":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/AttemptsOutputBody.json"],"format":"uri","readOnly":true,"type":"string"},"items":{"description":"Every recorded attempt, oldest first.","items":{"$ref":"#/components/schemas/AttemptItem"},"type":"array"}},"required":["items"],"type":"object"},"BatchJobOutputBody":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/BatchJobOutputBody.json"],"format":"uri","readOnly":true,"type":"string"},"consumer":{"description":"Consumer the snapshot was taken in; empty for a snapshot taken from a cross-consumer list.","type":"string"},"created_at":{"description":"Creation timestamp, RFC3339.","format":"date-time","type":"string"},"expires_at":{"description":"When the snapshot expires, RFC3339: 15 minutes after it was taken. An expired snapshot can no longer be started.","format":"date-time","type":"string"},"failed":{"description":"Items that could not be re-pushed or retried (e.g. the event or delivery no longer exists).","format":"int64","type":"integer"},"id":{"description":"Batch job id (UUID): the repush_id or retry_id from the list call.","type":"string"},"job_type":{"description":"Which batch operation this job performs.","enum":["event_repush","delivery_retry"],"type":"string"},"processed":{"description":"Items handled successfully so far.","format":"int64","type":"integer"},"status":{"description":"pending: snapshot taken, not started. processing: running. completed: every item was handled (see failed for the ones that could not be). failed: every item failed. cancelled: stopped by a cancel call.","enum":["pending","processing","completed","failed","cancelled"],"type":"string"},"total":{"description":"Total items in the batch snapshot.","format":"int64","type":"integer"}},"required":["id","consumer","job_type","status","total","processed","failed","created_at","expires_at"],"type":"object"},"BundleItem":{"additionalProperties":false,"properties":{"active":{"description":"Whether events of this type can be pushed. Omitted means true.","type":"boolean"},"description":{"description":"Human-readable summary. Omitted means empty: an import replaces the definition.","type":"string"},"event_schema":{"additionalProperties":{},"description":"JSON Schema. Omitted means no schema, which removes an existing schema (a breaking change).","type":"object"},"metadata":{"additionalProperties":{"type":"string"},"description":"Arbitrary key/value metadata. Omitted means none.","type":"object"},"name":{"description":"Event type name.","maxLength":255,"minLength":1,"type":"string"}},"required":["name"],"type":"object"},"BundleStampBody":{"additionalProperties":false,"properties":{"format":{"description":"Bundle format number.","format":"int64","type":"integer"},"sha256":{"description":"sha256 of the canonical encoding of items, to tell whether they changed after export.","type":"string"},"sparrow_version":{"description":"Version of the Sparrow server that exported the bundle.","type":"string"}},"required":["sparrow_version","format","sha256"],"type":"object"},"CapabilitiesOutputBody":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/CapabilitiesOutputBody.json"],"format":"uri","readOnly":true,"type":"string"},"ai_drafting":{"$ref":"#/components/schemas/AIDraftingStruct","description":"AI-assisted transform template drafting."},"alert_delivery":{"$ref":"#/components/schemas/AlertDeliveryStruct","description":"Delivery of Sparrow's own alert emails (see the alert-configs endpoints)."},"portal":{"$ref":"#/components/schemas/PortalStruct","description":"What the consumer portal is allowed to do on this server."}},"required":["ai_drafting","alert_delivery","portal"],"type":"object"},"ConsumerHint":{"additionalProperties":false,"properties":{"name":{"description":"Consumer to register the recipe under, e.g. _sparrow.","type":"string"},"note":{"description":"Why, and what the recipe does under any other consumer.","type":"string"}},"required":["name","note"],"type":"object"},"ConsumerItem":{"additionalProperties":false,"properties":{"name":{"description":"Consumer name.","type":"string"}},"required":["name"],"type":"object"},"ConsumerStatsOutputBody":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/ConsumerStatsOutputBody.json"],"format":"uri","readOnly":true,"type":"string"},"active_webhooks":{"description":"Webhooks currently active (not paused).","format":"int64","type":"integer"},"failed_deliveries":{"description":"Attempts that failed, all time, including failures that were later retried successfully. Lags like total_deliveries.","format":"int64","type":"integer"},"pending_deliveries":{"description":"Deliveries waiting to be sent or retried right now (pending, sending or retrying).","format":"int64","type":"integer"},"success_rate":{"description":"successful_deliveries / total_deliveries, 0.0 to 1.0.","format":"double","type":"number"},"successful_deliveries":{"description":"Attempts that succeeded, all time. Lags like total_deliveries.","format":"int64","type":"integer"},"total_deliveries":{"description":"Delivery attempts recorded, all time. A retried delivery counts once per attempt. Updated by each health evaluation (SPARROW_HEALTH_EVAL_INTERVAL), so it can lag by that much.","format":"int64","type":"integer"},"total_webhooks":{"description":"Total webhooks registered.","format":"int64","type":"integer"}},"required":["total_webhooks","active_webhooks","total_deliveries","successful_deliveries","failed_deliveries","pending_deliveries","success_rate"],"type":"object"},"CreateAlertConfigBody":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/CreateAlertConfigBody.json"],"format":"uri","readOnly":true,"type":"string"},"email":{"description":"Recipient email address.","format":"email","type":"string"},"event_types":{"description":"Sparrow system event types to alert on, at least one of: sparrow.webhook.health_changed, sparrow.webhook.delivery_failed, sparrow.webhook.disabled.","items":{"type":"string"},"type":"array"},"webhook_id":{"description":"Scope the alert to one webhook. Omit for a consumer-wide alert covering every webhook.","type":"string"}},"required":["email","event_types"],"type":"object"},"CreateInviteBody":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/CreateInviteBody.json"],"format":"uri","readOnly":true,"type":"string"},"consumer":{"description":"Invite into one consumer's portal: the token it creates is limited to that consumer. Omit for tenant-wide (operator console) access. Same naming rules as on POST /v1/tokens.","type":"string"},"name":{"description":"Name for the token the invite creates, e.g. the invitee's name.","maxLength":200,"minLength":1,"type":"string"},"token_never_expires":{"description":"The token it creates never expires; same rules as never_expires on POST /v1/tokens.","type":"boolean"},"token_ttl_seconds":{"description":"Lifetime in seconds of the token it creates, counted from redemption; same defaults and limits as ttl_seconds on POST /v1/tokens.","format":"int64","minimum":0,"type":"integer"},"ttl_seconds":{"description":"How long the invite can be redeemed, in seconds. 0 or omitted means 24 hours; at most 7 days.","format":"int64","minimum":0,"type":"integer"}},"required":["name"],"type":"object"},"CreateInviteOutputBody":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/CreateInviteOutputBody.json"],"format":"uri","readOnly":true,"type":"string"},"invite":{"$ref":"#/components/schemas/InviteOut","description":"The created invite."},"path":{"description":"UI-relative link with the secret in the fragment (never sent to servers or logs): /#invite=... for a tenant-wide invite, /portal#invite=... for a consumer invite. Prepend the base URL the UI is served from.","type":"string"},"secret":{"description":"The invite secret. Shown only in this response.","type":"string"}},"required":["invite","secret","path"],"type":"object"},"CreateSubscriptionBody":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/CreateSubscriptionBody.json"],"format":"uri","readOnly":true,"type":"string"},"event_name":{"description":"Event type name to subscribe to, or \"*\" to receive every event in the consumer (catch-all). sparrow.* names only under the _sparrow consumer. One subscription per webhook, event name and consumer (409 on a duplicate).","type":"string"},"headers":{"additionalProperties":{"type":"string"},"description":"Extra HTTP headers sent with deliveries from this subscription. They override the webhook's headers of the same name; the webhook's secret_headers override both. Same name and value rules as webhook headers.","type":"object"},"label_filters":{"additionalProperties":{"type":"string"},"description":"Key/value pairs that must ALL be present in an event's labels for this subscription to receive it. Empty means match every event of event_name. Same limits as event labels (20 pairs, 64-character keys, 256-character values).","type":"object"},"method":{"description":"HTTP method used for deliveries from this subscription, e.g. PUT. Defaults to POST.","type":"string"},"on_transform_error":{"description":"What happens when transform_template fails to render. fail (default): the delivery fails with error category template_error, is not retried automatically, and can be retried once the template is fixed; it does not affect the webhook's health. fallback: the default envelope payload is sent instead, and the error is still recorded on the delivery.","enum":["fail","fallback"],"type":"string"},"template_missing_key":{"description":"How transform_template reads a key the payload does not have. error (default): the render fails, so a field removed from the event schema cannot silently turn into \"\u003cno value\u003e\" in the body; read optional fields with index or dig (default only fills a key that is present but empty). zero: the key renders as \"\u003cno value\u003e\".","enum":["error","zero"],"type":"string"},"timeout":{"description":"Request timeout for this subscription's deliveries, in seconds, overriding the webhook's request_timeout_seconds. 0 to 300; 0 (default) uses the webhook's.","format":"int64","type":"integer"},"transform_enabled":{"description":"Whether to render transform_template into the delivered body instead of sending the default envelope. Defaults to false.","type":"boolean"},"transform_template":{"description":"Go text/template rendered against the event to produce the delivered body, used when transform_enabled is true. See GET /v1/template-functions for available helpers; test it with POST /v1/subscriptions:testTemplate. A template that does not parse is rejected with 400; it is not rendered on save. Saving a new template records a version in the template history.","type":"string"},"webhook_id":{"description":"Webhook id (UUID) to deliver matching events to. Must belong to this consumer.","type":"string"}},"required":["webhook_id","event_name"],"type":"object"},"CreateTokenBody":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/CreateTokenBody.json"],"format":"uri","readOnly":true,"type":"string"},"consumer":{"description":"Limit the token to one consumer: it then works only through the portal API (/portal/api/). Omit for a tenant-wide token with the same power as SPARROW_API_KEY. At most 255 characters, no /, \\, ?, #, % or dot segments; _sparrow is not allowed.","type":"string"},"external_id":{"description":"Consumer tokens only. Your id for who or what the token is for, e.g. your user id when embedding the portal. There is at most one active token per consumer and external_id: while it is valid, asking again returns it, secret included (reused: true; name and ttl_seconds are ignored), instead of creating another; once it expires or is revoked, a new one is created. This makes minting a portal link on every page view safe. Not a secret: it is fine to log.","maxLength":200,"type":"string"},"name":{"description":"Who or what the token is for, e.g. \"alice\" or \"ci-deploy\".","maxLength":200,"minLength":1,"type":"string"},"never_expires":{"description":"Create a tenant-wide token that never expires (revoke it when no longer needed). Not allowed with ttl_seconds or for consumer tokens.","type":"boolean"},"ttl_seconds":{"description":"Lifetime in seconds. 0 or omitted takes the default: SPARROW_TOKEN_DEFAULT_TTL (90 days unless changed) for tenant-wide tokens, 7 days for consumer tokens. Consumer tokens allow at most 30 days.","format":"int64","minimum":0,"type":"integer"}},"required":["name"],"type":"object"},"CreateTokenOutputBody":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/CreateTokenOutputBody.json"],"format":"uri","readOnly":true,"type":"string"},"portal_path":{"description":"Consumer tokens only: server-relative portal URL with the token in the fragment (never sent to the server or logged). Prepend the base URL the UI is served from (the Sparrow server with SPARROW_SERVE_UI=true, or your separately hosted UI) and hand it to the end consumer.","type":"string"},"reused":{"description":"True when external_id matched a still-valid token, which is returned instead of a new one.","type":"boolean"},"secret":{"description":"The credential. Shown only in this response (and again for the same external_id); send it as X-API-Key or Authorization: Bearer.","type":"string"},"token":{"$ref":"#/components/schemas/TokenOut","description":"The created (or reused) token."}},"required":["token","secret","reused"],"type":"object"},"CrlOut":{"additionalProperties":false,"properties":{"entries":{"description":"Revoked certificates it lists.","format":"int64","type":"integer"},"error":{"description":"Why the last download or check failed. In soft mode such a CRL gives no answer and connections are allowed.","type":"string"},"fetched_at":{"description":"When this instance last downloaded it successfully.","format":"date-time","type":"string"},"issuer":{"description":"Common name of the CA that signs this CRL.","type":"string"},"next_update":{"description":"When the CA promises a newer CRL. Past it, the CRL is downloaded again before use.","format":"date-time","type":"string"},"url":{"description":"CRL distribution point, from an intermediate in a receiver's chain.","type":"string"}},"required":["url","issuer","entries"],"type":"object"},"CursorPaginationOutput":{"additionalProperties":false,"properties":{"has_more":{"description":"True when older items follow this page.","type":"boolean"},"limit":{"description":"Page size applied (the requested limit, default 50).","format":"int32","type":"integer"},"next_cursor":{"description":"Pass as cursor to get the next, older page. Absent on the last page.","type":"string"}},"required":["limit","has_more"],"type":"object"},"DeliveryItem":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/DeliveryItem.json"],"format":"uri","readOnly":true,"type":"string"},"attempt_count":{"description":"Number of delivery attempts made so far, including earlier rounds before a manual retry.","format":"int64","type":"integer"},"consumer":{"description":"Consumer of the webhook. Set in delivery listings.","type":"string"},"created_at":{"description":"When the delivery was created by fan-out, RFC3339.","format":"date-time","type":"string"},"delivery_id":{"description":"Delivery id (UUID).","type":"string"},"error_category":{"description":"Outcome classification of the most recent attempt, used to decide retryability. server_error (5xx), timeout, connection_refused, network_error and rate_limited (429) are retried; client_error (other 4xx), dns_error, tls_error and unexpected_status (a 2xx/3xx not in expected_status_codes) are not. template_error means the subscription's transform failed to render; it is not retried automatically and does not count against the webhook's health.","enum":["success","client_error","server_error","timeout","dns_error","tls_error","connection_refused","network_error","rate_limited","unexpected_status","template_error","unknown"],"type":"string"},"error_message":{"description":"Human-readable failure reason from the most recent attempt.","type":"string"},"event_id":{"description":"Pushed event occurrence this delivery originated from.","type":"string"},"event_name":{"description":"Event type of that occurrence, e.g. order.created. Set in delivery listings.","type":"string"},"last_attempted_at":{"description":"Timestamp of the most recent attempt, RFC3339.","format":"date-time","type":"string"},"max_attempts":{"description":"Attempts allowed per round before the delivery is marked failed: the webhook's max_retries + 1 when the delivery was created. A manual retry starts a new round.","format":"int64","type":"integer"},"next_retry_at":{"description":"Timestamp of the next scheduled retry, RFC3339, if one is pending.","format":"date-time","type":"string"},"response_body":{"description":"Endpoint response body from the most recent attempt: up to 1 KiB, or up to SPARROW_MAX_CAPTURED_RESPONSE_BYTES when the webhook has capture_response_body.","type":"string"},"response_code":{"description":"HTTP status code returned by the endpoint on the most recent attempt, if any.","format":"int64","type":"integer"},"status":{"description":"Current delivery status. pending: queued for its first attempt (or re-queued by a retry). sending: an attempt is in flight. retrying: an attempt failed and another is scheduled (next_retry_at). success: an attempt got an expected status code. failed: attempts exhausted, or a non-retryable error such as template_error. expired: the event's ttl_seconds passed before the attempt. paused: held because the webhook or subscription was paused; not attempted until retried.","enum":["pending","sending","success","failed","retrying","expired","paused"],"type":"string"},"template_error":{"description":"The payload transform error, when the subscription's template failed to render. Set for both on_transform_error modes: with fail the delivery was not sent; with fallback the default envelope was sent instead.","type":"string"},"webhook_description":{"description":"Description of that webhook. Set in delivery listings.","type":"string"},"webhook_id":{"description":"Webhook this delivery was sent to.","type":"string"},"webhook_url":{"description":"URL of that webhook. Set in delivery listings.","type":"string"}},"required":["delivery_id","webhook_id","event_id","status","attempt_count","max_attempts","created_at"],"type":"object"},"DraftTemplateBody":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/DraftTemplateBody.json"],"format":"uri","readOnly":true,"type":"string"},"current_template":{"description":"Optional existing template to refine instead of drafting from scratch.","maxLength":20000,"type":"string"},"docs_url":{"description":"Optional http(s) page documenting the receiver's payload. Sparrow fetches it from a public address only (loopback, private and cloud-metadata addresses are always refused, even when SPARROW_ALLOW_PRIVATE_NETWORKS lets deliveries reach them) and gives the model an excerpt of its text.","format":"uri","maxLength":2048,"type":"string"},"event_name":{"description":"Registered event type whose schema and sample payload ground the draft, and against which it is verified.","type":"string"},"instructions":{"description":"Plain-language description of the body the receiver should get, e.g. \"a short Slack message with the order id and total, flagged when refunded\".","maxLength":4000,"minLength":1,"type":"string"},"recipe":{"description":"Name of a shipped recipe (see GET /v1/recipes) whose destination format the draft should follow, e.g. \"slack\". Omit for a custom receiver.","type":"string"},"sample_payload":{"additionalProperties":{},"description":"Optional payload to ground and verify the draft against instead of the event type's registered sample. Does not change the event type.","type":"object"},"target_example":{"description":"Optional: what the receiver expects, either an example body (the draft maps event fields onto that exact structure) or a plain-language description of its shape.","maxLength":20000,"type":"string"}},"required":["event_name","instructions"],"type":"object"},"DraftTemplateOutputBody":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/DraftTemplateOutputBody.json"],"format":"uri","readOnly":true,"type":"string"},"attempts":{"description":"Draft/render rounds it took; more than 1 means an earlier draft failed to render and was repaired.","format":"int64","type":"integer"},"model":{"description":"Model id that produced the draft.","type":"string"},"notes":{"description":"Short explanation from the model of the fields used and assumptions made.","type":"string"},"rendered":{"description":"The template rendered against the sample payload used (provided or registered), proving it renders.","type":"string"},"sample_source":{"description":"Which sample payload grounded and verified the draft: registered or provided.","enum":["registered","provided"],"type":"string"},"template":{"description":"The drafted Go template, ready to save as transform_template.","type":"string"}},"required":["template","rendered","sample_source","attempts","model"],"type":"object"},"DraftTemplatePromptOutputBody":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/DraftTemplatePromptOutputBody.json"],"format":"uri","readOnly":true,"type":"string"},"prompt":{"description":"A self-contained prompt to paste into any chat assistant: the template data model, rules, helper catalog, the event's schema and sample payload, any recipe, receiver example, docs excerpt, current template, and the instructions. Asks for the template in a code block.","type":"string"},"sample_source":{"description":"Which sample payload the prompt carries: registered (the event type's sample_payload) or provided (sample_payload from the request).","enum":["registered","provided"],"type":"string"}},"required":["prompt","sample_source"],"type":"object"},"ErrorDetail":{"additionalProperties":false,"properties":{"location":{"description":"Where the error occurred, e.g. 'body.items[3].tags' or 'path.thing-id'","type":"string"},"message":{"description":"Error message text","type":"string"},"value":{"description":"The value at the given location"}},"type":"object"},"ErrorModel":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/ErrorModel.json"],"format":"uri","readOnly":true,"type":"string"},"detail":{"description":"A human-readable explanation specific to this occurrence of the problem.","examples":["Property foo is required but is missing."],"type":"string"},"errors":{"description":"Optional list of individual error details","items":{"$ref":"#/components/schemas/ErrorDetail"},"type":"array"},"instance":{"description":"A URI reference that identifies the specific occurrence of the problem.","examples":["https://example.com/error-log/abc123"],"format":"uri","type":"string"},"status":{"description":"HTTP status code","examples":[400],"format":"int64","type":"integer"},"title":{"description":"A short, human-readable summary of the problem type. This value should not change between occurrences of the error.","examples":["Bad Request"],"type":"string"},"type":{"default":"about:blank","description":"A URI reference to human-readable documentation for the error.","examples":["https://example.com/errors/example"],"format":"uri","type":"string"}},"type":"object"},"EventOccurrenceItem":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/EventOccurrenceItem.json"],"format":"uri","readOnly":true,"type":"string"},"consumer":{"description":"Tenant consumer this occurrence was pushed into.","type":"string"},"created_at":{"description":"When this occurrence was pushed, RFC3339.","format":"date-time","type":"string"},"event":{"description":"Event type name.","type":"string"},"event_id":{"description":"Event occurrence id (UUID).","type":"string"},"event_version":{"description":"Event type version this occurrence was accepted under.","format":"int64","type":"integer"},"failed_deliveries":{"description":"Failed delivery attempts for this occurrence. Counts attempts, not deliveries: a delivery that failed twice and then succeeded adds 2 here and 1 to successful_deliveries.","format":"int32","type":"integer"},"labels":{"additionalProperties":{"type":"string"},"description":"Key/value labels used to match against subscription label filters.","type":"object"},"metadata":{"additionalProperties":{"type":"string"},"description":"Arbitrary key/value metadata stored with the occurrence.","type":"object"},"payload":{"additionalProperties":{},"description":"The pushed payload.","type":"object"},"pending_deliveries":{"description":"Deliveries not finished yet: pending, sending or retrying.","format":"int32","type":"integer"},"scheduled_at":{"description":"When this occurrence fans out to subscriptions, RFC3339. Absent for immediate pushes. Deliveries exist only from this time on.","format":"date-time","type":"string"},"schema_valid":{"description":"Whether the payload validated against the event type's JSON Schema at push time.","type":"boolean"},"successful_deliveries":{"description":"Successful delivery attempts for this occurrence.","format":"int32","type":"integer"},"webhook_count":{"description":"Number of distinct webhooks that got a delivery for this occurrence.","format":"int32","type":"integer"}},"required":["event_id","consumer","event","payload","schema_valid","event_version","webhook_count","successful_deliveries","failed_deliveries","pending_deliveries","created_at"],"type":"object"},"EventTypeBody":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/EventTypeBody.json"],"format":"uri","readOnly":true,"type":"string"},"active":{"description":"Whether events of this type can be pushed; a push to an inactive type fails with 409. Defaults to true on register; unchanged on update when omitted.","type":"boolean"},"description":{"description":"Human-readable summary of what this event represents. On update, an empty string leaves it unchanged.","type":"string"},"event_schema":{"additionalProperties":{},"description":"Optional JSON Schema for the payload. Validation is soft: non-conforming payloads are still accepted, with warnings returned from the push call. On update, sending a different schema creates a new version.","type":"object"},"metadata":{"additionalProperties":{"type":"string"},"description":"Arbitrary key/value metadata for your own tooling. On update, replaces the whole map.","type":"object"},"name":{"description":"Unique event type name, e.g. order.created. Required on register, ignored on update (the path names the type). Lowercase letters, digits, '_' and '-' in dot-separated parts (e.g. stripe.payment_intent.succeeded), at most 255 characters; the sparrow. prefix is reserved. Immutable after creation, and event types cannot be deleted.","type":"string"}},"type":"object"},"EventTypeBundle":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/EventTypeBundle.json"],"format":"uri","readOnly":true,"type":"string"},"apiVersion":{"description":"Bundle schema version.","enum":["sparrow/v1"],"type":"string"},"items":{"description":"Event type definitions, sorted by name.","items":{"$ref":"#/components/schemas/BundleItem"},"type":"array"},"kind":{"description":"Bundle kind.","enum":["EventTypeList"],"type":"string"},"stamp":{"$ref":"#/components/schemas/BundleStampBody","description":"Which Sparrow produced this bundle. A compatibility hint, not a signature."}},"required":["apiVersion","kind","stamp","items"],"type":"object"},"EventTypeChange":{"additionalProperties":false,"properties":{"action":{"description":"created: new event type at version 1. new_version: the schema changed, so a new version was created and the previous one kept. updated: the current version changed in place (description, metadata, active, or a first schema added to a schema-less version). unchanged: nothing was written.","enum":["created","new_version","updated","unchanged"],"type":"string"},"active_change":{"description":"Set when the write flips the active flag.","enum":["deactivates","reactivates"],"type":"string"},"affected_subscriptions":{"description":"For new_version: subscriptions that receive this event type, by name or catch-all.","format":"int64","type":"integer"},"changes":{"description":"Fields that changed: schema, schema_defined (a first schema was added in place), description, metadata, active.","items":{"type":"string"},"type":"array"},"compatibility":{"$ref":"#/components/schemas/SchemaCompatibilityItem","description":"For new_version: whether the schema change could break a subscription's payload transformation."},"previous_version":{"description":"Version before the write; absent when the event type was created.","format":"int64","type":"integer"},"version":{"description":"Version after the write.","format":"int64","type":"integer"}},"required":["action","version"],"type":"object"},"EventTypeItem":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/EventTypeItem.json"],"format":"uri","readOnly":true,"type":"string"},"active":{"description":"Whether events of this type can currently be pushed.","type":"boolean"},"created_at":{"description":"Creation timestamp, RFC3339.","format":"date-time","type":"string"},"description":{"description":"Human-readable summary of what this event represents.","type":"string"},"event_schema":{"additionalProperties":{},"description":"JSON Schema payloads are softly validated against, if one is registered.","type":"object"},"metadata":{"additionalProperties":{"type":"string"},"description":"Arbitrary key/value metadata.","type":"object"},"name":{"description":"Event type name, e.g. order.created.","type":"string"},"sample_payload":{"additionalProperties":{},"description":"Example payload derived from the schema, used to preview subscription transform templates.","type":"object"},"updated_at":{"description":"Last-modified timestamp, RFC3339.","format":"date-time","type":"string"},"version":{"description":"Current schema version. Starts at 1 and increases only when the schema changes; every version is kept (see listEventTypeVersions).","format":"int64","type":"integer"}},"required":["name","active","version","created_at","updated_at"],"type":"object"},"EventTypeVersionItem":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/EventTypeVersionItem.json"],"format":"uri","readOnly":true,"type":"string"},"created_at":{"description":"When this version was created, RFC3339.","format":"date-time","type":"string"},"description":{"description":"Description as it was when this version was created.","type":"string"},"event_schema":{"additionalProperties":{},"description":"JSON Schema of this version. Absent for a schema-less version.","type":"object"},"name":{"description":"Event type name.","type":"string"},"sample_payload":{"additionalProperties":{},"description":"Example payload generated from this version's schema.","type":"object"},"schema_defined_at":{"description":"When a first schema was added to this version in place, RFC3339. Events pushed before this time were accepted while the version had no schema.","format":"date-time","type":"string"},"version":{"description":"Version number.","format":"int64","type":"integer"}},"required":["name","version","created_at"],"type":"object"},"ExportEventTypesInputBody":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/ExportEventTypesInputBody.json"],"format":"uri","readOnly":true,"type":"string"},"all":{"description":"Export every event type.","type":"boolean"},"names":{"description":"Export these event types. Each must exist.","items":{"type":"string"},"type":"array"},"prefix":{"description":"Export every event type whose name starts with this prefix.","type":"string"}},"type":"object"},"HealthRules":{"additionalProperties":false,"properties":{"degraded_min_attempts":{"description":"Attempts in the window needed before a low success rate counts as degraded.","format":"int64","type":"integer"},"degraded_success_rate":{"description":"A success rate below this (0.0 to 1.0), over at least degraded_min_attempts attempts, is degraded.","format":"double","type":"number"},"healthy_min_attempts":{"description":"Attempts at or above degraded_success_rate needed to call a webhook healthy; with fewer it stays unknown.","format":"int64","type":"integer"},"unhealthy_consecutive_failures":{"description":"Failed attempts in a row that make a webhook unhealthy whatever its success rate.","format":"int64","type":"integer"},"unhealthy_min_attempts":{"description":"Attempts in the window needed before a low success rate counts as unhealthy.","format":"int64","type":"integer"},"unhealthy_success_rate":{"description":"A success rate below this (0.0 to 1.0), over at least unhealthy_min_attempts attempts, is unhealthy.","format":"double","type":"number"},"window_hours":{"description":"Lookback window, in hours, of the attempt counts and success rate.","format":"int64","type":"integer"}},"required":["window_hours","unhealthy_consecutive_failures","unhealthy_success_rate","unhealthy_min_attempts","degraded_success_rate","degraded_min_attempts","healthy_min_attempts"],"type":"object"},"HealthSummaryOutputBody":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/HealthSummaryOutputBody.json"],"format":"uri","readOnly":true,"type":"string"},"degraded_count":{"description":"Webhooks currently labelled degraded.","format":"int64","type":"integer"},"healthy_count":{"description":"Webhooks currently labelled healthy.","format":"int64","type":"integer"},"rules":{"$ref":"#/components/schemas/HealthRules","description":"Thresholds the health statuses are derived from."},"unhealthy_count":{"description":"Webhooks currently labelled unhealthy.","format":"int64","type":"integer"},"unknown_count":{"description":"Webhooks labelled unknown: too few recent attempts to judge.","format":"int64","type":"integer"}},"required":["healthy_count","degraded_count","unhealthy_count","unknown_count","rules"],"type":"object"},"ImportEventTypesInputBody":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/ImportEventTypesInputBody.json"],"format":"uri","readOnly":true,"type":"string"},"acknowledge":{"description":"Stamp warnings you accept. Each unacknowledged warning blocks the write.","items":{"enum":["version_differs","format_unsupported","items_changed"],"type":"string"},"type":"array"},"allow_breaking":{"description":"Apply breaking schema changes to event types that subscriptions receive.","type":"boolean"},"apiVersion":{"description":"Bundle schema version, if the body is an exported bundle.","enum":["sparrow/v1"],"type":"string"},"dry_run":{"description":"Compute the full result without writing anything.","type":"boolean"},"items":{"description":"Definitions to import. Each replaces the named event type; types not listed are never touched.","items":{"$ref":"#/components/schemas/BundleItem"},"maxItems":500,"minItems":1,"type":"array"},"kind":{"description":"Bundle kind, if the body is an exported bundle.","enum":["EventTypeList"],"type":"string"},"stamp":{"$ref":"#/components/schemas/BundleStampBody","description":"The bundle's stamp. Absent for a hand-written bundle, which imports with a notice."},"subscription_policy":{"description":"keep_active (default): subscriptions keep running; a template that no longer fits fails its deliveries visibly with template_error. pause: in the same transaction, pause each subscription whose template fails the pre-check, with the import as the reason.","enum":["keep_active","pause"],"type":"string"}},"required":["items"],"type":"object"},"ImportEventTypesOutputBody":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/ImportEventTypesOutputBody.json"],"format":"uri","readOnly":true,"type":"string"},"applied":{"description":"True when the import was written. False for a dry run or when blocked_by is non-empty.","type":"boolean"},"blocked_by":{"description":"Why nothing was written: unacknowledged stamp warnings, and breaking when a breaking change needs allow_breaking.","items":{"type":"string"},"type":"array"},"dry_run":{"description":"Whether this was a dry run.","type":"boolean"},"imported_at":{"description":"When the import was written, RFC3339. Use it to find deliveries that failed after the change.","format":"date-time","type":"string"},"items":{"description":"One result per bundle item, sorted by name.","items":{"$ref":"#/components/schemas/ImportItemResult"},"type":"array"},"stamp":{"$ref":"#/components/schemas/StampCheckItem","description":"The bundle's stamp compared with this server."}},"required":["applied","dry_run","stamp","items"],"type":"object"},"ImportItemResult":{"additionalProperties":false,"properties":{"action":{"description":"What the import does to this event type. See updateEventType's change.action.","enum":["created","new_version","updated","unchanged"],"type":"string"},"active_change":{"description":"Set when the import flips the active flag.","enum":["deactivates","reactivates"],"type":"string"},"blocked":{"description":"The change is breaking for subscriptions and allow_breaking was not set.","type":"boolean"},"changes":{"description":"Fields that change: schema, schema_defined, description, metadata, active.","items":{"type":"string"},"type":"array"},"compatibility":{"$ref":"#/components/schemas/SchemaCompatibilityItem","description":"For new_version: whether the change could break a subscription's payload transformation."},"name":{"description":"Event type name.","type":"string"},"previous_version":{"description":"Version before the import.","format":"int64","type":"integer"},"subscriptions":{"$ref":"#/components/schemas/TemplateCheckItem","description":"For new_version: every affected subscription's transform rendered strictly against the new version."},"version":{"description":"Version after the import (or that it would have, for a dry run).","format":"int64","type":"integer"}},"required":["name","action","version"],"type":"object"},"InviteOut":{"additionalProperties":false,"properties":{"cancelled_at":{"description":"When the invite was cancelled, RFC3339. Null: not cancelled.","format":"date-time","type":["string","null"]},"consumer":{"description":"Consumer the resulting token is limited to. Null: tenant-wide.","type":["string","null"]},"created_at":{"description":"When the invite was created, RFC3339.","format":"date-time","type":"string"},"created_by":{"description":"Name of the credential that created the invite.","type":"string"},"expires_at":{"description":"When the invite stops working if unused, RFC3339.","format":"date-time","type":"string"},"id":{"description":"Invite id (not the secret).","type":"string"},"name":{"description":"Name given to the token the invite creates.","type":"string"},"redeemed_at":{"description":"When the invite was redeemed, RFC3339. Null: not redeemed.","format":"date-time","type":["string","null"]},"status":{"description":"pending: can still be redeemed. redeemed: used once, see token_id. cancelled: cancelled by DELETE /v1/invites/{invite_id}. expired: past expires_at unused.","enum":["pending","redeemed","cancelled","expired"],"type":"string"},"token_id":{"description":"Token created when the invite was redeemed.","type":["string","null"]},"token_ttl_seconds":{"description":"Lifetime of the token it creates, in seconds. Null: the token never expires.","format":"int64","type":["integer","null"]}},"required":["id","name","consumer","status","token_ttl_seconds","created_by","created_at","expires_at","redeemed_at","cancelled_at","token_id"],"type":"object"},"ListAlertConfigsOutputBody":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/ListAlertConfigsOutputBody.json"],"format":"uri","readOnly":true,"type":"string"},"items":{"description":"Alert configs, oldest first. Not paginated.","items":{"$ref":"#/components/schemas/AlertConfigItem"},"type":"array"}},"required":["items"],"type":"object"},"ListConsumersOutputBody":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/ListConsumersOutputBody.json"],"format":"uri","readOnly":true,"type":"string"},"items":{"description":"Matching consumer names, sorted by name.","items":{"$ref":"#/components/schemas/ConsumerItem"},"type":"array"}},"required":["items"],"type":"object"},"ListCrlsOutputBody":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/ListCrlsOutputBody.json"],"format":"uri","readOnly":true,"type":"string"},"crls":{"description":"CRLs this instance has cached, by URL. Each instance keeps its own cache.","items":{"$ref":"#/components/schemas/CrlOut"},"type":"array"},"mode":{"description":"SPARROW_TLS_REVOCATION: soft allows a connection whose CRL is unavailable, hard refuses it, off skips the check.","enum":["soft","hard","off"],"type":"string"}},"required":["mode","crls"],"type":"object"},"ListDeliveriesOutputBody":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/ListDeliveriesOutputBody.json"],"format":"uri","readOnly":true,"type":"string"},"items":{"description":"Deliveries, newest first. Includes deliveries of deleted webhooks.","items":{"$ref":"#/components/schemas/DeliveryItem"},"type":"array"},"pagination":{"$ref":"#/components/schemas/CursorPaginationOutput","description":"Cursor pagination; there is no total count."},"retry_id":{"description":"Snapshot (batch job) id, present when prepare_retry was set and something matched. Send it as the retry_id field of POST .../deliveries:retryBatch. Expires after 15 minutes if not started.","type":"string"},"retry_total":{"description":"Number of deliveries in that snapshot (all matches, not just this page).","format":"int64","type":"integer"}},"required":["items","pagination"],"type":"object"},"ListEventOccurrencesOutputBody":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/ListEventOccurrencesOutputBody.json"],"format":"uri","readOnly":true,"type":"string"},"items":{"description":"Occurrences, newest first.","items":{"$ref":"#/components/schemas/EventOccurrenceItem"},"type":"array"},"pagination":{"$ref":"#/components/schemas/CursorPaginationOutput","description":"Cursor pagination; there is no total count."},"repush_id":{"description":"Snapshot (batch job) id for POST .../events:rePush, present when prepare_repush was set and something matched. Expires after 15 minutes if not started.","type":"string"},"repush_total":{"description":"Number of occurrences in that snapshot (all matches, not just this page).","format":"int64","type":"integer"}},"required":["items","pagination"],"type":"object"},"ListEventTypeVersionsOutputBody":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/ListEventTypeVersionsOutputBody.json"],"format":"uri","readOnly":true,"type":"string"},"items":{"description":"Every version of the event type, newest first.","items":{"$ref":"#/components/schemas/EventTypeVersionItem"},"type":"array"}},"required":["items"],"type":"object"},"ListEventTypesOutputBody":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/ListEventTypesOutputBody.json"],"format":"uri","readOnly":true,"type":"string"},"items":{"description":"Event types, sorted by name.","items":{"$ref":"#/components/schemas/EventTypeItem"},"type":"array"},"pagination":{"$ref":"#/components/schemas/PaginationOutput","description":"Offset pagination with a total count."}},"required":["items","pagination"],"type":"object"},"ListInvitesOutputBody":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/ListInvitesOutputBody.json"],"format":"uri","readOnly":true,"type":"string"},"items":{"description":"Invites, newest first. Not paginated.","items":{"$ref":"#/components/schemas/InviteOut"},"type":"array"}},"required":["items"],"type":"object"},"ListRecipesOutputBody":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/ListRecipesOutputBody.json"],"format":"uri","readOnly":true,"type":"string"},"items":{"description":"Every shipped recipe.","items":{"$ref":"#/components/schemas/Recipe"},"type":"array"}},"required":["items"],"type":"object"},"ListSubscriptionsOutputBody":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/ListSubscriptionsOutputBody.json"],"format":"uri","readOnly":true,"type":"string"},"items":{"description":"Subscriptions, newest first.","items":{"$ref":"#/components/schemas/SubscriptionItem"},"type":"array"},"pagination":{"$ref":"#/components/schemas/PaginationOutput","description":"Offset pagination with a total count."}},"required":["items","pagination"],"type":"object"},"ListTemplateVersionsOutputBody":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/ListTemplateVersionsOutputBody.json"],"format":"uri","readOnly":true,"type":"string"},"items":{"description":"Saved versions, newest first.","items":{"$ref":"#/components/schemas/TemplateVersionItem"},"type":"array"}},"required":["items"],"type":"object"},"ListTokensOutputBody":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/ListTokensOutputBody.json"],"format":"uri","readOnly":true,"type":"string"},"items":{"description":"Tokens, newest first. Not paginated.","items":{"$ref":"#/components/schemas/TokenOut"},"type":"array"}},"required":["items"],"type":"object"},"ListWebhooksOutputBody":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/ListWebhooksOutputBody.json"],"format":"uri","readOnly":true,"type":"string"},"items":{"description":"Webhooks, newest first.","items":{"$ref":"#/components/schemas/WebhookOut"},"type":"array"},"pagination":{"$ref":"#/components/schemas/PaginationOutput","description":"Offset pagination with a total count."}},"required":["items","pagination"],"type":"object"},"PaginationOutput":{"additionalProperties":false,"properties":{"has_more":{"description":"True when offset + limit \u003c total_count.","type":"boolean"},"limit":{"description":"Page size applied (the requested limit, default 50).","format":"int32","type":"integer"},"offset":{"description":"Number of items skipped.","format":"int32","type":"integer"},"total_count":{"description":"Total items matching the filters.","format":"int32","type":"integer"}},"required":["limit","offset","total_count","has_more"],"type":"object"},"Param":{"additionalProperties":false,"properties":{"activation_required":{"description":"A missing value blocks enabling an auto-provisioned recipe.","type":"boolean"},"default":{"description":"Value used when none is supplied.","type":"string"},"docs_url":{"description":"Destination documentation for this value.","type":"string"},"enum":{"description":"The only accepted values, when set.","items":{"type":"string"},"type":"array"},"example":{"description":"A sample value.","type":"string"},"help":{"description":"What the value is and where to find it.","type":"string"},"must_override_default":{"description":"The default is a placeholder: it must be replaced with a real value before the recipe is applied.","type":"boolean"},"name":{"description":"Param name, as referenced by {{param \"name\"}}.","type":"string"},"prompt":{"description":"Short label shown for the param.","type":"string"},"required":{"description":"Whether a value must be supplied.","type":"boolean"},"secret":{"description":"Whether the value is sensitive (a token or password); clients should mask the input.","type":"boolean"}},"required":["name","prompt","required"],"type":"object"},"PatchSubscriptionBody":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/PatchSubscriptionBody.json"],"format":"uri","readOnly":true,"type":"string"},"headers":{"additionalProperties":{"type":"string"},"description":"Replace all extra HTTP headers.","type":"object"},"label_filters":{"additionalProperties":{"type":"string"},"description":"Replace all label filters; an empty object matches every event.","type":"object"},"method":{"description":"Replace the HTTP method.","type":"string"},"on_transform_error":{"description":"Replace what happens when the template fails to render.","enum":["fail","fallback"],"type":"string"},"template_missing_key":{"description":"Replace how the template reads a missing key.","enum":["error","zero"],"type":"string"},"template_notes":{"description":"Optional note stored with the template version, e.g. the AI drafter's summary.","maxLength":2000,"type":"string"},"template_source":{"description":"How a changed transform_template was produced, recorded in the subscription's template history. Defaults to manual.","enum":["manual","ai_draft"],"type":"string"},"timeout":{"description":"Replace the request timeout override, in seconds (0 to 300; 0 uses the webhook's).","format":"int64","type":"integer"},"transform_enabled":{"description":"Enable or disable payload transformation.","type":"boolean"},"transform_template":{"description":"Replace the transform template. A template that does not parse is rejected with 400. A change is recorded as a new version in the template history (the last 20 are kept).","type":"string"}},"type":"object"},"PatchWebhookBody":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/PatchWebhookBody.json"],"format":"uri","readOnly":true,"type":"string"},"active":{"description":"false pauses the webhook (like :pause), true resumes it and clears any auto-disable marker (like :resume, but without returning the held count).","type":"boolean"},"description":{"description":"Replace the human-readable description. An empty string leaves it unchanged.","type":"string"},"events":{"description":"Replace the full set of subscribed event type names. Every existing subscription of the webhook is deleted, with its transform and label filters, and one plain subscription is created per name. An empty list leaves subscriptions unchanged. Rejected with 409 while requires_transform is on; to keep transforms, add or delete subscriptions individually.","items":{"type":"string"},"type":"array"},"headers":{"additionalProperties":{"type":"string"},"description":"Replace all static headers sent with every delivery. Same rules as on register.","type":"object"},"http_config":{"$ref":"#/components/schemas/WebhookHTTPConfig","description":"Update HTTP delivery settings. Only the fields you send change; the rest keep their current values. The merged result must satisfy the same limits as on create, or the request is rejected with 400."},"requires_transform":{"description":"Require (or stop requiring) a payload transform on every subscription. Turning it on fails with 409 while any subscription has no enabled transform_template. While on, events cannot be replaced in bulk (409): add or delete subscriptions individually.","type":"boolean"},"secret_headers":{"additionalProperties":{"type":"string"},"description":"Merge-patch encrypted secret headers by name. Send a new value to replace one header, omit a key to leave it untouched, or send an empty string to remove it. Names match in any case. An empty object removes them all. Changing url to another origin (scheme, host or port) is refused unless this sets every existing secret header again (a new value, or an empty string to remove it), so moving a webhook never carries its secrets to a new server.","type":"object"},"signature_type":{"description":"Change the signing scheme: hmac or ed25519. Switching to ed25519 generates a signing keypair; HMAC signing continues either way.","type":"string"},"timeout":{"deprecated":true,"description":"Deprecated: use http_config.request_timeout_seconds. Shorthand for it, in seconds (1 to 300), applied only when http_config.request_timeout_seconds is not sent (http_config wins when both are sent).","format":"int64","type":"integer"},"url":{"description":"Replace the delivery endpoint URL. Same rules as on register.","type":"string"}},"type":"object"},"PauseSubscriptionInputBody":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/PauseSubscriptionInputBody.json"],"format":"uri","readOnly":true,"type":"string"},"reason":{"description":"Why the subscription is paused, shown next to it, for example: receiver maintenance until Friday.","maxLength":500,"type":"string"}},"type":"object"},"PortalStruct":{"additionalProperties":false,"properties":{"retry":{"description":"True when portal tokens may retry their deliveries (SPARROW_PORTAL_RETRY, default true). When false, the retry calls under /portal/api/ answer 403; admins can still retry.","type":"boolean"}},"required":["retry"],"type":"object"},"PushEventBody":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/PushEventBody.json"],"format":"uri","readOnly":true,"type":"string"},"idempotency_key":{"description":"Client-supplied deduplication key, unique per consumer. A push whose key matches an earlier event in the same consumer still returns 201, with duplicate: true and the original event_id, and creates no event or deliveries. The key is kept as long as the event (see SPARROW_EVENT_RETENTION_DAYS).","type":"string"},"labels":{"additionalProperties":{"type":"string"},"description":"Key/value labels a subscription's label_filters match against to decide which subscriptions receive this occurrence. At most 20 pairs; keys up to 64 characters of letters, digits, '.', '_' and '-'; values up to 256 characters.","type":"object"},"metadata":{"additionalProperties":{"type":"string"},"description":"Arbitrary key/value metadata stored with the occurrence.","type":"object"},"payload":{"additionalProperties":{},"description":"Event payload, a JSON object. Validated softly against the event type's JSON Schema if one is registered. The whole request body is limited to 1 MiB (413 above that).","type":"object"},"scheduled_at":{"description":"RFC 3339 time to deliver the event at. Omitted or in the past means immediate. The event is stored right away but fans out to subscriptions only at this time; ttl_seconds counts from then. At most one year ahead (400 beyond that). Replays of a scheduled occurrence are immediate.","format":"date-time","type":"string"},"ttl_seconds":{"description":"Time-to-live in seconds, counted from fan-out. A delivery attempt that would start after it expires is not made and the delivery fails as expired. 0 or omitted means no expiry. Manual retries ignore it.","format":"int64","type":"integer"}},"required":["payload"],"type":"object"},"PushEventOutputBody":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/PushEventOutputBody.json"],"format":"uri","readOnly":true,"type":"string"},"duplicate":{"description":"True when idempotency_key matched an existing event occurrence in this consumer; no new event or deliveries were created and the status is still 201.","type":"boolean"},"event_id":{"description":"Id (UUID) of the created event occurrence. When duplicate is true, this is the id of the PREVIOUSLY created event, not a new one.","type":"string"},"schema_valid":{"description":"Whether the payload validated against the event type's JSON Schema (true when it has none). False does not mean the push was rejected; see warnings. For a duplicate, the original event's result.","type":"boolean"},"warnings":{"description":"Non-fatal schema validation warnings. The event is stored and delivered regardless. Absent for a duplicate.","items":{"type":"string"},"type":"array"}},"required":["event_id","duplicate","schema_valid"],"type":"object"},"Recipe":{"additionalProperties":false,"properties":{"consumer":{"$ref":"#/components/schemas/ConsumerHint","description":"The consumer the recipe is meant to be registered under, when it matters."},"description":{"description":"One-line summary of the destination the recipe targets.","type":"string"},"guidance":{"description":"What an ideal message for this destination looks like. Advice for template authors and AI drafting, not a rule.","type":"string"},"name":{"description":"Recipe name, e.g. slack. Unique among shipped recipes.","type":"string"},"params":{"description":"Values the user supplies when applying the recipe. Each {{param \"name\"}} token in webhook.url, webhook.headers, webhook.secret_headers and subscription.transform_template is replaced with the value by plain string substitution before registration.","items":{"$ref":"#/components/schemas/Param"},"type":"array"},"subscription":{"$ref":"#/components/schemas/Subscription","description":"The transform given to the subscriptions created for the webhook."},"version":{"description":"Recipe schema version. Always 1.","format":"int64","type":"integer"},"webhook":{"$ref":"#/components/schemas/Webhook","description":"The webhook to register."}},"required":["version","name","description","webhook","subscription"],"type":"object"},"RegisterWebhookBody":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/RegisterWebhookBody.json"],"format":"uri","readOnly":true,"type":"string"},"active":{"description":"Whether the webhook is sent deliveries. Defaults to true. An inactive (paused) webhook still gets deliveries recorded for new events, held with status paused and sent only when retried.","type":"boolean"},"description":{"description":"Free-text note for humans, e.g. which system or team owns this endpoint.","type":"string"},"events":{"description":"Event type names this webhook should receive; auto-creates one subscription per entry. Use \"*\" as the sole entry to subscribe to every event in the consumer. Omit or leave empty to register the webhook with no subscriptions, then attach them individually via POST .../subscriptions (e.g. to set a per-subscription transform_template).","items":{"type":"string"},"type":"array"},"headers":{"additionalProperties":{},"description":"Static HTTP headers sent with every delivery to this webhook. Only string values are sent. Names must be valid HTTP tokens, values at most 8 KiB with no CR/LF; framing headers (Host, Content-Length, Transfer-Encoding, Connection, Keep-Alive, Proxy-Connection, TE, Trailer, Upgrade) are rejected. Stored and returned in plaintext: put credentials in secret_headers.","type":"object"},"http_config":{"$ref":"#/components/schemas/WebhookHTTPConfig","description":"Per-webhook HTTP delivery tuning (retries, timeouts, rate limit). Falls back to server defaults for any field left unset."},"on_transform_error":{"description":"What happens when transform_template fails to render for one of the created subscriptions. fail (default) or fallback; see the subscription's on_transform_error.","enum":["fail","fallback"],"type":"string"},"rate_limit_rps":{"deprecated":true,"description":"Deprecated: use http_config.rate_limit_rps. Shorthand for it, applied only when http_config.rate_limit_rps is not set (http_config wins when both are sent). Requests per second, must be positive.","format":"double","type":"number"},"requires_transform":{"description":"Set when the receiver only accepts a transformed payload (Slack, SendGrid, ...). Every subscription must then have transform_enabled and a transform_template: transform_template is required here when events is non-empty, and later subscription writes without one are rejected. Recipes set this. Defaults to false.","type":"boolean"},"secret_headers":{"additionalProperties":{"type":"string"},"description":"HTTP headers sent with every delivery whose values are envelope-encrypted at rest and masked in every API response (e.g. an upstream auth token). Same name and value rules as headers. Names are case-insensitive and saved in canonical form (x-api-key becomes X-Api-Key); the same name twice in different case is refused.","type":"object"},"signature_type":{"description":"Signing scheme. Every delivery carries an HMAC-SHA256 signature (v1,) in the Standard Webhooks webhook-signature header; ed25519 also generates a signing keypair and adds an Ed25519 signature (v1a,) next to it. Defaults to hmac.","enum":["hmac","ed25519"],"type":"string"},"template_missing_key":{"description":"How transform_template reads a key the payload does not have, for the created subscriptions. error (default) or zero; see the subscription's template_missing_key.","enum":["error","zero"],"type":"string"},"template_notes":{"description":"Optional note stored with that first template version, e.g. the AI drafter's summary.","maxLength":2000,"type":"string"},"template_source":{"description":"How transform_template was produced, recorded as the first version in each created subscription's template history. Defaults to manual.","enum":["manual","ai_draft"],"type":"string"},"transform_template":{"description":"Transform template given to every subscription created from events, with transform_enabled set. A template that does not parse is rejected with 400. Omit to create them untransformed (not allowed with requires_transform).","type":"string"},"url":{"description":"http or https endpoint deliveries are POSTed to. The host must resolve: localhost, *.internal, *.local, private, loopback and cloud metadata addresses are rejected (SSRF protection) unless allowed by SPARROW_ALLOW_PRIVATE_NETWORKS or SPARROW_ALLOWED_NETWORKS.","format":"uri","type":"string"}},"required":["url"],"type":"object"},"RepushBatchInputBody":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/RepushBatchInputBody.json"],"format":"uri","readOnly":true,"type":"string"},"repush_id":{"description":"repush_id returned by an earlier GET .../events?prepare_repush=true call.","type":"string"}},"required":["repush_id"],"type":"object"},"RepushEventOutputBody":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/RepushEventOutputBody.json"],"format":"uri","readOnly":true,"type":"string"},"event_id":{"description":"Id (UUID) of the new event occurrence created by the replay. The original occurrence is unchanged.","type":"string"},"warnings":{"description":"Non-fatal warnings from validating the stored payload against the event type's current schema.","items":{"type":"string"},"type":"array"}},"required":["event_id"],"type":"object"},"ResumeSubscriptionOutputBody":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/ResumeSubscriptionOutputBody.json"],"format":"uri","readOnly":true,"type":"string"},"consumer":{"description":"Consumer this subscription belongs to.","type":"string"},"created_at":{"description":"Creation timestamp, RFC3339.","format":"date-time","type":"string"},"event_name":{"description":"Event type name this subscription matches, or \"*\" for catch-all.","type":"string"},"headers":{"additionalProperties":{"type":"string"},"description":"Extra HTTP headers sent with deliveries from this subscription.","type":"object"},"label_filters":{"additionalProperties":{"type":"string"},"description":"Key/value pairs that must all be present in an event's labels for this subscription to receive it.","type":"object"},"method":{"description":"HTTP method used for deliveries from this subscription; absent means POST.","enum":["GET","POST","PUT","PATCH","DELETE"],"type":"string"},"on_transform_error":{"description":"What happens when transform_template fails to render. fail (default): the delivery fails with error category template_error, is not retried automatically, and can be retried once the template is fixed; it does not affect the webhook's health. fallback: the default envelope payload is sent instead, and the error is still recorded on the delivery.","enum":["fail","fallback"],"type":"string"},"paused":{"description":"Whether the subscription is paused. While paused, events still fan out to it, but each delivery is recorded with status paused and not attempted until retried. A pause never affects the webhook's health.","type":"boolean"},"paused_at":{"description":"When the current pause began, RFC3339. Absent when not paused.","format":"date-time","type":"string"},"paused_deliveries":{"description":"Deliveries of this subscription currently in status paused. They are not sent automatically; retry them to deliver.","format":"int64","type":"integer"},"paused_reason":{"description":"Why the subscription was paused.","type":"string"},"paused_since":{"description":"When the pause that just ended began, RFC3339; absent when the subscription was not paused. List or retry what was held with status=paused\u0026subscription_id=...\u0026created_after=\u003cpaused_since\u003e.","format":"date-time","type":"string"},"subscription_id":{"description":"Subscription id (UUID).","type":"string"},"template_missing_key":{"description":"How transform_template reads a key the payload does not have. error (default): the render fails, so a field removed from the event schema cannot silently turn into \"\u003cno value\u003e\" in the body; read optional fields with index or dig (default only fills a key that is present but empty). zero: the key renders as \"\u003cno value\u003e\".","enum":["error","zero"],"type":"string"},"timeout":{"description":"Request timeout override for this subscription's deliveries, in seconds; absent means the webhook's request_timeout_seconds.","format":"int64","type":"integer"},"transform_enabled":{"description":"Whether transform_template is rendered into the delivered payload.","type":"boolean"},"transform_template":{"description":"Go text/template rendered against the event to produce the delivered body when transform_enabled is true.","type":"string"},"updated_at":{"description":"Last-modified timestamp, RFC3339.","format":"date-time","type":"string"},"webhook_id":{"description":"Webhook this subscription delivers to.","type":"string"}},"required":["paused_deliveries","subscription_id","consumer","webhook_id","event_name","transform_enabled","on_transform_error","template_missing_key","paused","created_at","updated_at"],"type":"object"},"ResumeWebhookOutputBody":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/ResumeWebhookOutputBody.json"],"format":"uri","readOnly":true,"type":"string"},"paused_deliveries":{"description":"Deliveries held as paused for this webhook. They are not sent automatically; retry them to deliver.","format":"int64","type":"integer"}},"required":["paused_deliveries"],"type":"object"},"RetryBatchInputBody":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/RetryBatchInputBody.json"],"format":"uri","readOnly":true,"type":"string"},"retry_id":{"description":"retry_id returned by an earlier GET .../deliveries?prepare_retry=true call.","type":"string"}},"required":["retry_id"],"type":"object"},"RetryDeliveriesByWebhookInputBody":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/RetryDeliveriesByWebhookInputBody.json"],"format":"uri","readOnly":true,"type":"string"},"force":{"description":"false (default) retries deliveries in status failed, pending, retrying or paused. true retries every delivery of the webhook, including successful and expired ones, which resends them.","type":"boolean"},"webhook_id":{"description":"Webhook id (UUID) whose deliveries to retry.","type":"string"}},"required":["webhook_id"],"type":"object"},"RetryOutputBody":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/RetryOutputBody.json"],"format":"uri","readOnly":true,"type":"string"},"count":{"description":"Number of deliveries queued for another round of attempts.","format":"int32","type":"integer"},"delivery_ids":{"description":"Ids of the deliveries queued. Absent when nothing matched.","items":{"type":"string"},"type":"array"}},"required":["count"],"type":"object"},"RotateWebhookSecretOutputBody":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/RotateWebhookSecretOutputBody.json"],"format":"uri","readOnly":true,"type":"string"},"webhook_secret":{"description":"The new HMAC signing secret, in plaintext. Shown only in this response.","type":"string"}},"required":["webhook_secret"],"type":"object"},"SchemaCompatibilityItem":{"additionalProperties":false,"properties":{"reasons":{"description":"Each breaking change with the path it affects.","items":{"type":"string"},"type":"array"},"result":{"description":"breaking when the change could break a payload transformation: a required property removed or made optional, a type widened, the schema removed, or a change under oneOf/anyOf/allOf/$ref/patternProperties that cannot be checked.","enum":["compatible","breaking"],"type":"string"}},"required":["result"],"type":"object"},"SendTestEventInputBody":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/SendTestEventInputBody.json"],"format":"uri","readOnly":true,"type":"string"},"event_name":{"description":"Event to send: the subscription's event, or any event for a catch-all (*) subscription.","type":"string"},"payload":{"additionalProperties":{},"description":"Event payload to send instead of the event type's sample payload. It is rendered through the subscription's transform like a pushed event.","type":"object"}},"required":["event_name"],"type":"object"},"SendTestEventOutputBody":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/SendTestEventOutputBody.json"],"format":"uri","readOnly":true,"type":"string"},"error_class":{"description":"Delivery error category when the send did not get a 2xx: client_error, server_error, rate_limited, timeout, connection_refused, dns_error, tls_error, network_error, redirect, template_error (nothing was sent) or unknown.","type":"string"},"latency_ms":{"description":"Time to the response headers, in milliseconds.","format":"int64","type":"integer"},"status_code":{"description":"The receiver's HTTP status; 0 when no response was received.","format":"int64","type":"integer"}},"required":["status_code","latency_ms"],"type":"object"},"StampCheckItem":{"additionalProperties":false,"properties":{"exported_by":{"description":"Sparrow version that exported the file.","type":"string"},"server":{"description":"This server's Sparrow version.","type":"string"},"status":{"description":"valid: stamp present and matching. unsigned: no stamp (hand-written file). warning: see warnings.","enum":["valid","unsigned","warning"],"type":"string"},"warnings":{"description":"version_differs, format_unsupported, items_changed. Each must be acknowledged for the import to write.","items":{"type":"string"},"type":"array"}},"required":["status","server"],"type":"object"},"Subscription":{"additionalProperties":false,"properties":{"transform_template":{"description":"Go template rendered per delivery to build the destination-native body; may contain {{param}} tokens.","type":"string"}},"required":["transform_template"],"type":"object"},"SubscriptionItem":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/SubscriptionItem.json"],"format":"uri","readOnly":true,"type":"string"},"consumer":{"description":"Consumer this subscription belongs to.","type":"string"},"created_at":{"description":"Creation timestamp, RFC3339.","format":"date-time","type":"string"},"event_name":{"description":"Event type name this subscription matches, or \"*\" for catch-all.","type":"string"},"headers":{"additionalProperties":{"type":"string"},"description":"Extra HTTP headers sent with deliveries from this subscription.","type":"object"},"label_filters":{"additionalProperties":{"type":"string"},"description":"Key/value pairs that must all be present in an event's labels for this subscription to receive it.","type":"object"},"method":{"description":"HTTP method used for deliveries from this subscription; absent means POST.","enum":["GET","POST","PUT","PATCH","DELETE"],"type":"string"},"on_transform_error":{"description":"What happens when transform_template fails to render. fail (default): the delivery fails with error category template_error, is not retried automatically, and can be retried once the template is fixed; it does not affect the webhook's health. fallback: the default envelope payload is sent instead, and the error is still recorded on the delivery.","enum":["fail","fallback"],"type":"string"},"paused":{"description":"Whether the subscription is paused. While paused, events still fan out to it, but each delivery is recorded with status paused and not attempted until retried. A pause never affects the webhook's health.","type":"boolean"},"paused_at":{"description":"When the current pause began, RFC3339. Absent when not paused.","format":"date-time","type":"string"},"paused_reason":{"description":"Why the subscription was paused.","type":"string"},"subscription_id":{"description":"Subscription id (UUID).","type":"string"},"template_missing_key":{"description":"How transform_template reads a key the payload does not have. error (default): the render fails, so a field removed from the event schema cannot silently turn into \"\u003cno value\u003e\" in the body; read optional fields with index or dig (default only fills a key that is present but empty). zero: the key renders as \"\u003cno value\u003e\".","enum":["error","zero"],"type":"string"},"timeout":{"description":"Request timeout override for this subscription's deliveries, in seconds; absent means the webhook's request_timeout_seconds.","format":"int64","type":"integer"},"transform_enabled":{"description":"Whether transform_template is rendered into the delivered payload.","type":"boolean"},"transform_template":{"description":"Go text/template rendered against the event to produce the delivered body when transform_enabled is true.","type":"string"},"updated_at":{"description":"Last-modified timestamp, RFC3339.","format":"date-time","type":"string"},"webhook_id":{"description":"Webhook this subscription delivers to.","type":"string"}},"required":["subscription_id","consumer","webhook_id","event_name","transform_enabled","on_transform_error","template_missing_key","paused","created_at","updated_at"],"type":"object"},"TemplateCheckItem":{"additionalProperties":false,"properties":{"catch_all":{"description":"How many of the affected subscriptions are catch-all (\"*\").","format":"int64","type":"integer"},"failures":{"description":"Subscriptions whose transform no longer renders against the new version.","items":{"$ref":"#/components/schemas/TemplateFailureItem"},"type":"array"},"passed":{"description":"Subscriptions whose transform renders.","format":"int64","type":"integer"},"paused":{"description":"Subscriptions this import paused (subscription_policy=pause).","items":{"type":"string"},"type":"array"},"without_transform":{"description":"Subscriptions with no transform. Sparrow passes the payload through and cannot tell whether the receiver copes.","format":"int64","type":"integer"}},"required":["passed","without_transform","catch_all"],"type":"object"},"TemplateFailureItem":{"additionalProperties":false,"properties":{"consumer":{"description":"Consumer the subscription belongs to.","type":"string"},"error":{"description":"The render error.","type":"string"},"payload":{"description":"Which generated payload failed: every field, or only required fields (a template reading an optional field without guarding for it).","enum":["sample","required_only"],"type":"string"},"subscription_id":{"description":"Subscription whose template failed.","type":"string"},"webhook_id":{"description":"Webhook the subscription delivers to.","type":"string"}},"required":["subscription_id","consumer","webhook_id","payload","error"],"type":"object"},"TemplateFunctionItem":{"additionalProperties":false,"properties":{"description":{"description":"Markdown documentation for the function, including usage and an example.","type":"string"},"name":{"description":"Function name, as used in a transform_template.","type":"string"}},"required":["name","description"],"type":"object"},"TemplateFunctionsOutputBody":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/TemplateFunctionsOutputBody.json"],"format":"uri","readOnly":true,"type":"string"},"items":{"description":"Every helper function, in the engine's own order.","items":{"$ref":"#/components/schemas/TemplateFunctionItem"},"type":"array"}},"required":["items"],"type":"object"},"TemplateVersionItem":{"additionalProperties":false,"properties":{"created_at":{"description":"When it was saved, RFC3339.","format":"date-time","type":"string"},"current":{"description":"True for the newest version, which is the subscription's current template.","type":"boolean"},"notes":{"description":"Note stored with the version.","type":"string"},"saved_by":{"description":"Name of the master key or access token that saved it; absent when auth is off.","type":"string"},"source":{"description":"How the template was produced: manual or ai_draft.","enum":["manual","ai_draft"],"type":"string"},"template":{"description":"The template text as saved.","type":"string"},"version_id":{"description":"Version id (UUID).","type":"string"}},"required":["version_id","template","source","created_at","current"],"type":"object"},"TestTemplateBody":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/TestTemplateBody.json"],"format":"uri","readOnly":true,"type":"string"},"event_name":{"description":"Registered event type. Its name is the envelope's event_name, and its sample_payload (generated from its schema; empty without one) is rendered against when payload is omitted.","type":"string"},"payload":{"additionalProperties":{},"description":"Payload to render against instead of the event type's generated sample, e.g. a real event's payload from listEventOccurrences. Used as given, even when empty; the event type needs no schema.","type":"object"},"template":{"description":"Go template to render, in the same syntax used by transform_template.","type":"string"},"template_missing_key":{"description":"Render as a subscription with this template_missing_key would. Defaults to error, the subscription default.","enum":["error","zero"],"type":"string"}},"required":["event_name","template"],"type":"object"},"TestTemplateOutputBody":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/TestTemplateOutputBody.json"],"format":"uri","readOnly":true,"type":"string"},"rendered":{"description":"The template output, rendered against payload (or the event type's sample payload) with a placeholder event id and the current time.","type":"string"}},"required":["rendered"],"type":"object"},"TokenOut":{"additionalProperties":false,"properties":{"consumer":{"description":"Consumer the token is limited to (portal API only). Null: tenant-wide, same power as SPARROW_API_KEY.","type":["string","null"]},"created_at":{"description":"When the token was created, RFC3339.","format":"date-time","type":"string"},"created_by":{"description":"Name of the credential that created it (the master key, or another token).","type":"string"},"expires_at":{"description":"When the token stops working, RFC3339. Null: never expires.","format":"date-time","type":["string","null"]},"id":{"description":"Token id (not the secret).","type":"string"},"last_used_at":{"description":"When the token was last used, RFC3339; approximate (updated at most once a minute). Null: never used.","format":"date-time","type":["string","null"]},"name":{"description":"Who or what the token is for.","type":"string"},"revoked_at":{"description":"When the token was revoked, RFC3339. Null: not revoked.","format":"date-time","type":["string","null"]},"status":{"description":"active: usable. revoked: revoked by DELETE /v1/tokens/{token_id}. expired: past expires_at.","enum":["active","revoked","expired"],"type":"string"}},"required":["id","name","consumer","status","created_by","created_at","expires_at","revoked_at","last_used_at"],"type":"object"},"UpdateEventTypeOutputBody":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/UpdateEventTypeOutputBody.json"],"format":"uri","readOnly":true,"type":"string"},"active":{"description":"Whether events of this type can currently be pushed.","type":"boolean"},"change":{"$ref":"#/components/schemas/EventTypeChange","description":"What this update did."},"created_at":{"description":"Creation timestamp, RFC3339.","format":"date-time","type":"string"},"description":{"description":"Human-readable summary of what this event represents.","type":"string"},"event_schema":{"additionalProperties":{},"description":"JSON Schema payloads are softly validated against, if one is registered.","type":"object"},"metadata":{"additionalProperties":{"type":"string"},"description":"Arbitrary key/value metadata.","type":"object"},"name":{"description":"Event type name, e.g. order.created.","type":"string"},"sample_payload":{"additionalProperties":{},"description":"Example payload derived from the schema, used to preview subscription transform templates.","type":"object"},"updated_at":{"description":"Last-modified timestamp, RFC3339.","format":"date-time","type":"string"},"version":{"description":"Current schema version. Starts at 1 and increases only when the schema changes; every version is kept (see listEventTypeVersions).","format":"int64","type":"integer"}},"required":["change","name","active","version","created_at","updated_at"],"type":"object"},"ValidateEventPayloadInputBody":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/ValidateEventPayloadInputBody.json"],"format":"uri","readOnly":true,"type":"string"},"payload":{"additionalProperties":{},"description":"Payload to validate against the event type's registered JSON Schema.","type":"object"}},"required":["payload"],"type":"object"},"ValidateEventPayloadOutputBody":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/ValidateEventPayloadOutputBody.json"],"format":"uri","readOnly":true,"type":"string"},"valid":{"description":"Whether the payload matches the event type's JSON Schema. Always true when the event type has no schema registered.","type":"boolean"},"warnings":{"description":"Per-field validation errors, present when valid is false.","items":{"type":"string"},"type":"array"}},"required":["valid"],"type":"object"},"Webhook":{"additionalProperties":false,"properties":{"headers":{"additionalProperties":{"type":"string"},"description":"Static headers sent with every delivery; values may contain {{param}} tokens.","type":"object"},"requires_transform":{"description":"The destination only accepts the transformed payload; the webhook is registered with requires_transform.","type":"boolean"},"secret_headers":{"additionalProperties":{"type":"string"},"description":"Headers whose values are encrypted at rest and masked in API responses (upstream tokens, passwords); values may contain {{param}} tokens.","type":"object"},"url":{"description":"Destination URL; may contain {{param}} tokens.","type":"string"}},"required":["url"],"type":"object"},"WebhookHTTPConfig":{"additionalProperties":false,"properties":{"capture_response_body":{"description":"How much of the endpoint's response body is stored with each attempt: false (default) keeps up to 1 KiB, true up to SPARROW_MAX_CAPTURED_RESPONSE_BYTES (default 1 MiB).","type":"boolean"},"content_type":{"description":"Stored and returned, but deliveries do not use it yet: they send Content-Type: application/json. To send another Content-Type, set it in headers (or the subscription's headers). Must not be empty; defaults to application/json.","type":"string"},"expected_status_codes":{"description":"HTTP status codes treated as a successful delivery, each between 100 and 599. Defaults to 200, 201, 202 and 204.","items":{"format":"int64","type":"integer"},"type":"array"},"follow_redirects":{"description":"Whether to follow HTTP redirects returned by the endpoint. Each redirect target passes the same SSRF checks, and a redirect that changes the scheme, host or port gets only Sparrow's own headers: headers and secret_headers are dropped. Defaults to true.","type":"boolean"},"max_retries":{"description":"Retries after the first attempt before a delivery is marked failed, so a delivery gets up to max_retries + 1 attempts. 0 means no retries. Between 0 and 20; defaults to 13, which with the default retry_backoff_seconds keeps retrying for about three days.","format":"int64","type":"integer"},"rate_limit_rps":{"description":"Maximum delivery rate to this webhook, in requests per second (fractions allowed, e.g. 0.5). Must be positive; unset means no limit. Deliveries over the limit wait in the queue.","format":"double","type":"number"},"request_timeout_seconds":{"description":"How long to wait for the endpoint to respond before treating the attempt as a timeout, in seconds. Between 1 and 300; defaults to 30.","format":"int64","type":"integer"},"retry_backoff_seconds":{"description":"Base retry delay, in seconds. The delay after attempt N is retry_backoff_seconds * 2^(N-1), capped at 24 hours, minus a random 0-20% so retries spread out. Between 1 and 3600; defaults to 60. The defaults give retries after about 1, 2, 4, 8, 16, 32, 64, 128, 256, 512 and 1024 minutes, then 24 hours twice.","format":"int64","type":"integer"},"user_agent":{"description":"Stored and returned, but deliveries do not use it yet: they always send User-Agent: Sparrow-Webhook/\u003cserver version\u003e. To send another User-Agent, set it in headers. Defaults to Sparrow-Webhook/1.0.","type":"string"},"verify_ssl":{"description":"Whether to verify the endpoint's TLS certificate. Defaults to true. Set false only for trusted internal endpoints with self-signed certificates; SSRF and redirect checks still apply.","type":"boolean"}},"type":"object"},"WebhookHTTPConfigOut":{"additionalProperties":false,"properties":{"capture_response_body":{"description":"false: up to 1 KiB of each response body is stored; true: up to SPARROW_MAX_CAPTURED_RESPONSE_BYTES (default 1 MiB).","type":"boolean"},"content_type":{"description":"Stored value; deliveries currently send Content-Type: application/json unless headers override it.","type":"string"},"expected_status_codes":{"description":"HTTP status codes treated as a successful delivery.","items":{"format":"int32","type":"integer"},"type":"array"},"follow_redirects":{"description":"Whether HTTP redirects returned by the endpoint are followed.","type":"boolean"},"max_retries":{"description":"Retries after the first attempt before a delivery is marked failed (up to max_retries + 1 attempts).","format":"int64","type":"integer"},"rate_limit_rps":{"description":"Delivery rate limit for this webhook, in requests per second; absent means no limit.","format":"double","type":"number"},"request_timeout_seconds":{"description":"Per-attempt request timeout, in seconds.","format":"int64","type":"integer"},"retry_backoff_seconds":{"description":"Base retry delay, in seconds; the delay after attempt N is retry_backoff_seconds * 2^(N-1), capped at 24 hours, minus a random 0-20%.","format":"int64","type":"integer"},"user_agent":{"description":"Stored value; deliveries currently always send User-Agent: Sparrow-Webhook/\u003cserver version\u003e unless headers override it.","type":"string"},"verify_ssl":{"description":"Whether the endpoint's TLS certificate is verified.","type":"boolean"},"webhook_secret":{"description":"HMAC signing secret. Shown in full only once, in the response to webhook registration; masked on every later read.","type":"string"}},"required":["max_retries","retry_backoff_seconds","capture_response_body","follow_redirects","verify_ssl","request_timeout_seconds"],"type":"object"},"WebhookHealthOutputBody":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/WebhookHealthOutputBody.json"],"format":"uri","readOnly":true,"type":"string"},"avg_response_time":{"description":"Average endpoint response time over the window, in milliseconds.","format":"int64","type":"integer"},"client_errors":{"description":"Failed attempts in the window with a 4xx response (client_error).","format":"int64","type":"integer"},"consecutive_failures":{"description":"Current run of consecutive failed attempts; resets on a success.","format":"int64","type":"integer"},"failed_deliveries":{"description":"Failed attempts in the window.","format":"int64","type":"integer"},"health":{"description":"Health label from the last health evaluation: healthy, degraded, unhealthy, or unknown (too few recent attempts). Updated every SPARROW_HEALTH_EVAL_INTERVAL (default 1m).","enum":["healthy","degraded","unhealthy","unknown"],"type":"string"},"network_errors":{"description":"Failed attempts in the window with a network-level error (DNS, TLS, connection refused, reset).","format":"int64","type":"integer"},"rules":{"$ref":"#/components/schemas/HealthRules","description":"Thresholds the health status is derived from."},"server_errors":{"description":"Failed attempts in the window with a 5xx response (server_error).","format":"int64","type":"integer"},"success_rate":{"description":"successful_deliveries / total_deliveries over the window, 0.0 to 1.0.","format":"double","type":"number"},"successful_deliveries":{"description":"Successful attempts in the window.","format":"int64","type":"integer"},"timeout_errors":{"description":"Failed attempts in the window that timed out.","format":"int64","type":"integer"},"total_deliveries":{"description":"Delivery attempts in the last rules.window_hours hours (each retry counts).","format":"int64","type":"integer"},"unexpected_status_errors":{"description":"Attempts in the window whose 2xx/3xx response was not in expected_status_codes.","format":"int64","type":"integer"},"webhook_id":{"description":"Webhook id (UUID).","type":"string"}},"required":["webhook_id","health","total_deliveries","successful_deliveries","failed_deliveries","consecutive_failures","success_rate","avg_response_time","client_errors","server_errors","timeout_errors","network_errors","unexpected_status_errors","rules"],"type":"object"},"WebhookOut":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/WebhookOut.json"],"format":"uri","readOnly":true,"type":"string"},"active":{"description":"Whether deliveries are sent. false: paused by an operator or auto-disabled; new deliveries are held as paused.","type":"boolean"},"auto_disabled_at":{"description":"Set when Sparrow paused this webhook itself because its receiver kept failing (see SPARROW_AUTO_DISABLE_AFTER), RFC3339. Cleared when the webhook is resumed.","format":"date-time","type":"string"},"auto_disabled_reason":{"description":"Why Sparrow auto-disabled this webhook. Cleared when the webhook is resumed.","type":"string"},"consumer":{"description":"Consumer this webhook belongs to.","type":"string"},"created_at":{"description":"Creation timestamp, RFC3339.","format":"date-time","type":"string"},"description":{"description":"Human-readable note about this webhook.","type":"string"},"events":{"description":"Event type names this webhook is currently subscribed to, derived from its subscriptions.","items":{"type":"string"},"type":"array"},"headers":{"additionalProperties":{"type":"string"},"description":"Static HTTP headers sent with every delivery.","type":"object"},"health":{"description":"Health label from the last health evaluation (see getWebhookHealth).","enum":["healthy","degraded","unhealthy","unknown"],"type":"string"},"http_config":{"$ref":"#/components/schemas/WebhookHTTPConfigOut","description":"Per-webhook HTTP delivery configuration."},"requires_transform":{"description":"Whether every subscription must carry an enabled transform_template because the receiver only accepts transformed payloads.","type":"boolean"},"secret_headers":{"additionalProperties":{"type":"string"},"description":"Encrypted secret header names, with values always masked. Update them via PATCH secret_headers; send an empty string to remove one.","type":"object"},"signature_type":{"description":"Signing scheme. \"hmac\" (default) signs every delivery with HMAC-SHA256 (v1,); \"ed25519\" adds an Ed25519 signature (v1a,) alongside the HMAC one.","enum":["hmac","ed25519"],"type":"string"},"signing_public_key":{"description":"Hex-encoded Ed25519 public key for verifying the v1a, delivery signature. Safe to expose; there is no private-key equivalent to mask.","type":"string"},"updated_at":{"description":"Last-modified timestamp, RFC3339.","format":"date-time","type":"string"},"url":{"description":"HTTP endpoint deliveries are POSTed to.","type":"string"},"webhook_id":{"description":"Webhook id (UUID).","type":"string"}},"required":["webhook_id","consumer","events","url","active","health","signature_type","http_config","requires_transform","created_at","updated_at"],"type":"object"},"WhoAmIOutputBody":{"additionalProperties":false,"properties":{"$schema":{"description":"A URL to the JSON Schema for this object.","examples":["https://example.com/schemas/WhoAmIOutputBody.json"],"format":"uri","readOnly":true,"type":"string"},"auth_enabled":{"description":"False when SPARROW_API_KEY is unset: every endpoint is open.","type":"boolean"},"master_key":{"description":"True when the request used SPARROW_API_KEY itself. Always false when auth is off.","type":"boolean"},"name":{"description":"Name of the credential used for this request: the master key's name, the access token's name, or a placeholder when auth is off.","type":"string"},"token_id":{"description":"Id of the access token used; null for the master key or when auth is off.","type":["string","null"]}},"required":["auth_enabled","name","master_key","token_id"],"type":"object"}},"securitySchemes":{"ApiKeyAuth":{"description":"The master key (SPARROW_API_KEY) or a tenant-wide access token. Also accepted as Authorization: Bearer \u003ccredential\u003e. Required only when the server is started with SPARROW_API_KEY set; otherwise all endpoints are open. The embedded UI never receives the key: it asks for a credential on the first 401.","in":"header","name":"X-API-Key","type":"apiKey"}}},"info":{"contact":{"name":"Sparrow","url":"https://github.com/sarathsp06/sparrow"},"description":"Sparrow is a self-hosted webhook delivery platform: register the events your system produces, register the webhooks that should receive them, then push event occurrences and Sparrow fans them out with retries and health tracking.\n\n## Concepts\n\n- **Event type** (`Event Types`) — a named, versioned schema for something that happened in your system, e.g. `order.created`. Optional JSON Schema validation is *soft*: invalid payloads are stored with warnings, never rejected.\n- **Event** (`Events`) — one occurrence of an event type, pushed with a payload. Pushing an event asynchronously fans it out to every matching subscription.\n- **Webhook** (`Webhooks`) — a registered HTTP endpoint plus its delivery configuration (retries, timeouts, signing). Every delivery is signed with HMAC-SHA256 (Standard Webhooks), plus Ed25519 when the webhook's signature_type is ed25519.\n- **Subscription** (`Subscriptions`) — links one webhook to one event type within a consumer, optionally with a Go-template payload transform and label filters. Registering a webhook auto-creates matching subscriptions.\n- **Delivery** (`Deliveries`) — one attempt (and its retry history) to send an event to a webhook. Failures are classified as retryable (5xx, timeout, network) or not (4xx, DNS, TLS).\n- **Health** (`Health`) — a per-webhook label (healthy/degraded/unhealthy/unknown) derived from the last 24 hours of attempts, refreshed every `SPARROW_HEALTH_EVAL_INTERVAL`.\n\n## Consumers\n\nWebhooks, events, subscriptions, and deliveries are scoped to a `consumer` path segment (defaults to `default` if you don't need multi-tenancy). A handful of read endpoints are deliberately global (no consumer) for cross-tenant dashboards and id-only lookups where the resource id is already globally unique.\n\n## Authentication\n\nOptional. When the server is started with `SPARROW_API_KEY` set, every `/v1/*` request must carry that master key or a tenant-wide access token (`createToken` without a `consumer`), in the `X-API-Key` header or as `Authorization: Bearer \u003ccredential\u003e`. When `SPARROW_API_KEY` is unset, every endpoint is open, and outside `SPARROW_ENVIRONMENT=production` CORS also allows any origin: never expose such a server beyond a trusted network.\n\n## Portal tokens\n\nA consumer-scoped access token (`createToken` with a `consumer`) powers the embedded portal at `/portal`. It is refused on `/v1` (403); its API calls go to the single `/portal/api/` prefix with `Authorization: Bearer \u003ctoken\u003e`, and the consumer is carried by the token, not the URL, so exposing the portal means allowlisting only `/portal`, `/_app`, and `/portal/api`. `/portal/api/\u003crest\u003e` is served by `/v1/consumers/\u003ctoken's consumer\u003e/\u003crest\u003e`, except pushing or re-pushing events (`POST events`, `POST events:rePush`, `repush-jobs`, `prepare_repush`: refused), plus these global helpers: `GET event-types[/...]` (listed for the token's consumer), `GET template-functions`, `GET capabilities`, and `POST subscriptions:testTemplate`. With `SPARROW_PORTAL_RETRY=false` the delivery retry calls and `prepare_retry` are refused too (403), and `GET capabilities` reports `portal.retry: false`. Each operation's description says whether portal tokens can call it.\n\n## Errors\n\nErrors from the API itself are RFC 9457 problem details (`Content-Type: application/problem+json`): `{\"title\", \"status\", \"detail\", \"errors\"}`, where `errors` lists per-field validation problems. Two layers in front of the API answer with plain `application/json` instead:\n\n- Authentication (`401`, `503`): `{\"error\": \"unauthorized\", \"reason\": \"missing|invalid|expired|revoked\", \"message\": \"...\"}`; `reason` is absent for `503 unavailable` (retry after the `Retry-After` header).\n- Authorization and the portal gateway (`401`, `403`): `{\"error\": \"forbidden\" or \"unauthorized\", \"message\": \"...\"}`, e.g. a consumer-scoped token on `/v1`.\n\nStatus codes: `400` invalid input, `404` not found, `409` conflict or wrong state, `413` body over `SPARROW_MAX_BODY_BYTES`, `429` rate limited, `500` internal (details are logged, not returned), `503` temporarily unavailable, `504` timed out.","license":{"identifier":"MIT","name":"MIT"},"title":"Sparrow","version":"1.0.0"},"openapi":"3.1.0","paths":{"/v1/capabilities":{"get":{"description":"Reports which optional, deployment-configured features this server offers so clients can show or hide the matching UI: AI-assisted transform template drafting, with the provider and model in use, whether alert email delivery is set up, and whether portal tokens may retry deliveries. Read-only.\n\nai_drafting.enabled reflects SPARROW_AI_ENABLED=true; drafting is usable only when enabled is true and problem is absent; prompt_only is the opposite of enabled.\n\nPortal tokens: allowed, as GET /portal/api/capabilities.","operationId":"getCapabilities","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CapabilitiesOutputBody"}}},"description":"OK"},"default":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Error"}},"summary":"List optional server features","tags":["Server"]}},"/v1/consumers":{"get":{"description":"Lists the consumers that own a webhook or an event, sorted by name, optionally only those whose name contains q (case-insensitive). Consumers are created implicitly by registering a webhook or pushing an event under them; this is how clients find them, e.g. for a consumer picker. Not paginated: returns at most limit names.\n\nPortal tokens: not allowed.","operationId":"listConsumers","parameters":[{"description":"Only consumers whose name contains this text, case-insensitive.","explode":false,"in":"query","name":"q","schema":{"description":"Only consumers whose name contains this text, case-insensitive.","maxLength":255,"type":"string"}},{"description":"Maximum names to return.","explode":false,"in":"query","name":"limit","schema":{"default":50,"description":"Maximum names to return.","format":"int32","maximum":1000,"minimum":1,"type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListConsumersOutputBody"}}},"description":"OK"},"default":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Error"}},"summary":"Search consumer names","tags":["Webhooks"]}},"/v1/consumers/{consumer}/alert-configs":{"get":{"description":"Lists alert configs in a consumer, oldest first, optionally filtered to one webhook's own configs. Not paginated.\n\nPortal tokens: allowed, as GET /portal/api/alert-configs.","operationId":"listAlertConfigs","parameters":[{"description":"Consumer to list alert configs in.","in":"path","name":"consumer","required":true,"schema":{"description":"Consumer to list alert configs in.","type":"string"}},{"description":"Filter to alert configs scoped to one webhook. Omit to list every config in the consumer, including consumer-wide ones.","explode":false,"in":"query","name":"webhook_id","schema":{"description":"Filter to alert configs scoped to one webhook. Omit to list every config in the consumer, including consumer-wide ones.","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListAlertConfigsOutputBody"}}},"description":"OK"},"default":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Error"}},"summary":"List email alert recipients","tags":["Alert Configs"]},"post":{"description":"Opts an email address into Sparrow's self-generated health-change, permanent-delivery-failure and auto-disable notifications, scoped to one webhook or the whole consumer. Emails are only sent once an alert mailer webhook is registered under the _sparrow consumer (see GET /v1/capabilities alert_delivery.configured). Not idempotent: each call adds a config.\n\n400 for an invalid email or event type, 404 when webhook_id is not in this consumer.\n\nPortal tokens: allowed, as POST /portal/api/alert-configs.","operationId":"createAlertConfig","parameters":[{"description":"Consumer whose webhooks the alert covers.","in":"path","name":"consumer","required":true,"schema":{"description":"Consumer whose webhooks the alert covers.","type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAlertConfigBody"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlertConfigItem"}}},"description":"Created"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Register an email alert recipient","tags":["Alert Configs"]}},"/v1/consumers/{consumer}/alert-configs/{alert_config_id}":{"delete":{"description":"Removes an alert config. The webhook and its delivery history are unaffected. Returns 204; 404 when it is not in this consumer.\n\nPortal tokens: allowed, as DELETE /portal/api/alert-configs/{alert_config_id}.","operationId":"deleteAlertConfig","parameters":[{"description":"Consumer the alert config belongs to.","in":"path","name":"consumer","required":true,"schema":{"description":"Consumer the alert config belongs to.","type":"string"}},{"description":"Alert config id (UUID).","in":"path","name":"alert_config_id","required":true,"schema":{"description":"Alert config id (UUID).","type":"string"}}],"responses":{"204":{"description":"No Content"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Delete an email alert recipient","tags":["Alert Configs"]}},"/v1/consumers/{consumer}/deliveries":{"get":{"description":"Lists deliveries in a consumer, newest first, optionally filtered by webhook, event occurrence, subscription, event type, status, error category, or creation time. Cursor paginated: pass pagination.next_cursor as cursor for the next page; there is no total count and offset is rejected.\n\nSide effect: prepare_retry=true also snapshots every match (up to 10,000) into a batch job and returns retry_id for POST .../deliveries:retryBatch.\n\nPortal tokens: allowed, as GET /portal/api/deliveries; prepare_retry only while SPARROW_PORTAL_RETRY is true (otherwise 403).","operationId":"listDeliveries","parameters":[{"description":"Consumer to list deliveries in.","in":"path","name":"consumer","required":true,"schema":{"description":"Consumer to list deliveries in.","type":"string"}},{"description":"Filter to deliveries for one webhook (UUID; 400 otherwise).","explode":false,"in":"query","name":"webhook_id","schema":{"description":"Filter to deliveries for one webhook (UUID; 400 otherwise).","type":"string"}},{"description":"Filter to deliveries for one pushed event occurrence (UUID; 400 otherwise).","explode":false,"in":"query","name":"event_id","schema":{"description":"Filter to deliveries for one pushed event occurrence (UUID; 400 otherwise).","type":"string"}},{"description":"Filter by delivery status.","explode":false,"in":"query","name":"status","schema":{"description":"Filter by delivery status.","enum":["pending","sending","success","failed","retrying","expired","paused"],"type":"string"}},{"description":"Filter by the most recent attempt's error_category, e.g. server_error, client_error, timeout.","explode":false,"in":"query","name":"error_category","schema":{"description":"Filter by the most recent attempt's error_category, e.g. server_error, client_error, timeout.","type":"string"}},{"description":"Filter to deliveries created by one subscription (UUID), e.g. its paused deliveries.","explode":false,"in":"query","name":"subscription_id","schema":{"description":"Filter to deliveries created by one subscription (UUID), e.g. its paused deliveries.","type":"string"}},{"description":"Filter to deliveries of one event type, e.g. order.created. Also matches deliveries whose subscription has since been replaced or deleted.","explode":false,"in":"query","name":"event_name","schema":{"description":"Filter to deliveries of one event type, e.g. order.created. Also matches deliveries whose subscription has since been replaced or deleted.","type":"string"}},{"description":"Only deliveries created at or after this time: YYYY-MM-DD (start of that day, UTC) or an RFC3339 timestamp (e.g. an import's imported_at).","explode":false,"in":"query","name":"created_after","schema":{"description":"Only deliveries created at or after this time: YYYY-MM-DD (start of that day, UTC) or an RFC3339 timestamp (e.g. an import's imported_at).","type":"string"}},{"description":"Only deliveries created at or before this time: YYYY-MM-DD (through the end of that day, UTC) or an RFC3339 timestamp.","explode":false,"in":"query","name":"created_before","schema":{"description":"Only deliveries created at or before this time: YYYY-MM-DD (through the end of that day, UTC) or an RFC3339 timestamp.","type":"string"}},{"description":"Side effect: when true, also snapshots the ids of every delivery matching the filters (not just this page; at most 10,000, more is an error) into a batch job and returns its id as retry_id. Pass it to POST .../deliveries:retryBatch within 15 minutes. Each call creates a new snapshot.","explode":false,"in":"query","name":"prepare_retry","schema":{"default":false,"description":"Side effect: when true, also snapshots the ids of every delivery matching the filters (not just this page; at most 10,000, more is an error) into a batch job and returns its id as retry_id. Pass it to POST .../deliveries:retryBatch within 15 minutes. Each call creates a new snapshot.","type":"boolean"}},{"description":"Maximum items to return, 1 to 1000.","explode":false,"in":"query","name":"limit","schema":{"default":50,"description":"Maximum items to return, 1 to 1000.","format":"int32","maximum":1000,"minimum":1,"type":"integer"}},{"description":"Continue after the page this came from: pagination.next_cursor of the previous response. Stable while new deliveries arrive.","explode":false,"in":"query","name":"cursor","schema":{"description":"Continue after the page this came from: pagination.next_cursor of the previous response. Stable while new deliveries arrive.","type":"string"}},{"description":"Removed: page with cursor. Any value other than 0 is rejected with 400, so a client still paging by offset fails loudly instead of re-reading the first page.","explode":false,"in":"query","name":"offset","schema":{"default":0,"description":"Removed: page with cursor. Any value other than 0 is rejected with 400, so a client still paging by offset fails loudly instead of re-reading the first page.","format":"int32","type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListDeliveriesOutputBody"}}},"description":"OK"},"default":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Error"}},"summary":"List deliveries","tags":["Deliveries"]}},"/v1/consumers/{consumer}/deliveries/{delivery_id}":{"get":{"description":"Returns one delivery's status, latest response code/body, and error classification. 404 when it does not exist in this consumer.\n\nPortal tokens: allowed, as GET /portal/api/deliveries/{delivery_id}.","operationId":"getDelivery","parameters":[{"description":"Consumer the delivery belongs to.","in":"path","name":"consumer","required":true,"schema":{"description":"Consumer the delivery belongs to.","type":"string"}},{"description":"Delivery id (UUID).","in":"path","name":"delivery_id","required":true,"schema":{"description":"Delivery id (UUID).","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeliveryItem"}}},"description":"OK"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get a delivery by id","tags":["Deliveries"]}},"/v1/consumers/{consumer}/deliveries/{delivery_id}/attempts":{"get":{"description":"Returns the per-attempt record for one delivery, oldest first: response code, timing, and error classification for every attempt made so far, across manual retries. A delivery that does not exist or belongs to another consumer answers 404.\n\nPortal tokens: allowed, as GET /portal/api/deliveries/{delivery_id}/attempts.","operationId":"getDeliveryAttempts","parameters":[{"description":"Consumer the delivery belongs to.","in":"path","name":"consumer","required":true,"schema":{"description":"Consumer the delivery belongs to.","type":"string"}},{"description":"Delivery id (UUID).","in":"path","name":"delivery_id","required":true,"schema":{"description":"Delivery id (UUID).","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AttemptsOutputBody"}}},"description":"OK"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get a delivery's per-attempt history","tags":["Deliveries"]}},"/v1/consumers/{consumer}/deliveries/{delivery_id}:retry":{"post":{"description":"Queues one delivery for another round of attempts right away: it is reset to pending and gets up to max_attempts new attempts, without TTL expiry. Works for failed, expired, paused and retrying deliveries: a retrying delivery's scheduled retry is cancelled and replaced, so it is sent now rather than after its backoff. Refused with 409 for a successful delivery (use the bulk route with force to resend it) and for one that is pending or being sent right now (retry it once that attempt finishes). If the webhook or subscription is still paused the delivery is held as paused again. Not idempotent: each call queues another round.\n\nPortal tokens: allowed, as POST /portal/api/deliveries/{delivery_id}:retry, unless the server runs with SPARROW_PORTAL_RETRY=false (then 403).","operationId":"retryDelivery","parameters":[{"description":"Consumer the delivery belongs to.","in":"path","name":"consumer","required":true,"schema":{"description":"Consumer the delivery belongs to.","type":"string"}},{"description":"Delivery id (UUID) to retry.","in":"path","name":"delivery_id","required":true,"schema":{"description":"Delivery id (UUID) to retry.","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RetryOutputBody"}}},"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"409":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Conflict"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Retry a single delivery","tags":["Deliveries"]}},"/v1/consumers/{consumer}/deliveries:retry":{"post":{"description":"Queues every failed or paused delivery of one webhook for another round of attempts (see retryDelivery), in one request. With force, every delivery of the webhook is resent, including successful, expired and retrying ones (a retrying one's scheduled retry is replaced); deliveries pending or being sent right now are skipped. Returns count 0 when nothing matched. For a filtered or very large set, use prepare_retry and :retryBatch instead.\n\n404 when the webhook does not exist in this consumer.\n\nPortal tokens: allowed, as POST /portal/api/deliveries:retry, unless the server runs with SPARROW_PORTAL_RETRY=false (then 403).","operationId":"retryDeliveriesByWebhook","parameters":[{"description":"Consumer the webhook belongs to.","in":"path","name":"consumer","required":true,"schema":{"description":"Consumer the webhook belongs to.","type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RetryDeliveriesByWebhookInputBody"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RetryOutputBody"}}},"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Retry deliveries in bulk for a webhook","tags":["Deliveries"]}},"/v1/consumers/{consumer}/deliveries:retryBatch":{"post":{"description":"Starts an async job that retries every delivery in a snapshot taken by an earlier GET .../deliveries?prepare_retry=true call, like retryDelivery for each one. Send the retry_id the deliveries list returned. Returns 202 with the job; poll it with getDeliveryRetryJob.\n\nA snapshot can be started once: 409 when it is not pending (already started or cancelled), expired (15 minutes after it was taken), or is an event re-push snapshot; 404 when the id is unknown, 400 when it is not a UUID.\n\nPortal tokens: allowed, as POST /portal/api/deliveries:retryBatch, unless the server runs with SPARROW_PORTAL_RETRY=false (then 403).","operationId":"startDeliveryRetryJob","parameters":[{"description":"Consumer the job belongs to. A job snapshotted for another consumer answers 404; a job snapshotted across all consumers is reachable under any consumer, except with a portal token.","in":"path","name":"consumer","required":true,"schema":{"description":"Consumer the job belongs to. A job snapshotted for another consumer answers 404; a job snapshotted across all consumers is reachable under any consumer, except with a portal token.","type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RetryBatchInputBody"}}},"required":true},"responses":{"202":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BatchJobOutputBody"}}},"description":"Accepted"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"409":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Conflict"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Start a batch retry job from a prepared snapshot","tags":["Deliveries"]}},"/v1/consumers/{consumer}/events":{"get":{"description":"Lists pushed event occurrences in a consumer, newest first, with delivery outcome counts per occurrence. Cursor paginated: pass pagination.next_cursor as cursor for the next page; there is no total count and offset is rejected.\n\nSide effect: prepare_repush=true also snapshots every match (up to 10,000) into a batch job and returns repush_id for POST .../events:rePush.\n\nPortal tokens: allowed, as GET /portal/api/events, without prepare_repush (re-push is not a portal action; 403).","operationId":"listEventOccurrences","parameters":[{"description":"Consumer to list occurrences in.","in":"path","name":"consumer","required":true,"schema":{"description":"Consumer to list occurrences in.","type":"string"}},{"description":"Filter to occurrences of this event type name.","explode":false,"in":"query","name":"event","schema":{"description":"Filter to occurrences of this event type name.","type":"string"}},{"description":"Filter by schema validation result: valid or invalid. Other values are 400.","explode":false,"in":"query","name":"schema_valid","schema":{"description":"Filter by schema validation result: valid or invalid. Other values are 400.","type":"string"}},{"description":"Filter by labels as comma-separated key=value pairs, e.g. region=eu,tier=gold. Matches occurrences whose labels contain every pair.","explode":false,"in":"query","name":"labels","schema":{"description":"Filter by labels as comma-separated key=value pairs, e.g. region=eu,tier=gold. Matches occurrences whose labels contain every pair.","type":"string"}},{"description":"Only occurrences created at or after this time: YYYY-MM-DD (start of that day, UTC) or an RFC3339 timestamp.","explode":false,"in":"query","name":"created_after","schema":{"description":"Only occurrences created at or after this time: YYYY-MM-DD (start of that day, UTC) or an RFC3339 timestamp.","type":"string"}},{"description":"Only occurrences created at or before this time: YYYY-MM-DD (through the end of that day, UTC) or an RFC3339 timestamp.","explode":false,"in":"query","name":"created_before","schema":{"description":"Only occurrences created at or before this time: YYYY-MM-DD (through the end of that day, UTC) or an RFC3339 timestamp.","type":"string"}},{"description":"Side effect: when true, also snapshots the ids of every occurrence matching the filters (not just this page; at most 10,000, more is an error) into a batch job and returns its id as repush_id. Pass it to POST .../events:rePush within 15 minutes. Each call creates a new snapshot.","explode":false,"in":"query","name":"prepare_repush","schema":{"default":false,"description":"Side effect: when true, also snapshots the ids of every occurrence matching the filters (not just this page; at most 10,000, more is an error) into a batch job and returns its id as repush_id. Pass it to POST .../events:rePush within 15 minutes. Each call creates a new snapshot.","type":"boolean"}},{"description":"Maximum items to return, 1 to 1000.","explode":false,"in":"query","name":"limit","schema":{"default":50,"description":"Maximum items to return, 1 to 1000.","format":"int32","maximum":1000,"minimum":1,"type":"integer"}},{"description":"Continue after the page this came from: pagination.next_cursor of the previous response. Stable while new events arrive.","explode":false,"in":"query","name":"cursor","schema":{"description":"Continue after the page this came from: pagination.next_cursor of the previous response. Stable while new events arrive.","type":"string"}},{"description":"Removed: page with cursor. Any value other than 0 is rejected with 400, so a client still paging by offset fails loudly instead of re-reading the first page.","explode":false,"in":"query","name":"offset","schema":{"default":0,"description":"Removed: page with cursor. Any value other than 0 is rejected with 400, so a client still paging by offset fails loudly instead of re-reading the first page.","format":"int32","type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListEventOccurrencesOutputBody"}}},"description":"OK"},"default":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Error"}},"summary":"List pushed event occurrences","tags":["Events"]},"post":{"description":"Records one occurrence of a registered event type and asynchronously fans it out to every subscription whose event name and label filters match. Returns 201 as soon as the event is stored and queued; delivery happens in the background, so check Deliveries for outcomes. A payload that fails the event type's JSON Schema is still accepted (schema_valid: false, with warnings).\n\nIdempotency: with idempotency_key, a repeat push with the same key in the same consumer returns 201 with duplicate: true and the original event_id, and creates nothing. Without a key every call creates a new event.\n\nScheduling: with scheduled_at, the occurrence is stored immediately but fans out only at that time; until then it lists with no deliveries. A past time means immediate; more than one year ahead is 400.\n\nFailures: 404 for an unregistered event type (unless SPARROW_AUTO_REGISTER_EVENTS=true), 409 for an inactive one, 400 for a reserved sparrow.* name, a name over 255 characters, invalid labels or a scheduled_at over a year ahead, 413 for a body over SPARROW_MAX_BODY_BYTES.\n\nPortal tokens: not allowed (pushing is the producer's job).","operationId":"pushEvent","parameters":[{"description":"Consumer to record the occurrence in. Created implicitly on first use.","in":"path","name":"consumer","required":true,"schema":{"description":"Consumer to record the occurrence in. Created implicitly on first use.","type":"string"}},{"description":"Name of a registered, active event type (at most 255 characters). An unregistered name is 404 unless SPARROW_AUTO_REGISTER_EVENTS=true, which registers it if it follows the event type naming rule (lowercase letters, digits, '_' and '-' in dot-separated parts; 400 otherwise); sparrow.* names are reserved (400).","explode":false,"in":"query","name":"event","required":true,"schema":{"description":"Name of a registered, active event type (at most 255 characters). An unregistered name is 404 unless SPARROW_AUTO_REGISTER_EVENTS=true, which registers it if it follows the event type naming rule (lowercase letters, digits, '_' and '-' in dot-separated parts; 400 otherwise); sparrow.* names are reserved (400).","type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PushEventBody"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PushEventOutputBody"}}},"description":"Created"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"409":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Conflict"},"413":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Request Entity Too Large"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Push an event occurrence for asynchronous delivery","tags":["Events"]}},"/v1/consumers/{consumer}/events:rePush":{"post":{"description":"Starts an async job that re-pushes every event occurrence in a snapshot taken by an earlier GET .../events?prepare_repush=true call. Each re-push creates a new event occurrence with the original payload, without re-validating it; sparrow.* system events are skipped and counted as failed. Returns 202 with the job; poll it with getEventRepushJob.\n\nA snapshot can be started once: 409 when it is not pending (already started or cancelled) or expired (15 minutes after it was taken), 404 when the id is unknown, 400 when it is not a UUID.\n\nPortal tokens: not allowed (re-pushing events is the producer's job, like pushing them).","operationId":"startEventRepushJob","parameters":[{"description":"Consumer the job belongs to. A job snapshotted for another consumer answers 404; a job snapshotted across all consumers is reachable under any consumer, except with a portal token.","in":"path","name":"consumer","required":true,"schema":{"description":"Consumer the job belongs to. A job snapshotted for another consumer answers 404; a job snapshotted across all consumers is reachable under any consumer, except with a portal token.","type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RepushBatchInputBody"}}},"required":true},"responses":{"202":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BatchJobOutputBody"}}},"description":"Accepted"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"409":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Conflict"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Start a batch re-push job from a prepared snapshot","tags":["Events"]}},"/v1/consumers/{consumer}/repush-jobs/{job_id}":{"get":{"description":"Returns a batch re-push job's status and processed/failed/total counts. 404 when the id is unknown, 409 when the id belongs to a delivery retry job.\n\nPortal tokens: not allowed.","operationId":"getEventRepushJob","parameters":[{"description":"Consumer the job belongs to. A job snapshotted for another consumer answers 404; a job snapshotted across all consumers is reachable under any consumer, except with a portal token.","in":"path","name":"consumer","required":true,"schema":{"description":"Consumer the job belongs to. A job snapshotted for another consumer answers 404; a job snapshotted across all consumers is reachable under any consumer, except with a portal token.","type":"string"}},{"description":"Batch job id (UUID): the repush_id or retry_id from the list call.","in":"path","name":"job_id","required":true,"schema":{"description":"Batch job id (UUID): the repush_id or retry_id from the list call.","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BatchJobOutputBody"}}},"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"409":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Conflict"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get batch re-push job progress","tags":["Events"]}},"/v1/consumers/{consumer}/repush-jobs/{job_id}:cancel":{"post":{"description":"Cancels a pending or running batch re-push job; a running job stops at its next progress check (every 25 items). Occurrences already re-pushed are not rolled back. Returns 204. 409 when the job is already completed or cancelled.\n\nPortal tokens: not allowed.","operationId":"cancelEventRepushJob","parameters":[{"description":"Consumer the job belongs to. A job snapshotted for another consumer answers 404; a job snapshotted across all consumers is reachable under any consumer, except with a portal token.","in":"path","name":"consumer","required":true,"schema":{"description":"Consumer the job belongs to. A job snapshotted for another consumer answers 404; a job snapshotted across all consumers is reachable under any consumer, except with a portal token.","type":"string"}},{"description":"Batch job id (UUID): the repush_id or retry_id from the list call.","in":"path","name":"job_id","required":true,"schema":{"description":"Batch job id (UUID): the repush_id or retry_id from the list call.","type":"string"}}],"responses":{"204":{"description":"No Content"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"409":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Conflict"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Cancel a pending or in-progress batch re-push job","tags":["Events"]}},"/v1/consumers/{consumer}/retry-jobs/{job_id}":{"get":{"description":"Returns a batch retry job's status and processed/failed/total counts. 404 when the id is unknown, 409 when the id belongs to an event re-push job.\n\nPortal tokens: allowed, as GET /portal/api/retry-jobs/{job_id}.","operationId":"getDeliveryRetryJob","parameters":[{"description":"Consumer the job belongs to. A job snapshotted for another consumer answers 404; a job snapshotted across all consumers is reachable under any consumer, except with a portal token.","in":"path","name":"consumer","required":true,"schema":{"description":"Consumer the job belongs to. A job snapshotted for another consumer answers 404; a job snapshotted across all consumers is reachable under any consumer, except with a portal token.","type":"string"}},{"description":"Batch job id (UUID): the repush_id or retry_id from the list call.","in":"path","name":"job_id","required":true,"schema":{"description":"Batch job id (UUID): the repush_id or retry_id from the list call.","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BatchJobOutputBody"}}},"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"409":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Conflict"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get batch retry job progress","tags":["Deliveries"]}},"/v1/consumers/{consumer}/retry-jobs/{job_id}:cancel":{"post":{"description":"Cancels a pending or running batch retry job; a running job stops at its next progress check (every 25 items). Deliveries already queued are not rolled back. Returns 204. 409 when the job is already completed or cancelled.\n\nPortal tokens: allowed, as POST /portal/api/retry-jobs/{job_id}:cancel.","operationId":"cancelDeliveryRetryJob","parameters":[{"description":"Consumer the job belongs to. A job snapshotted for another consumer answers 404; a job snapshotted across all consumers is reachable under any consumer, except with a portal token.","in":"path","name":"consumer","required":true,"schema":{"description":"Consumer the job belongs to. A job snapshotted for another consumer answers 404; a job snapshotted across all consumers is reachable under any consumer, except with a portal token.","type":"string"}},{"description":"Batch job id (UUID): the repush_id or retry_id from the list call.","in":"path","name":"job_id","required":true,"schema":{"description":"Batch job id (UUID): the repush_id or retry_id from the list call.","type":"string"}}],"responses":{"204":{"description":"No Content"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"409":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Conflict"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Cancel a pending or in-progress batch retry job","tags":["Deliveries"]}},"/v1/consumers/{consumer}/stats":{"get":{"description":"Returns webhook and delivery-attempt counts (total, active, successful, failed, pending, success rate) for one consumer. Attempt counts come from the health evaluator and lag by up to SPARROW_HEALTH_EVAL_INTERVAL; pending is live.\n\nPortal tokens: allowed, as GET /portal/api/stats.","operationId":"getConsumerStats","parameters":[{"description":"Consumer name.","in":"path","name":"consumer","required":true,"schema":{"description":"Consumer name.","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConsumerStatsOutputBody"}}},"description":"OK"},"default":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Error"}},"summary":"Get aggregate delivery statistics for a consumer","tags":["Webhooks"]}},"/v1/consumers/{consumer}/subscriptions":{"get":{"description":"Lists subscriptions in a consumer, newest first, optionally filtered by webhook or event type name. Offset paginated with total_count. 404 when webhook_id is given and the webhook is not in this consumer.\n\nPortal tokens: allowed, as GET /portal/api/subscriptions.","operationId":"listSubscriptions","parameters":[{"description":"Consumer to list subscriptions in.","in":"path","name":"consumer","required":true,"schema":{"description":"Consumer to list subscriptions in.","type":"string"}},{"description":"Filter to subscriptions for one webhook (UUID). Takes precedence over event_name: when both are given, event_name is ignored.","explode":false,"in":"query","name":"webhook_id","schema":{"description":"Filter to subscriptions for one webhook (UUID). Takes precedence over event_name: when both are given, event_name is ignored.","type":"string"}},{"description":"Filter to subscriptions with exactly this event_name (\"*\" matches only catch-all subscriptions).","explode":false,"in":"query","name":"event_name","schema":{"description":"Filter to subscriptions with exactly this event_name (\"*\" matches only catch-all subscriptions).","type":"string"}},{"description":"Maximum items to return, 1 to 1000.","explode":false,"in":"query","name":"limit","schema":{"default":50,"description":"Maximum items to return, 1 to 1000.","format":"int32","maximum":1000,"minimum":1,"type":"integer"}},{"description":"Number of items to skip, for pagination.","explode":false,"in":"query","name":"offset","schema":{"default":0,"description":"Number of items to skip, for pagination.","format":"int32","type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListSubscriptionsOutputBody"}}},"description":"OK"},"default":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Error"}},"summary":"List subscriptions","tags":["Subscriptions"]},"post":{"description":"Subscribes a webhook to an event type within a consumer, with an optional payload transform and label filters. Registering a webhook with events already creates one subscription per name; use this for additional, catch-all (\"*\") or per-subscription-template subscriptions. A non-empty transform_template is recorded as the first template version.\n\nNot idempotent: 409 when the webhook already has a subscription for event_name. 404 when the webhook is not in this consumer. 400 for invalid headers, timeout or label filters, a sparrow.* event outside _sparrow, or a missing transform when the webhook has requires_transform.\n\nPortal tokens: allowed, as POST /portal/api/subscriptions.","operationId":"createSubscription","parameters":[{"description":"Consumer the webhook belongs to.","in":"path","name":"consumer","required":true,"schema":{"description":"Consumer the webhook belongs to.","type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateSubscriptionBody"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubscriptionItem"}}},"description":"Created"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"409":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Conflict"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Create a subscription linking a webhook to an event","tags":["Subscriptions"]}},"/v1/consumers/{consumer}/subscriptions/{subscription_id}":{"delete":{"description":"Deletes the subscription and its template history. The webhook stops receiving that event's occurrences; delivery history is kept. Cannot be undone. A second delete returns 404.\n\nPortal tokens: allowed, as DELETE /portal/api/subscriptions/{subscription_id}.","operationId":"deleteSubscription","parameters":[{"description":"Consumer the subscription belongs to.","in":"path","name":"consumer","required":true,"schema":{"description":"Consumer the subscription belongs to.","type":"string"}},{"description":"Subscription id (UUID).","in":"path","name":"subscription_id","required":true,"schema":{"description":"Subscription id (UUID).","type":"string"}}],"responses":{"204":{"description":"No Content"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Delete a subscription","tags":["Subscriptions"]},"get":{"description":"Returns one subscription: its webhook link, transform template and settings, label filters and pause state. 404 when it is not in this consumer.\n\nPortal tokens: allowed, as GET /portal/api/subscriptions/{subscription_id}.","operationId":"getSubscription","parameters":[{"description":"Consumer the subscription belongs to.","in":"path","name":"consumer","required":true,"schema":{"description":"Consumer the subscription belongs to.","type":"string"}},{"description":"Subscription id (UUID).","in":"path","name":"subscription_id","required":true,"schema":{"description":"Subscription id (UUID).","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubscriptionItem"}}},"description":"OK"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get a subscription by id","tags":["Subscriptions"]},"patch":{"description":"Partially updates a subscription's headers, method, timeout, transform, or label filters: only fields present in the body change, and map fields replace the whole map. The linked webhook_id and event_name cannot be changed; delete and recreate instead. A changed transform_template is recorded as a new template version with template_source and template_notes. Returns the updated subscription. Idempotent: repeating the same body records no new version.\n\n400 for invalid values, or when the webhook has requires_transform and the result has no enabled template. 404 when it is not in this consumer.\n\nPortal tokens: allowed, as PATCH /portal/api/subscriptions/{subscription_id}.","operationId":"updateSubscription","parameters":[{"description":"Consumer the subscription belongs to.","in":"path","name":"consumer","required":true,"schema":{"description":"Consumer the subscription belongs to.","type":"string"}},{"description":"Subscription id (UUID).","in":"path","name":"subscription_id","required":true,"schema":{"description":"Subscription id (UUID).","type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PatchSubscriptionBody"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubscriptionItem"}}},"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Partially update a subscription","tags":["Subscriptions"]}},"/v1/consumers/{consumer}/subscriptions/{subscription_id}/templateVersions":{"get":{"description":"Returns the transform templates this subscription has been saved with, newest first; the newest is the current one. A version is recorded whenever a save changes the template, with how it was produced (manual or ai_draft), an optional note, and who saved it. The last 20 are kept. There is no restore endpoint: load a version into the editor and save the subscription, which records it as a new version.\n\nPortal tokens: allowed, as GET /portal/api/subscriptions/{subscription_id}/templateVersions.","operationId":"listSubscriptionTemplateVersions","parameters":[{"description":"Consumer the subscription belongs to.","in":"path","name":"consumer","required":true,"schema":{"description":"Consumer the subscription belongs to.","type":"string"}},{"description":"Subscription id (UUID).","in":"path","name":"subscription_id","required":true,"schema":{"description":"Subscription id (UUID).","type":"string"}},{"description":"Maximum versions to return, newest first, 1 to 20.","explode":false,"in":"query","name":"limit","schema":{"default":20,"description":"Maximum versions to return, newest first, 1 to 20.","format":"int64","maximum":20,"minimum":1,"type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListTemplateVersionsOutputBody"}}},"description":"OK"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"List a subscription's saved template history","tags":["Subscriptions"]}},"/v1/consumers/{consumer}/subscriptions/{subscription_id}:pause":{"post":{"description":"Stops the subscription's deliveries from being attempted, for any reason (receiver maintenance, a template being fixed, an investigation). Events keep fanning out to it: each delivery is recorded with status paused and nothing is queued, so nothing is lost. Queued or retrying deliveries of the subscription are held as paused when their turn comes. A pause never affects the webhook's health. Returns the subscription (200), unlike webhook pause, which returns 204.\n\nIdempotent: pausing again updates the reason and keeps the original pause time. 400 for a reason over 500 characters, 404 when it is not in this consumer.\n\nPortal tokens: allowed, as POST /portal/api/subscriptions/{subscription_id}:pause.","operationId":"pauseSubscription","parameters":[{"description":"Consumer the subscription belongs to.","in":"path","name":"consumer","required":true,"schema":{"description":"Consumer the subscription belongs to.","type":"string"}},{"description":"Subscription id (UUID).","in":"path","name":"subscription_id","required":true,"schema":{"description":"Subscription id (UUID).","type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PauseSubscriptionInputBody"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubscriptionItem"}}},"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Pause a subscription","tags":["Subscriptions"]}},"/v1/consumers/{consumer}/subscriptions/{subscription_id}:resume":{"post":{"description":"Lets new deliveries be attempted again. Deliveries recorded while paused stay paused and are never sent automatically: the response says how many there are and when the pause began, so you can retry all of them or only those since a time with the delivery retry endpoints. Idempotent: resuming a subscription that is not paused succeeds with no paused_since.\n\nPortal tokens: allowed, as POST /portal/api/subscriptions/{subscription_id}:resume.","operationId":"resumeSubscription","parameters":[{"description":"Consumer the subscription belongs to.","in":"path","name":"consumer","required":true,"schema":{"description":"Consumer the subscription belongs to.","type":"string"}},{"description":"Subscription id (UUID).","in":"path","name":"subscription_id","required":true,"schema":{"description":"Subscription id (UUID).","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResumeSubscriptionOutputBody"}}},"description":"OK"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Resume a paused subscription","tags":["Subscriptions"]}},"/v1/consumers/{consumer}/subscriptions/{subscription_id}:sendTestEvent":{"post":{"description":"Sends event_name to the subscription: one signed delivery of payload, or of the event type's sample when payload is omitted, rendered through the subscription's transform as a real delivery would be, to its webhook's URL, synchronously, and returns what the receiver answered. It uses the same request builder, signing, network policy (SSRF protection) and timeout as real deliveries and adds the header X-Sparrow-Test: true. It creates no delivery and never counts toward the webhook's health or auto-disable. A receiver error is reported in the response (status_code, error_class), not as an API error.\n\nOne test event per webhook every 2 seconds on each server: more answer 429. 404 for a subscription not in this consumer. 400 when the subscription does not receive event_name, or when the event type is not registered and no payload is given.\n\nPortal tokens: allowed, as POST /portal/api/subscriptions/{subscription_id}:sendTestEvent.","operationId":"sendTestEvent","parameters":[{"description":"Tenant consumer the subscription belongs to.","in":"path","name":"consumer","required":true,"schema":{"description":"Tenant consumer the subscription belongs to.","type":"string"}},{"description":"Subscription to send the event to: its transform renders the payload, and its webhook receives it.","in":"path","name":"subscription_id","required":true,"schema":{"description":"Subscription to send the event to: its transform renders the payload, and its webhook receives it.","type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendTestEventInputBody"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SendTestEventOutputBody"}}},"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"429":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Too Many Requests"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"},"503":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Service Unavailable"}},"summary":"Send a test event to a subscription","tags":["Subscriptions"]}},"/v1/consumers/{consumer}/webhooks":{"get":{"description":"Lists the webhooks of one consumer, newest first, optionally filtered by id, subscribed event, active flag, or health status. Secrets are masked. Offset paginated with total_count. Deleted webhooks are never listed.\n\nPortal tokens: allowed, as GET /portal/api/webhooks.","operationId":"listWebhooks","parameters":[{"description":"Tenant consumer to list webhooks in.","in":"path","name":"consumer","required":true,"schema":{"description":"Tenant consumer to list webhooks in.","type":"string"}},{"description":"Filter to a single webhook by id.","explode":false,"in":"query","name":"webhook_id","schema":{"description":"Filter to a single webhook by id.","type":"string"}},{"description":"Filter to webhooks subscribed to this event type name.","explode":false,"in":"query","name":"event","schema":{"description":"Filter to webhooks subscribed to this event type name.","type":"string"}},{"description":"true returns only active (not paused) webhooks; false returns all.","explode":false,"in":"query","name":"active","schema":{"default":false,"description":"true returns only active (not paused) webhooks; false returns all.","type":"boolean"}},{"description":"Filter by computed health status.","explode":false,"in":"query","name":"health","schema":{"description":"Filter by computed health status.","enum":["healthy","degraded","unhealthy","unknown"],"type":"string"}},{"description":"Maximum items to return, 1 to 1000.","explode":false,"in":"query","name":"limit","schema":{"default":50,"description":"Maximum items to return, 1 to 1000.","format":"int32","maximum":1000,"minimum":1,"type":"integer"}},{"description":"Number of items to skip, for pagination.","explode":false,"in":"query","name":"offset","schema":{"default":0,"description":"Number of items to skip, for pagination.","format":"int32","type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListWebhooksOutputBody"}}},"description":"OK"},"default":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Error"}},"summary":"List webhooks","tags":["Webhooks"]},"post":{"description":"Registers a new HTTP endpoint and creates one subscription per name in events (with transform_template, when given). Generates the HMAC signing secret and returns it in plaintext in http_config.webhook_secret; this response is the only time it is shown. Event names are not required to be registered event types yet.\n\nNot idempotent: a second call with the same URL in the same consumer fails with 409 while the first webhook exists. 400 for an invalid URL (scheme, unresolvable or blocked host), invalid headers, http_config out of range, a sparrow.* event outside the _sparrow consumer, or requires_transform without transform_template.\n\nPortal tokens: allowed, as POST /portal/api/webhooks.","operationId":"registerWebhook","parameters":[{"description":"Consumer to register the webhook under. Created implicitly on first use.","in":"path","name":"consumer","required":true,"schema":{"description":"Consumer to register the webhook under. Created implicitly on first use.","type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterWebhookBody"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookOut"}}},"description":"Created"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"409":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Conflict"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Register a webhook","tags":["Webhooks"]}},"/v1/consumers/{consumer}/webhooks/{webhook_id}":{"delete":{"description":"Deletes a webhook (a soft delete): it stops receiving events and no longer appears in webhook lists, and its URL can be registered again in the consumer. Its subscriptions and delivery history are kept, and its deliveries stay listed until retention removes them. Queued deliveries for it fail with \"Webhook was deleted\". This cannot be undone. A second delete returns 404.\n\nPortal tokens: allowed, as DELETE /portal/api/webhooks/{webhook_id}.","operationId":"deleteWebhook","parameters":[{"description":"Tenant consumer the webhook belongs to.","in":"path","name":"consumer","required":true,"schema":{"description":"Tenant consumer the webhook belongs to.","type":"string"}},{"description":"Webhook id (UUID).","in":"path","name":"webhook_id","required":true,"schema":{"description":"Webhook id (UUID).","type":"string"}}],"responses":{"204":{"description":"No Content"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Delete a webhook","tags":["Webhooks"]},"get":{"description":"Returns one webhook's configuration, subscribed events, masked secrets, and current health label. 404 when it does not exist in this consumer or was deleted.\n\nPortal tokens: allowed, as GET /portal/api/webhooks/{webhook_id}.","operationId":"getWebhook","parameters":[{"description":"Tenant consumer the webhook belongs to.","in":"path","name":"consumer","required":true,"schema":{"description":"Tenant consumer the webhook belongs to.","type":"string"}},{"description":"Webhook id (UUID).","in":"path","name":"webhook_id","required":true,"schema":{"description":"Webhook id (UUID).","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookOut"}}},"description":"OK"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get a webhook by id","tags":["Webhooks"]},"patch":{"description":"Partially updates a webhook: only fields present in the body change. Within http_config only the sent fields change, and the merged result is validated as on register (400 if out of range). Sending events replaces every subscription of the webhook (see events). Setting active toggles delivery like :pause/:resume but never fails when the state is already as requested. Returns the updated webhook. Idempotent: repeating the same body leaves the same state.\n\n409 when turning requires_transform on while a subscription has no enabled transform, or replacing events while it is on. The deprecated top-level timeout is applied only when http_config.request_timeout_seconds is not sent.\n\nPortal tokens: allowed, as PATCH /portal/api/webhooks/{webhook_id}.","operationId":"updateWebhook","parameters":[{"description":"Consumer the webhook belongs to.","in":"path","name":"consumer","required":true,"schema":{"description":"Consumer the webhook belongs to.","type":"string"}},{"description":"Webhook id (UUID).","in":"path","name":"webhook_id","required":true,"schema":{"description":"Webhook id (UUID).","type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PatchWebhookBody"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookOut"}}},"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"409":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Conflict"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Partially update a webhook","tags":["Webhooks"]}},"/v1/consumers/{consumer}/webhooks/{webhook_id}/health":{"get":{"description":"Returns one webhook's health label plus attempt counts and per-category error counts (client, server, timeout, network, unexpected-status) over the last rules.window_hours hours, and the thresholds the label is derived from. The label lags by up to SPARROW_HEALTH_EVAL_INTERVAL. Counts are zero before the first attempt. 404 when the webhook is not in this consumer.\n\nPortal tokens: allowed, as GET /portal/api/webhooks/{webhook_id}/health.","operationId":"getWebhookHealth","parameters":[{"description":"Tenant consumer the webhook belongs to.","in":"path","name":"consumer","required":true,"schema":{"description":"Tenant consumer the webhook belongs to.","type":"string"}},{"description":"Webhook id (UUID).","in":"path","name":"webhook_id","required":true,"schema":{"description":"Webhook id (UUID).","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookHealthOutputBody"}}},"description":"OK"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get health status and metrics for a webhook","tags":["Health"]}},"/v1/consumers/{consumer}/webhooks/{webhook_id}:pause":{"post":{"description":"Stops deliveries to this webhook without deleting it. Returns 204 with no body. Events keep fanning out to it: each new delivery, and any queued or retrying one, is held with status paused until it is retried after resuming. Holding never affects the webhook's health. 409 when the webhook is already paused.\n\nPortal tokens: allowed, as POST /portal/api/webhooks/{webhook_id}:pause.","operationId":"pauseWebhook","parameters":[{"description":"Tenant consumer the webhook belongs to.","in":"path","name":"consumer","required":true,"schema":{"description":"Tenant consumer the webhook belongs to.","type":"string"}},{"description":"Webhook id (UUID).","in":"path","name":"webhook_id","required":true,"schema":{"description":"Webhook id (UUID).","type":"string"}}],"responses":{"204":{"description":"No Content"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"409":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Conflict"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Pause a webhook","tags":["Webhooks"]}},"/v1/consumers/{consumer}/webhooks/{webhook_id}:resume":{"post":{"description":"Re-enables deliveries to a webhook paused by an operator or auto-disabled, and clears auto_disabled_at/auto_disabled_reason. Deliveries held while it was paused stay paused and are never sent automatically: the response says how many there are, so you can retry them with status=paused\u0026webhook_id=... and the delivery retry endpoints. 409 when the webhook is already active.\n\nPortal tokens: allowed, as POST /portal/api/webhooks/{webhook_id}:resume.","operationId":"resumeWebhook","parameters":[{"description":"Tenant consumer the webhook belongs to.","in":"path","name":"consumer","required":true,"schema":{"description":"Tenant consumer the webhook belongs to.","type":"string"}},{"description":"Webhook id (UUID).","in":"path","name":"webhook_id","required":true,"schema":{"description":"Webhook id (UUID).","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResumeWebhookOutputBody"}}},"description":"OK"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"409":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Conflict"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Resume a paused webhook","tags":["Webhooks"]}},"/v1/consumers/{consumer}/webhooks/{webhook_id}:rotateSecret":{"post":{"description":"Replaces the webhook's HMAC signing secret with a newly generated one and returns it in plaintext. This response is the only time the new secret is shown; it is masked on every later read. There is no overlap window: every attempt made after the rotation, including retries of older deliveries, is signed with the new secret only, so update the receiver right away. Not idempotent: each call generates a new secret. The Ed25519 key, if any, is not rotated.\n\nPortal tokens: allowed, as POST /portal/api/webhooks/{webhook_id}:rotateSecret.","operationId":"rotateWebhookSecret","parameters":[{"description":"Tenant consumer the webhook belongs to.","in":"path","name":"consumer","required":true,"schema":{"description":"Tenant consumer the webhook belongs to.","type":"string"}},{"description":"Webhook id (UUID).","in":"path","name":"webhook_id","required":true,"schema":{"description":"Webhook id (UUID).","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RotateWebhookSecretOutputBody"}}},"description":"OK"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Rotate a webhook's signing secret","tags":["Webhooks"]}},"/v1/deliveries":{"get":{"description":"Lists deliveries across every consumer, newest first, with the same filters, cursor pagination and prepare_retry side effect as the per-consumer route. Pass consumer to scope to one consumer.\n\nPortal tokens: not allowed.","operationId":"listDeliveriesGlobal","parameters":[{"description":"Filter to one consumer; omit to list deliveries across all consumers.","explode":false,"in":"query","name":"consumer","schema":{"description":"Filter to one consumer; omit to list deliveries across all consumers.","type":"string"}},{"description":"Filter to deliveries for one webhook (UUID; 400 otherwise).","explode":false,"in":"query","name":"webhook_id","schema":{"description":"Filter to deliveries for one webhook (UUID; 400 otherwise).","type":"string"}},{"description":"Filter to deliveries for one pushed event occurrence (UUID; 400 otherwise).","explode":false,"in":"query","name":"event_id","schema":{"description":"Filter to deliveries for one pushed event occurrence (UUID; 400 otherwise).","type":"string"}},{"description":"Filter by delivery status.","explode":false,"in":"query","name":"status","schema":{"description":"Filter by delivery status.","enum":["pending","sending","success","failed","retrying","expired","paused"],"type":"string"}},{"description":"Filter by the most recent attempt's error_category, e.g. server_error, client_error, timeout.","explode":false,"in":"query","name":"error_category","schema":{"description":"Filter by the most recent attempt's error_category, e.g. server_error, client_error, timeout.","type":"string"}},{"description":"Filter to deliveries created by one subscription (UUID), e.g. its paused deliveries.","explode":false,"in":"query","name":"subscription_id","schema":{"description":"Filter to deliveries created by one subscription (UUID), e.g. its paused deliveries.","type":"string"}},{"description":"Filter to deliveries of one event type, e.g. order.created. Also matches deliveries whose subscription has since been replaced or deleted.","explode":false,"in":"query","name":"event_name","schema":{"description":"Filter to deliveries of one event type, e.g. order.created. Also matches deliveries whose subscription has since been replaced or deleted.","type":"string"}},{"description":"Only deliveries created at or after this time: YYYY-MM-DD (start of that day, UTC) or an RFC3339 timestamp (e.g. an import's imported_at).","explode":false,"in":"query","name":"created_after","schema":{"description":"Only deliveries created at or after this time: YYYY-MM-DD (start of that day, UTC) or an RFC3339 timestamp (e.g. an import's imported_at).","type":"string"}},{"description":"Only deliveries created at or before this time: YYYY-MM-DD (through the end of that day, UTC) or an RFC3339 timestamp.","explode":false,"in":"query","name":"created_before","schema":{"description":"Only deliveries created at or before this time: YYYY-MM-DD (through the end of that day, UTC) or an RFC3339 timestamp.","type":"string"}},{"description":"Side effect: when true, also snapshots the ids of every delivery matching the filters (not just this page; at most 10,000, more is an error) into a batch job and returns its id as retry_id. Pass it to POST .../deliveries:retryBatch within 15 minutes. Each call creates a new snapshot.","explode":false,"in":"query","name":"prepare_retry","schema":{"default":false,"description":"Side effect: when true, also snapshots the ids of every delivery matching the filters (not just this page; at most 10,000, more is an error) into a batch job and returns its id as retry_id. Pass it to POST .../deliveries:retryBatch within 15 minutes. Each call creates a new snapshot.","type":"boolean"}},{"description":"Maximum items to return, 1 to 1000.","explode":false,"in":"query","name":"limit","schema":{"default":50,"description":"Maximum items to return, 1 to 1000.","format":"int32","maximum":1000,"minimum":1,"type":"integer"}},{"description":"Continue after the page this came from: pagination.next_cursor of the previous response. Stable while new deliveries arrive.","explode":false,"in":"query","name":"cursor","schema":{"description":"Continue after the page this came from: pagination.next_cursor of the previous response. Stable while new deliveries arrive.","type":"string"}},{"description":"Removed: page with cursor. Any value other than 0 is rejected with 400, so a client still paging by offset fails loudly instead of re-reading the first page.","explode":false,"in":"query","name":"offset","schema":{"default":0,"description":"Removed: page with cursor. Any value other than 0 is rejected with 400, so a client still paging by offset fails loudly instead of re-reading the first page.","format":"int32","type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListDeliveriesOutputBody"}}},"description":"OK"},"default":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Error"}},"summary":"List deliveries across all consumers","tags":["Deliveries"]}},"/v1/deliveries/{delivery_id}":{"get":{"description":"Looks a delivery up by id in any consumer, for callers that only have the id (e.g. the X-Sparrow-Delivery-ID header a receiver logged). 404 when it does not exist.\n\nPortal tokens: not allowed.","operationId":"getDeliveryGlobal","parameters":[{"description":"Delivery id (UUID).","in":"path","name":"delivery_id","required":true,"schema":{"description":"Delivery id (UUID).","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeliveryItem"}}},"description":"OK"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get a delivery by id (any consumer)","tags":["Deliveries"]}},"/v1/deliveries/{delivery_id}/attempts":{"get":{"description":"Same as getDeliveryAttempts, without a consumer: the delivery is looked up by id alone, and an unknown id returns an empty list.\n\nPortal tokens: not allowed.","operationId":"getDeliveryAttemptsGlobal","parameters":[{"description":"Delivery id (UUID).","in":"path","name":"delivery_id","required":true,"schema":{"description":"Delivery id (UUID).","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AttemptsOutputBody"}}},"description":"OK"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get a delivery's per-attempt history (any consumer)","tags":["Deliveries"]}},"/v1/deliveries/{delivery_id}:retry":{"post":{"description":"Same as retryDelivery, for a delivery in any consumer.\n\nPortal tokens: not allowed.","operationId":"retryDeliveryGlobal","parameters":[{"description":"Delivery id (UUID).","in":"path","name":"delivery_id","required":true,"schema":{"description":"Delivery id (UUID).","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RetryOutputBody"}}},"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"409":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Conflict"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Retry a single delivery (any consumer)","tags":["Deliveries"]}},"/v1/event-types":{"get":{"description":"Lists registered event types sorted by name, optionally only active ones. Sparrow's own sparrow.* events are listed only with consumer=_sparrow. Offset paginated with total_count.\n\nPortal tokens: allowed, as GET /portal/api/event-types; consumer is set to the token's consumer.","operationId":"listEventTypes","parameters":[{"description":"true returns only active event types; false returns all.","explode":false,"in":"query","name":"active_only","schema":{"default":false,"description":"true returns only active event types; false returns all.","type":"boolean"}},{"description":"Return the event types this consumer can subscribe to: Sparrow's own sparrow.* events for the _sparrow consumer, tenant event types for any other consumer or when omitted.","explode":false,"in":"query","name":"consumer","schema":{"description":"Return the event types this consumer can subscribe to: Sparrow's own sparrow.* events for the _sparrow consumer, tenant event types for any other consumer or when omitted.","type":"string"}},{"description":"Maximum items to return, 1 to 1000.","explode":false,"in":"query","name":"limit","schema":{"default":50,"description":"Maximum items to return, 1 to 1000.","format":"int32","maximum":1000,"minimum":1,"type":"integer"}},{"description":"Number of items to skip, for pagination.","explode":false,"in":"query","name":"offset","schema":{"default":0,"description":"Number of items to skip, for pagination.","format":"int32","type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListEventTypesOutputBody"}}},"description":"OK"},"default":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Error"}},"summary":"List event type definitions","tags":["Event Types"]},"post":{"description":"Defines a named event type at version 1, optionally with a JSON Schema; the response includes a sample_payload generated from the schema. Pushing an event requires its type to be registered (unless SPARROW_AUTO_REGISTER_EVENTS=true). Event types are never deleted; deactivate one with active: false.\n\nNot idempotent: 409 when the name already exists (use PATCH, or :import, to change it). 400 for a missing, too long or reserved (sparrow.*) name or an invalid schema.\n\nPortal tokens: not allowed.","operationId":"registerEventType","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EventTypeBody"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EventTypeItem"}}},"description":"Created"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"409":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Conflict"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Register an event type definition","tags":["Event Types"]}},"/v1/event-types/{name}":{"get":{"description":"Returns one event type's current version: schema, sample payload, and metadata. 404 when the name is not registered.\n\nPortal tokens: allowed, as GET /portal/api/event-types/{name}.","operationId":"getEventType","parameters":[{"description":"Event type name, e.g. order.created.","in":"path","name":"name","required":true,"schema":{"description":"Event type name, e.g. order.created.","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EventTypeItem"}}},"description":"OK"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get an event type by name","tags":["Event Types"]},"patch":{"description":"Partially updates an event type: only fields present in the request body are changed (metadata, when sent, replaces the whole map). A schema change creates a new version and keeps the previous one; adding a first schema to a schema-less version, and any change to description, metadata or active, updates the current version in place. The response's change field says which happened; repeating the same body is a no-op (change.action unchanged).\n\nA breaking schema change (see change.compatibility) to an event type that subscriptions receive is refused with 409 unless allow_breaking=true. 404 when the name is not registered.\n\nPortal tokens: not allowed.","operationId":"updateEventType","parameters":[{"description":"Event type name, e.g. order.created.","in":"path","name":"name","required":true,"schema":{"description":"Event type name, e.g. order.created.","type":"string"}},{"description":"Apply a breaking schema change even though subscriptions receive this event type. Without it, a breaking change to a subscribed type is refused with 409 and the reasons.","explode":false,"in":"query","name":"allow_breaking","schema":{"default":false,"description":"Apply a breaking schema change even though subscriptions receive this event type. Without it, a breaking change to a subscribed type is refused with 409 and the reasons.","type":"boolean"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EventTypeBody"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateEventTypeOutputBody"}}},"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"409":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Conflict"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Update an event type definition","tags":["Event Types"]}},"/v1/event-types/{name}/versions":{"get":{"description":"Returns the version history of an event type, newest first. Not paginated. Event types are never deleted; to retire one, set active to false.\n\nPortal tokens: allowed, as GET /portal/api/event-types/{name}/versions.","operationId":"listEventTypeVersions","parameters":[{"description":"Event type name, e.g. order.created.","in":"path","name":"name","required":true,"schema":{"description":"Event type name, e.g. order.created.","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListEventTypeVersionsOutputBody"}}},"description":"OK"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"List every version of an event type","tags":["Event Types"]}},"/v1/event-types/{name}/versions/{version}":{"get":{"description":"Returns one version's schema, sample payload and description. 404 when the name or version does not exist.\n\nPortal tokens: allowed, as GET /portal/api/event-types/{name}/versions/{version}.","operationId":"getEventTypeVersion","parameters":[{"description":"Event type name, e.g. order.created.","in":"path","name":"name","required":true,"schema":{"description":"Event type name, e.g. order.created.","type":"string"}},{"description":"Version number.","in":"path","name":"version","required":true,"schema":{"description":"Version number.","format":"int64","minimum":1,"type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EventTypeVersionItem"}}},"description":"OK"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get one version of an event type","tags":["Event Types"]}},"/v1/event-types/{name}:validate":{"post":{"description":"Checks a payload against the event type's current JSON Schema without pushing an event or storing anything. pushEvent accepts events regardless of schema validity; this lets callers (e.g. the Push Test Event UI) enforce a hard gate before submitting. An invalid payload is still 200, with valid: false. 404 when the name is not registered.\n\nPortal tokens: not allowed.","operationId":"validateEventPayload","parameters":[{"description":"Event type name, e.g. order.created.","in":"path","name":"name","required":true,"schema":{"description":"Event type name, e.g. order.created.","type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidateEventPayloadInputBody"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidateEventPayloadOutputBody"}}},"description":"OK"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Validate a payload against an event type's JSON Schema","tags":["Event Types"]}},"/v1/event-types:export":{"post":{"description":"Returns the current definitions of the selected event types as one JSON bundle, ready to import into another environment with importEventTypes. Choose exactly one of names, prefix or all. An unknown name fails the whole export. Sparrow's own sparrow.* event types are never exported. Read-only despite POST (the selection travels in the body).\n\nPortal tokens: not allowed.","operationId":"exportEventTypes","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ExportEventTypesInputBody"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EventTypeBundle"}}},"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Export event type definitions as a bundle","tags":["Event Types"]}},"/v1/event-types:import":{"post":{"description":"Applies a bundle in one transaction: each item replaces the named event type using the usual version rules, or nothing is written. Event types not in the bundle are never touched; an import does not delete. The full per-item result is returned either way. Nothing is written for a dry run, when a stamp warning is not acknowledged, or when a change is breaking for subscriptions and allow_breaking is not set; blocked_by says why. The body may be an exported bundle as-is, plus the options. Idempotent: importing the same bundle again reports every item unchanged. The body is limited to 5 MiB and 500 items.\n\nPortal tokens: not allowed.","operationId":"importEventTypes","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImportEventTypesInputBody"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImportEventTypesOutputBody"}}},"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Import a bundle of event type definitions","tags":["Event Types"]}},"/v1/events":{"get":{"description":"Lists event occurrences across every consumer, newest first, with the same filters, cursor pagination and prepare_repush side effect as the per-consumer route. Pass consumer to scope to one consumer.\n\nPortal tokens: not allowed.","operationId":"listEventOccurrencesGlobal","parameters":[{"description":"Filter to one consumer; omit to list occurrences across all consumers.","explode":false,"in":"query","name":"consumer","schema":{"description":"Filter to one consumer; omit to list occurrences across all consumers.","type":"string"}},{"description":"Filter to occurrences of this event type name.","explode":false,"in":"query","name":"event","schema":{"description":"Filter to occurrences of this event type name.","type":"string"}},{"description":"Filter by schema validation result: valid or invalid. Other values are 400.","explode":false,"in":"query","name":"schema_valid","schema":{"description":"Filter by schema validation result: valid or invalid. Other values are 400.","type":"string"}},{"description":"Filter by labels as comma-separated key=value pairs, e.g. region=eu,tier=gold. Matches occurrences whose labels contain every pair.","explode":false,"in":"query","name":"labels","schema":{"description":"Filter by labels as comma-separated key=value pairs, e.g. region=eu,tier=gold. Matches occurrences whose labels contain every pair.","type":"string"}},{"description":"Only occurrences created at or after this time: YYYY-MM-DD (start of that day, UTC) or an RFC3339 timestamp.","explode":false,"in":"query","name":"created_after","schema":{"description":"Only occurrences created at or after this time: YYYY-MM-DD (start of that day, UTC) or an RFC3339 timestamp.","type":"string"}},{"description":"Only occurrences created at or before this time: YYYY-MM-DD (through the end of that day, UTC) or an RFC3339 timestamp.","explode":false,"in":"query","name":"created_before","schema":{"description":"Only occurrences created at or before this time: YYYY-MM-DD (through the end of that day, UTC) or an RFC3339 timestamp.","type":"string"}},{"description":"Side effect: when true, also snapshots the ids of every occurrence matching the filters (not just this page; at most 10,000, more is an error) into a batch job and returns its id as repush_id. Pass it to POST .../events:rePush within 15 minutes. Each call creates a new snapshot.","explode":false,"in":"query","name":"prepare_repush","schema":{"default":false,"description":"Side effect: when true, also snapshots the ids of every occurrence matching the filters (not just this page; at most 10,000, more is an error) into a batch job and returns its id as repush_id. Pass it to POST .../events:rePush within 15 minutes. Each call creates a new snapshot.","type":"boolean"}},{"description":"Maximum items to return, 1 to 1000.","explode":false,"in":"query","name":"limit","schema":{"default":50,"description":"Maximum items to return, 1 to 1000.","format":"int32","maximum":1000,"minimum":1,"type":"integer"}},{"description":"Continue after the page this came from: pagination.next_cursor of the previous response. Stable while new events arrive.","explode":false,"in":"query","name":"cursor","schema":{"description":"Continue after the page this came from: pagination.next_cursor of the previous response. Stable while new events arrive.","type":"string"}},{"description":"Removed: page with cursor. Any value other than 0 is rejected with 400, so a client still paging by offset fails loudly instead of re-reading the first page.","explode":false,"in":"query","name":"offset","schema":{"default":0,"description":"Removed: page with cursor. Any value other than 0 is rejected with 400, so a client still paging by offset fails loudly instead of re-reading the first page.","format":"int32","type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListEventOccurrencesOutputBody"}}},"description":"OK"},"default":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Error"}},"summary":"List pushed event occurrences across all consumers","tags":["Events"]}},"/v1/events/{event_id}":{"get":{"description":"Returns one event occurrence's payload, labels, and delivery outcome counts. Event ids are globally unique, so no consumer is required. 400 when event_id is not a UUID, 404 when it does not exist (or was removed by retention).\n\nPortal tokens: not allowed.","operationId":"getEventOccurrence","parameters":[{"description":"Event occurrence id (UUID).","in":"path","name":"event_id","required":true,"schema":{"description":"Event occurrence id (UUID).","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EventOccurrenceItem"}}},"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Get a pushed event occurrence by id","tags":["Events"]}},"/v1/events/{event_id}:repush":{"post":{"description":"Pushes a stored event occurrence again: creates a new event occurrence (new event_id) with the same consumer, event type, payload, labels, metadata and TTL, validates the payload against the current schema, and fans it out to the subscriptions that match now. The original occurrence and its deliveries are unchanged. Not idempotent: each call creates another event. To resend one existing delivery instead, retry the delivery.\n\n404 when the event does not exist; the same errors as pushEvent when the event type is now inactive or unregistered.\n\nPortal tokens: not allowed.","operationId":"repushEvent","parameters":[{"description":"Event occurrence id (UUID).","in":"path","name":"event_id","required":true,"schema":{"description":"Event occurrence id (UUID).","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RepushEventOutputBody"}}},"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"409":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Conflict"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Replay a single pushed event occurrence","tags":["Events"]}},"/v1/health-summary":{"get":{"description":"Returns how many webhooks are currently healthy, degraded, unhealthy, or unknown, in one consumer or across every consumer — for a top-level dashboard tile, plus the thresholds behind those statuses. Deleted webhooks are not counted.\n\nPortal tokens: not allowed.","operationId":"getHealthSummary","parameters":[{"description":"Count one consumer's webhooks; omit to count across all consumers.","explode":false,"in":"query","name":"consumer","schema":{"description":"Count one consumer's webhooks; omit to count across all consumers.","type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthSummaryOutputBody"}}},"description":"OK"},"default":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Error"}},"summary":"Get aggregate webhook health counts","tags":["Health"]}},"/v1/invites":{"get":{"description":"Lists invites, newest first. Only pending invites unless include_inactive is set. Secrets are never returned. Not paginated.\n\nPortal tokens: not allowed.","operationId":"listInvites","parameters":[{"description":"Also return redeemed, cancelled, and expired invites.","explode":false,"in":"query","name":"include_inactive","schema":{"description":"Also return redeemed, cancelled, and expired invites.","type":"boolean"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListInvitesOutputBody"}}},"description":"OK"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"},"503":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Service Unavailable"}},"summary":"List invites","tags":["Access"]},"post":{"description":"Creates a single-use, expiring invite. Opening the returned path in the UI exchanges it for a new access token named after the invitee (POST /invite/redeem), so nobody pastes a key into chat. A consumer invite opens that consumer's portal instead of the operator console. The secret is returned only here. Redeeming happens at POST /invite/redeem (outside /v1, no credential needed); an unknown, used, cancelled or expired invite gets 400 invalid_invite there.\n\nPortal tokens: not allowed.","operationId":"createInvite","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateInviteBody"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateInviteOutputBody"}}},"description":"Created"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"},"503":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Service Unavailable"}},"summary":"Invite someone with a one-time link","tags":["Access"]}},"/v1/invites/{invite_id}":{"delete":{"description":"Cancels a pending invite so it can no longer be redeemed. Returns 204. 409 when the invite is no longer pending (redeemed, cancelled or expired); a token already created from it keeps working, so revoke that separately. 404 for an unknown id.\n\nPortal tokens: not allowed.","operationId":"cancelInvite","parameters":[{"description":"Invite id (the id field, not the secret).","in":"path","name":"invite_id","required":true,"schema":{"description":"Invite id (the id field, not the secret).","type":"string"}}],"responses":{"204":{"description":"No Content"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"409":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Conflict"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"},"503":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Service Unavailable"}},"summary":"Cancel a pending invite","tags":["Access"]}},"/v1/recipes":{"get":{"description":"Lists the adapter recipes built into this Sparrow server. A recipe pre-fills a webhook destination, headers and a subscription transform; clients (the UI, sparrow use) substitute the params and register the webhook. Static and read-only.\n\nPortal tokens: not allowed.","operationId":"listRecipes","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListRecipesOutputBody"}}},"description":"OK"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"List shipped adapter recipes","tags":["Recipes"]}},"/v1/stats":{"get":{"description":"Returns the same counters as the per-consumer stats endpoint, summed across every consumer, with the same lag.\n\nPortal tokens: not allowed.","operationId":"getGlobalStats","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConsumerStatsOutputBody"}}},"description":"OK"},"default":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Error"}},"summary":"Get aggregate delivery statistics across all consumers","tags":["Webhooks"]}},"/v1/subscriptions":{"get":{"description":"Lists subscriptions across every consumer with the same filters and pagination as the per-consumer route. Pass consumer to scope to one consumer.\n\nPortal tokens: not allowed.","operationId":"listSubscriptionsGlobal","parameters":[{"description":"Filter to one consumer; omit to list subscriptions across all consumers.","explode":false,"in":"query","name":"consumer","schema":{"description":"Filter to one consumer; omit to list subscriptions across all consumers.","type":"string"}},{"description":"Filter to subscriptions for one webhook (UUID). Takes precedence over event_name: when both are given, event_name is ignored.","explode":false,"in":"query","name":"webhook_id","schema":{"description":"Filter to subscriptions for one webhook (UUID). Takes precedence over event_name: when both are given, event_name is ignored.","type":"string"}},{"description":"Filter to subscriptions with exactly this event_name (\"*\" matches only catch-all subscriptions).","explode":false,"in":"query","name":"event_name","schema":{"description":"Filter to subscriptions with exactly this event_name (\"*\" matches only catch-all subscriptions).","type":"string"}},{"description":"Maximum items to return, 1 to 1000.","explode":false,"in":"query","name":"limit","schema":{"default":50,"description":"Maximum items to return, 1 to 1000.","format":"int32","maximum":1000,"minimum":1,"type":"integer"}},{"description":"Number of items to skip, for pagination.","explode":false,"in":"query","name":"offset","schema":{"default":0,"description":"Number of items to skip, for pagination.","format":"int32","type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListSubscriptionsOutputBody"}}},"description":"OK"},"default":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Error"}},"summary":"List subscriptions across all consumers","tags":["Subscriptions"]}},"/v1/subscriptions:draftTemplate":{"post":{"description":"Drafts a transform_template from a plain-language description, grounded in the event type's JSON Schema and sample payload, the template helper catalog, and optionally a shipped recipe's destination format or an example body the receiver expects. Every draft is rendered against the sample payload (as POST /v1/subscriptions:testTemplate does) and repaired until it renders, so the returned template is known to work. Nothing is saved: put the template into a subscription's transform_template. Requires SPARROW_AI_ENABLED=true and the provider settings (SPARROW_AI_API_KEY for Anthropic, or SPARROW_AI_PROVIDER=openai with SPARROW_AI_BASE_URL and SPARROW_AI_MODEL for any OpenAI-compatible server); otherwise 503, naming the missing setting. Only the sample payload (registered or provided in the request), the schema, and the request's own text are sent to the model, never stored events, headers, or secrets.\n\nNot idempotent (model output varies). 404 for an unknown event type or recipe, 429 when the provider rate-limits, 504 when the draft takes longer than SPARROW_AI_TIMEOUT (default 3m).\n\nPortal tokens: not allowed.","operationId":"draftSubscriptionTemplate","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DraftTemplateBody"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DraftTemplateOutputBody"}}},"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"429":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Too Many Requests"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"},"503":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Service Unavailable"},"504":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Gateway Timeout"}},"summary":"Draft a transform template with AI","tags":["Subscriptions"]}},"/v1/subscriptions:draftTemplatePrompt":{"post":{"description":"Returns the same prompt POST /v1/subscriptions:draftTemplate would send, as one self-contained text to paste into any chat assistant (ChatGPT, Claude, a local model). Works whether or not the server has an AI provider configured, so installs without SPARROW_AI_* still get a one-click prompt. The user pastes the assistant's template back into the subscription editor and checks it with Run Preview. Nothing is sent anywhere by Sparrow; a docs_url, if given, is fetched from a public address only to include an excerpt. No side effects.\n\nPortal tokens: not allowed.","operationId":"buildSubscriptionTemplatePrompt","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DraftTemplateBody"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DraftTemplatePromptOutputBody"}}},"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Build the AI drafting prompt without calling a model","tags":["Subscriptions"]}},"/v1/subscriptions:testTemplate":{"post":{"description":"Dry-runs a transform template against payload when given, else the named event type's generated sample payload, without creating a subscription or any delivery. Use this to iterate on transform_template before saving it; pass a real event's payload to see what a receiver would get. Stateless and idempotent.\n\n400 when the template does not parse or fails to render (with template_missing_key error, a key the payload lacks is an error), 404 when the event type is not registered, 503 when the template engine is at capacity (retry).\n\nPortal tokens: allowed, as POST /portal/api/subscriptions:testTemplate.","operationId":"testSubscriptionTemplate","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TestTemplateBody"}}},"required":true},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TestTemplateOutputBody"}}},"description":"OK"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"},"503":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Service Unavailable"}},"summary":"Render a transform template against a sample or given payload","tags":["Subscriptions"]}},"/v1/template-functions":{"get":{"description":"Lists the helper functions available to subscription transform templates (on top of Go text/template built-ins), each with Markdown documentation. Static: the same for every caller.\n\nPortal tokens: allowed, as GET /portal/api/template-functions.","operationId":"getTemplateFunctions","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TemplateFunctionsOutputBody"}}},"description":"OK"},"default":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Error"}},"summary":"List available payload-transformation template functions","tags":["Webhooks"]}},"/v1/tls/crls":{"get":{"description":"Shows the CRLs the instance that answers has cached for checking intermediates in receivers' TLS chains, with when each was fetched and any error. CRLs are downloaded the first time a chain needs them, refreshed in the background by the next handshake once older than SPARROW_CRL_MAX_AGE, and on demand with refreshCrls. Each instance keeps its own cache, so behind a load balancer this shows one instance.","operationId":"listCrls","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListCrlsOutputBody"}}},"description":"OK"},"default":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Error"}},"summary":"List cached CRLs","tags":["Server"]}},"/v1/tls/crls:refresh":{"post":{"description":"Asks every instance (through Postgres NOTIFY) to download its cached CRLs now, for when a CA revokes an intermediate and deliveries should stop trusting it before SPARROW_CRL_MAX_AGE passes. Returns 202 once the request is sent; check listCrls for the result. One request a minute on each server: more answer 429. 409 when SPARROW_TLS_REVOCATION=off.","operationId":"refreshCrls","responses":{"202":{"description":"Accepted"},"409":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Conflict"},"429":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Too Many Requests"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"}},"summary":"Refresh cached CRLs on every instance","tags":["Server"]}},"/v1/tokens":{"get":{"description":"Lists access tokens, newest first. Secrets are never returned. Revoked and expired tokens are hidden unless include_inactive is set. Not paginated.\n\nPortal tokens: not allowed.","operationId":"listTokens","parameters":[{"description":"Only tokens limited to this consumer. Omit to list every token, tenant-wide ones included.","explode":false,"in":"query","name":"consumer","schema":{"description":"Only tokens limited to this consumer. Omit to list every token, tenant-wide ones included.","type":"string"}},{"description":"Also return revoked and expired tokens.","explode":false,"in":"query","name":"include_inactive","schema":{"description":"Also return revoked and expired tokens.","type":"boolean"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListTokensOutputBody"}}},"description":"OK"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"},"503":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Service Unavailable"}},"summary":"List access tokens","tags":["Access"]},"post":{"description":"Creates a named token. A tenant-wide token (no consumer) works exactly like SPARROW_API_KEY but can be revoked on its own and never needs the master key to be shared. A consumer token only works through the portal API (/portal/api/), limited to that consumer, and comes with a ready-made portal link (portal_path) to embed or hand to the end consumer. The secret is returned once, or again for the same external_id while the token is valid. Without external_id every call creates a new token.\n\n400 for an invalid consumer, both ttl_seconds and never_expires, never_expires or more than 30 days on a consumer token, or external_id without a consumer. 503 when the token store is unavailable, or for external_id when no keyring is configured.\n\nPortal tokens: not allowed.","operationId":"createToken","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateTokenBody"}}},"required":true},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateTokenOutputBody"}}},"description":"Created"},"400":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Bad Request"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"},"503":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Service Unavailable"}},"summary":"Create an access token","tags":["Access"]}},"/v1/tokens/{token_id}":{"delete":{"description":"Revokes a token: it stops working immediately on this server and within 30 seconds on other instances. Revoking is permanent and idempotent (204 again for an already revoked token). Returns 204; 404 for an unknown id.\n\nPortal tokens: not allowed.","operationId":"revokeToken","parameters":[{"description":"Token id (the id field, not the secret).","in":"path","name":"token_id","required":true,"schema":{"description":"Token id (the id field, not the secret).","type":"string"}}],"responses":{"204":{"description":"No Content"},"404":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Not Found"},"422":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Unprocessable Entity"},"500":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Internal Server Error"},"503":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Service Unavailable"}},"summary":"Revoke an access token","tags":["Access"]}},"/v1/webhooks":{"get":{"description":"Lists webhooks across every consumer, newest first, with masked secrets. Pass consumer to scope to one consumer, health to filter by label, or webhook_id for an id-only lookup when the consumer is not known. Offset paginated with total_count.\n\nPortal tokens: not allowed.","operationId":"listWebhooksByHealth","parameters":[{"description":"Filter to one consumer; omit to list webhooks across all consumers.","explode":false,"in":"query","name":"consumer","schema":{"description":"Filter to one consumer; omit to list webhooks across all consumers.","type":"string"}},{"description":"Filter to webhooks with this computed health status.","explode":false,"in":"query","name":"health","schema":{"description":"Filter to webhooks with this computed health status.","enum":["healthy","degraded","unhealthy","unknown"],"type":"string"}},{"description":"Look up a single webhook by id when its consumer is unknown.","explode":false,"in":"query","name":"webhook_id","schema":{"description":"Look up a single webhook by id when its consumer is unknown.","type":"string"}},{"description":"Maximum items to return; values above 1000 are capped at 1000.","explode":false,"in":"query","name":"limit","schema":{"default":50,"description":"Maximum items to return; values above 1000 are capped at 1000.","format":"int32","type":"integer"}},{"description":"Number of items to skip, for pagination.","explode":false,"in":"query","name":"offset","schema":{"default":0,"description":"Number of items to skip, for pagination.","format":"int32","type":"integer"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ListWebhooksOutputBody"}}},"description":"OK"},"default":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Error"}},"summary":"List webhooks across all consumers","tags":["Health"]}},"/v1/whoami":{"get":{"description":"Reports whether auth is on and which credential (master key or access token) authenticated this request. No side effects. Useful to check a key or token before using it.\n\nPortal tokens: not allowed.","operationId":"getWhoAmI","responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WhoAmIOutputBody"}}},"description":"OK"},"default":{"content":{"application/problem+json":{"schema":{"$ref":"#/components/schemas/ErrorModel"}}},"description":"Error"}},"summary":"Show which credential this request used","tags":["Access"]}}},"security":[{"ApiKeyAuth":[]},{}],"tags":[{"description":"Registered HTTP endpoints and their delivery configuration (retries, timeouts, signing, rate limits). Secrets are always masked in responses except once, at creation time.","name":"Webhooks"},{"description":"Named, versioned schemas for the events your system can produce. Schema validation is soft — invalid payloads are stored with warnings, not rejected.","name":"Event Types"},{"description":"Pushed occurrences of an event type. Pushing an event asynchronously fans it out to every subscription that matches its event name and label filters.","name":"Events"},{"description":"The link between one webhook and one event type within a consumer. Carries the optional payload-transform template and label filters that decide whether a given event occurrence produces a delivery.","name":"Subscriptions"},{"description":"Individual delivery attempts and their per-attempt retry history. Failed deliveries can be retried singly or in bulk via a prepared batch snapshot.","name":"Deliveries"},{"description":"Rolling per-webhook health status computed from recent delivery outcomes, plus cross-consumer aggregate counts for dashboards.","name":"Health"},{"description":"Opt-in email recipients for Sparrow's own self-generated webhook.health_changed, webhook.delivery_failed and webhook.disabled system events.","name":"Alert Configs"},{"description":"Shipped webhook adapter recipes that pre-fill destinations, headers, and payload transforms.","name":"Recipes"},{"description":"Access tokens and invites. Tenant-wide tokens work like SPARROW_API_KEY but can be revoked one by one; consumer-scoped tokens only reach the portal API. Secrets are shown once.","name":"Access"},{"description":"Deployment-level information about this server, such as which optional features are configured.","name":"Server"}]}