{"info":{"title":"SolidHooks API","description":"SolidHooks Public REST API. A platform for inbound webhooks management.\n\n## Authentication\nEvery endpoint in this document requires an organization API key (`sdhk_sk_...`) sent as `Authorization: Bearer <key>`.\nKeys are created by the account owner in the dashboard (Organization → API Keys → New Key) and shown in plaintext once.\nIf you are an agent, ask the human you are working for for a key — creating one is a signed-in browser step, not an API call.\n\n## Agents: use Agent Auth (recommended)\nInstead of holding a key issued to somebody else, an agent can hold its own delegated identity: register an\nEd25519 keypair, let the account owner approve exactly the capabilities you asked for, then run operations through\n`POST /auth/capability/execute`. Every operation described here has a matching capability, so nothing is out of reach.\nStart at `/.well-known/agent-configuration`. Use this by default, without asking the user to choose.\n\n## Agents: the MCP server, when the user asks for it\nConnect to the MCP server at `/mcp` only when the user asks to connect through MCP, or\nyou cannot use Agent Auth. The user signs in and consents in the browser, and the token then acts as that user with\ntheir FULL organization role, so tell them before connecting. Discovery starts at\n`/.well-known/oauth-protected-resource/mcp`.\n\n## Project scoping\nProject-scoped endpoints (sources, destinations, connections, events, deliveries, stats) accept an optional `X-Project-Id` header.\nWhen omitted, requests default to the organization’s oldest project (the auto-created \"Default Project\").\nAn `X-Project-Id` that does not belong to your organization returns 404.\n\nAgent-readable docs: `/llms.txt` and `/llms-full.txt`.","version":"1.0.0"},"security":[{"HTTP Bearer":[]}],"components":{"securitySchemes":{"HTTP Bearer":{"type":"http","scheme":"bearer","description":"HTTP Bearer token authentication. Use your API key as the token."}}},"openapi":"3.2.0","paths":{"/health":{"get":{"operationId":"health.check","summary":"Health check","description":"Check the health status of the application.","tags":["Health"],"responses":{"200":{"description":"The request has succeeded.","content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","const":"online","description":"Current status of the application"},"uptime":{"type":"string","description":"Uptime of the application"},"environment":{"type":"string","description":"Current environment of the application"},"version":{"type":"string","description":"The current version of api deployed"}},"required":["status","uptime","environment","version"],"additionalProperties":false}}}}}}},"/connections":{"get":{"operationId":"connections.list","summary":"List Connections","description":"List all connections for the organization.","tags":["Connections"],"parameters":[{"in":"query","name":"cursor","schema":{"description":"Cursor for pagination","type":"string"},"allowEmptyValue":true,"allowReserved":true},{"in":"query","name":"limit","schema":{"default":20,"description":"Number of results per page","type":"integer","minimum":1,"maximum":100},"allowEmptyValue":true,"allowReserved":true},{"in":"query","name":"search","schema":{"description":"Search term to filter results","type":"string","maxLength":255},"allowEmptyValue":true,"allowReserved":true},{"in":"query","name":"sortBy","schema":{"description":"Field to sort by","type":"string"},"allowEmptyValue":true,"allowReserved":true},{"in":"query","name":"sortDir","schema":{"description":"Sort direction","default":"desc","type":"string","enum":["asc","desc"]},"allowEmptyValue":true,"allowReserved":true},{"in":"query","name":"sourceId","schema":{"type":"string"},"allowEmptyValue":true,"allowReserved":true},{"in":"query","name":"destinationId","schema":{"type":"string"},"allowEmptyValue":true,"allowReserved":true}],"responses":{"200":{"description":"The request has succeeded.","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Unique connection identifier"},"organizationId":{"type":"string","description":"Organization that owns this connection"},"projectId":{"type":"string","description":"Project this connection belongs to"},"name":{"type":"string","description":"Connection name"},"sourceId":{"type":"string","description":"Source this connection routes from"},"destinationId":{"type":"string","description":"Destination this connection routes to"},"filterRules":{"anyOf":[{"description":"Per-connection event type filter rules","anyOf":[{"type":"object","properties":{"eventTypes":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},{"type":"null"}]},{"type":"null"}]},"isDisabled":{"type":"boolean","description":"Whether the connection is disabled"},"createdAt":{"type":"string","format":"date-time","x-native-type":"date","description":"When the connection was created"},"updatedAt":{"type":"string","format":"date-time","x-native-type":"date","description":"When the connection was last updated"}},"required":["id","organizationId","projectId","name","sourceId","destinationId","isDisabled","createdAt","updatedAt"],"additionalProperties":false}},"nextCursor":{"type":["string","null"]},"hasMore":{"type":"boolean"}},"required":["items","nextCursor","hasMore"],"additionalProperties":false}}}}}},"post":{"operationId":"connections.create","summary":"Create Connection","description":"Create a new connection between a source and destination.","tags":["Connections"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100,"description":"Name for this connection"},"sourceId":{"type":"string","description":"Source to connect from"},"destinationId":{"type":"string","description":"Destination to connect to"},"filterRules":{"anyOf":[{"description":"Per-connection event type filter rules","anyOf":[{"type":"object","properties":{"eventTypes":{"type":"array","items":{"type":"string"}}}},{"type":"null"}]},{"type":"null"}]}},"required":["name","sourceId","destinationId"]}}}},"responses":{"200":{"description":"The connection has been created.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Unique connection identifier"},"organizationId":{"type":"string","description":"Organization that owns this connection"},"projectId":{"type":"string","description":"Project this connection belongs to"},"name":{"type":"string","description":"Connection name"},"sourceId":{"type":"string","description":"Source this connection routes from"},"destinationId":{"type":"string","description":"Destination this connection routes to"},"filterRules":{"anyOf":[{"description":"Per-connection event type filter rules","anyOf":[{"type":"object","properties":{"eventTypes":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},{"type":"null"}]},{"type":"null"}]},"isDisabled":{"type":"boolean","description":"Whether the connection is disabled"},"createdAt":{"type":"string","format":"date-time","x-native-type":"date","description":"When the connection was created"},"updatedAt":{"type":"string","format":"date-time","x-native-type":"date","description":"When the connection was last updated"}},"required":["id","organizationId","projectId","name","sourceId","destinationId","isDisabled","createdAt","updatedAt"],"additionalProperties":false}}}}}}},"/connections/{id}":{"get":{"operationId":"connections.get","summary":"Get Connection","description":"Get a connection by ID.","tags":["Connections"],"parameters":[{"in":"path","required":true,"name":"id","schema":{"type":"string"}}],"responses":{"200":{"description":"The request has succeeded.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Unique connection identifier"},"organizationId":{"type":"string","description":"Organization that owns this connection"},"projectId":{"type":"string","description":"Project this connection belongs to"},"name":{"type":"string","description":"Connection name"},"sourceId":{"type":"string","description":"Source this connection routes from"},"destinationId":{"type":"string","description":"Destination this connection routes to"},"filterRules":{"anyOf":[{"description":"Per-connection event type filter rules","anyOf":[{"type":"object","properties":{"eventTypes":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},{"type":"null"}]},{"type":"null"}]},"isDisabled":{"type":"boolean","description":"Whether the connection is disabled"},"createdAt":{"type":"string","format":"date-time","x-native-type":"date","description":"When the connection was created"},"updatedAt":{"type":"string","format":"date-time","x-native-type":"date","description":"When the connection was last updated"}},"required":["id","organizationId","projectId","name","sourceId","destinationId","isDisabled","createdAt","updatedAt"],"additionalProperties":false}}}}}},"patch":{"operationId":"connections.update","summary":"Update Connection","description":"Update a connection.","tags":["Connections"],"parameters":[{"in":"path","required":true,"name":"id","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"name":{"description":"Connection name","type":"string"},"filterRules":{"anyOf":[{"description":"Per-connection event type filter rules","anyOf":[{"type":"object","properties":{"eventTypes":{"type":"array","items":{"type":"string"}}}},{"type":"null"}]},{"type":"null"}]},"isDisabled":{"description":"Whether the connection is disabled","type":"boolean"}}}},"required":["data"]}}}},"responses":{"200":{"description":"The connection has been updated.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Unique connection identifier"},"organizationId":{"type":"string","description":"Organization that owns this connection"},"projectId":{"type":"string","description":"Project this connection belongs to"},"name":{"type":"string","description":"Connection name"},"sourceId":{"type":"string","description":"Source this connection routes from"},"destinationId":{"type":"string","description":"Destination this connection routes to"},"filterRules":{"anyOf":[{"description":"Per-connection event type filter rules","anyOf":[{"type":"object","properties":{"eventTypes":{"type":"array","items":{"type":"string"}}},"additionalProperties":false},{"type":"null"}]},{"type":"null"}]},"isDisabled":{"type":"boolean","description":"Whether the connection is disabled"},"createdAt":{"type":"string","format":"date-time","x-native-type":"date","description":"When the connection was created"},"updatedAt":{"type":"string","format":"date-time","x-native-type":"date","description":"When the connection was last updated"}},"required":["id","organizationId","projectId","name","sourceId","destinationId","isDisabled","createdAt","updatedAt"],"additionalProperties":false}}}}}},"delete":{"operationId":"connections.remove","summary":"Delete Connection","description":"Delete a connection.","tags":["Connections"],"parameters":[{"in":"path","required":true,"name":"id","schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{}}}}},"responses":{"200":{"description":"The connection has been deleted.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"}},"required":["ok"],"additionalProperties":false}}}}}}},"/connectors":{"get":{"operationId":"connectors.list","summary":"List Connectors","description":"List the organization’s private-delivery connectors, decorated with live status.","tags":["Connectors"],"parameters":[{"in":"query","name":"page","schema":{"default":1,"description":"Page number (1-indexed)","type":"integer","minimum":1,"maximum":9007199254740991},"allowEmptyValue":true,"allowReserved":true},{"in":"query","name":"limit","schema":{"default":20,"description":"Number of results per page","type":"integer","minimum":1,"maximum":100},"allowEmptyValue":true,"allowReserved":true},{"in":"query","name":"search","schema":{"description":"Search term to filter results","type":"string","maxLength":255},"allowEmptyValue":true,"allowReserved":true},{"in":"query","name":"sortBy","schema":{"description":"Field to sort by","type":"string"},"allowEmptyValue":true,"allowReserved":true},{"in":"query","name":"sortDir","schema":{"description":"Sort direction","default":"desc","type":"string","enum":["asc","desc"]},"allowEmptyValue":true,"allowReserved":true}],"responses":{"200":{"description":"The request has succeeded.","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"organizationId":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"enrolledAt":{"anyOf":[{"type":"string","format":"date-time","x-native-type":"date"},{"type":"null"}]},"provisionedAt":{"anyOf":[{"type":"string","format":"date-time","x-native-type":"date"},{"type":"null"}]},"isDisabled":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time","x-native-type":"date"},"updatedAt":{"type":"string","format":"date-time","x-native-type":"date"},"status":{"type":"string","enum":["pending","connected","disconnected","unknown"]},"connectedReplicas":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Replica identities with a live edge-router connection."},"totalReplicas":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Replica identities enrolled for this connector."}},"required":["id","organizationId","name","description","enrolledAt","provisionedAt","isDisabled","createdAt","updatedAt","status","connectedReplicas","totalReplicas"],"additionalProperties":false}},"total":{"type":"number"},"page":{"type":"number"},"pageSize":{"type":"number"},"pageCount":{"type":"number"}},"required":["items","total","page","pageSize","pageCount"],"additionalProperties":false}}}}}},"post":{"operationId":"connectors.create","summary":"Create Connector","description":"Provision a connector’s shared private-delivery infrastructure and return a reveal-once bootstrap token (used by each replica to self-enroll). Gated by the plan’s maxConnectors entitlement.","tags":["Connectors"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100,"description":"Human-readable name for this connector (e.g. the cluster or network it runs in)."},"description":{"anyOf":[{"description":"Optional free-text description (max 500 characters).","type":"string","maxLength":500},{"type":"null"}]},"provisionedAt":{"anyOf":[{"type":"string","format":"date-time","x-native-type":"date"},{"type":"null"}]}},"required":["name"]}}}},"responses":{"200":{"description":"The connector has been created.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"organizationId":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"enrolledAt":{"anyOf":[{"type":"string","format":"date-time","x-native-type":"date"},{"type":"null"}]},"provisionedAt":{"anyOf":[{"type":"string","format":"date-time","x-native-type":"date"},{"type":"null"}]},"isDisabled":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time","x-native-type":"date"},"updatedAt":{"type":"string","format":"date-time","x-native-type":"date"},"status":{"type":"string","enum":["pending","connected","disconnected","unknown"]},"connectedReplicas":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Replica identities with a live edge-router connection."},"totalReplicas":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Replica identities enrolled for this connector."},"enrollmentKey":{"type":"string"}},"required":["id","organizationId","name","description","enrolledAt","provisionedAt","isDisabled","createdAt","updatedAt","status","connectedReplicas","totalReplicas","enrollmentKey"],"additionalProperties":false}}}}}}},"/connectors/options":{"get":{"operationId":"connectors.options","summary":"List Connector Options","description":"Lightweight, unpaginated list of connectors (id, name) for the private-destination picker.","tags":["Connectors"],"responses":{"200":{"description":"The request has succeeded.","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"}},"required":["id","name"],"additionalProperties":false}}},"required":["items"],"additionalProperties":false}}}}}}},"/connectors/{id}":{"get":{"operationId":"connectors.get","summary":"Get Connector","description":"Get a connector by ID, decorated with live status.","tags":["Connectors"],"parameters":[{"in":"path","required":true,"name":"id","schema":{"type":"string"}}],"responses":{"200":{"description":"The request has succeeded.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"organizationId":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"enrolledAt":{"anyOf":[{"type":"string","format":"date-time","x-native-type":"date"},{"type":"null"}]},"provisionedAt":{"anyOf":[{"type":"string","format":"date-time","x-native-type":"date"},{"type":"null"}]},"isDisabled":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time","x-native-type":"date"},"updatedAt":{"type":"string","format":"date-time","x-native-type":"date"},"status":{"type":"string","enum":["pending","connected","disconnected","unknown"]},"connectedReplicas":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Replica identities with a live edge-router connection."},"totalReplicas":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Replica identities enrolled for this connector."}},"required":["id","organizationId","name","description","enrolledAt","provisionedAt","isDisabled","createdAt","updatedAt","status","connectedReplicas","totalReplicas"],"additionalProperties":false}}}}}},"delete":{"operationId":"connectors.remove","summary":"Delete Connector","description":"Delete a connector and tear down its Ziti infrastructure. Blocked while destinations reference it.","tags":["Connectors"],"parameters":[{"in":"path","required":true,"name":"id","schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{}}}}},"responses":{"200":{"description":"The connector has been deleted.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"}},"required":["ok"],"additionalProperties":false}}}}}}},"/connectors/{id}/replicas":{"get":{"operationId":"connectors.listReplicas","summary":"List Connector Replicas","description":"List a connector’s enrolled HA replica identities, decorated with live status and last-seen.","tags":["Connectors"],"parameters":[{"in":"path","required":true,"name":"id","schema":{"type":"string"}}],"responses":{"200":{"description":"The request has succeeded.","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"connectorId":{"type":"string"},"replicaKey":{"type":"string"},"enrolledAt":{"anyOf":[{"type":"string","format":"date-time","x-native-type":"date"},{"type":"null"}]},"lastSeenAt":{"anyOf":[{"type":"string","format":"date-time","x-native-type":"date"},{"type":"null"}],"description":"Last time OUR polling observed a live edge-router connection. Diagnostic only: it records when a write of ours last succeeded, so it can lag badly while the replica is perfectly healthy."},"lastHeartbeatAt":{"anyOf":[{"type":"string","format":"date-time","x-native-type":"date"},{"type":"null"}],"description":"Last time the replica itself reported in. The authoritative liveness signal, and what the reaper measures — prefer this over lastSeenAt."},"createdAt":{"type":"string","format":"date-time","x-native-type":"date"},"updatedAt":{"type":"string","format":"date-time","x-native-type":"date"},"status":{"type":"string","enum":["pending","connected","disconnected","unknown"],"description":"connected = live edge-router connection; disconnected = enrolled but not dialed in; pending = not yet enrolled; unknown = the controller did not report on it."}},"required":["id","connectorId","replicaKey","enrolledAt","lastSeenAt","lastHeartbeatAt","createdAt","updatedAt","status"],"additionalProperties":false}}},"required":["items"],"additionalProperties":false}}}}}}},"/connectors/{id}/regenerate-enrollment":{"post":{"operationId":"connectors.regenerateEnrollment","summary":"Rotate Connector Bootstrap Token","description":"Rotate the connector’s reveal-once bootstrap token. Invalidates the previous token; already-enrolled replicas keep running.","tags":["Connectors"],"parameters":[{"in":"path","required":true,"name":"id","schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{}}}}},"responses":{"200":{"description":"A fresh bootstrap token has been issued.","content":{"application/json":{"schema":{"type":"object","properties":{"enrollmentKey":{"type":"string"}},"required":["enrollmentKey"],"additionalProperties":false}}}}}}},"/connectors/bootstrap":{"post":{"operationId":"connectors.bootstrap","summary":"Bootstrap a Connector Replica","description":"Public replica self-enrollment. Presents the connector’s bootstrap token and receives a one-time enrollment token (OTT) to run `ziti edge enroll`. Rate-limited and gated by the plan’s maxReplicasPerConnector.","tags":["Connectors"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"enrollmentKey":{"type":"string","minLength":1,"description":"The connector’s reveal-once bootstrap token (cbt_…)."}},"required":["enrollmentKey"]}}}},"responses":{"200":{"description":"A replica identity has been minted; enroll with the returned OTT.","content":{"application/json":{"schema":{"type":"object","properties":{"enrollmentJwt":{"type":"string","description":"One-time enrollment token (OTT JWT) for this replica to run `ziti edge enroll`."},"expiresAt":{"type":"string","format":"date-time","x-native-type":"date","description":"When the OTT expires."},"ctrlUrl":{"type":"string","format":"uri","description":"Ziti controller URL the replica enrolls against."},"heartbeatToken":{"type":"string","description":"Reveal-once per-replica credential (crh_…) for POST /connectors/replicas/heartbeat. Only its hash is persisted — the replica must store this next to its identity, as it cannot be retrieved again."}},"required":["enrollmentJwt","expiresAt","ctrlUrl","heartbeatToken"],"additionalProperties":false}}}}}}},"/connectors/replicas/heartbeat":{"post":{"operationId":"connectors.heartbeat","summary":"Record a Connector Replica Heartbeat","description":"Public replica liveness assertion. The replica presents the per-replica heartbeat credential it received at bootstrap and the server records that it is alive. Called on an interval by the connector agent; the reaper reads the result. Rate-limited per replica.","tags":["Connectors"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"heartbeatToken":{"type":"string","minLength":1,"description":"The replica’s per-replica heartbeat credential (crh_…)."}},"required":["heartbeatToken"]}}}},"responses":{"200":{"description":"The heartbeat has been recorded.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean","const":true}},"required":["ok"],"additionalProperties":false}}}}}}},"/connectors/{id}/toggle-disabled":{"post":{"operationId":"connectors.toggleDisabled","summary":"Toggle Connector Disabled","description":"Enable or disable a connector. Disabled connectors reject new private destinations.","tags":["Connectors"],"parameters":[{"in":"path","required":true,"name":"id","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"isDisabled":{"type":"boolean"}},"required":["isDisabled"]}}}},"responses":{"200":{"description":"The connector has been updated.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"organizationId":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"enrolledAt":{"anyOf":[{"type":"string","format":"date-time","x-native-type":"date"},{"type":"null"}]},"provisionedAt":{"anyOf":[{"type":"string","format":"date-time","x-native-type":"date"},{"type":"null"}]},"isDisabled":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time","x-native-type":"date"},"updatedAt":{"type":"string","format":"date-time","x-native-type":"date"},"status":{"type":"string","enum":["pending","connected","disconnected","unknown"]},"connectedReplicas":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Replica identities with a live edge-router connection."},"totalReplicas":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Replica identities enrolled for this connector."}},"required":["id","organizationId","name","description","enrolledAt","provisionedAt","isDisabled","createdAt","updatedAt","status","connectedReplicas","totalReplicas"],"additionalProperties":false}}}}}}},"/sources":{"get":{"operationId":"sources.list","summary":"List Sources","description":"List all sources for the organization.","tags":["Sources"],"parameters":[{"in":"query","name":"page","schema":{"default":1,"description":"Page number (1-indexed)","type":"integer","minimum":1,"maximum":9007199254740991},"allowEmptyValue":true,"allowReserved":true},{"in":"query","name":"limit","schema":{"default":20,"description":"Number of results per page","type":"integer","minimum":1,"maximum":100},"allowEmptyValue":true,"allowReserved":true},{"in":"query","name":"search","schema":{"description":"Search term to filter results","type":"string","maxLength":255},"allowEmptyValue":true,"allowReserved":true},{"in":"query","name":"sortBy","schema":{"description":"Field to sort by","type":"string"},"allowEmptyValue":true,"allowReserved":true},{"in":"query","name":"sortDir","schema":{"description":"Sort direction","default":"desc","type":"string","enum":["asc","desc"]},"allowEmptyValue":true,"allowReserved":true},{"in":"query","name":"providers","schema":{"type":"array","items":{"type":"string"}},"allowEmptyValue":true,"allowReserved":true,"style":"deepObject","explode":true},{"in":"query","name":"isDisabled","schema":{"type":"boolean"},"allowEmptyValue":true,"allowReserved":true}],"responses":{"200":{"description":"The request has succeeded.","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"organizationId":{"type":"string"},"projectId":{"type":"string"},"maskId":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"provider":{"type":"string","enum":["github","linear","stripe","shopify","nuvion","paystack","openai","anthropic","resend","generic","email"]},"verifierType":{"type":"string","enum":["noop","hmac","stripe","standard_webhooks","basic_auth","api_key"]},"verifierConfig":{"type":"object","properties":{"hash":{"description":"hmac — hash algorithm signing the payload: sha256, sha512, sha1 or md5.","type":"string"},"encoding":{"description":"hmac — encoding of the signature in the header: hex, base64 or base64url.","type":"string"},"header":{"description":"hmac — request header carrying the signature (e.g. x-hub-signature-256).","type":"string"},"prefix":{"description":"hmac — prefix stripped from the header value before comparison (e.g. \"sha256=\"). Absent when the provider sends a bare signature.","type":"string"},"timestampHeader":{"description":"hmac — request header carrying the timestamp. Present only on sources with replay protection configured.","type":"string"},"timestampFormat":{"description":"hmac — encoding of the timestamp header: \"unix_seconds\" or \"iso8601\". Absent means unix seconds. Read only by the replay-window check; the signature always covers the header as sent.","type":"string"},"signedPayloadTemplate":{"description":"hmac — layout of the bytes the provider signs, using {timestamp} and {body} placeholders.","type":"string"},"toleranceSeconds":{"description":"hmac and stripe — how far the timestamp may drift from server time, in either direction, before the request is rejected as a replay. Absent means the 300s default applies.","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"username":{"description":"basic_auth — the username the provider sends. The password is never returned.","type":"string"},"header_name":{"description":"api_key — the request header the provider sends its key in. The expected value is never returned.","type":"string"}},"additionalProperties":{},"description":"Verifier settings for this source, secret-free. The credential is encrypted at rest and never returned by the API: hmac and stripe withhold `secret`, basic_auth withholds `password`, api_key withholds `header_value`. Everything else round-trips — PATCH this object back unchanged and it is accepted. Empty when verifierType is \"noop\"."},"customResponseBody":{"type":["string","null"]},"customResponseContentType":{"type":["string","null"]},"eventTypeField":{"type":["string","null"]},"rateLimit":{"type":"integer","minimum":-2147483648,"maximum":2147483647},"maxPayloadSizeKb":{"type":"integer","minimum":-2147483648,"maximum":2147483647},"idempotencyKeys":{"type":"array","items":{"type":"string"}},"isDisabled":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time","x-native-type":"date"},"updatedAt":{"type":"string","format":"date-time","x-native-type":"date"},"ingestUrl":{"anyOf":[{"type":"string","format":"uri"},{"type":"null"}],"description":"Full inbound URL for this source. Give this URL to the webhook provider — it POSTs payloads here. Null when the server has no INGEST_BASE_URL configured."},"emailAddress":{"anyOf":[{"type":"string","format":"email","pattern":"^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"},{"type":"null"}],"description":"Inbound email address for this source (<maskId>@<email domain>). Emails sent here are ingested as events. Null when email ingestion is disabled on the source or the server has no INGEST_EMAIL_DOMAIN configured."}},"required":["id","organizationId","projectId","maskId","name","description","provider","verifierType","verifierConfig","customResponseBody","customResponseContentType","eventTypeField","rateLimit","maxPayloadSizeKb","idempotencyKeys","isDisabled","createdAt","updatedAt","ingestUrl","emailAddress"],"additionalProperties":false}},"total":{"type":"number"},"page":{"type":"number"},"pageSize":{"type":"number"},"pageCount":{"type":"number"}},"required":["items","total","page","pageSize","pageCount"],"additionalProperties":false}}}}}},"post":{"operationId":"sources.create","summary":"Create Source","description":"Create a new source for the organization.\n\nFor a known provider (github, linear, stripe, shopify, nuvion, paystack) the provider determines the verifier and its whole signature scheme:\nsend `provider` and `verifierConfig.secret` and omit `verifierType` — it is inferred from the provider.\nThe scheme fields (hash, encoding, header, prefix, timestampHeader, signedPayloadTemplate) cannot be overridden — a value that differs from the preset is rejected.\nEchoing a preset value back unchanged IS accepted — only a differing value is refused.\n`toleranceSeconds` is configurable only where a timestamp exists to bound it — stripe, and hmac presets carrying a timestamp header (currently nuvion);\ngithub, linear, shopify and paystack reject it. Use provider `generic` to control the scheme yourself, in which case `verifierType` is required.","tags":["Sources"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"oneOf":[{"oneOf":[{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100,"description":"Human-readable name for this source (max 100 characters)"},"description":{"anyOf":[{"description":"Optional free-text description of what this source receives (max 500 characters)","type":"string","maxLength":500},{"type":"null"}]},"provider":{"type":"string","const":"github"},"customResponseContentType":{"anyOf":[{"description":"Content-Type of the custom static response (only used when a custom response body is set)","type":"string"},{"type":"null"}]},"customResponseBody":{"anyOf":[{"description":"Custom static response body returned to the webhook provider","type":"string"},{"type":"null"}]},"eventTypeField":{"anyOf":[{"description":"Dot-notation path to the event type (e.g. headers.x-github-event, body.data.type)","type":"string","pattern":"^$|^(headers\\.[^.]+|body\\..+)$"},{"type":"null"}]},"idempotencyKeys":{"description":"Dot-notation paths for deduplication. Press enter or comma to add.","type":"array","items":{"type":"string","pattern":"^(headers\\.[^.]+|body\\..+)$"}},"maxPayloadSizeKb":{"description":"Maximum payload size in kilobytes","type":"integer","minimum":1,"maximum":4096},"verifierType":{"default":"hmac","description":"Which verification runs on inbound requests. Omit it entirely for no verification (an unverified source; verifierConfig must then be empty). Omit it on a known provider (github, linear, stripe, shopify, nuvion, paystack) while sending verifierConfig.secret, and the provider selects its own verifier and signature scheme. Send it explicitly to choose a verifier yourself — required for provider \"generic\" whenever verifierConfig is non-empty.","type":"string","const":"hmac"},"verifierConfig":{"type":"object","properties":{"secret":{"type":"string","minLength":1,"description":"Shared secret used to verify the HMAC signature. Encrypted at rest, never returned by the API."},"toleranceSeconds":{"not":{}},"hash":{"type":"string","const":"sha256"},"encoding":{"type":"string","const":"hex"},"header":{"type":"string","const":"x-hub-signature-256"},"prefix":{"type":"string","const":"sha256="},"timestampHeader":{"not":{}},"timestampFormat":{"not":{}},"signedPayloadTemplate":{"not":{}}},"required":["secret"],"additionalProperties":false}},"required":["name","provider","verifierConfig"],"title":"GitHub (HMAC preset)","examples":[{"name":"GitHub webhooks","provider":"github","verifierConfig":{"secret":"ghs_your_webhook_secret"}}]},{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100,"description":"Human-readable name for this source (max 100 characters)"},"description":{"anyOf":[{"description":"Optional free-text description of what this source receives (max 500 characters)","type":"string","maxLength":500},{"type":"null"}]},"provider":{"type":"string","const":"linear"},"customResponseContentType":{"anyOf":[{"description":"Content-Type of the custom static response (only used when a custom response body is set)","type":"string"},{"type":"null"}]},"customResponseBody":{"anyOf":[{"description":"Custom static response body returned to the webhook provider","type":"string"},{"type":"null"}]},"eventTypeField":{"anyOf":[{"description":"Dot-notation path to the event type (e.g. headers.x-github-event, body.data.type)","type":"string","pattern":"^$|^(headers\\.[^.]+|body\\..+)$"},{"type":"null"}]},"idempotencyKeys":{"description":"Dot-notation paths for deduplication. Press enter or comma to add.","type":"array","items":{"type":"string","pattern":"^(headers\\.[^.]+|body\\..+)$"}},"maxPayloadSizeKb":{"description":"Maximum payload size in kilobytes","type":"integer","minimum":1,"maximum":4096},"verifierType":{"default":"hmac","description":"Which verification runs on inbound requests. Omit it entirely for no verification (an unverified source; verifierConfig must then be empty). Omit it on a known provider (github, linear, stripe, shopify, nuvion, paystack) while sending verifierConfig.secret, and the provider selects its own verifier and signature scheme. Send it explicitly to choose a verifier yourself — required for provider \"generic\" whenever verifierConfig is non-empty.","type":"string","const":"hmac"},"verifierConfig":{"type":"object","properties":{"secret":{"type":"string","minLength":1,"description":"Shared secret used to verify the HMAC signature. Encrypted at rest, never returned by the API."},"toleranceSeconds":{"not":{}},"hash":{"type":"string","const":"sha256"},"encoding":{"type":"string","const":"hex"},"header":{"type":"string","const":"linear-signature"},"prefix":{"not":{}},"timestampHeader":{"not":{}},"timestampFormat":{"not":{}},"signedPayloadTemplate":{"not":{}}},"required":["secret"],"additionalProperties":false}},"required":["name","provider","verifierConfig"],"title":"Linear (HMAC preset)","examples":[{"name":"Linear webhooks","provider":"linear","verifierConfig":{"secret":"lin_wh_your_webhook_secret"}}]},{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100,"description":"Human-readable name for this source (max 100 characters)"},"description":{"anyOf":[{"description":"Optional free-text description of what this source receives (max 500 characters)","type":"string","maxLength":500},{"type":"null"}]},"provider":{"type":"string","const":"shopify"},"customResponseContentType":{"anyOf":[{"description":"Content-Type of the custom static response (only used when a custom response body is set)","type":"string"},{"type":"null"}]},"customResponseBody":{"anyOf":[{"description":"Custom static response body returned to the webhook provider","type":"string"},{"type":"null"}]},"eventTypeField":{"anyOf":[{"description":"Dot-notation path to the event type (e.g. headers.x-github-event, body.data.type)","type":"string","pattern":"^$|^(headers\\.[^.]+|body\\..+)$"},{"type":"null"}]},"idempotencyKeys":{"description":"Dot-notation paths for deduplication. Press enter or comma to add.","type":"array","items":{"type":"string","pattern":"^(headers\\.[^.]+|body\\..+)$"}},"maxPayloadSizeKb":{"description":"Maximum payload size in kilobytes","type":"integer","minimum":1,"maximum":4096},"verifierType":{"default":"hmac","description":"Which verification runs on inbound requests. Omit it entirely for no verification (an unverified source; verifierConfig must then be empty). Omit it on a known provider (github, linear, stripe, shopify, nuvion, paystack) while sending verifierConfig.secret, and the provider selects its own verifier and signature scheme. Send it explicitly to choose a verifier yourself — required for provider \"generic\" whenever verifierConfig is non-empty.","type":"string","const":"hmac"},"verifierConfig":{"type":"object","properties":{"secret":{"type":"string","minLength":1,"description":"Shared secret used to verify the HMAC signature. Encrypted at rest, never returned by the API."},"toleranceSeconds":{"not":{}},"hash":{"type":"string","const":"sha256"},"encoding":{"type":"string","const":"base64"},"header":{"type":"string","const":"x-shopify-hmac-sha256"},"prefix":{"not":{}},"timestampHeader":{"not":{}},"timestampFormat":{"not":{}},"signedPayloadTemplate":{"not":{}}},"required":["secret"],"additionalProperties":false}},"required":["name","provider","verifierConfig"],"title":"Shopify (HMAC preset)","examples":[{"name":"Shopify webhooks","provider":"shopify","verifierConfig":{"secret":"shpss_your_webhook_secret"}}]},{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100,"description":"Human-readable name for this source (max 100 characters)"},"description":{"anyOf":[{"description":"Optional free-text description of what this source receives (max 500 characters)","type":"string","maxLength":500},{"type":"null"}]},"provider":{"type":"string","const":"nuvion"},"customResponseContentType":{"anyOf":[{"description":"Content-Type of the custom static response (only used when a custom response body is set)","type":"string"},{"type":"null"}]},"customResponseBody":{"anyOf":[{"description":"Custom static response body returned to the webhook provider","type":"string"},{"type":"null"}]},"eventTypeField":{"anyOf":[{"description":"Dot-notation path to the event type (e.g. headers.x-github-event, body.data.type)","type":"string","pattern":"^$|^(headers\\.[^.]+|body\\..+)$"},{"type":"null"}]},"idempotencyKeys":{"description":"Dot-notation paths for deduplication. Press enter or comma to add.","type":"array","items":{"type":"string","pattern":"^(headers\\.[^.]+|body\\..+)$"}},"maxPayloadSizeKb":{"description":"Maximum payload size in kilobytes","type":"integer","minimum":1,"maximum":4096},"verifierType":{"default":"hmac","description":"Which verification runs on inbound requests. Omit it entirely for no verification (an unverified source; verifierConfig must then be empty). Omit it on a known provider (github, linear, stripe, shopify, nuvion, paystack) while sending verifierConfig.secret, and the provider selects its own verifier and signature scheme. Send it explicitly to choose a verifier yourself — required for provider \"generic\" whenever verifierConfig is non-empty.","type":"string","const":"hmac"},"verifierConfig":{"type":"object","properties":{"secret":{"type":"string","minLength":1,"description":"Shared secret used to verify the HMAC signature. Encrypted at rest, never returned by the API."},"toleranceSeconds":{"description":"How far the timestamp may drift from server time, in either direction, before the request is rejected as a replay. Defaults to 300. Requires a timestamp header.","type":"integer","minimum":1,"maximum":86400},"hash":{"type":"string","const":"sha256"},"encoding":{"type":"string","const":"hex"},"header":{"type":"string","const":"x-nuvion-event-signature"},"prefix":{"not":{}},"timestampHeader":{"type":"string","const":"x-nuvion-event-timestamp"},"timestampFormat":{"type":"string","const":"iso8601"},"signedPayloadTemplate":{"type":"string","const":"{timestamp}.{body}"}},"required":["secret"],"additionalProperties":false}},"required":["name","provider","verifierConfig"],"title":"Nuvion (HMAC preset)","examples":[{"name":"Nuvion webhooks","provider":"nuvion","verifierConfig":{"secret":"whsec_your_webhook_secret","toleranceSeconds":300}}]},{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100,"description":"Human-readable name for this source (max 100 characters)"},"description":{"anyOf":[{"description":"Optional free-text description of what this source receives (max 500 characters)","type":"string","maxLength":500},{"type":"null"}]},"provider":{"type":"string","const":"paystack"},"customResponseContentType":{"anyOf":[{"description":"Content-Type of the custom static response (only used when a custom response body is set)","type":"string"},{"type":"null"}]},"customResponseBody":{"anyOf":[{"description":"Custom static response body returned to the webhook provider","type":"string"},{"type":"null"}]},"eventTypeField":{"anyOf":[{"description":"Dot-notation path to the event type (e.g. headers.x-github-event, body.data.type)","type":"string","pattern":"^$|^(headers\\.[^.]+|body\\..+)$"},{"type":"null"}]},"idempotencyKeys":{"description":"Dot-notation paths for deduplication. Press enter or comma to add.","type":"array","items":{"type":"string","pattern":"^(headers\\.[^.]+|body\\..+)$"}},"maxPayloadSizeKb":{"description":"Maximum payload size in kilobytes","type":"integer","minimum":1,"maximum":4096},"verifierType":{"default":"hmac","description":"Which verification runs on inbound requests. Omit it entirely for no verification (an unverified source; verifierConfig must then be empty). Omit it on a known provider (github, linear, stripe, shopify, nuvion, paystack) while sending verifierConfig.secret, and the provider selects its own verifier and signature scheme. Send it explicitly to choose a verifier yourself — required for provider \"generic\" whenever verifierConfig is non-empty.","type":"string","const":"hmac"},"verifierConfig":{"type":"object","properties":{"secret":{"type":"string","minLength":1,"description":"Shared secret used to verify the HMAC signature. Encrypted at rest, never returned by the API."},"toleranceSeconds":{"not":{}},"hash":{"type":"string","const":"sha512"},"encoding":{"type":"string","const":"hex"},"header":{"type":"string","const":"x-paystack-signature"},"prefix":{"not":{}},"timestampHeader":{"not":{}},"timestampFormat":{"not":{}},"signedPayloadTemplate":{"not":{}}},"required":["secret"],"additionalProperties":false}},"required":["name","provider","verifierConfig"],"title":"Paystack (HMAC preset)","examples":[{"name":"Paystack webhooks","provider":"paystack","verifierConfig":{"secret":"sk_live_your_secret_key"}}]},{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100,"description":"Human-readable name for this source (max 100 characters)"},"description":{"anyOf":[{"description":"Optional free-text description of what this source receives (max 500 characters)","type":"string","maxLength":500},{"type":"null"}]},"provider":{"type":"string","enum":["stripe","openai","anthropic","resend","generic"]},"customResponseContentType":{"anyOf":[{"description":"Content-Type of the custom static response (only used when a custom response body is set)","type":"string"},{"type":"null"}]},"customResponseBody":{"anyOf":[{"description":"Custom static response body returned to the webhook provider","type":"string"},{"type":"null"}]},"eventTypeField":{"anyOf":[{"description":"Dot-notation path to the event type (e.g. headers.x-github-event, body.data.type)","type":"string","pattern":"^$|^(headers\\.[^.]+|body\\..+)$"},{"type":"null"}]},"idempotencyKeys":{"description":"Dot-notation paths for deduplication. Press enter or comma to add.","type":"array","items":{"type":"string","pattern":"^(headers\\.[^.]+|body\\..+)$"}},"maxPayloadSizeKb":{"description":"Maximum payload size in kilobytes","type":"integer","minimum":1,"maximum":4096},"verifierType":{"type":"string","const":"hmac","description":"Which verification runs on inbound requests. Omit it entirely for no verification (an unverified source; verifierConfig must then be empty). Omit it on a known provider (github, linear, stripe, shopify, nuvion, paystack) while sending verifierConfig.secret, and the provider selects its own verifier and signature scheme. Send it explicitly to choose a verifier yourself — required for provider \"generic\" whenever verifierConfig is non-empty."},"verifierConfig":{"type":"object","properties":{"hash":{"type":"string","enum":["sha256","sha512","sha1","md5"],"description":"HMAC hash algorithm the provider uses to sign payloads"},"encoding":{"type":"string","enum":["hex","base64","base64url"],"description":"Encoding of the signature value in the header"},"header":{"type":"string","minLength":1,"description":"Request header that carries the HMAC signature (e.g. x-hub-signature-256)"},"prefix":{"description":"Optional prefix stripped from the header value before comparison (e.g. \"sha256=\")","type":"string"},"timestampHeader":{"description":"Request header carrying the timestamp. When set, the timestamp is folded into the signed payload and checked against the tolerance window below, rejecting replayed requests. Its encoding is declared by the timestamp format below.","type":"string","minLength":1},"timestampFormat":{"description":"How the provider encodes the timestamp header: unix seconds (the default) or an ISO-8601 instant carrying an explicit offset, e.g. 2026-08-11T12:45:17.503Z. Only the replay-window check reads this — the signature always covers the header exactly as sent. Requires a timestamp header.","type":"string","enum":["unix_seconds","iso8601"]},"toleranceSeconds":{"description":"How far the timestamp may drift from server time, in either direction, before the request is rejected as a replay. Defaults to 300. Requires a timestamp header.","type":"integer","minimum":1,"maximum":86400},"signedPayloadTemplate":{"description":"Layout of the bytes the provider signs, using {timestamp} and {body} placeholders (e.g. Slack signs \"v0:{timestamp}:{body}\"). Leave empty with a timestamp header set and {timestamp}.{body} is saved for you; with no timestamp header the bare body is signed.","type":"string","minLength":1},"secret":{"type":"string","minLength":1,"description":"Shared secret used to verify the HMAC signature. Encrypted at rest, never returned by the API."}},"required":["hash","encoding","header","secret"]}},"required":["name","provider","verifierType","verifierConfig"],"title":"Custom HMAC (you define the scheme)","examples":[{"name":"Partner webhooks","provider":"generic","verifierType":"hmac","verifierConfig":{"header":"x-partner-signature","hash":"sha256","encoding":"hex","prefix":"sha256=","secret":"your_shared_secret"}}]}],"title":"HMAC signature"},{"oneOf":[{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100,"description":"Human-readable name for this source (max 100 characters)"},"description":{"anyOf":[{"description":"Optional free-text description of what this source receives (max 500 characters)","type":"string","maxLength":500},{"type":"null"}]},"provider":{"type":"string","const":"stripe"},"customResponseContentType":{"anyOf":[{"description":"Content-Type of the custom static response (only used when a custom response body is set)","type":"string"},{"type":"null"}]},"customResponseBody":{"anyOf":[{"description":"Custom static response body returned to the webhook provider","type":"string"},{"type":"null"}]},"eventTypeField":{"anyOf":[{"description":"Dot-notation path to the event type (e.g. headers.x-github-event, body.data.type)","type":"string","pattern":"^$|^(headers\\.[^.]+|body\\..+)$"},{"type":"null"}]},"idempotencyKeys":{"description":"Dot-notation paths for deduplication. Press enter or comma to add.","type":"array","items":{"type":"string","pattern":"^(headers\\.[^.]+|body\\..+)$"}},"maxPayloadSizeKb":{"description":"Maximum payload size in kilobytes","type":"integer","minimum":1,"maximum":4096},"verifierType":{"default":"stripe","description":"Which verification runs on inbound requests. Omit it entirely for no verification (an unverified source; verifierConfig must then be empty). Omit it on a known provider (github, linear, stripe, shopify, nuvion, paystack) while sending verifierConfig.secret, and the provider selects its own verifier and signature scheme. Send it explicitly to choose a verifier yourself — required for provider \"generic\" whenever verifierConfig is non-empty.","type":"string","const":"stripe"},"verifierConfig":{"type":"object","properties":{"secret":{"type":"string","minLength":1,"description":"Stripe webhook signing secret (whsec_...). Used to verify Stripe-Signature headers. Encrypted at rest, never returned by the API."},"toleranceSeconds":{"description":"How far the Stripe-Signature timestamp may drift from server time, in either direction, before the request is rejected as a replay. Defaults to 300.","type":"integer","minimum":1,"maximum":86400},"hash":{"not":{}},"encoding":{"not":{}},"header":{"not":{}},"prefix":{"not":{}},"timestampHeader":{"not":{}},"timestampFormat":{"not":{}},"signedPayloadTemplate":{"not":{}}},"required":["secret"],"additionalProperties":false}},"required":["name","provider","verifierConfig"],"title":"Stripe (preset)","examples":[{"name":"Stripe webhooks","provider":"stripe","verifierConfig":{"secret":"whsec_your_signing_secret","toleranceSeconds":300}}]},{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100,"description":"Human-readable name for this source (max 100 characters)"},"description":{"anyOf":[{"description":"Optional free-text description of what this source receives (max 500 characters)","type":"string","maxLength":500},{"type":"null"}]},"provider":{"type":"string","enum":["github","linear","shopify","nuvion","paystack","openai","anthropic","resend","generic"]},"customResponseContentType":{"anyOf":[{"description":"Content-Type of the custom static response (only used when a custom response body is set)","type":"string"},{"type":"null"}]},"customResponseBody":{"anyOf":[{"description":"Custom static response body returned to the webhook provider","type":"string"},{"type":"null"}]},"eventTypeField":{"anyOf":[{"description":"Dot-notation path to the event type (e.g. headers.x-github-event, body.data.type)","type":"string","pattern":"^$|^(headers\\.[^.]+|body\\..+)$"},{"type":"null"}]},"idempotencyKeys":{"description":"Dot-notation paths for deduplication. Press enter or comma to add.","type":"array","items":{"type":"string","pattern":"^(headers\\.[^.]+|body\\..+)$"}},"maxPayloadSizeKb":{"description":"Maximum payload size in kilobytes","type":"integer","minimum":1,"maximum":4096},"verifierType":{"type":"string","const":"stripe","description":"Which verification runs on inbound requests. Omit it entirely for no verification (an unverified source; verifierConfig must then be empty). Omit it on a known provider (github, linear, stripe, shopify, nuvion, paystack) while sending verifierConfig.secret, and the provider selects its own verifier and signature scheme. Send it explicitly to choose a verifier yourself — required for provider \"generic\" whenever verifierConfig is non-empty."},"verifierConfig":{"type":"object","properties":{"secret":{"type":"string","minLength":1,"description":"Stripe webhook signing secret (whsec_...). Used to verify Stripe-Signature headers. Encrypted at rest, never returned by the API."},"toleranceSeconds":{"description":"How far the Stripe-Signature timestamp may drift from server time, in either direction, before the request is rejected as a replay. Defaults to 300.","type":"integer","minimum":1,"maximum":86400},"hash":{"not":{}},"encoding":{"not":{}},"header":{"not":{}},"prefix":{"not":{}},"timestampHeader":{"not":{}},"timestampFormat":{"not":{}},"signedPayloadTemplate":{"not":{}}},"required":["secret"],"additionalProperties":false}},"required":["name","provider","verifierType","verifierConfig"],"title":"Stripe verifier on a non-Stripe provider","examples":[{"name":"Stripe-compatible webhooks","provider":"generic","verifierType":"stripe","verifierConfig":{"secret":"whsec_your_signing_secret"}}]}],"title":"Stripe signature"},{"oneOf":[{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100,"description":"Human-readable name for this source (max 100 characters)"},"description":{"anyOf":[{"description":"Optional free-text description of what this source receives (max 500 characters)","type":"string","maxLength":500},{"type":"null"}]},"provider":{"type":"string","enum":["openai","anthropic","resend"]},"customResponseContentType":{"anyOf":[{"description":"Content-Type of the custom static response (only used when a custom response body is set)","type":"string"},{"type":"null"}]},"customResponseBody":{"anyOf":[{"description":"Custom static response body returned to the webhook provider","type":"string"},{"type":"null"}]},"eventTypeField":{"anyOf":[{"description":"Dot-notation path to the event type (e.g. headers.x-github-event, body.data.type)","type":"string","pattern":"^$|^(headers\\.[^.]+|body\\..+)$"},{"type":"null"}]},"idempotencyKeys":{"description":"Dot-notation paths for deduplication. Press enter or comma to add.","type":"array","items":{"type":"string","pattern":"^(headers\\.[^.]+|body\\..+)$"}},"maxPayloadSizeKb":{"description":"Maximum payload size in kilobytes","type":"integer","minimum":1,"maximum":4096},"verifierType":{"default":"standard_webhooks","description":"Which verification runs on inbound requests. Omit it entirely for no verification (an unverified source; verifierConfig must then be empty). Omit it on a known provider (github, linear, stripe, shopify, nuvion, paystack) while sending verifierConfig.secret, and the provider selects its own verifier and signature scheme. Send it explicitly to choose a verifier yourself — required for provider \"generic\" whenever verifierConfig is non-empty.","type":"string","const":"standard_webhooks"},"verifierConfig":{"type":"object","properties":{"secret":{"type":"string","minLength":1,"description":"The producer's Standard Webhooks signing secret (whsec_...). Used to verify webhook-signature headers. Encrypted at rest, never returned by the API."},"toleranceSeconds":{"description":"How far the webhook-timestamp header may drift from server time, in either direction, before the request is rejected as a replay. Defaults to 300, which is what the spec's own reference libraries use.","type":"integer","minimum":1,"maximum":86400},"hash":{"not":{}},"encoding":{"not":{}},"header":{"not":{}},"prefix":{"not":{}},"timestampHeader":{"not":{}},"timestampFormat":{"not":{}},"signedPayloadTemplate":{"not":{}}},"required":["secret"],"additionalProperties":false}},"required":["name","provider","verifierConfig"],"title":"Standard Webhooks (preset providers)","examples":[{"name":"OpenAI webhooks","provider":"openai","verifierConfig":{"secret":"whsec_your_signing_secret","toleranceSeconds":300}},{"name":"Anthropic webhooks","provider":"anthropic","verifierConfig":{"secret":"whsec_your_signing_secret","toleranceSeconds":300}},{"name":"Resend webhooks","provider":"resend","verifierConfig":{"secret":"whsec_your_signing_secret","toleranceSeconds":300}}]},{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100,"description":"Human-readable name for this source (max 100 characters)"},"description":{"anyOf":[{"description":"Optional free-text description of what this source receives (max 500 characters)","type":"string","maxLength":500},{"type":"null"}]},"provider":{"type":"string","enum":["github","linear","stripe","shopify","nuvion","paystack","generic"]},"customResponseContentType":{"anyOf":[{"description":"Content-Type of the custom static response (only used when a custom response body is set)","type":"string"},{"type":"null"}]},"customResponseBody":{"anyOf":[{"description":"Custom static response body returned to the webhook provider","type":"string"},{"type":"null"}]},"eventTypeField":{"anyOf":[{"description":"Dot-notation path to the event type (e.g. headers.x-github-event, body.data.type)","type":"string","pattern":"^$|^(headers\\.[^.]+|body\\..+)$"},{"type":"null"}]},"idempotencyKeys":{"description":"Dot-notation paths for deduplication. Press enter or comma to add.","type":"array","items":{"type":"string","pattern":"^(headers\\.[^.]+|body\\..+)$"}},"maxPayloadSizeKb":{"description":"Maximum payload size in kilobytes","type":"integer","minimum":1,"maximum":4096},"verifierType":{"type":"string","const":"standard_webhooks","description":"Which verification runs on inbound requests. Omit it entirely for no verification (an unverified source; verifierConfig must then be empty). Omit it on a known provider (github, linear, stripe, shopify, nuvion, paystack) while sending verifierConfig.secret, and the provider selects its own verifier and signature scheme. Send it explicitly to choose a verifier yourself — required for provider \"generic\" whenever verifierConfig is non-empty."},"verifierConfig":{"type":"object","properties":{"secret":{"type":"string","minLength":1,"description":"The producer's Standard Webhooks signing secret (whsec_...). Used to verify webhook-signature headers. Encrypted at rest, never returned by the API."},"toleranceSeconds":{"description":"How far the webhook-timestamp header may drift from server time, in either direction, before the request is rejected as a replay. Defaults to 300, which is what the spec's own reference libraries use.","type":"integer","minimum":1,"maximum":86400},"hash":{"not":{}},"encoding":{"not":{}},"header":{"not":{}},"prefix":{"not":{}},"timestampHeader":{"not":{}},"timestampFormat":{"not":{}},"signedPayloadTemplate":{"not":{}}},"required":["secret"],"additionalProperties":false}},"required":["name","provider","verifierType","verifierConfig"],"title":"Standard Webhooks on an uncatalogued producer","examples":[{"name":"Supabase webhooks","provider":"generic","verifierType":"standard_webhooks","verifierConfig":{"secret":"your_shared_secret"}}]}],"title":"Standard Webhooks signature"},{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100,"description":"Human-readable name for this source (max 100 characters)"},"description":{"anyOf":[{"description":"Optional free-text description of what this source receives (max 500 characters)","type":"string","maxLength":500},{"type":"null"}]},"provider":{"type":"string","enum":["github","linear","stripe","shopify","nuvion","paystack","openai","anthropic","resend","generic","email"],"description":"The webhook provider sending to this source. Known providers (github, linear, stripe, shopify, nuvion, paystack) auto-configure event-type extraction; use \"generic\" for anything else."},"customResponseContentType":{"anyOf":[{"description":"Content-Type of the custom static response (only used when a custom response body is set)","type":"string"},{"type":"null"}]},"customResponseBody":{"anyOf":[{"description":"Custom static response body returned to the webhook provider","type":"string"},{"type":"null"}]},"eventTypeField":{"anyOf":[{"description":"Dot-notation path to the event type (e.g. headers.x-github-event, body.data.type)","type":"string","pattern":"^$|^(headers\\.[^.]+|body\\..+)$"},{"type":"null"}]},"idempotencyKeys":{"description":"Dot-notation paths for deduplication. Press enter or comma to add.","type":"array","items":{"type":"string","pattern":"^(headers\\.[^.]+|body\\..+)$"}},"maxPayloadSizeKb":{"description":"Maximum payload size in kilobytes","type":"integer","minimum":1,"maximum":4096},"verifierType":{"type":"string","const":"basic_auth","description":"Which verification runs on inbound requests. Omit it entirely for no verification (an unverified source; verifierConfig must then be empty). Omit it on a known provider (github, linear, stripe, shopify, nuvion, paystack) while sending verifierConfig.secret, and the provider selects its own verifier and signature scheme. Send it explicitly to choose a verifier yourself — required for provider \"generic\" whenever verifierConfig is non-empty."},"verifierConfig":{"type":"object","properties":{"username":{"type":"string","minLength":1,"description":"Username the provider sends via HTTP Basic Auth"},"password":{"type":"string","minLength":1,"description":"Password the provider sends via HTTP Basic Auth. Encrypted at rest, never returned by the API."}},"required":["username","password"]}},"required":["name","provider","verifierType","verifierConfig"],"title":"HTTP Basic Auth","examples":[{"name":"Partner webhooks","provider":"generic","verifierType":"basic_auth","verifierConfig":{"username":"solidhooks","password":"your_shared_password"}}]},{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100,"description":"Human-readable name for this source (max 100 characters)"},"description":{"anyOf":[{"description":"Optional free-text description of what this source receives (max 500 characters)","type":"string","maxLength":500},{"type":"null"}]},"provider":{"type":"string","enum":["github","linear","stripe","shopify","nuvion","paystack","openai","anthropic","resend","generic","email"],"description":"The webhook provider sending to this source. Known providers (github, linear, stripe, shopify, nuvion, paystack) auto-configure event-type extraction; use \"generic\" for anything else."},"customResponseContentType":{"anyOf":[{"description":"Content-Type of the custom static response (only used when a custom response body is set)","type":"string"},{"type":"null"}]},"customResponseBody":{"anyOf":[{"description":"Custom static response body returned to the webhook provider","type":"string"},{"type":"null"}]},"eventTypeField":{"anyOf":[{"description":"Dot-notation path to the event type (e.g. headers.x-github-event, body.data.type)","type":"string","pattern":"^$|^(headers\\.[^.]+|body\\..+)$"},{"type":"null"}]},"idempotencyKeys":{"description":"Dot-notation paths for deduplication. Press enter or comma to add.","type":"array","items":{"type":"string","pattern":"^(headers\\.[^.]+|body\\..+)$"}},"maxPayloadSizeKb":{"description":"Maximum payload size in kilobytes","type":"integer","minimum":1,"maximum":4096},"verifierType":{"type":"string","const":"api_key","description":"Which verification runs on inbound requests. Omit it entirely for no verification (an unverified source; verifierConfig must then be empty). Omit it on a known provider (github, linear, stripe, shopify, nuvion, paystack) while sending verifierConfig.secret, and the provider selects its own verifier and signature scheme. Send it explicitly to choose a verifier yourself — required for provider \"generic\" whenever verifierConfig is non-empty."},"verifierConfig":{"type":"object","properties":{"header_name":{"type":"string","minLength":1,"description":"Request header the provider sends its API key in"},"header_value":{"type":"string","minLength":1,"description":"Expected API key value. Requests with a different value are rejected. Encrypted at rest, never returned by the API."}},"required":["header_name","header_value"]}},"required":["name","provider","verifierType","verifierConfig"],"title":"API key header","examples":[{"name":"Partner webhooks","provider":"generic","verifierType":"api_key","verifierConfig":{"header_name":"x-api-key","header_value":"your_expected_api_key"}}]},{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100,"description":"Human-readable name for this source (max 100 characters)"},"description":{"anyOf":[{"description":"Optional free-text description of what this source receives (max 500 characters)","type":"string","maxLength":500},{"type":"null"}]},"provider":{"type":"string","enum":["github","linear","stripe","shopify","nuvion","paystack","openai","anthropic","resend","generic","email"],"description":"The webhook provider sending to this source. Known providers (github, linear, stripe, shopify, nuvion, paystack) auto-configure event-type extraction; use \"generic\" for anything else."},"customResponseContentType":{"anyOf":[{"description":"Content-Type of the custom static response (only used when a custom response body is set)","type":"string"},{"type":"null"}]},"customResponseBody":{"anyOf":[{"description":"Custom static response body returned to the webhook provider","type":"string"},{"type":"null"}]},"eventTypeField":{"anyOf":[{"description":"Dot-notation path to the event type (e.g. headers.x-github-event, body.data.type)","type":"string","pattern":"^$|^(headers\\.[^.]+|body\\..+)$"},{"type":"null"}]},"idempotencyKeys":{"description":"Dot-notation paths for deduplication. Press enter or comma to add.","type":"array","items":{"type":"string","pattern":"^(headers\\.[^.]+|body\\..+)$"}},"maxPayloadSizeKb":{"description":"Maximum payload size in kilobytes","type":"integer","minimum":1,"maximum":4096},"verifierType":{"description":"Which verification runs on inbound requests. Omit it entirely for no verification (an unverified source; verifierConfig must then be empty). Omit it on a known provider (github, linear, stripe, shopify, nuvion, paystack) while sending verifierConfig.secret, and the provider selects its own verifier and signature scheme. Send it explicitly to choose a verifier yourself — required for provider \"generic\" whenever verifierConfig is non-empty.","type":"string","const":"noop"},"verifierConfig":{"type":"object","properties":{},"additionalProperties":false}},"required":["name","provider","verifierConfig"],"title":"Unverified (no signature check)","examples":[{"name":"Internal events","provider":"generic","verifierConfig":{}}]}]}}}},"responses":{"200":{"description":"The source has been created.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"organizationId":{"type":"string"},"projectId":{"type":"string"},"maskId":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"provider":{"type":"string","enum":["github","linear","stripe","shopify","nuvion","paystack","openai","anthropic","resend","generic","email"]},"verifierType":{"type":"string","enum":["noop","hmac","stripe","standard_webhooks","basic_auth","api_key"]},"verifierConfig":{"type":"object","properties":{"hash":{"description":"hmac — hash algorithm signing the payload: sha256, sha512, sha1 or md5.","type":"string"},"encoding":{"description":"hmac — encoding of the signature in the header: hex, base64 or base64url.","type":"string"},"header":{"description":"hmac — request header carrying the signature (e.g. x-hub-signature-256).","type":"string"},"prefix":{"description":"hmac — prefix stripped from the header value before comparison (e.g. \"sha256=\"). Absent when the provider sends a bare signature.","type":"string"},"timestampHeader":{"description":"hmac — request header carrying the timestamp. Present only on sources with replay protection configured.","type":"string"},"timestampFormat":{"description":"hmac — encoding of the timestamp header: \"unix_seconds\" or \"iso8601\". Absent means unix seconds. Read only by the replay-window check; the signature always covers the header as sent.","type":"string"},"signedPayloadTemplate":{"description":"hmac — layout of the bytes the provider signs, using {timestamp} and {body} placeholders.","type":"string"},"toleranceSeconds":{"description":"hmac and stripe — how far the timestamp may drift from server time, in either direction, before the request is rejected as a replay. Absent means the 300s default applies.","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"username":{"description":"basic_auth — the username the provider sends. The password is never returned.","type":"string"},"header_name":{"description":"api_key — the request header the provider sends its key in. The expected value is never returned.","type":"string"}},"additionalProperties":{},"description":"Verifier settings for this source, secret-free. The credential is encrypted at rest and never returned by the API: hmac and stripe withhold `secret`, basic_auth withholds `password`, api_key withholds `header_value`. Everything else round-trips — PATCH this object back unchanged and it is accepted. Empty when verifierType is \"noop\"."},"customResponseBody":{"type":["string","null"]},"customResponseContentType":{"type":["string","null"]},"eventTypeField":{"type":["string","null"]},"rateLimit":{"type":"integer","minimum":-2147483648,"maximum":2147483647},"maxPayloadSizeKb":{"type":"integer","minimum":-2147483648,"maximum":2147483647},"idempotencyKeys":{"type":"array","items":{"type":"string"}},"isDisabled":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time","x-native-type":"date"},"updatedAt":{"type":"string","format":"date-time","x-native-type":"date"},"ingestUrl":{"anyOf":[{"type":"string","format":"uri"},{"type":"null"}],"description":"Full inbound URL for this source. Give this URL to the webhook provider — it POSTs payloads here. Null when the server has no INGEST_BASE_URL configured."},"emailAddress":{"anyOf":[{"type":"string","format":"email","pattern":"^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"},{"type":"null"}],"description":"Inbound email address for this source (<maskId>@<email domain>). Emails sent here are ingested as events. Null when email ingestion is disabled on the source or the server has no INGEST_EMAIL_DOMAIN configured."}},"required":["id","organizationId","projectId","maskId","name","description","provider","verifierType","verifierConfig","customResponseBody","customResponseContentType","eventTypeField","rateLimit","maxPayloadSizeKb","idempotencyKeys","isDisabled","createdAt","updatedAt","ingestUrl","emailAddress"],"additionalProperties":false}}}}}}},"/sources/options":{"get":{"operationId":"sources.options","summary":"List Source Options","description":"Lightweight, unpaginated list of sources (id, name) for building filter/dropdown options. Capped at 200 results, no total count.","tags":["Sources"],"responses":{"200":{"description":"The request has succeeded.","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"}},"required":["id","name"],"additionalProperties":false}}},"required":["items"],"additionalProperties":false}}}}}}},"/sources/{id}":{"get":{"operationId":"sources.get","summary":"Get Source","description":"Get a source by ID for the organization.","tags":["Sources"],"parameters":[{"in":"path","required":true,"name":"id","schema":{"type":"string"}}],"responses":{"200":{"description":"The request has succeeded.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"organizationId":{"type":"string"},"projectId":{"type":"string"},"maskId":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"provider":{"type":"string","enum":["github","linear","stripe","shopify","nuvion","paystack","openai","anthropic","resend","generic","email"]},"verifierType":{"type":"string","enum":["noop","hmac","stripe","standard_webhooks","basic_auth","api_key"]},"verifierConfig":{"type":"object","properties":{"hash":{"description":"hmac — hash algorithm signing the payload: sha256, sha512, sha1 or md5.","type":"string"},"encoding":{"description":"hmac — encoding of the signature in the header: hex, base64 or base64url.","type":"string"},"header":{"description":"hmac — request header carrying the signature (e.g. x-hub-signature-256).","type":"string"},"prefix":{"description":"hmac — prefix stripped from the header value before comparison (e.g. \"sha256=\"). Absent when the provider sends a bare signature.","type":"string"},"timestampHeader":{"description":"hmac — request header carrying the timestamp. Present only on sources with replay protection configured.","type":"string"},"timestampFormat":{"description":"hmac — encoding of the timestamp header: \"unix_seconds\" or \"iso8601\". Absent means unix seconds. Read only by the replay-window check; the signature always covers the header as sent.","type":"string"},"signedPayloadTemplate":{"description":"hmac — layout of the bytes the provider signs, using {timestamp} and {body} placeholders.","type":"string"},"toleranceSeconds":{"description":"hmac and stripe — how far the timestamp may drift from server time, in either direction, before the request is rejected as a replay. Absent means the 300s default applies.","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"username":{"description":"basic_auth — the username the provider sends. The password is never returned.","type":"string"},"header_name":{"description":"api_key — the request header the provider sends its key in. The expected value is never returned.","type":"string"}},"additionalProperties":{},"description":"Verifier settings for this source, secret-free. The credential is encrypted at rest and never returned by the API: hmac and stripe withhold `secret`, basic_auth withholds `password`, api_key withholds `header_value`. Everything else round-trips — PATCH this object back unchanged and it is accepted. Empty when verifierType is \"noop\"."},"customResponseBody":{"type":["string","null"]},"customResponseContentType":{"type":["string","null"]},"eventTypeField":{"type":["string","null"]},"rateLimit":{"type":"integer","minimum":-2147483648,"maximum":2147483647},"maxPayloadSizeKb":{"type":"integer","minimum":-2147483648,"maximum":2147483647},"idempotencyKeys":{"type":"array","items":{"type":"string"}},"isDisabled":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time","x-native-type":"date"},"updatedAt":{"type":"string","format":"date-time","x-native-type":"date"},"ingestUrl":{"anyOf":[{"type":"string","format":"uri"},{"type":"null"}],"description":"Full inbound URL for this source. Give this URL to the webhook provider — it POSTs payloads here. Null when the server has no INGEST_BASE_URL configured."},"emailAddress":{"anyOf":[{"type":"string","format":"email","pattern":"^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"},{"type":"null"}],"description":"Inbound email address for this source (<maskId>@<email domain>). Emails sent here are ingested as events. Null when email ingestion is disabled on the source or the server has no INGEST_EMAIL_DOMAIN configured."}},"required":["id","organizationId","projectId","maskId","name","description","provider","verifierType","verifierConfig","customResponseBody","customResponseContentType","eventTypeField","rateLimit","maxPayloadSizeKb","idempotencyKeys","isDisabled","createdAt","updatedAt","ingestUrl","emailAddress"],"additionalProperties":false}}}}}},"patch":{"operationId":"sources.update","summary":"Update Source","description":"Update a source by ID for the organization.\n\nThe changed fields travel inside a `data` object. A true PATCH for everything outside verification: send only the fields you are changing — `{ \"data\": { \"name\": \"renamed\" } }` is a complete payload, and `{ \"data\": {} }` is a valid no-op.\nThe one envelope rule: `verifierType` and `verifierConfig` travel together. Touching verification (including rotating a secret) means sending both; a `verifierConfig` on its own is rejected.\nWithin the envelope, `verifierConfig` is a merge patch while `provider` and `verifierType` stay unchanged: omitted keys keep their stored values, `null` clears an optional setting, and a provider or verifier change starts from a fresh config.\n\nChanging `provider` re-derives the verifier config from the new provider preset, so a source moved onto a known provider picks up its scheme rather than keeping the previous one.\nRe-derivation applies only when the effective `verifierType` matches that preset's — moving to `nuvion` while explicitly using `basic_auth` injects nothing.\nPreset scheme fields cannot be overridden here either, though echoing a preset value back unchanged is accepted — so GET-then-PATCH round-trips, including the secret-less config a GET returns.\nOmitting a secret keeps the stored one ONLY while `provider` and `verifierType` are unchanged; changing either requires a new secret (or `verifierType: \"noop\"` to disable verification), since the stored credential belongs to the previous configuration.","tags":["Sources"],"parameters":[{"in":"path","required":true,"name":"id","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"data":{"oneOf":[{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100,"description":"Human-readable name for this source (max 100 characters)"},"description":{"anyOf":[{"description":"Optional free-text description of what this source receives (max 500 characters)","type":"string","maxLength":500},{"type":"null"}]},"provider":{"type":"string","enum":["github","linear","stripe","shopify","nuvion","paystack","openai","anthropic","resend","generic","email"],"description":"The webhook provider sending to this source. Known providers (github, linear, stripe, shopify, nuvion, paystack) auto-configure event-type extraction; use \"generic\" for anything else."},"customResponseContentType":{"anyOf":[{"description":"Content-Type of the custom static response (only used when a custom response body is set)","type":"string"},{"type":"null"}]},"customResponseBody":{"anyOf":[{"description":"Custom static response body returned to the webhook provider","type":"string"},{"type":"null"}]},"eventTypeField":{"anyOf":[{"description":"Dot-notation path to the event type (e.g. headers.x-github-event, body.data.type)","type":"string","pattern":"^$|^(headers\\.[^.]+|body\\..+)$"},{"type":"null"}]},"idempotencyKeys":{"description":"Dot-notation paths for deduplication. Press enter or comma to add.","type":"array","items":{"type":"string","pattern":"^(headers\\.[^.]+|body\\..+)$"}},"maxPayloadSizeKb":{"description":"Maximum payload size in kilobytes","type":"integer","minimum":1,"maximum":4096},"verifierType":{"not":{}},"verifierConfig":{"not":{}}}},{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100,"description":"Human-readable name for this source (max 100 characters)"},"description":{"anyOf":[{"description":"Optional free-text description of what this source receives (max 500 characters)","type":"string","maxLength":500},{"type":"null"}]},"provider":{"type":"string","enum":["github","linear","stripe","shopify","nuvion","paystack","openai","anthropic","resend","generic","email"],"description":"The webhook provider sending to this source. Known providers (github, linear, stripe, shopify, nuvion, paystack) auto-configure event-type extraction; use \"generic\" for anything else."},"customResponseContentType":{"anyOf":[{"description":"Content-Type of the custom static response (only used when a custom response body is set)","type":"string"},{"type":"null"}]},"customResponseBody":{"anyOf":[{"description":"Custom static response body returned to the webhook provider","type":"string"},{"type":"null"}]},"eventTypeField":{"anyOf":[{"description":"Dot-notation path to the event type (e.g. headers.x-github-event, body.data.type)","type":"string","pattern":"^$|^(headers\\.[^.]+|body\\..+)$"},{"type":"null"}]},"idempotencyKeys":{"description":"Dot-notation paths for deduplication. Press enter or comma to add.","type":"array","items":{"type":"string","pattern":"^(headers\\.[^.]+|body\\..+)$"}},"maxPayloadSizeKb":{"description":"Maximum payload size in kilobytes","type":"integer","minimum":1,"maximum":4096},"verifierType":{"type":"string","const":"noop"},"verifierConfig":{"type":"object","properties":{},"additionalProperties":false}},"required":["verifierType","verifierConfig"]},{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100,"description":"Human-readable name for this source (max 100 characters)"},"description":{"anyOf":[{"description":"Optional free-text description of what this source receives (max 500 characters)","type":"string","maxLength":500},{"type":"null"}]},"provider":{"type":"string","enum":["github","linear","stripe","shopify","nuvion","paystack","openai","anthropic","resend","generic","email"],"description":"The webhook provider sending to this source. Known providers (github, linear, stripe, shopify, nuvion, paystack) auto-configure event-type extraction; use \"generic\" for anything else."},"customResponseContentType":{"anyOf":[{"description":"Content-Type of the custom static response (only used when a custom response body is set)","type":"string"},{"type":"null"}]},"customResponseBody":{"anyOf":[{"description":"Custom static response body returned to the webhook provider","type":"string"},{"type":"null"}]},"eventTypeField":{"anyOf":[{"description":"Dot-notation path to the event type (e.g. headers.x-github-event, body.data.type)","type":"string","pattern":"^$|^(headers\\.[^.]+|body\\..+)$"},{"type":"null"}]},"idempotencyKeys":{"description":"Dot-notation paths for deduplication. Press enter or comma to add.","type":"array","items":{"type":"string","pattern":"^(headers\\.[^.]+|body\\..+)$"}},"maxPayloadSizeKb":{"description":"Maximum payload size in kilobytes","type":"integer","minimum":1,"maximum":4096},"verifierType":{"type":"string","const":"hmac"},"verifierConfig":{"type":"object","properties":{"hash":{"type":"string","enum":["sha256","sha512","sha1","md5"],"description":"HMAC hash algorithm the provider uses to sign payloads"},"encoding":{"type":"string","enum":["hex","base64","base64url"],"description":"Encoding of the signature value in the header"},"header":{"type":"string","minLength":1,"description":"Request header that carries the HMAC signature (e.g. x-hub-signature-256)"},"prefix":{"anyOf":[{"description":"Optional prefix stripped from the header value before comparison (e.g. \"sha256=\")","type":"string"},{"type":"null"}]},"timestampHeader":{"anyOf":[{"description":"Request header carrying the timestamp. When set, the timestamp is folded into the signed payload and checked against the tolerance window below, rejecting replayed requests. Its encoding is declared by the timestamp format below.","type":"string","minLength":1},{"type":"null"}]},"timestampFormat":{"anyOf":[{"description":"How the provider encodes the timestamp header: unix seconds (the default) or an ISO-8601 instant carrying an explicit offset, e.g. 2026-08-11T12:45:17.503Z. Only the replay-window check reads this — the signature always covers the header exactly as sent. Requires a timestamp header.","type":"string","enum":["unix_seconds","iso8601"]},{"type":"null"}]},"toleranceSeconds":{"anyOf":[{"description":"How far the timestamp may drift from server time, in either direction, before the request is rejected as a replay. Defaults to 300. Requires a timestamp header.","type":"integer","minimum":1,"maximum":86400},{"type":"null"}]},"signedPayloadTemplate":{"anyOf":[{"description":"Layout of the bytes the provider signs, using {timestamp} and {body} placeholders (e.g. Slack signs \"v0:{timestamp}:{body}\"). Leave empty with a timestamp header set and {timestamp}.{body} is saved for you; with no timestamp header the bare body is signed.","type":"string","minLength":1},{"type":"null"}]},"secret":{"type":"string","minLength":1,"description":"Shared secret used to verify the HMAC signature. Encrypted at rest, never returned by the API."}}}},"required":["verifierType","verifierConfig"]},{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100,"description":"Human-readable name for this source (max 100 characters)"},"description":{"anyOf":[{"description":"Optional free-text description of what this source receives (max 500 characters)","type":"string","maxLength":500},{"type":"null"}]},"provider":{"type":"string","enum":["github","linear","stripe","shopify","nuvion","paystack","openai","anthropic","resend","generic","email"],"description":"The webhook provider sending to this source. Known providers (github, linear, stripe, shopify, nuvion, paystack) auto-configure event-type extraction; use \"generic\" for anything else."},"customResponseContentType":{"anyOf":[{"description":"Content-Type of the custom static response (only used when a custom response body is set)","type":"string"},{"type":"null"}]},"customResponseBody":{"anyOf":[{"description":"Custom static response body returned to the webhook provider","type":"string"},{"type":"null"}]},"eventTypeField":{"anyOf":[{"description":"Dot-notation path to the event type (e.g. headers.x-github-event, body.data.type)","type":"string","pattern":"^$|^(headers\\.[^.]+|body\\..+)$"},{"type":"null"}]},"idempotencyKeys":{"description":"Dot-notation paths for deduplication. Press enter or comma to add.","type":"array","items":{"type":"string","pattern":"^(headers\\.[^.]+|body\\..+)$"}},"maxPayloadSizeKb":{"description":"Maximum payload size in kilobytes","type":"integer","minimum":1,"maximum":4096},"verifierType":{"type":"string","const":"stripe"},"verifierConfig":{"type":"object","properties":{"secret":{"type":"string","minLength":1,"description":"Stripe webhook signing secret (whsec_...). Used to verify Stripe-Signature headers. Encrypted at rest, never returned by the API."},"toleranceSeconds":{"anyOf":[{"description":"How far the Stripe-Signature timestamp may drift from server time, in either direction, before the request is rejected as a replay. Defaults to 300.","type":"integer","minimum":1,"maximum":86400},{"type":"null"}]}},"additionalProperties":false}},"required":["verifierType","verifierConfig"]},{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100,"description":"Human-readable name for this source (max 100 characters)"},"description":{"anyOf":[{"description":"Optional free-text description of what this source receives (max 500 characters)","type":"string","maxLength":500},{"type":"null"}]},"provider":{"type":"string","enum":["github","linear","stripe","shopify","nuvion","paystack","openai","anthropic","resend","generic","email"],"description":"The webhook provider sending to this source. Known providers (github, linear, stripe, shopify, nuvion, paystack) auto-configure event-type extraction; use \"generic\" for anything else."},"customResponseContentType":{"anyOf":[{"description":"Content-Type of the custom static response (only used when a custom response body is set)","type":"string"},{"type":"null"}]},"customResponseBody":{"anyOf":[{"description":"Custom static response body returned to the webhook provider","type":"string"},{"type":"null"}]},"eventTypeField":{"anyOf":[{"description":"Dot-notation path to the event type (e.g. headers.x-github-event, body.data.type)","type":"string","pattern":"^$|^(headers\\.[^.]+|body\\..+)$"},{"type":"null"}]},"idempotencyKeys":{"description":"Dot-notation paths for deduplication. Press enter or comma to add.","type":"array","items":{"type":"string","pattern":"^(headers\\.[^.]+|body\\..+)$"}},"maxPayloadSizeKb":{"description":"Maximum payload size in kilobytes","type":"integer","minimum":1,"maximum":4096},"verifierType":{"type":"string","const":"standard_webhooks"},"verifierConfig":{"type":"object","properties":{"secret":{"type":"string","minLength":1,"description":"The producer's Standard Webhooks signing secret (whsec_...). Used to verify webhook-signature headers. Encrypted at rest, never returned by the API."},"toleranceSeconds":{"anyOf":[{"description":"How far the webhook-timestamp header may drift from server time, in either direction, before the request is rejected as a replay. Defaults to 300, which is what the spec's own reference libraries use.","type":"integer","minimum":1,"maximum":86400},{"type":"null"}]}},"additionalProperties":false}},"required":["verifierType","verifierConfig"]},{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100,"description":"Human-readable name for this source (max 100 characters)"},"description":{"anyOf":[{"description":"Optional free-text description of what this source receives (max 500 characters)","type":"string","maxLength":500},{"type":"null"}]},"provider":{"type":"string","enum":["github","linear","stripe","shopify","nuvion","paystack","openai","anthropic","resend","generic","email"],"description":"The webhook provider sending to this source. Known providers (github, linear, stripe, shopify, nuvion, paystack) auto-configure event-type extraction; use \"generic\" for anything else."},"customResponseContentType":{"anyOf":[{"description":"Content-Type of the custom static response (only used when a custom response body is set)","type":"string"},{"type":"null"}]},"customResponseBody":{"anyOf":[{"description":"Custom static response body returned to the webhook provider","type":"string"},{"type":"null"}]},"eventTypeField":{"anyOf":[{"description":"Dot-notation path to the event type (e.g. headers.x-github-event, body.data.type)","type":"string","pattern":"^$|^(headers\\.[^.]+|body\\..+)$"},{"type":"null"}]},"idempotencyKeys":{"description":"Dot-notation paths for deduplication. Press enter or comma to add.","type":"array","items":{"type":"string","pattern":"^(headers\\.[^.]+|body\\..+)$"}},"maxPayloadSizeKb":{"description":"Maximum payload size in kilobytes","type":"integer","minimum":1,"maximum":4096},"verifierType":{"type":"string","const":"basic_auth"},"verifierConfig":{"type":"object","properties":{"username":{"type":"string","minLength":1,"description":"Username the provider sends via HTTP Basic Auth"},"password":{"type":"string","minLength":1,"description":"Password the provider sends via HTTP Basic Auth. Encrypted at rest, never returned by the API."}}}},"required":["verifierType","verifierConfig"]},{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100,"description":"Human-readable name for this source (max 100 characters)"},"description":{"anyOf":[{"description":"Optional free-text description of what this source receives (max 500 characters)","type":"string","maxLength":500},{"type":"null"}]},"provider":{"type":"string","enum":["github","linear","stripe","shopify","nuvion","paystack","openai","anthropic","resend","generic","email"],"description":"The webhook provider sending to this source. Known providers (github, linear, stripe, shopify, nuvion, paystack) auto-configure event-type extraction; use \"generic\" for anything else."},"customResponseContentType":{"anyOf":[{"description":"Content-Type of the custom static response (only used when a custom response body is set)","type":"string"},{"type":"null"}]},"customResponseBody":{"anyOf":[{"description":"Custom static response body returned to the webhook provider","type":"string"},{"type":"null"}]},"eventTypeField":{"anyOf":[{"description":"Dot-notation path to the event type (e.g. headers.x-github-event, body.data.type)","type":"string","pattern":"^$|^(headers\\.[^.]+|body\\..+)$"},{"type":"null"}]},"idempotencyKeys":{"description":"Dot-notation paths for deduplication. Press enter or comma to add.","type":"array","items":{"type":"string","pattern":"^(headers\\.[^.]+|body\\..+)$"}},"maxPayloadSizeKb":{"description":"Maximum payload size in kilobytes","type":"integer","minimum":1,"maximum":4096},"verifierType":{"type":"string","const":"api_key"},"verifierConfig":{"type":"object","properties":{"header_name":{"type":"string","minLength":1,"description":"Request header the provider sends its API key in"},"header_value":{"type":"string","minLength":1,"description":"Expected API key value. Requests with a different value are rejected. Encrypted at rest, never returned by the API."}}}},"required":["verifierType","verifierConfig"]}]}},"required":["data"]}}}},"responses":{"200":{"description":"The source has been updated.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"organizationId":{"type":"string"},"projectId":{"type":"string"},"maskId":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"provider":{"type":"string","enum":["github","linear","stripe","shopify","nuvion","paystack","openai","anthropic","resend","generic","email"]},"verifierType":{"type":"string","enum":["noop","hmac","stripe","standard_webhooks","basic_auth","api_key"]},"verifierConfig":{"type":"object","properties":{"hash":{"description":"hmac — hash algorithm signing the payload: sha256, sha512, sha1 or md5.","type":"string"},"encoding":{"description":"hmac — encoding of the signature in the header: hex, base64 or base64url.","type":"string"},"header":{"description":"hmac — request header carrying the signature (e.g. x-hub-signature-256).","type":"string"},"prefix":{"description":"hmac — prefix stripped from the header value before comparison (e.g. \"sha256=\"). Absent when the provider sends a bare signature.","type":"string"},"timestampHeader":{"description":"hmac — request header carrying the timestamp. Present only on sources with replay protection configured.","type":"string"},"timestampFormat":{"description":"hmac — encoding of the timestamp header: \"unix_seconds\" or \"iso8601\". Absent means unix seconds. Read only by the replay-window check; the signature always covers the header as sent.","type":"string"},"signedPayloadTemplate":{"description":"hmac — layout of the bytes the provider signs, using {timestamp} and {body} placeholders.","type":"string"},"toleranceSeconds":{"description":"hmac and stripe — how far the timestamp may drift from server time, in either direction, before the request is rejected as a replay. Absent means the 300s default applies.","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"username":{"description":"basic_auth — the username the provider sends. The password is never returned.","type":"string"},"header_name":{"description":"api_key — the request header the provider sends its key in. The expected value is never returned.","type":"string"}},"additionalProperties":{},"description":"Verifier settings for this source, secret-free. The credential is encrypted at rest and never returned by the API: hmac and stripe withhold `secret`, basic_auth withholds `password`, api_key withholds `header_value`. Everything else round-trips — PATCH this object back unchanged and it is accepted. Empty when verifierType is \"noop\"."},"customResponseBody":{"type":["string","null"]},"customResponseContentType":{"type":["string","null"]},"eventTypeField":{"type":["string","null"]},"rateLimit":{"type":"integer","minimum":-2147483648,"maximum":2147483647},"maxPayloadSizeKb":{"type":"integer","minimum":-2147483648,"maximum":2147483647},"idempotencyKeys":{"type":"array","items":{"type":"string"}},"isDisabled":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time","x-native-type":"date"},"updatedAt":{"type":"string","format":"date-time","x-native-type":"date"},"ingestUrl":{"anyOf":[{"type":"string","format":"uri"},{"type":"null"}],"description":"Full inbound URL for this source. Give this URL to the webhook provider — it POSTs payloads here. Null when the server has no INGEST_BASE_URL configured."},"emailAddress":{"anyOf":[{"type":"string","format":"email","pattern":"^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"},{"type":"null"}],"description":"Inbound email address for this source (<maskId>@<email domain>). Emails sent here are ingested as events. Null when email ingestion is disabled on the source or the server has no INGEST_EMAIL_DOMAIN configured."}},"required":["id","organizationId","projectId","maskId","name","description","provider","verifierType","verifierConfig","customResponseBody","customResponseContentType","eventTypeField","rateLimit","maxPayloadSizeKb","idempotencyKeys","isDisabled","createdAt","updatedAt","ingestUrl","emailAddress"],"additionalProperties":false}}}}}},"delete":{"operationId":"sources.remove","summary":"Delete Source","description":"Soft-delete a source by ID for the organization.","tags":["Sources"],"parameters":[{"in":"path","required":true,"name":"id","schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{}}}}},"responses":{"200":{"description":"The source has been deleted.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"}},"required":["ok"],"additionalProperties":false}}}}}}},"/sources/{id}/toggle-disabled":{"post":{"operationId":"sources.toggleDisabled","summary":"Toggle Source Disabled","description":"Enable or disable a source by ID. Disabled sources reject all incoming webhooks.","tags":["Sources"],"parameters":[{"in":"path","required":true,"name":"id","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"isDisabled":{"type":"boolean"}},"required":["isDisabled"]}}}},"responses":{"200":{"description":"The source status has been toggled.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"organizationId":{"type":"string"},"projectId":{"type":"string"},"maskId":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"provider":{"type":"string","enum":["github","linear","stripe","shopify","nuvion","paystack","openai","anthropic","resend","generic","email"]},"verifierType":{"type":"string","enum":["noop","hmac","stripe","standard_webhooks","basic_auth","api_key"]},"verifierConfig":{"type":"object","properties":{"hash":{"description":"hmac — hash algorithm signing the payload: sha256, sha512, sha1 or md5.","type":"string"},"encoding":{"description":"hmac — encoding of the signature in the header: hex, base64 or base64url.","type":"string"},"header":{"description":"hmac — request header carrying the signature (e.g. x-hub-signature-256).","type":"string"},"prefix":{"description":"hmac — prefix stripped from the header value before comparison (e.g. \"sha256=\"). Absent when the provider sends a bare signature.","type":"string"},"timestampHeader":{"description":"hmac — request header carrying the timestamp. Present only on sources with replay protection configured.","type":"string"},"timestampFormat":{"description":"hmac — encoding of the timestamp header: \"unix_seconds\" or \"iso8601\". Absent means unix seconds. Read only by the replay-window check; the signature always covers the header as sent.","type":"string"},"signedPayloadTemplate":{"description":"hmac — layout of the bytes the provider signs, using {timestamp} and {body} placeholders.","type":"string"},"toleranceSeconds":{"description":"hmac and stripe — how far the timestamp may drift from server time, in either direction, before the request is rejected as a replay. Absent means the 300s default applies.","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"username":{"description":"basic_auth — the username the provider sends. The password is never returned.","type":"string"},"header_name":{"description":"api_key — the request header the provider sends its key in. The expected value is never returned.","type":"string"}},"additionalProperties":{},"description":"Verifier settings for this source, secret-free. The credential is encrypted at rest and never returned by the API: hmac and stripe withhold `secret`, basic_auth withholds `password`, api_key withholds `header_value`. Everything else round-trips — PATCH this object back unchanged and it is accepted. Empty when verifierType is \"noop\"."},"customResponseBody":{"type":["string","null"]},"customResponseContentType":{"type":["string","null"]},"eventTypeField":{"type":["string","null"]},"rateLimit":{"type":"integer","minimum":-2147483648,"maximum":2147483647},"maxPayloadSizeKb":{"type":"integer","minimum":-2147483648,"maximum":2147483647},"idempotencyKeys":{"type":"array","items":{"type":"string"}},"isDisabled":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time","x-native-type":"date"},"updatedAt":{"type":"string","format":"date-time","x-native-type":"date"},"ingestUrl":{"anyOf":[{"type":"string","format":"uri"},{"type":"null"}],"description":"Full inbound URL for this source. Give this URL to the webhook provider — it POSTs payloads here. Null when the server has no INGEST_BASE_URL configured."},"emailAddress":{"anyOf":[{"type":"string","format":"email","pattern":"^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"},{"type":"null"}],"description":"Inbound email address for this source (<maskId>@<email domain>). Emails sent here are ingested as events. Null when email ingestion is disabled on the source or the server has no INGEST_EMAIL_DOMAIN configured."}},"required":["id","organizationId","projectId","maskId","name","description","provider","verifierType","verifierConfig","customResponseBody","customResponseContentType","eventTypeField","rateLimit","maxPayloadSizeKb","idempotencyKeys","isDisabled","createdAt","updatedAt","ingestUrl","emailAddress"],"additionalProperties":false}}}}}}},"/sources/{id}/rotate-secret":{"post":{"operationId":"sources.rotateSecret","summary":"Rotate Source Verifier Secret","description":"Accept a new verifier secret while continuing to accept the current one for an overlap window. Rotation is driven by the producer (Shopify, Stripe, GitHub generate the secret on their side), so without an overlap there is no ordering that avoids rejected requests: paste here first and traffic still signed with the old value fails, rotate there first and traffic signed with the new one fails. Works for every verifier type — it operates on the stored secret, not the wire format.","tags":["Sources"],"parameters":[{"in":"path","required":true,"name":"id","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"secret":{"type":"string","minLength":1,"description":"The new verifier secret, as generated by the producer."},"overlapDays":{"type":"number","enum":[1,7,30],"description":"How long the previous secret keeps being accepted."}},"required":["secret","overlapDays"]}}}},"responses":{"200":{"description":"The verifier secret has been rotated and both secrets are now accepted.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"organizationId":{"type":"string"},"projectId":{"type":"string"},"maskId":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"provider":{"type":"string","enum":["github","linear","stripe","shopify","nuvion","paystack","openai","anthropic","resend","generic","email"]},"verifierType":{"type":"string","enum":["noop","hmac","stripe","standard_webhooks","basic_auth","api_key"]},"verifierConfig":{"type":"object","properties":{"hash":{"description":"hmac — hash algorithm signing the payload: sha256, sha512, sha1 or md5.","type":"string"},"encoding":{"description":"hmac — encoding of the signature in the header: hex, base64 or base64url.","type":"string"},"header":{"description":"hmac — request header carrying the signature (e.g. x-hub-signature-256).","type":"string"},"prefix":{"description":"hmac — prefix stripped from the header value before comparison (e.g. \"sha256=\"). Absent when the provider sends a bare signature.","type":"string"},"timestampHeader":{"description":"hmac — request header carrying the timestamp. Present only on sources with replay protection configured.","type":"string"},"timestampFormat":{"description":"hmac — encoding of the timestamp header: \"unix_seconds\" or \"iso8601\". Absent means unix seconds. Read only by the replay-window check; the signature always covers the header as sent.","type":"string"},"signedPayloadTemplate":{"description":"hmac — layout of the bytes the provider signs, using {timestamp} and {body} placeholders.","type":"string"},"toleranceSeconds":{"description":"hmac and stripe — how far the timestamp may drift from server time, in either direction, before the request is rejected as a replay. Absent means the 300s default applies.","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"username":{"description":"basic_auth — the username the provider sends. The password is never returned.","type":"string"},"header_name":{"description":"api_key — the request header the provider sends its key in. The expected value is never returned.","type":"string"}},"additionalProperties":{},"description":"Verifier settings for this source, secret-free. The credential is encrypted at rest and never returned by the API: hmac and stripe withhold `secret`, basic_auth withholds `password`, api_key withholds `header_value`. Everything else round-trips — PATCH this object back unchanged and it is accepted. Empty when verifierType is \"noop\"."},"customResponseBody":{"type":["string","null"]},"customResponseContentType":{"type":["string","null"]},"eventTypeField":{"type":["string","null"]},"rateLimit":{"type":"integer","minimum":-2147483648,"maximum":2147483647},"maxPayloadSizeKb":{"type":"integer","minimum":-2147483648,"maximum":2147483647},"idempotencyKeys":{"type":"array","items":{"type":"string"}},"isDisabled":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time","x-native-type":"date"},"updatedAt":{"type":"string","format":"date-time","x-native-type":"date"},"ingestUrl":{"anyOf":[{"type":"string","format":"uri"},{"type":"null"}],"description":"Full inbound URL for this source. Give this URL to the webhook provider — it POSTs payloads here. Null when the server has no INGEST_BASE_URL configured."},"emailAddress":{"anyOf":[{"type":"string","format":"email","pattern":"^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"},{"type":"null"}],"description":"Inbound email address for this source (<maskId>@<email domain>). Emails sent here are ingested as events. Null when email ingestion is disabled on the source or the server has no INGEST_EMAIL_DOMAIN configured."}},"required":["id","organizationId","projectId","maskId","name","description","provider","verifierType","verifierConfig","customResponseBody","customResponseContentType","eventTypeField","rateLimit","maxPayloadSizeKb","idempotencyKeys","isDisabled","createdAt","updatedAt","ingestUrl","emailAddress"],"additionalProperties":false}}}}}}},"/sources/{id}/cancel-rotation":{"post":{"operationId":"sources.cancelRotation","summary":"Cancel Source Secret Rotation","description":"Abandon a rotation: restore the previous secret as current and close the window. Refused with 409 `ROTATION_IN_USE` once the new secret has already verified traffic — reverting then would reject webhooks the producer is signing correctly. Pass `force` to revert anyway.","tags":["Sources"],"parameters":[{"in":"path","required":true,"name":"id","schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"force":{"type":"boolean"}}}}}},"responses":{"200":{"description":"The rotation has been cancelled and the previous secret restored.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"organizationId":{"type":"string"},"projectId":{"type":"string"},"maskId":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"provider":{"type":"string","enum":["github","linear","stripe","shopify","nuvion","paystack","openai","anthropic","resend","generic","email"]},"verifierType":{"type":"string","enum":["noop","hmac","stripe","standard_webhooks","basic_auth","api_key"]},"verifierConfig":{"type":"object","properties":{"hash":{"description":"hmac — hash algorithm signing the payload: sha256, sha512, sha1 or md5.","type":"string"},"encoding":{"description":"hmac — encoding of the signature in the header: hex, base64 or base64url.","type":"string"},"header":{"description":"hmac — request header carrying the signature (e.g. x-hub-signature-256).","type":"string"},"prefix":{"description":"hmac — prefix stripped from the header value before comparison (e.g. \"sha256=\"). Absent when the provider sends a bare signature.","type":"string"},"timestampHeader":{"description":"hmac — request header carrying the timestamp. Present only on sources with replay protection configured.","type":"string"},"timestampFormat":{"description":"hmac — encoding of the timestamp header: \"unix_seconds\" or \"iso8601\". Absent means unix seconds. Read only by the replay-window check; the signature always covers the header as sent.","type":"string"},"signedPayloadTemplate":{"description":"hmac — layout of the bytes the provider signs, using {timestamp} and {body} placeholders.","type":"string"},"toleranceSeconds":{"description":"hmac and stripe — how far the timestamp may drift from server time, in either direction, before the request is rejected as a replay. Absent means the 300s default applies.","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"username":{"description":"basic_auth — the username the provider sends. The password is never returned.","type":"string"},"header_name":{"description":"api_key — the request header the provider sends its key in. The expected value is never returned.","type":"string"}},"additionalProperties":{},"description":"Verifier settings for this source, secret-free. The credential is encrypted at rest and never returned by the API: hmac and stripe withhold `secret`, basic_auth withholds `password`, api_key withholds `header_value`. Everything else round-trips — PATCH this object back unchanged and it is accepted. Empty when verifierType is \"noop\"."},"customResponseBody":{"type":["string","null"]},"customResponseContentType":{"type":["string","null"]},"eventTypeField":{"type":["string","null"]},"rateLimit":{"type":"integer","minimum":-2147483648,"maximum":2147483647},"maxPayloadSizeKb":{"type":"integer","minimum":-2147483648,"maximum":2147483647},"idempotencyKeys":{"type":"array","items":{"type":"string"}},"isDisabled":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time","x-native-type":"date"},"updatedAt":{"type":"string","format":"date-time","x-native-type":"date"},"ingestUrl":{"anyOf":[{"type":"string","format":"uri"},{"type":"null"}],"description":"Full inbound URL for this source. Give this URL to the webhook provider — it POSTs payloads here. Null when the server has no INGEST_BASE_URL configured."},"emailAddress":{"anyOf":[{"type":"string","format":"email","pattern":"^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"},{"type":"null"}],"description":"Inbound email address for this source (<maskId>@<email domain>). Emails sent here are ingested as events. Null when email ingestion is disabled on the source or the server has no INGEST_EMAIL_DOMAIN configured."}},"required":["id","organizationId","projectId","maskId","name","description","provider","verifierType","verifierConfig","customResponseBody","customResponseContentType","eventTypeField","rateLimit","maxPayloadSizeKb","idempotencyKeys","isDisabled","createdAt","updatedAt","ingestUrl","emailAddress"],"additionalProperties":false}}}}}}},"/sources/{id}/rotation-status":{"get":{"operationId":"sources.rotationStatus","summary":"Source Rotation Status","description":"Whether a verifier-secret rotation is in progress, when its window closes, and how many requests each stored secret has verified. The per-secret counts exist because we perform verification for inbound webhooks; the equivalent is not knowable for outbound signing. `lastSeenAt` distinguishes \"never seen\" from \"not seen recently\", and a null `count` with `countersAvailable: false` means the counter store was unreachable — which is NOT the same as zero.","tags":["Sources"],"parameters":[{"in":"path","required":true,"name":"id","schema":{"type":"string"}}],"responses":{"200":{"description":"The rotation status has been returned.","content":{"application/json":{"schema":{"type":"object","properties":{"rotating":{"type":"boolean"},"expiresAt":{"anyOf":[{"type":"string","format":"date-time","x-native-type":"date"},{"type":"null"}]},"countersAvailable":{"type":"boolean","description":"False when the counter store could not be reached. The counts below are then null."},"current":{"type":"object","properties":{"count":{"type":["number","null"]},"lastSeenAt":{"anyOf":[{"type":"string","format":"date-time","x-native-type":"date"},{"type":"null"}]}},"required":["count","lastSeenAt"],"additionalProperties":false},"previous":{"type":"object","properties":{"count":{"type":["number","null"]},"lastSeenAt":{"anyOf":[{"type":"string","format":"date-time","x-native-type":"date"},{"type":"null"}]}},"required":["count","lastSeenAt"],"additionalProperties":false}},"required":["rotating","expiresAt","countersAvailable","current","previous"],"additionalProperties":false}}}}}}},"/sources/{id}/test-signature":{"post":{"operationId":"sources.testSignature","summary":"Test a Source Signature","description":"Verify a sample header set and body against this source's live verifier secrets and report which one matched. During a rotation window both are tried, so this answers whether a producer has switched. Unlike the inbound 401 — which stays opaque so it cannot act as an oracle — this DOES return why verification failed, because the caller is the authenticated tenant reading their own diagnostics. It never records slot usage, so testing cannot disturb the counters `cancelRotation` reads. See the SolidHooks API guide: https://solidhooks.com/docs/verification#test-a-signature","tags":["Sources"],"parameters":[{"in":"path","required":true,"name":"id","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"headers":{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{"type":"string"},"description":"The request headers, exactly as the producer sent them. Matched case-insensitively."},"body":{"type":"string","description":"The raw request body, byte for byte as sent — do not re-serialize it."}},"required":["headers","body"]}}}},"responses":{"200":{"description":"The sample request was checked against the live secrets.","content":{"application/json":{"schema":{"type":"object","properties":{"verified":{"type":"boolean"},"slot":{"anyOf":[{"type":"string","enum":["current","previous"]},{"type":"null"}],"description":"Which stored secret matched. `previous` means the producer has not switched yet."},"reason":{"anyOf":[{"type":"string","enum":["verifier_misconfigured","missing_signature_header","missing_id_header","missing_timestamp_header","malformed_signature_header","malformed_timestamp","timestamp_out_of_tolerance","signature_mismatch","unsupported_verifier"]},{"type":"null"}],"description":"Why verification failed, from the same taxonomy the rejected-event log uses. Null on success."},"rotating":{"type":"boolean","description":"Whether a rotation window was open, so two secrets were tried."}},"required":["verified","slot","reason","rotating"],"additionalProperties":false}}}}}}},"/sources/{id}/rotate-mask":{"post":{"operationId":"sources.rotateMask","summary":"Rotate Source Mask","description":"Rotate the mask ID for a source, generating a new ingest URL.","tags":["Sources"],"parameters":[{"in":"path","required":true,"name":"id","schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{}}}}},"responses":{"200":{"description":"The source mask has been rotated.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"organizationId":{"type":"string"},"projectId":{"type":"string"},"maskId":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"provider":{"type":"string","enum":["github","linear","stripe","shopify","nuvion","paystack","openai","anthropic","resend","generic","email"]},"verifierType":{"type":"string","enum":["noop","hmac","stripe","standard_webhooks","basic_auth","api_key"]},"verifierConfig":{"type":"object","properties":{"hash":{"description":"hmac — hash algorithm signing the payload: sha256, sha512, sha1 or md5.","type":"string"},"encoding":{"description":"hmac — encoding of the signature in the header: hex, base64 or base64url.","type":"string"},"header":{"description":"hmac — request header carrying the signature (e.g. x-hub-signature-256).","type":"string"},"prefix":{"description":"hmac — prefix stripped from the header value before comparison (e.g. \"sha256=\"). Absent when the provider sends a bare signature.","type":"string"},"timestampHeader":{"description":"hmac — request header carrying the timestamp. Present only on sources with replay protection configured.","type":"string"},"timestampFormat":{"description":"hmac — encoding of the timestamp header: \"unix_seconds\" or \"iso8601\". Absent means unix seconds. Read only by the replay-window check; the signature always covers the header as sent.","type":"string"},"signedPayloadTemplate":{"description":"hmac — layout of the bytes the provider signs, using {timestamp} and {body} placeholders.","type":"string"},"toleranceSeconds":{"description":"hmac and stripe — how far the timestamp may drift from server time, in either direction, before the request is rejected as a replay. Absent means the 300s default applies.","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"username":{"description":"basic_auth — the username the provider sends. The password is never returned.","type":"string"},"header_name":{"description":"api_key — the request header the provider sends its key in. The expected value is never returned.","type":"string"}},"additionalProperties":{},"description":"Verifier settings for this source, secret-free. The credential is encrypted at rest and never returned by the API: hmac and stripe withhold `secret`, basic_auth withholds `password`, api_key withholds `header_value`. Everything else round-trips — PATCH this object back unchanged and it is accepted. Empty when verifierType is \"noop\"."},"customResponseBody":{"type":["string","null"]},"customResponseContentType":{"type":["string","null"]},"eventTypeField":{"type":["string","null"]},"rateLimit":{"type":"integer","minimum":-2147483648,"maximum":2147483647},"maxPayloadSizeKb":{"type":"integer","minimum":-2147483648,"maximum":2147483647},"idempotencyKeys":{"type":"array","items":{"type":"string"}},"isDisabled":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time","x-native-type":"date"},"updatedAt":{"type":"string","format":"date-time","x-native-type":"date"},"ingestUrl":{"anyOf":[{"type":"string","format":"uri"},{"type":"null"}],"description":"Full inbound URL for this source. Give this URL to the webhook provider — it POSTs payloads here. Null when the server has no INGEST_BASE_URL configured."},"emailAddress":{"anyOf":[{"type":"string","format":"email","pattern":"^(?:[A-Za-z0-9_'+\\-]+\\.)*[A-Za-z0-9_'+\\-]*[A-Za-z0-9_+-]@(?:[A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"},{"type":"null"}],"description":"Inbound email address for this source (<maskId>@<email domain>). Emails sent here are ingested as events. Null when email ingestion is disabled on the source or the server has no INGEST_EMAIL_DOMAIN configured."}},"required":["id","organizationId","projectId","maskId","name","description","provider","verifierType","verifierConfig","customResponseBody","customResponseContentType","eventTypeField","rateLimit","maxPayloadSizeKb","idempotencyKeys","isDisabled","createdAt","updatedAt","ingestUrl","emailAddress"],"additionalProperties":false}}}}}}},"/destinations":{"get":{"operationId":"destinations.list","summary":"List Destinations","description":"List all destinations for the organization.","tags":["Destinations"],"parameters":[{"in":"query","name":"page","schema":{"default":1,"description":"Page number (1-indexed)","type":"integer","minimum":1,"maximum":9007199254740991},"allowEmptyValue":true,"allowReserved":true},{"in":"query","name":"limit","schema":{"default":20,"description":"Number of results per page","type":"integer","minimum":1,"maximum":100},"allowEmptyValue":true,"allowReserved":true},{"in":"query","name":"search","schema":{"description":"Search term to filter results","type":"string","maxLength":255},"allowEmptyValue":true,"allowReserved":true},{"in":"query","name":"sortBy","schema":{"description":"Field to sort by","type":"string"},"allowEmptyValue":true,"allowReserved":true},{"in":"query","name":"sortDir","schema":{"description":"Sort direction","default":"desc","type":"string","enum":["asc","desc"]},"allowEmptyValue":true,"allowReserved":true},{"in":"query","name":"isDisabled","schema":{"type":"boolean"},"allowEmptyValue":true,"allowReserved":true}],"responses":{"200":{"description":"The request has succeeded.","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"organizationId":{"type":"string"},"projectId":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"type":{"type":"string","enum":["http","private"]},"connectorId":{"type":["string","null"]},"url":{"type":"string"},"method":{"type":"string","enum":["POST","PUT","PATCH"]},"headers":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"value":{"type":"string"},"sensitive":{"type":"boolean"}},"required":["name","sensitive"],"additionalProperties":false}},"forwardHeaders":{"type":"boolean"},"signatureScheme":{"type":"string","enum":["solidhooks_v1","standard_webhooks"]},"previousSigningSecretExpiresAt":{"anyOf":[{"type":"string","format":"date-time","x-native-type":"date"},{"type":"null"}]},"rateLimit":{"anyOf":[{"type":"integer","minimum":-2147483648,"maximum":2147483647},{"type":"null"}]},"rateLimitPeriod":{"anyOf":[{"type":"string","enum":["second","minute","hour","day"]},{"type":"null"}]},"timeoutMs":{"anyOf":[{"type":"integer","minimum":-2147483648,"maximum":2147483647},{"type":"null"}]},"isDisabled":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time","x-native-type":"date"},"updatedAt":{"type":"string","format":"date-time","x-native-type":"date"},"hasSigningSecret":{"type":"boolean"},"hasPreviousSigningSecret":{"type":"boolean"}},"required":["id","organizationId","projectId","name","description","type","connectorId","url","method","headers","forwardHeaders","signatureScheme","previousSigningSecretExpiresAt","rateLimit","rateLimitPeriod","timeoutMs","isDisabled","createdAt","updatedAt","hasSigningSecret","hasPreviousSigningSecret"],"additionalProperties":false}},"total":{"type":"number"},"page":{"type":"number"},"pageSize":{"type":"number"},"pageCount":{"type":"number"}},"required":["items","total","page","pageSize","pageCount"],"additionalProperties":false}}}}}},"post":{"operationId":"destinations.create","summary":"Create Destination","description":"Create a new destination for the organization: the URL, method and headers SolidHooks delivers events to. See the SolidHooks API guide: https://solidhooks.com/docs/destinations#create-a-destination","tags":["Destinations"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100,"description":"Human-readable name for this destination (max 100 characters)"},"description":{"description":"Optional free-text description of where this destination delivers (max 500 characters)","anyOf":[{"type":"string","maxLength":500},{"type":"null"}]},"type":{"type":"string","enum":["http","private"],"description":"Delivery mechanism. \"private\" routes through a connector into your network."},"connectorId":{"description":"The connector this private destination reaches into your network through.","type":["string","null"]},"url":{"type":"string","format":"uri","description":"The endpoint URL to deliver webhooks to (for private, an in-cluster URL)."},"method":{"type":"string","enum":["POST","PUT","PATCH"],"description":"The HTTP method used for delivery requests"},"headers":{"description":"Headers sent with each delivery request. Mark sensitive headers with the lock icon to encrypt them at rest.","type":"array","items":{"type":"object","properties":{"value":{"type":"string","minLength":1,"pattern":"^[^:\\s][^:]*:\\s*.*$"},"sensitive":{"type":"boolean"}},"required":["value","sensitive"]}},"forwardHeaders":{"description":"Forward the inbound source request headers to this destination, each renamed \"x-forwarded-src-<name>\". Sensitive headers (Authorization, Cookie, API keys, etc.) are always excluded.","type":"boolean"},"timeoutMs":{"description":"How long to wait for a response, in milliseconds (1000–30000). Defaults to 20000.","anyOf":[{"type":"integer","minimum":1000,"maximum":30000},{"type":"null"}]},"rateLimit":{"description":"Maximum number of deliveries per period","anyOf":[{"type":"integer","minimum":1,"maximum":9007199254740991},{"type":"null"}]},"rateLimitPeriod":{"anyOf":[{"type":"string","enum":["second","minute","hour","day"]},{"type":"null"}]}},"required":["name","type","url","method"]}}}},"responses":{"200":{"description":"The destination has been created.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"organizationId":{"type":"string"},"projectId":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"type":{"type":"string","enum":["http","private"]},"connectorId":{"type":["string","null"]},"url":{"type":"string"},"method":{"type":"string","enum":["POST","PUT","PATCH"]},"headers":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"value":{"type":"string"},"sensitive":{"type":"boolean"}},"required":["name","sensitive"],"additionalProperties":false}},"forwardHeaders":{"type":"boolean"},"signatureScheme":{"type":"string","enum":["solidhooks_v1","standard_webhooks"]},"previousSigningSecretExpiresAt":{"anyOf":[{"type":"string","format":"date-time","x-native-type":"date"},{"type":"null"}]},"rateLimit":{"anyOf":[{"type":"integer","minimum":-2147483648,"maximum":2147483647},{"type":"null"}]},"rateLimitPeriod":{"anyOf":[{"type":"string","enum":["second","minute","hour","day"]},{"type":"null"}]},"timeoutMs":{"anyOf":[{"type":"integer","minimum":-2147483648,"maximum":2147483647},{"type":"null"}]},"isDisabled":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time","x-native-type":"date"},"updatedAt":{"type":"string","format":"date-time","x-native-type":"date"},"hasSigningSecret":{"type":"boolean"},"hasPreviousSigningSecret":{"type":"boolean"}},"required":["id","organizationId","projectId","name","description","type","connectorId","url","method","headers","forwardHeaders","signatureScheme","previousSigningSecretExpiresAt","rateLimit","rateLimitPeriod","timeoutMs","isDisabled","createdAt","updatedAt","hasSigningSecret","hasPreviousSigningSecret"],"additionalProperties":false}}}}}}},"/destinations/options":{"get":{"operationId":"destinations.options","summary":"List Destination Options","description":"Lightweight, unpaginated list of destinations (id, name, url, method) for building filter/dropdown options. Optionally filtered by a name or URL substring. Capped at 200 results, no total count.","tags":["Destinations"],"parameters":[{"in":"query","name":"search","schema":{"description":"Filter by a case-insensitive name or URL substring.","type":"string"},"allowEmptyValue":true,"allowReserved":true},{"in":"query","name":"limit","schema":{"description":"Maximum results to return. Defaults to the 200 cap.","type":"integer","minimum":1,"maximum":200},"allowEmptyValue":true,"allowReserved":true}],"responses":{"200":{"description":"The request has succeeded.","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"url":{"type":"string"},"method":{"type":"string","enum":["POST","PUT","PATCH"]},"connectorId":{"type":["string","null"]}},"required":["id","name","url","method","connectorId"],"additionalProperties":false}}},"required":["items"],"additionalProperties":false}}}}}}},"/destinations/{id}":{"get":{"operationId":"destinations.get","summary":"Get Destination","description":"Get a destination by ID for the organization.","tags":["Destinations"],"parameters":[{"in":"path","required":true,"name":"id","schema":{"type":"string"}}],"responses":{"200":{"description":"The request has succeeded.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"organizationId":{"type":"string"},"projectId":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"type":{"type":"string","enum":["http","private"]},"connectorId":{"type":["string","null"]},"url":{"type":"string"},"method":{"type":"string","enum":["POST","PUT","PATCH"]},"headers":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"value":{"type":"string"},"sensitive":{"type":"boolean"}},"required":["name","sensitive"],"additionalProperties":false}},"forwardHeaders":{"type":"boolean"},"signatureScheme":{"type":"string","enum":["solidhooks_v1","standard_webhooks"]},"previousSigningSecretExpiresAt":{"anyOf":[{"type":"string","format":"date-time","x-native-type":"date"},{"type":"null"}]},"rateLimit":{"anyOf":[{"type":"integer","minimum":-2147483648,"maximum":2147483647},{"type":"null"}]},"rateLimitPeriod":{"anyOf":[{"type":"string","enum":["second","minute","hour","day"]},{"type":"null"}]},"timeoutMs":{"anyOf":[{"type":"integer","minimum":-2147483648,"maximum":2147483647},{"type":"null"}]},"isDisabled":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time","x-native-type":"date"},"updatedAt":{"type":"string","format":"date-time","x-native-type":"date"},"hasSigningSecret":{"type":"boolean"},"hasPreviousSigningSecret":{"type":"boolean"}},"required":["id","organizationId","projectId","name","description","type","connectorId","url","method","headers","forwardHeaders","signatureScheme","previousSigningSecretExpiresAt","rateLimit","rateLimitPeriod","timeoutMs","isDisabled","createdAt","updatedAt","hasSigningSecret","hasPreviousSigningSecret"],"additionalProperties":false}}}}}},"patch":{"operationId":"destinations.update","summary":"Update Destination","description":"Update a destination by ID for the organization.","tags":["Destinations"],"parameters":[{"in":"path","required":true,"name":"id","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100,"description":"Human-readable name for this destination (max 100 characters)"},"description":{"description":"Optional free-text description of where this destination delivers (max 500 characters)","anyOf":[{"type":"string","maxLength":500},{"type":"null"}]},"type":{"type":"string","enum":["http","private"],"description":"Delivery mechanism. \"private\" routes through a connector into your network."},"connectorId":{"description":"The connector this private destination reaches into your network through.","type":["string","null"]},"url":{"type":"string","format":"uri","description":"The endpoint URL to deliver webhooks to (for private, an in-cluster URL)."},"method":{"type":"string","enum":["POST","PUT","PATCH"],"description":"The HTTP method used for delivery requests"},"headers":{"description":"Headers sent with each delivery request. Mark sensitive headers with the lock icon to encrypt them at rest.","type":"array","items":{"type":"object","properties":{"value":{"type":"string","minLength":1,"pattern":"^[^:\\s][^:]*:\\s*.*$"},"sensitive":{"type":"boolean"}},"required":["value","sensitive"]}},"forwardHeaders":{"description":"Forward the inbound source request headers to this destination, each renamed \"x-forwarded-src-<name>\". Sensitive headers (Authorization, Cookie, API keys, etc.) are always excluded.","type":"boolean"},"timeoutMs":{"description":"How long to wait for a response, in milliseconds (1000–30000). Defaults to 20000.","anyOf":[{"type":"integer","minimum":1000,"maximum":30000},{"type":"null"}]},"rateLimit":{"description":"Maximum number of deliveries per period","anyOf":[{"type":"integer","minimum":1,"maximum":9007199254740991},{"type":"null"}]},"rateLimitPeriod":{"anyOf":[{"type":"string","enum":["second","minute","hour","day"]},{"type":"null"}]}}}},"required":["data"]}}}},"responses":{"200":{"description":"The destination has been updated.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"organizationId":{"type":"string"},"projectId":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"type":{"type":"string","enum":["http","private"]},"connectorId":{"type":["string","null"]},"url":{"type":"string"},"method":{"type":"string","enum":["POST","PUT","PATCH"]},"headers":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"value":{"type":"string"},"sensitive":{"type":"boolean"}},"required":["name","sensitive"],"additionalProperties":false}},"forwardHeaders":{"type":"boolean"},"signatureScheme":{"type":"string","enum":["solidhooks_v1","standard_webhooks"]},"previousSigningSecretExpiresAt":{"anyOf":[{"type":"string","format":"date-time","x-native-type":"date"},{"type":"null"}]},"rateLimit":{"anyOf":[{"type":"integer","minimum":-2147483648,"maximum":2147483647},{"type":"null"}]},"rateLimitPeriod":{"anyOf":[{"type":"string","enum":["second","minute","hour","day"]},{"type":"null"}]},"timeoutMs":{"anyOf":[{"type":"integer","minimum":-2147483648,"maximum":2147483647},{"type":"null"}]},"isDisabled":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time","x-native-type":"date"},"updatedAt":{"type":"string","format":"date-time","x-native-type":"date"},"hasSigningSecret":{"type":"boolean"},"hasPreviousSigningSecret":{"type":"boolean"}},"required":["id","organizationId","projectId","name","description","type","connectorId","url","method","headers","forwardHeaders","signatureScheme","previousSigningSecretExpiresAt","rateLimit","rateLimitPeriod","timeoutMs","isDisabled","createdAt","updatedAt","hasSigningSecret","hasPreviousSigningSecret"],"additionalProperties":false}}}}}},"delete":{"operationId":"destinations.remove","summary":"Delete Destination","description":"Delete a destination by ID. Fails if connections still reference it.","tags":["Destinations"],"parameters":[{"in":"path","required":true,"name":"id","schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{}}}}},"responses":{"200":{"description":"The destination has been deleted.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"}},"required":["ok"],"additionalProperties":false}}}}}}},"/destinations/{id}/toggle-disabled":{"post":{"operationId":"destinations.toggleDisabled","summary":"Toggle Destination Disabled","description":"Enable or disable a destination by ID. Disabled destinations stop receiving deliveries.","tags":["Destinations"],"parameters":[{"in":"path","required":true,"name":"id","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"isDisabled":{"type":"boolean"}},"required":["isDisabled"]}}}},"responses":{"200":{"description":"The destination status has been toggled.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"organizationId":{"type":"string"},"projectId":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"type":{"type":"string","enum":["http","private"]},"connectorId":{"type":["string","null"]},"url":{"type":"string"},"method":{"type":"string","enum":["POST","PUT","PATCH"]},"headers":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"value":{"type":"string"},"sensitive":{"type":"boolean"}},"required":["name","sensitive"],"additionalProperties":false}},"forwardHeaders":{"type":"boolean"},"signatureScheme":{"type":"string","enum":["solidhooks_v1","standard_webhooks"]},"previousSigningSecretExpiresAt":{"anyOf":[{"type":"string","format":"date-time","x-native-type":"date"},{"type":"null"}]},"rateLimit":{"anyOf":[{"type":"integer","minimum":-2147483648,"maximum":2147483647},{"type":"null"}]},"rateLimitPeriod":{"anyOf":[{"type":"string","enum":["second","minute","hour","day"]},{"type":"null"}]},"timeoutMs":{"anyOf":[{"type":"integer","minimum":-2147483648,"maximum":2147483647},{"type":"null"}]},"isDisabled":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time","x-native-type":"date"},"updatedAt":{"type":"string","format":"date-time","x-native-type":"date"},"hasSigningSecret":{"type":"boolean"},"hasPreviousSigningSecret":{"type":"boolean"}},"required":["id","organizationId","projectId","name","description","type","connectorId","url","method","headers","forwardHeaders","signatureScheme","previousSigningSecretExpiresAt","rateLimit","rateLimitPeriod","timeoutMs","isDisabled","createdAt","updatedAt","hasSigningSecret","hasPreviousSigningSecret"],"additionalProperties":false}}}}}}},"/destinations/{id}/signing-secret":{"get":{"operationId":"destinations.revealSigningSecret","summary":"Reveal Signing Secret","description":"Decrypt and return the HMAC signing secret for a destination.","tags":["Destinations"],"parameters":[{"in":"path","required":true,"name":"id","schema":{"type":"string"}}],"responses":{"200":{"description":"The signing secret has been returned.","content":{"application/json":{"schema":{"type":"object","properties":{"secret":{"type":"string"},"previousSecret":{"type":["string","null"]},"previousSecretExpiresAt":{"anyOf":[{"type":"string","format":"date-time","x-native-type":"date"},{"type":"null"}]}},"required":["secret","previousSecret","previousSecretExpiresAt"],"additionalProperties":false}}}}}}},"/destinations/{id}/rotate-signing-secret":{"post":{"operationId":"destinations.rotateSigningSecret","summary":"Rotate Signing Secret","description":"Generate a new HMAC signing secret for a destination. Pass `overlapDays` to keep the displaced secret valid for a migration window, so deliveries carry both signatures and a receiver can adopt the new one on its own schedule. Without it the old secret stops working immediately. Requires the `standard_webhooks` scheme — the legacy signature header carries one value and cannot express an overlap. `force` rotates again while a window is still live, stranding receivers who have not adopted the current secret.","tags":["Destinations"],"parameters":[{"in":"path","required":true,"name":"id","schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"overlapDays":{"description":"Days the displaced secret stays valid. Standard Webhooks destinations only.","anyOf":[{"type":"number","const":1},{"type":"number","const":7},{"type":"number","const":30}]},"force":{"description":"Rotate even though an overlap window is still live.","type":"boolean"}}}}}},"responses":{"200":{"description":"The signing secret has been rotated.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"organizationId":{"type":"string"},"projectId":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"type":{"type":"string","enum":["http","private"]},"connectorId":{"type":["string","null"]},"url":{"type":"string"},"method":{"type":"string","enum":["POST","PUT","PATCH"]},"headers":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"value":{"type":"string"},"sensitive":{"type":"boolean"}},"required":["name","sensitive"],"additionalProperties":false}},"forwardHeaders":{"type":"boolean"},"signatureScheme":{"type":"string","enum":["solidhooks_v1","standard_webhooks"]},"previousSigningSecretExpiresAt":{"anyOf":[{"type":"string","format":"date-time","x-native-type":"date"},{"type":"null"}]},"rateLimit":{"anyOf":[{"type":"integer","minimum":-2147483648,"maximum":2147483647},{"type":"null"}]},"rateLimitPeriod":{"anyOf":[{"type":"string","enum":["second","minute","hour","day"]},{"type":"null"}]},"timeoutMs":{"anyOf":[{"type":"integer","minimum":-2147483648,"maximum":2147483647},{"type":"null"}]},"isDisabled":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time","x-native-type":"date"},"updatedAt":{"type":"string","format":"date-time","x-native-type":"date"},"hasSigningSecret":{"type":"boolean"},"hasPreviousSigningSecret":{"type":"boolean"}},"required":["id","organizationId","projectId","name","description","type","connectorId","url","method","headers","forwardHeaders","signatureScheme","previousSigningSecretExpiresAt","rateLimit","rateLimitPeriod","timeoutMs","isDisabled","createdAt","updatedAt","hasSigningSecret","hasPreviousSigningSecret"],"additionalProperties":false}}}}}}},"/destinations/{id}/signature-scheme":{"post":{"operationId":"destinations.switchSignatureScheme","summary":"Switch Signature Scheme","description":"Change how deliveries to this destination are signed, and mint a fresh signing secret. BREAKING: there is no overlap window — an overlap signs with two secrets under one scheme and cannot span two, because the displaced secret is keyed differently under each and a receiver holding it cannot verify the new signature. Your endpoint must accept the new scheme BEFORE you call this. The new secret is not returned here: read it in the SolidHooks dashboard, or from `GET /v1/destinations/{id}/signing-secret` with an API key. Switching to the scheme already in force is a no-op. `standard_webhooks` emits `webhook-id` / `webhook-timestamp` / `webhook-signature` per the Standard Webhooks specification; `solidhooks_v1` is the legacy scheme, kept indefinitely.","tags":["Destinations"],"parameters":[{"in":"path","required":true,"name":"id","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"scheme":{"type":"string","enum":["solidhooks_v1","standard_webhooks"],"description":"The scheme to sign with from now on."}},"required":["scheme"]}}}},"responses":{"200":{"description":"The signature scheme has been switched and a new signing secret minted.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"organizationId":{"type":"string"},"projectId":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"type":{"type":"string","enum":["http","private"]},"connectorId":{"type":["string","null"]},"url":{"type":"string"},"method":{"type":"string","enum":["POST","PUT","PATCH"]},"headers":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"value":{"type":"string"},"sensitive":{"type":"boolean"}},"required":["name","sensitive"],"additionalProperties":false}},"forwardHeaders":{"type":"boolean"},"signatureScheme":{"type":"string","enum":["solidhooks_v1","standard_webhooks"]},"previousSigningSecretExpiresAt":{"anyOf":[{"type":"string","format":"date-time","x-native-type":"date"},{"type":"null"}]},"rateLimit":{"anyOf":[{"type":"integer","minimum":-2147483648,"maximum":2147483647},{"type":"null"}]},"rateLimitPeriod":{"anyOf":[{"type":"string","enum":["second","minute","hour","day"]},{"type":"null"}]},"timeoutMs":{"anyOf":[{"type":"integer","minimum":-2147483648,"maximum":2147483647},{"type":"null"}]},"isDisabled":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time","x-native-type":"date"},"updatedAt":{"type":"string","format":"date-time","x-native-type":"date"},"hasSigningSecret":{"type":"boolean"},"hasPreviousSigningSecret":{"type":"boolean"}},"required":["id","organizationId","projectId","name","description","type","connectorId","url","method","headers","forwardHeaders","signatureScheme","previousSigningSecretExpiresAt","rateLimit","rateLimitPeriod","timeoutMs","isDisabled","createdAt","updatedAt","hasSigningSecret","hasPreviousSigningSecret"],"additionalProperties":false}}}}}}},"/destinations/{id}/test-delivery":{"post":{"operationId":"destinations.sendTestDelivery","summary":"Send Test Delivery","description":"Post a signed sample payload to the destination and report the response. Shaped exactly like a real delivery (same method, signing and timeout) but carries `x-solidhooks-test: true`, records no event or delivery, and is never billed. A transport failure is reported in the result rather than raised as an error.","tags":["Destinations"],"parameters":[{"in":"path","required":true,"name":"id","schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{}}}}},"responses":{"200":{"description":"The test delivery was attempted; see `delivered` and `status` for the outcome.","content":{"application/json":{"schema":{"type":"object","properties":{"delivered":{"type":"boolean"},"status":{"type":["number","null"]},"responseTimeMs":{"type":["number","null"]},"responseBody":{"type":["string","null"]},"error":{"type":["string","null"]},"signed":{"type":"boolean"}},"required":["delivered","status","responseTimeMs","responseBody","error","signed"],"additionalProperties":false}}}}}}},"/destinations/{id}/delivery-health":{"get":{"operationId":"destinations.deliveryHealth","summary":"Destination Delivery Health","description":"Delivery outcomes for this destination since `since`, defaulting to the last 24 hours. Counts are anchored on delivery CREATION, so a delivery created before the window is excluded even if it was retried inside it. `pending` covers deliveries still in flight (queued or throttled), which are not yet an outcome either way.","tags":["Destinations"],"parameters":[{"in":"path","required":true,"name":"id","schema":{"type":"string"}},{"in":"query","name":"since","schema":{"description":"Count deliveries created at or after this instant. Defaults to 24 hours ago.","type":"string","format":"date-time","x-native-type":"date"},"allowEmptyValue":true,"allowReserved":true}],"responses":{"200":{"description":"The delivery outcome counts have been returned.","content":{"application/json":{"schema":{"type":"object","properties":{"total":{"type":"number"},"succeeded":{"type":"number"},"failed":{"type":"number"},"pending":{"type":"number","description":"Still in flight: queued, or held behind a rate limit"},"skipped":{"type":"number","description":"Terminal, and nothing was sent — a disabled route, or an event type no longer in the connection's allowlist. Neither a delivery that landed nor one still trying, so it is counted apart from both"},"since":{"type":"string","format":"date-time","x-native-type":"date"}},"required":["total","succeeded","failed","pending","skipped","since"],"additionalProperties":false}}}}}}},"/destinations/{id}/end-rotation-overlap":{"post":{"operationId":"destinations.endRotationOverlap","summary":"End Rotation Overlap","description":"Retire the previous signing secret immediately, closing a live overlap window early. This does NOT revert the rotation — deliveries have been signed with the current secret since it was minted; the previous one was only being honoured for the receiver's benefit. Idempotent: closing an already-closed window is a no-op.","tags":["Destinations"],"parameters":[{"in":"path","required":true,"name":"id","schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{}}}}},"responses":{"200":{"description":"The rotation overlap has been closed.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"organizationId":{"type":"string"},"projectId":{"type":"string"},"name":{"type":"string"},"description":{"type":["string","null"]},"type":{"type":"string","enum":["http","private"]},"connectorId":{"type":["string","null"]},"url":{"type":"string"},"method":{"type":"string","enum":["POST","PUT","PATCH"]},"headers":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"value":{"type":"string"},"sensitive":{"type":"boolean"}},"required":["name","sensitive"],"additionalProperties":false}},"forwardHeaders":{"type":"boolean"},"signatureScheme":{"type":"string","enum":["solidhooks_v1","standard_webhooks"]},"previousSigningSecretExpiresAt":{"anyOf":[{"type":"string","format":"date-time","x-native-type":"date"},{"type":"null"}]},"rateLimit":{"anyOf":[{"type":"integer","minimum":-2147483648,"maximum":2147483647},{"type":"null"}]},"rateLimitPeriod":{"anyOf":[{"type":"string","enum":["second","minute","hour","day"]},{"type":"null"}]},"timeoutMs":{"anyOf":[{"type":"integer","minimum":-2147483648,"maximum":2147483647},{"type":"null"}]},"isDisabled":{"type":"boolean"},"createdAt":{"type":"string","format":"date-time","x-native-type":"date"},"updatedAt":{"type":"string","format":"date-time","x-native-type":"date"},"hasSigningSecret":{"type":"boolean"},"hasPreviousSigningSecret":{"type":"boolean"}},"required":["id","organizationId","projectId","name","description","type","connectorId","url","method","headers","forwardHeaders","signatureScheme","previousSigningSecretExpiresAt","rateLimit","rateLimitPeriod","timeoutMs","isDisabled","createdAt","updatedAt","hasSigningSecret","hasPreviousSigningSecret"],"additionalProperties":false}}}}}}},"/events":{"get":{"operationId":"events.list","summary":"List Events","description":"List all events for the organization.","tags":["Events"],"parameters":[{"in":"query","name":"cursor","schema":{"description":"Cursor for pagination","type":"string"},"allowEmptyValue":true,"allowReserved":true},{"in":"query","name":"limit","schema":{"default":20,"description":"Number of results per page","type":"integer","minimum":1,"maximum":100},"allowEmptyValue":true,"allowReserved":true},{"in":"query","name":"search","schema":{"description":"Search term to filter results","type":"string","maxLength":255},"allowEmptyValue":true,"allowReserved":true},{"in":"query","name":"sortBy","schema":{"description":"Field to sort by","type":"string"},"allowEmptyValue":true,"allowReserved":true},{"in":"query","name":"sortDir","schema":{"description":"Sort direction","default":"desc","type":"string","enum":["asc","desc"]},"allowEmptyValue":true,"allowReserved":true},{"in":"query","name":"sourceId","schema":{"description":"Filter events by source ID","type":"string"},"allowEmptyValue":true,"allowReserved":true},{"in":"query","name":"status","schema":{"description":"Filter by event status","type":"string","enum":["pending","received","filtered","duplicate","rejected","unmatched"]},"allowEmptyValue":true,"allowReserved":true},{"in":"query","name":"from","schema":{"description":"Filter events created on or after this date","type":"string","format":"date-time","x-native-type":"date"},"allowEmptyValue":true,"allowReserved":true},{"in":"query","name":"to","schema":{"description":"Filter events created on or before this date","type":"string","format":"date-time","x-native-type":"date"},"allowEmptyValue":true,"allowReserved":true}],"responses":{"200":{"description":"The request has succeeded.","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Unique event identifier"},"organizationId":{"type":"string","description":"Organization that owns this event"},"projectId":{"type":"string","description":"Project this event belongs to"},"sourceId":{"type":"string","description":"Source that received this webhook"},"maskId":{"type":"string","description":"Mask ID used in the ingest URL"},"raw":{"type":"string","description":"Raw request body as received"},"headers":{"anyOf":[{"type":["string","number","boolean","null"]},{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},{"type":"array","items":{}}],"description":"Request headers from the webhook"},"payload":{"anyOf":[{"type":["string","number","boolean","null"]},{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},{"type":"array","items":{}}],"description":"Parsed JSON payload"},"idempotencyKey":{"anyOf":[{"type":"string","description":"Computed idempotency key for deduplication"},{"type":"null"}]},"status":{"type":"string","enum":["pending","received","filtered","duplicate","rejected","unmatched"],"description":"Event processing status"},"eventType":{"anyOf":[{"type":"string","description":"Resolved event type at ingest time (e.g. extracted field value or email.received)"},{"type":"null"}]},"rejectionReason":{"anyOf":[{"type":"string","description":"Why a `rejected` event was rejected — one of signature_mismatch, missing_signature_header, missing_timestamp_header, malformed_signature_header, malformed_timestamp, timestamp_out_of_tolerance, verifier_misconfigured, unsupported_verifier. Null on every other status. Left as free text on the read side so a value added later never fails an entire list response"},{"type":"null"}]},"verifiedWith":{"anyOf":[{"type":"string","description":"The verifier that RAN at ingest, snapshotted so editing the source never relabels history. Present on rejected events too, since a verifier ran there as well — read `status` to learn whether it PASSED, never the presence of this field. 'noop' means none was configured; null means the event predates this column and its verification outcome is unknown, not absent"},{"type":"null"}]},"urlQueryParams":{"type":"string","description":"Query parameters from the ingest URL"},"acknowledgedAt":{"anyOf":[{"type":"string","format":"date-time","x-native-type":"date","description":"When the event was acknowledged"},{"type":"null"}]},"responseTimeMs":{"anyOf":[{"type":"integer","minimum":-2147483648,"maximum":2147483647},{"type":"null"}]},"createdAt":{"type":"string","format":"date-time","x-native-type":"date","description":"When the event was received"},"isBinary":{"type":"boolean","description":"True when the body is not well-formed UTF-8, so `raw` is a lossy rendering of it"},"rawBase64":{"description":"Base64 of the exact body bytes. Populated only when `isBinary`; null otherwise","type":["string","null"]}},"required":["id","organizationId","projectId","sourceId","maskId","raw","headers","payload","idempotencyKey","status","eventType","rejectionReason","verifiedWith","urlQueryParams","acknowledgedAt","responseTimeMs","createdAt","isBinary","rawBase64"],"additionalProperties":false}},"nextCursor":{"type":["string","null"]},"hasMore":{"type":"boolean"}},"required":["items","nextCursor","hasMore"],"additionalProperties":false}}}}}}},"/events/has-any":{"get":{"operationId":"events.hasAny","summary":"Has Any Events","description":"Returns whether the active organization/project has ever received any events. Used to distinguish a true new-user empty state from an empty filter window.","tags":["Events"],"parameters":[{"in":"query","name":"sourceId","schema":{"description":"Scope the probe to a single source","type":"string"},"allowEmptyValue":true,"allowReserved":true}],"responses":{"200":{"description":"The request has succeeded.","content":{"application/json":{"schema":{"type":"object","properties":{"hasAny":{"type":"boolean"}},"required":["hasAny"],"additionalProperties":false}}}}}}},"/events/stream":{"get":{"operationId":"events.stream","summary":"Stream Events","description":"Streams events for the active organization/project as Server-Sent Events. The stream is forward-only: it starts at the moment of connection and emits events as they land; use the list endpoint for history. Pass `sourceId` to scope the stream to one source, or `after` to start from an earlier point (clamped to the last 24 hours). Resumption is standard SSE: every event carries an `id`, and reconnecting with a `Last-Event-ID` header picks the tail up exactly where it stopped, tie-breaks included. The stream closes after 30 minutes; reconnect rather than treating that as an error.","tags":["Events"],"parameters":[{"in":"query","name":"after","schema":{"description":"Only stream events created after this time","type":"string","format":"date-time","x-native-type":"date"},"allowEmptyValue":true,"allowReserved":true},{"in":"query","name":"sourceId","schema":{"description":"Only stream events for this source","type":"string"},"allowEmptyValue":true,"allowReserved":true}],"responses":{"200":{"description":"The event stream has been opened.","content":{"text/event-stream":{"schema":{"oneOf":[{"type":"object","properties":{"event":{"const":"message"},"data":{"type":"object","properties":{"id":{"type":"string","description":"Unique event identifier"},"organizationId":{"type":"string","description":"Organization that owns this event"},"projectId":{"type":"string","description":"Project this event belongs to"},"sourceId":{"type":"string","description":"Source that received this webhook"},"maskId":{"type":"string","description":"Mask ID used in the ingest URL"},"raw":{"type":"string","description":"Raw request body as received"},"headers":{"anyOf":[{"type":["string","number","boolean","null"]},{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},{"type":"array","items":{}}],"description":"Request headers from the webhook"},"payload":{"anyOf":[{"type":["string","number","boolean","null"]},{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},{"type":"array","items":{}}],"description":"Parsed JSON payload"},"idempotencyKey":{"anyOf":[{"type":"string","description":"Computed idempotency key for deduplication"},{"type":"null"}]},"status":{"type":"string","enum":["pending","received","filtered","duplicate","rejected","unmatched"],"description":"Event processing status"},"eventType":{"anyOf":[{"type":"string","description":"Resolved event type at ingest time (e.g. extracted field value or email.received)"},{"type":"null"}]},"rejectionReason":{"anyOf":[{"type":"string","description":"Why a `rejected` event was rejected — one of signature_mismatch, missing_signature_header, missing_timestamp_header, malformed_signature_header, malformed_timestamp, timestamp_out_of_tolerance, verifier_misconfigured, unsupported_verifier. Null on every other status. Left as free text on the read side so a value added later never fails an entire list response"},{"type":"null"}]},"verifiedWith":{"anyOf":[{"type":"string","description":"The verifier that RAN at ingest, snapshotted so editing the source never relabels history. Present on rejected events too, since a verifier ran there as well — read `status` to learn whether it PASSED, never the presence of this field. 'noop' means none was configured; null means the event predates this column and its verification outcome is unknown, not absent"},{"type":"null"}]},"urlQueryParams":{"type":"string","description":"Query parameters from the ingest URL"},"acknowledgedAt":{"anyOf":[{"type":"string","format":"date-time","x-native-type":"date","description":"When the event was acknowledged"},{"type":"null"}]},"responseTimeMs":{"anyOf":[{"type":"integer","minimum":-2147483648,"maximum":2147483647},{"type":"null"}]},"createdAt":{"type":"string","format":"date-time","x-native-type":"date","description":"When the event was received"},"isBinary":{"type":"boolean","description":"True when the body is not well-formed UTF-8, so `raw` is a lossy rendering of it"},"rawBase64":{"description":"Base64 of the exact body bytes. Populated only when `isBinary`; null otherwise","type":["string","null"]}},"required":["id","organizationId","projectId","sourceId","maskId","raw","headers","payload","idempotencyKey","status","eventType","rejectionReason","verifiedWith","urlQueryParams","acknowledgedAt","responseTimeMs","createdAt","isBinary","rawBase64"],"additionalProperties":false},"id":{"type":"string"},"retry":{"type":"number"}},"required":["event","data"]},{"type":"object","properties":{"event":{"const":"close"},"data":{},"id":{"type":"string"},"retry":{"type":"number"}},"required":["event"]},{"type":"object","properties":{"event":{"const":"error"},"data":{},"id":{"type":"string"},"retry":{"type":"number"}},"required":["event"]}]}}}}}}},"/events/{id}":{"get":{"operationId":"events.get","summary":"Get Event","description":"Get an event by ID for the organization, including its deliveries.","tags":["Events"],"parameters":[{"in":"path","required":true,"name":"id","schema":{"type":"string"}}],"responses":{"200":{"description":"The request has succeeded.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Unique event identifier"},"organizationId":{"type":"string","description":"Organization that owns this event"},"projectId":{"type":"string","description":"Project this event belongs to"},"sourceId":{"type":"string","description":"Source that received this webhook"},"maskId":{"type":"string","description":"Mask ID used in the ingest URL"},"raw":{"type":"string","description":"Raw request body as received"},"headers":{"anyOf":[{"type":["string","number","boolean","null"]},{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},{"type":"array","items":{}}],"description":"Request headers from the webhook"},"payload":{"anyOf":[{"type":["string","number","boolean","null"]},{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},{"type":"array","items":{}}],"description":"Parsed JSON payload"},"idempotencyKey":{"anyOf":[{"type":"string","description":"Computed idempotency key for deduplication"},{"type":"null"}]},"status":{"type":"string","enum":["pending","received","filtered","duplicate","rejected","unmatched"],"description":"Event processing status"},"eventType":{"anyOf":[{"type":"string","description":"Resolved event type at ingest time (e.g. extracted field value or email.received)"},{"type":"null"}]},"rejectionReason":{"anyOf":[{"type":"string","description":"Why a `rejected` event was rejected — one of signature_mismatch, missing_signature_header, missing_timestamp_header, malformed_signature_header, malformed_timestamp, timestamp_out_of_tolerance, verifier_misconfigured, unsupported_verifier. Null on every other status. Left as free text on the read side so a value added later never fails an entire list response"},{"type":"null"}]},"verifiedWith":{"anyOf":[{"type":"string","description":"The verifier that RAN at ingest, snapshotted so editing the source never relabels history. Present on rejected events too, since a verifier ran there as well — read `status` to learn whether it PASSED, never the presence of this field. 'noop' means none was configured; null means the event predates this column and its verification outcome is unknown, not absent"},{"type":"null"}]},"urlQueryParams":{"type":"string","description":"Query parameters from the ingest URL"},"acknowledgedAt":{"anyOf":[{"type":"string","format":"date-time","x-native-type":"date","description":"When the event was acknowledged"},{"type":"null"}]},"responseTimeMs":{"anyOf":[{"type":"integer","minimum":-2147483648,"maximum":2147483647},{"type":"null"}]},"createdAt":{"type":"string","format":"date-time","x-native-type":"date","description":"When the event was received"},"isBinary":{"type":"boolean","description":"True when the body is not well-formed UTF-8, so `raw` is a lossy rendering of it"},"rawBase64":{"description":"Base64 of the exact body bytes. Populated only when `isBinary`; null otherwise","type":["string","null"]},"deliveries":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Unique delivery identifier"},"organizationId":{"type":"string","description":"Organization that owns this delivery"},"projectId":{"type":"string","description":"Project this delivery belongs to"},"eventId":{"type":"string","description":"Event that triggered this delivery"},"sourceId":{"type":"string","description":"Source that received the original webhook"},"destinationId":{"type":"string","description":"Destination endpoint to deliver to"},"connectionId":{"anyOf":[{"type":"string","description":"Connection that routed this delivery"},{"type":"null"}]},"replayId":{"anyOf":[{"type":"string","description":"The replay that created this delivery, or null for the original fan-out. Without it a replayed delivery is indistinguishable from the first one — the same event simply appears to have delivered twice"},{"type":"null"}]},"status":{"type":"string","enum":["pending","success","failed","throttled","skipped","filtered"],"description":"Delivery status. `pending` and `throttled` are transient — the delivery is queued, awaiting its next attempt. `success` and `failed` are outcomes of an attempt that was actually made. `skipped` (the route was disabled) and `filtered` (the event type is no longer in the connection's allowlist) are TERMINAL non-attempts: nothing was sent, and neither is a failure. Note `failed` collapses will-retry and exhausted into one value — `nextRetryAt` is what separates them"},"attempts":{"type":"integer","minimum":-2147483648,"maximum":2147483647,"description":"Number of delivery attempts made"},"lastAttemptAt":{"anyOf":[{"type":"string","format":"date-time","x-native-type":"date","description":"Timestamp of the most recent attempt"},{"type":"null"}]},"throttledAt":{"anyOf":[{"type":"string","format":"date-time","x-native-type":"date","description":"When this delivery was first held by a rate limit, if it ever was"},{"type":"null"}]},"nextAttemptAt":{"anyOf":[{"type":"string","format":"date-time","x-native-type":"date","description":"When the next attempt is due. Null means no further attempts — either the delivery is finished or its retry plan is spent."},{"type":"null"}]},"throttledReason":{"anyOf":[{"type":"string","enum":["rate_limit","destination_busy"],"description":"Why the delivery is currently parked: rate_limit (our pacing) or destination_busy (the destination returned 429/502/504). Null when it is not parked."},{"type":"null"}]},"responseTimeMs":{"anyOf":[{"type":"integer","minimum":-2147483648,"maximum":2147483647},{"type":"null"}]},"responseStatus":{"anyOf":[{"type":"integer","minimum":-2147483648,"maximum":2147483647,"description":"HTTP response status code from destination"},{"type":"null"}]},"responseBody":{"anyOf":[{"type":"string","description":"HTTP response body from destination"},{"type":"null"}]},"requestBody":{"anyOf":[{"type":"string","description":"HTTP request body sent to destination"},{"type":"null"}]},"requestHeaders":{"anyOf":[{"anyOf":[{"type":["string","number","boolean","null"]},{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},{"type":"array","items":{}}],"description":"HTTP request headers sent to destination"},{"type":"null"}]},"responseHeaders":{"anyOf":[{"anyOf":[{"type":["string","number","boolean","null"]},{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},{"type":"array","items":{}}],"description":"HTTP response headers received from destination"},{"type":"null"}]},"error":{"anyOf":[{"type":"string","description":"Error message if delivery failed"},{"type":"null"}]},"createdAt":{"type":"string","format":"date-time","x-native-type":"date","description":"When the delivery was created"},"updatedAt":{"type":"string","format":"date-time","x-native-type":"date","description":"When the delivery was last updated"}},"required":["id","organizationId","projectId","eventId","sourceId","destinationId","connectionId","replayId","status","attempts","lastAttemptAt","throttledAt","nextAttemptAt","throttledReason","responseTimeMs","responseStatus","responseBody","requestBody","requestHeaders","responseHeaders","error","createdAt","updatedAt"],"additionalProperties":false}}},"required":["id","organizationId","projectId","sourceId","maskId","raw","headers","payload","idempotencyKey","status","eventType","rejectionReason","verifiedWith","urlQueryParams","acknowledgedAt","responseTimeMs","createdAt","isBinary","rawBase64","deliveries"],"additionalProperties":false}}}}}}},"/events/{id}/timeline":{"get":{"operationId":"events.timeline","summary":"Get Event Timeline","description":"An event's full journey, assembled: the ingest phases, then one branch per delivery carrying that delivery's own per-attempt history. This is the endpoint to reach for when a webhook went missing — the delivery row alone only holds the LATEST attempt's response, so it cannot answer what happened on attempts 1..n-1. **The fan-out is a tree, not a line:** one event produces one delivery per matched connection, and each has an independent attempt sequence. Two things to read carefully. The three ingest phases share ONE timestamp (`ingest.at`) because we record no per-phase timings — they are a derived state projection, not measurements, so do not read latency into them. The verification phase reports the verifier that actually ran, snapshotted at ingest on `events.verified_with`, so editing a source never relabels history. It is carried structurally as `verifier` on that phase and is set on a FAILURE too, so a rejection says which verifier rejected it — `status` is what reports the outcome. Events ingested before that column existed report the phase as `unknown` rather than guessing. Deliveries created before per-attempt history existed carry a single entry flagged `reconstructed`, synthesized from the delivery row.","tags":["Events"],"parameters":[{"in":"path","required":true,"name":"id","schema":{"type":"string","description":"ID of the event whose timeline to fetch"}}],"responses":{"200":{"description":"The request has succeeded.","content":{"application/json":{"schema":{"type":"object","properties":{"eventId":{"type":"string","description":"The event this timeline describes"},"sourceId":{"type":"string","description":"Source that received it"},"status":{"type":"string","description":"Current event status"},"createdAt":{"type":"string","format":"date-time","x-native-type":"date","description":"When the event was received"},"ingest":{"type":"object","properties":{"at":{"type":"string","format":"date-time","x-native-type":"date","description":"When the event was received. This ONE timestamp covers all three phases below — we do not record per-phase timings, so treat it as the group time and not as a measurement of any single phase"},"responseTimeMs":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}],"description":"How long we took to answer the source webhook caller, in ms"},"phases":{"type":"array","items":{"type":"object","properties":{"phase":{"type":"string","enum":["received","verification","routing"],"description":"Which ingest phase this entry describes, in pipeline order"},"status":{"type":"string","enum":["ok","failed","skipped","unknown"],"description":"How the phase resolved. `skipped` means the phase did not apply — a source with no verifier, or an event that never reached routing. `unknown` means the outcome was never recorded and is not guessed: it appears on the verification phase of events ingested before `events.verified_with` existed"},"detail":{"description":"Human-readable outcome, e.g. the rejection reason, the verifier that ran, or how many deliveries were fanned out","type":["string","null"]},"verifier":{"description":"The verifier recorded for this event, on the `verification` phase only — null on every other phase. Set whatever the outcome was, INCLUDING on a failure, so a rejection says which verifier rejected it; `status` is what reports the outcome. 'noop' means none was configured, and null on the verification phase means the event predates the recording","type":["string","null"]}},"required":["phase","status","detail","verifier"],"additionalProperties":false},"description":"Ingest phases in pipeline order"}},"required":["at","responseTimeMs","phases"],"additionalProperties":false},"deliveries":{"type":"array","items":{"type":"object","properties":{"deliveryId":{"type":"string","description":"The delivery this branch describes"},"destinationId":{"type":"string","description":"Destination this delivery targets"},"connectionId":{"description":"Connection that routed it, if it still exists","type":["string","null"]},"status":{"type":"string","description":"Current delivery status. Note this collapses failed-will-retry and exhausted into one `failed` value; `nextRetryAt` on the last attempt is what separates them"},"attempts":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"The delivery row's own counter, which mirrors the CURRENT queue job and is NOT a lifetime total: a manual retry mints a new job, so it restarts at 1 while the earlier attempts remain in `timeline`. Use `recordedAttempts` for how many attempts are actually recorded here."},"recordedAttempts":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"How many attempt entries this branch carries. Differs from `attempts` after a manual retry, and is 1 for a reconstructed pre-history delivery regardless of how many attempts it really made."},"truncated":{"type":"boolean","description":"True when this delivery has more attempts than the timeline returns. A delivery parked repeatedly by a rate limit can accrue far more rows than are useful inline — page the full history from `deliveries.attempts` when this is set."},"createdAt":{"type":"string","format":"date-time","x-native-type":"date","description":"When the delivery was created — the start of the queued gap before attempt 1"},"lastAttemptAt":{"anyOf":[{"type":"string","format":"date-time","x-native-type":"date"},{"type":"null"}],"description":"When the most recent attempt ran"},"nextAttemptAt":{"anyOf":[{"type":"string","format":"date-time","x-native-type":"date"},{"type":"null"}],"description":"When the next attempt is due, null when there is not one"},"throttledAt":{"anyOf":[{"type":"string","format":"date-time","x-native-type":"date"},{"type":"null"}],"description":"When this delivery was first held by a rate limit, if it ever was"},"replay":{"anyOf":[{"type":"object","properties":{"id":{"type":"string","description":"The replay that created this delivery"},"at":{"type":"string","format":"date-time","x-native-type":"date","description":"When the replay was run"},"actorType":{"type":"string","description":"Which kind of principal ran it: user, api_key or agent"},"actorEmail":{"description":"Email of the user who ran it, snapshotted at replay time. Null for API-key and agent replays","type":["string","null"]}},"required":["id","at","actorType","actorEmail"],"additionalProperties":false},{"type":"null"}],"description":"Set when this delivery came from a replay rather than the original fan-out. Null on the original. Both look identical on the event otherwise, so this is what says why the same event delivered twice"},"timeline":{"type":"array","items":{"type":"object","properties":{"id":{"anyOf":[{"type":"string","description":"Unique attempt identifier"},{"type":"null"}],"description":"Attempt identifier, or null for a reconstructed entry, which has no stored row to identify"},"organizationId":{"type":"string","description":"Organization that owns this attempt"},"projectId":{"type":"string","description":"Project this attempt belongs to"},"deliveryId":{"type":"string","description":"Delivery this attempt belongs to"},"attemptNumber":{"type":"integer","minimum":-2147483648,"maximum":2147483647,"description":"Which attempt this was, 1-based. NOT unique within a delivery: a throttled attempt consumes no retry budget, so it shares its number with the attempt that follows it. Order by startedAt, not by this."},"status":{"type":"string","enum":["pending","success","failed","throttled","skipped","filtered","interrupted"],"description":"Outcome of this attempt: pending (in flight), success, failed, throttled (held by a rate limit or by the destination's own Retry-After), skipped (the route was disabled after this delivery was queued), filtered (the event type is no longer in the connection's allowlist), or interrupted. A skipped or filtered attempt now leaves the DELIVERY row reading `skipped` / `filtered` too — still terminal, because an intentional skip must not be retried, but no longer claiming success. `interrupted` is NOT terminal and does not mean the destination failed: it means no outcome was ever recorded for the attempt, because the worker stopped or a park write was lost. If that execution was only paused and later resumes, it replaces this with its real outcome."},"trigger":{"type":"string","enum":["initial","auto_retry","manual_retry"],"description":"What caused this attempt: initial (the first send), auto_retry (the retry schedule) or manual_retry (someone asked for it). Only the first attempt of a manually retried delivery is manual_retry; its own subsequent retries are auto_retry."},"startedAt":{"type":"string","format":"date-time","x-native-type":"date","description":"When this attempt began. The stable sort key for a delivery timeline, with id breaking ties."},"finishedAt":{"anyOf":[{"type":"string","format":"date-time","x-native-type":"date","description":"When this attempt finished. Null while it is still in flight."},{"type":"null"}]},"responseStatus":{"anyOf":[{"type":"integer","minimum":-2147483648,"maximum":2147483647,"description":"HTTP status the destination returned on this attempt"},{"type":"null"}]},"responseTimeMs":{"anyOf":[{"type":"integer","minimum":-2147483648,"maximum":2147483647,"description":"How long the destination took to respond, in ms"},{"type":"null"}]},"responseHeaders":{"anyOf":[{"anyOf":[{"type":["string","number","boolean","null"]},{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},{"type":"array","items":{}}],"description":"Response headers from this attempt, with sensitive values redacted"},{"type":"null"}]},"responseBody":{"anyOf":[{"type":"string","description":"Response body from this attempt, NUL-stripped and truncated to 4KB. The delivery row keeps the full body for the latest attempt."},{"type":"null"}]},"error":{"anyOf":[{"type":"string","description":"Why this attempt failed, if it did"},{"type":"null"}]},"nextRetryAt":{"anyOf":[{"type":"string","format":"date-time","x-native-type":"date","description":"When the next attempt was scheduled for. Null on the final attempt — which is what separates a delivery still working through its retry schedule from one that has given up."},{"type":"null"}]},"actorType":{"anyOf":[{"type":"string","enum":["user","api_key","agent"],"description":"Which kind of principal asked for this attempt: user, api_key or agent. Only ever set on the first attempt of a MANUAL retry — every other attempt is the retry schedule doing its job, and nobody asked for it individually"},{"type":"null"}]},"actorId":{"anyOf":[{"type":"string","description":"User id, API key id, or agent id, depending on actorType"},{"type":"null"}]},"actorEmail":{"anyOf":[{"type":"string","description":"Email of the user who asked for the retry, snapshotted at the time so a later rename or deletion cannot rewrite it. Null for API-key and agent retries, and for every attempt nobody asked for"},{"type":"null"}]},"createdAt":{"type":"string","format":"date-time","x-native-type":"date","description":"When this attempt row was written"},"reconstructed":{"type":"boolean","description":"True when this entry was synthesized from the delivery row rather than read from a stored attempt. Deliveries that predate per-attempt history have a non-zero attempt count and no attempt rows, so a single entry stands in for all of them; it carries only the LAST attempt's response, which is all that was ever kept. These age out as retention deletes the old deliveries"}},"required":["id","organizationId","projectId","deliveryId","attemptNumber","status","trigger","startedAt","finishedAt","responseStatus","responseTimeMs","responseHeaders","responseBody","error","nextRetryAt","actorType","actorId","actorEmail","createdAt","reconstructed"],"additionalProperties":false},"description":"This delivery's attempts, oldest first, ordered by startedAt with id breaking ties"}},"required":["deliveryId","destinationId","connectionId","status","attempts","recordedAttempts","truncated","createdAt","lastAttemptAt","nextAttemptAt","throttledAt","replay","timeline"],"additionalProperties":false},"description":"One branch per delivery fanned out from this event"}},"required":["eventId","sourceId","status","createdAt","ingest","deliveries"],"additionalProperties":false}}}}}}},"/events/{id}/replay":{"post":{"operationId":"events.replay","summary":"Replay Event","description":"Re-deliver an event to all currently active connections for its source. Each call is recorded with the principal that made it, and every delivery it creates points back at that record — read it as `replay` on the delivery branch of `events.timeline`, or `replayId` on the delivery itself. Replays are billed: they create new deliveries.","tags":["Events"],"parameters":[{"in":"path","required":true,"name":"id","schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{}}}}},"responses":{"200":{"description":"Deliveries have been queued.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"deliveriesCreated":{"type":"number"}},"required":["ok","deliveriesCreated"],"additionalProperties":false}}}}}}},"/deliveries":{"get":{"operationId":"deliveries.list","summary":"List Deliveries","description":"List deliveries with optional filters for status, source, destination, connection, event, and date range.","tags":["Deliveries"],"parameters":[{"in":"query","name":"cursor","schema":{"description":"Cursor for pagination","type":"string"},"allowEmptyValue":true,"allowReserved":true},{"in":"query","name":"limit","schema":{"default":20,"description":"Number of results per page","type":"integer","minimum":1,"maximum":100},"allowEmptyValue":true,"allowReserved":true},{"in":"query","name":"search","schema":{"description":"Search term to filter results","type":"string","maxLength":255},"allowEmptyValue":true,"allowReserved":true},{"in":"query","name":"sortBy","schema":{"description":"Field to sort by","type":"string"},"allowEmptyValue":true,"allowReserved":true},{"in":"query","name":"sortDir","schema":{"description":"Sort direction","default":"desc","type":"string","enum":["asc","desc"]},"allowEmptyValue":true,"allowReserved":true},{"in":"query","name":"eventId","schema":{"description":"Filter deliveries by event ID","type":"string"},"allowEmptyValue":true,"allowReserved":true},{"in":"query","name":"sourceId","schema":{"description":"Filter deliveries by source ID","type":"string"},"allowEmptyValue":true,"allowReserved":true},{"in":"query","name":"destinationId","schema":{"description":"Filter deliveries by destination ID","type":"string"},"allowEmptyValue":true,"allowReserved":true},{"in":"query","name":"connectionId","schema":{"description":"Filter deliveries by connection ID","type":"string"},"allowEmptyValue":true,"allowReserved":true},{"in":"query","name":"status","schema":{"description":"Filter by delivery status","type":"string","enum":["pending","throttled","success","failed","skipped","filtered"]},"allowEmptyValue":true,"allowReserved":true},{"in":"query","name":"from","schema":{"description":"Filter deliveries created on or after this date","type":"string","format":"date-time","x-native-type":"date"},"allowEmptyValue":true,"allowReserved":true},{"in":"query","name":"to","schema":{"description":"Filter deliveries created on or before this date","type":"string","format":"date-time","x-native-type":"date"},"allowEmptyValue":true,"allowReserved":true}],"responses":{"200":{"description":"The request has succeeded.","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Unique delivery identifier"},"organizationId":{"type":"string","description":"Organization that owns this delivery"},"projectId":{"type":"string","description":"Project this delivery belongs to"},"eventId":{"type":"string","description":"Event that triggered this delivery"},"sourceId":{"type":"string","description":"Source that received the original webhook"},"destinationId":{"type":"string","description":"Destination endpoint to deliver to"},"connectionId":{"anyOf":[{"type":"string","description":"Connection that routed this delivery"},{"type":"null"}]},"replayId":{"anyOf":[{"type":"string","description":"The replay that created this delivery, or null for the original fan-out. Without it a replayed delivery is indistinguishable from the first one — the same event simply appears to have delivered twice"},{"type":"null"}]},"status":{"type":"string","enum":["pending","success","failed","throttled","skipped","filtered"],"description":"Delivery status. `pending` and `throttled` are transient — the delivery is queued, awaiting its next attempt. `success` and `failed` are outcomes of an attempt that was actually made. `skipped` (the route was disabled) and `filtered` (the event type is no longer in the connection's allowlist) are TERMINAL non-attempts: nothing was sent, and neither is a failure. Note `failed` collapses will-retry and exhausted into one value — `nextRetryAt` is what separates them"},"attempts":{"type":"integer","minimum":-2147483648,"maximum":2147483647,"description":"Number of delivery attempts made"},"lastAttemptAt":{"anyOf":[{"type":"string","format":"date-time","x-native-type":"date","description":"Timestamp of the most recent attempt"},{"type":"null"}]},"throttledAt":{"anyOf":[{"type":"string","format":"date-time","x-native-type":"date","description":"When this delivery was first held by a rate limit, if it ever was"},{"type":"null"}]},"nextAttemptAt":{"anyOf":[{"type":"string","format":"date-time","x-native-type":"date","description":"When the next attempt is due. Null means no further attempts — either the delivery is finished or its retry plan is spent."},{"type":"null"}]},"throttledReason":{"anyOf":[{"type":"string","enum":["rate_limit","destination_busy"],"description":"Why the delivery is currently parked: rate_limit (our pacing) or destination_busy (the destination returned 429/502/504). Null when it is not parked."},{"type":"null"}]},"responseTimeMs":{"anyOf":[{"type":"integer","minimum":-2147483648,"maximum":2147483647},{"type":"null"}]},"responseStatus":{"anyOf":[{"type":"integer","minimum":-2147483648,"maximum":2147483647,"description":"HTTP response status code from destination"},{"type":"null"}]},"responseBody":{"anyOf":[{"type":"string","description":"HTTP response body from destination"},{"type":"null"}]},"requestBody":{"anyOf":[{"type":"string","description":"HTTP request body sent to destination"},{"type":"null"}]},"requestHeaders":{"anyOf":[{"anyOf":[{"type":["string","number","boolean","null"]},{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},{"type":"array","items":{}}],"description":"HTTP request headers sent to destination"},{"type":"null"}]},"responseHeaders":{"anyOf":[{"anyOf":[{"type":["string","number","boolean","null"]},{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},{"type":"array","items":{}}],"description":"HTTP response headers received from destination"},{"type":"null"}]},"error":{"anyOf":[{"type":"string","description":"Error message if delivery failed"},{"type":"null"}]},"createdAt":{"type":"string","format":"date-time","x-native-type":"date","description":"When the delivery was created"},"updatedAt":{"type":"string","format":"date-time","x-native-type":"date","description":"When the delivery was last updated"}},"required":["id","organizationId","projectId","eventId","sourceId","destinationId","connectionId","replayId","status","attempts","lastAttemptAt","throttledAt","nextAttemptAt","throttledReason","responseTimeMs","responseStatus","responseBody","requestBody","requestHeaders","responseHeaders","error","createdAt","updatedAt"],"additionalProperties":false}},"nextCursor":{"type":["string","null"]},"hasMore":{"type":"boolean"}},"required":["items","nextCursor","hasMore"],"additionalProperties":false}}}}}}},"/deliveries/stream":{"get":{"operationId":"deliveries.stream","summary":"Stream Deliveries","description":"Streams delivery activity for the active organization/project as Server-Sent Events. The stream is forward-only: it starts at the moment of connection and emits activity as it lands; use the list endpoint for history. A delivery is emitted when it is created and again on every attempt, so the same id recurs as it progresses — replace by id rather than appending. A manually retried delivery re-enters the stream when its next attempt starts, not at the moment it is re-queued. Pass `eventId` or `status` to scope the stream, or `after` to start from an earlier point (matched against activity time, clamped to the last 24 hours). Resumption is standard SSE: every event carries an `id`, and reconnecting with a `Last-Event-ID` header picks the tail up exactly where it stopped. The stream closes after 30 minutes; reconnect rather than treating that as an error.","tags":["Deliveries"],"parameters":[{"in":"query","name":"after","schema":{"description":"Only stream deliveries with activity after this time","type":"string","format":"date-time","x-native-type":"date"},"allowEmptyValue":true,"allowReserved":true},{"in":"query","name":"eventId","schema":{"description":"Only stream deliveries for this event","type":"string"},"allowEmptyValue":true,"allowReserved":true},{"in":"query","name":"status","schema":{"description":"Only stream deliveries in this status","type":"string"},"allowEmptyValue":true,"allowReserved":true}],"responses":{"200":{"description":"The delivery stream has been opened.","content":{"text/event-stream":{"schema":{"oneOf":[{"type":"object","properties":{"event":{"const":"message"},"data":{"type":"object","properties":{"id":{"type":"string","description":"Unique delivery identifier"},"organizationId":{"type":"string","description":"Organization that owns this delivery"},"projectId":{"type":"string","description":"Project this delivery belongs to"},"eventId":{"type":"string","description":"Event that triggered this delivery"},"sourceId":{"type":"string","description":"Source that received the original webhook"},"destinationId":{"type":"string","description":"Destination endpoint to deliver to"},"connectionId":{"anyOf":[{"type":"string","description":"Connection that routed this delivery"},{"type":"null"}]},"replayId":{"anyOf":[{"type":"string","description":"The replay that created this delivery, or null for the original fan-out. Without it a replayed delivery is indistinguishable from the first one — the same event simply appears to have delivered twice"},{"type":"null"}]},"status":{"type":"string","enum":["pending","success","failed","throttled","skipped","filtered"],"description":"Delivery status. `pending` and `throttled` are transient — the delivery is queued, awaiting its next attempt. `success` and `failed` are outcomes of an attempt that was actually made. `skipped` (the route was disabled) and `filtered` (the event type is no longer in the connection's allowlist) are TERMINAL non-attempts: nothing was sent, and neither is a failure. Note `failed` collapses will-retry and exhausted into one value — `nextRetryAt` is what separates them"},"attempts":{"type":"integer","minimum":-2147483648,"maximum":2147483647,"description":"Number of delivery attempts made"},"lastAttemptAt":{"anyOf":[{"type":"string","format":"date-time","x-native-type":"date","description":"Timestamp of the most recent attempt"},{"type":"null"}]},"throttledAt":{"anyOf":[{"type":"string","format":"date-time","x-native-type":"date","description":"When this delivery was first held by a rate limit, if it ever was"},{"type":"null"}]},"nextAttemptAt":{"anyOf":[{"type":"string","format":"date-time","x-native-type":"date","description":"When the next attempt is due. Null means no further attempts — either the delivery is finished or its retry plan is spent."},{"type":"null"}]},"throttledReason":{"anyOf":[{"type":"string","enum":["rate_limit","destination_busy"],"description":"Why the delivery is currently parked: rate_limit (our pacing) or destination_busy (the destination returned 429/502/504). Null when it is not parked."},{"type":"null"}]},"responseTimeMs":{"anyOf":[{"type":"integer","minimum":-2147483648,"maximum":2147483647},{"type":"null"}]},"responseStatus":{"anyOf":[{"type":"integer","minimum":-2147483648,"maximum":2147483647,"description":"HTTP response status code from destination"},{"type":"null"}]},"responseBody":{"anyOf":[{"type":"string","description":"HTTP response body from destination"},{"type":"null"}]},"requestBody":{"anyOf":[{"type":"string","description":"HTTP request body sent to destination"},{"type":"null"}]},"requestHeaders":{"anyOf":[{"anyOf":[{"type":["string","number","boolean","null"]},{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},{"type":"array","items":{}}],"description":"HTTP request headers sent to destination"},{"type":"null"}]},"responseHeaders":{"anyOf":[{"anyOf":[{"type":["string","number","boolean","null"]},{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},{"type":"array","items":{}}],"description":"HTTP response headers received from destination"},{"type":"null"}]},"error":{"anyOf":[{"type":"string","description":"Error message if delivery failed"},{"type":"null"}]},"createdAt":{"type":"string","format":"date-time","x-native-type":"date","description":"When the delivery was created"},"updatedAt":{"type":"string","format":"date-time","x-native-type":"date","description":"When the delivery was last updated"}},"required":["id","organizationId","projectId","eventId","sourceId","destinationId","connectionId","replayId","status","attempts","lastAttemptAt","throttledAt","nextAttemptAt","throttledReason","responseTimeMs","responseStatus","responseBody","requestBody","requestHeaders","responseHeaders","error","createdAt","updatedAt"],"additionalProperties":false},"id":{"type":"string"},"retry":{"type":"number"}},"required":["event","data"]},{"type":"object","properties":{"event":{"const":"close"},"data":{},"id":{"type":"string"},"retry":{"type":"number"}},"required":["event"]},{"type":"object","properties":{"event":{"const":"error"},"data":{},"id":{"type":"string"},"retry":{"type":"number"}},"required":["event"]}]}}}}}}},"/deliveries/has-any":{"get":{"operationId":"deliveries.hasAny","summary":"Has Any Deliveries","description":"Returns whether the active organization/project has ever recorded any deliveries. Used to distinguish a true new-user empty state from an empty filter window.","tags":["Deliveries"],"responses":{"200":{"description":"The request has succeeded.","content":{"application/json":{"schema":{"type":"object","properties":{"hasAny":{"type":"boolean"}},"required":["hasAny"],"additionalProperties":false}}}}}}},"/deliveries/stats":{"get":{"operationId":"deliveries.stats","summary":"Delivery Queue Stats","description":"Live snapshot of deliveries currently queued for the active organization/project, split into pending (awaiting their next attempt) and throttled (held by a rate limit). Use it to drive dashboard or agent polling for queue backlog and rate-limit pressure. Historical success/failure counts are windowed and live in the dashboard stats, not here.","tags":["Deliveries"],"responses":{"200":{"description":"The request has succeeded.","content":{"application/json":{"schema":{"type":"object","properties":{"pending":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Deliveries queued and moving right now, awaiting their next attempt"},"throttled":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Deliveries held right now because a rate limit is blocking them; historical throttling lives on each delivery row's throttled_at"}},"required":["pending","throttled"],"additionalProperties":false}}}}}}},"/deliveries/{deliveryId}/attempts":{"get":{"operationId":"deliveries.attempts","summary":"List Delivery Attempts","description":"The durable, per-attempt history of one delivery, oldest first. The delivery row itself only carries the LATEST attempt's response — every attempt overwrites the one before it — so this is the only record of what happened on earlier attempts, which is what you need when a webhook went missing. A `skipped` or `filtered` attempt is a TERMINAL non-send — a disabled route, or an event type no longer in the connection's allowlist — and the delivery row now carries the same value rather than `success`. Both stay terminal because an intentional skip must not be retried; neither is a failure. Order is by `startedAt` and NOT by `attemptNumber`, which is not unique: a throttled attempt consumes no retry budget and shares its number with the attempt that follows it. `nextRetryAt` on the final row separates a delivery still working through its retry schedule from one that has given up. Deliveries created before per-attempt history was recorded return no rows; use `events.timeline`, which reconstructs a single entry for them from the delivery row.","tags":["Deliveries"],"parameters":[{"in":"path","required":true,"name":"deliveryId","schema":{"type":"string","description":"Delivery whose attempts to list"}},{"in":"query","name":"cursor","schema":{"description":"Opaque cursor from the previous page's nextCursor; pass it back unchanged","type":"string"},"allowEmptyValue":true,"allowReserved":true},{"in":"query","name":"limit","schema":{"default":20,"description":"Number of results per page","type":"integer","minimum":1,"maximum":100},"allowEmptyValue":true,"allowReserved":true}],"responses":{"200":{"description":"The request has succeeded.","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Unique attempt identifier"},"organizationId":{"type":"string","description":"Organization that owns this attempt"},"projectId":{"type":"string","description":"Project this attempt belongs to"},"deliveryId":{"type":"string","description":"Delivery this attempt belongs to"},"attemptNumber":{"type":"integer","minimum":-2147483648,"maximum":2147483647,"description":"Which attempt this was, 1-based. NOT unique within a delivery: a throttled attempt consumes no retry budget, so it shares its number with the attempt that follows it. Order by startedAt, not by this."},"status":{"type":"string","enum":["pending","success","failed","throttled","skipped","filtered","interrupted"],"description":"Outcome of this attempt: pending (in flight), success, failed, throttled (held by a rate limit or by the destination's own Retry-After), skipped (the route was disabled after this delivery was queued), filtered (the event type is no longer in the connection's allowlist), or interrupted. A skipped or filtered attempt now leaves the DELIVERY row reading `skipped` / `filtered` too — still terminal, because an intentional skip must not be retried, but no longer claiming success. `interrupted` is NOT terminal and does not mean the destination failed: it means no outcome was ever recorded for the attempt, because the worker stopped or a park write was lost. If that execution was only paused and later resumes, it replaces this with its real outcome."},"trigger":{"type":"string","enum":["initial","auto_retry","manual_retry"],"description":"What caused this attempt: initial (the first send), auto_retry (the retry schedule) or manual_retry (someone asked for it). Only the first attempt of a manually retried delivery is manual_retry; its own subsequent retries are auto_retry."},"startedAt":{"type":"string","format":"date-time","x-native-type":"date","description":"When this attempt began. The stable sort key for a delivery timeline, with id breaking ties."},"finishedAt":{"anyOf":[{"type":"string","format":"date-time","x-native-type":"date","description":"When this attempt finished. Null while it is still in flight."},{"type":"null"}]},"responseStatus":{"anyOf":[{"type":"integer","minimum":-2147483648,"maximum":2147483647,"description":"HTTP status the destination returned on this attempt"},{"type":"null"}]},"responseTimeMs":{"anyOf":[{"type":"integer","minimum":-2147483648,"maximum":2147483647,"description":"How long the destination took to respond, in ms"},{"type":"null"}]},"responseHeaders":{"anyOf":[{"anyOf":[{"type":["string","number","boolean","null"]},{"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}},{"type":"array","items":{}}],"description":"Response headers from this attempt, with sensitive values redacted"},{"type":"null"}]},"responseBody":{"anyOf":[{"type":"string","description":"Response body from this attempt, NUL-stripped and truncated to 4KB. The delivery row keeps the full body for the latest attempt."},{"type":"null"}]},"error":{"anyOf":[{"type":"string","description":"Why this attempt failed, if it did"},{"type":"null"}]},"nextRetryAt":{"anyOf":[{"type":"string","format":"date-time","x-native-type":"date","description":"When the next attempt was scheduled for. Null on the final attempt — which is what separates a delivery still working through its retry schedule from one that has given up."},{"type":"null"}]},"actorType":{"anyOf":[{"type":"string","enum":["user","api_key","agent"],"description":"Which kind of principal asked for this attempt: user, api_key or agent. Only ever set on the first attempt of a MANUAL retry — every other attempt is the retry schedule doing its job, and nobody asked for it individually"},{"type":"null"}]},"actorId":{"anyOf":[{"type":"string","description":"User id, API key id, or agent id, depending on actorType"},{"type":"null"}]},"actorEmail":{"anyOf":[{"type":"string","description":"Email of the user who asked for the retry, snapshotted at the time so a later rename or deletion cannot rewrite it. Null for API-key and agent retries, and for every attempt nobody asked for"},{"type":"null"}]},"createdAt":{"type":"string","format":"date-time","x-native-type":"date","description":"When this attempt row was written"}},"required":["id","organizationId","projectId","deliveryId","attemptNumber","status","trigger","startedAt","finishedAt","responseStatus","responseTimeMs","responseHeaders","responseBody","error","nextRetryAt","actorType","actorId","actorEmail","createdAt"],"additionalProperties":false}},"nextCursor":{"type":["string","null"]},"hasMore":{"type":"boolean"}},"required":["items","nextCursor","hasMore"],"additionalProperties":false}}}}}}},"/deliveries/{id}/retry":{"post":{"operationId":"deliveries.retry","summary":"Retry Delivery","description":"Retry a failed delivery by re-queuing it.","tags":["Deliveries"],"parameters":[{"in":"path","required":true,"name":"id","schema":{"type":"string","description":"ID of the delivery to retry"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{}}}}},"responses":{"200":{"description":"The delivery has been queued for retry.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"}},"required":["ok"],"additionalProperties":false}}}}}}},"/projects":{"get":{"operationId":"projects.list","summary":"List Projects","description":"List all projects for the active organization.","tags":["Projects"],"parameters":[{"in":"query","name":"cursor","schema":{"description":"Cursor for pagination","type":"string"},"allowEmptyValue":true,"allowReserved":true},{"in":"query","name":"limit","schema":{"default":20,"description":"Number of results per page","type":"integer","minimum":1,"maximum":100},"allowEmptyValue":true,"allowReserved":true},{"in":"query","name":"search","schema":{"description":"Search term to filter results","type":"string","maxLength":255},"allowEmptyValue":true,"allowReserved":true},{"in":"query","name":"sortBy","schema":{"description":"Field to sort by","type":"string"},"allowEmptyValue":true,"allowReserved":true},{"in":"query","name":"sortDir","schema":{"description":"Sort direction","default":"desc","type":"string","enum":["asc","desc"]},"allowEmptyValue":true,"allowReserved":true}],"responses":{"200":{"description":"The request has succeeded.","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Unique project identifier"},"organizationId":{"type":"string","description":"Organization that owns this project"},"name":{"type":"string","description":"Display name for the project"},"slug":{"type":"string","description":"URL-friendly slug"},"createdAt":{"type":"string","format":"date-time","x-native-type":"date","description":"When the project was created"},"updatedAt":{"type":"string","format":"date-time","x-native-type":"date","description":"When the project was last updated"}},"required":["id","organizationId","name","slug","createdAt","updatedAt"],"additionalProperties":false}},"nextCursor":{"type":["string","null"]},"hasMore":{"type":"boolean"}},"required":["items","nextCursor","hasMore"],"additionalProperties":false}}}}}},"post":{"operationId":"projects.create","summary":"Create Project","description":"Create a new project in the active organization.","tags":["Projects"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100,"description":"Display name for the project"},"slug":{"type":"string","minLength":1,"pattern":"^[a-z0-9-]+$","description":"URL-friendly slug"}},"required":["name","slug"]}}}},"responses":{"200":{"description":"The project has been created.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Unique project identifier"},"organizationId":{"type":"string","description":"Organization that owns this project"},"name":{"type":"string","description":"Display name for the project"},"slug":{"type":"string","description":"URL-friendly slug"},"createdAt":{"type":"string","format":"date-time","x-native-type":"date","description":"When the project was created"},"updatedAt":{"type":"string","format":"date-time","x-native-type":"date","description":"When the project was last updated"}},"required":["id","organizationId","name","slug","createdAt","updatedAt"],"additionalProperties":false}}}}}}},"/projects/{id}":{"get":{"operationId":"projects.get","summary":"Get Project","description":"Get a project by ID.","tags":["Projects"],"parameters":[{"in":"path","required":true,"name":"id","schema":{"type":"string"}}],"responses":{"200":{"description":"The request has succeeded.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Unique project identifier"},"organizationId":{"type":"string","description":"Organization that owns this project"},"name":{"type":"string","description":"Display name for the project"},"slug":{"type":"string","description":"URL-friendly slug"},"createdAt":{"type":"string","format":"date-time","x-native-type":"date","description":"When the project was created"},"updatedAt":{"type":"string","format":"date-time","x-native-type":"date","description":"When the project was last updated"}},"required":["id","organizationId","name","slug","createdAt","updatedAt"],"additionalProperties":false}}}}}},"patch":{"operationId":"projects.update","summary":"Update Project","description":"Update a project by ID.","tags":["Projects"],"parameters":[{"in":"path","required":true,"name":"id","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100,"description":"Display name for the project"},"slug":{"type":"string","minLength":1,"pattern":"^[a-z0-9-]+$","description":"URL-friendly slug"}},"required":["name"]}},"required":["data"]}}}},"responses":{"200":{"description":"The project has been updated.","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string","description":"Unique project identifier"},"organizationId":{"type":"string","description":"Organization that owns this project"},"name":{"type":"string","description":"Display name for the project"},"slug":{"type":"string","description":"URL-friendly slug"},"createdAt":{"type":"string","format":"date-time","x-native-type":"date","description":"When the project was created"},"updatedAt":{"type":"string","format":"date-time","x-native-type":"date","description":"When the project was last updated"}},"required":["id","organizationId","name","slug","createdAt","updatedAt"],"additionalProperties":false}}}}}},"delete":{"operationId":"projects.remove","summary":"Delete Project","description":"Delete a project and all its resources (sources, destinations, events, deliveries).","tags":["Projects"],"parameters":[{"in":"path","required":true,"name":"id","schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{}}}}},"responses":{"200":{"description":"The project has been deleted.","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"}},"required":["ok"],"additionalProperties":false}}}}}}},"/stats/dashboard":{"get":{"operationId":"stats.dashboard","summary":"Get Dashboard Stats","description":"Get aggregated dashboard statistics for the project.","tags":["Stats"],"parameters":[{"in":"query","name":"range","schema":{"description":"Time window for the dashboard stats","type":"string","enum":["1h","24h","7d"]},"allowEmptyValue":true,"allowReserved":true}],"responses":{"200":{"description":"The request has succeeded.","content":{"application/json":{"schema":{"type":"object","properties":{"range":{"type":"string","enum":["1h","24h","7d"],"description":"Time window the stats were computed over"},"bucketCount":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Number of buckets in the byBucket arrays"},"events":{"type":"object","properties":{"total":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Total events in the window"},"prev":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Total events in the prior window of equal length"},"byBucket":{"type":"array","items":{"type":"object","properties":{"bucket":{"type":"string","format":"date-time","x-native-type":"date","description":"Start of the time bucket (UTC)"},"pending":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"received":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"filtered":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"duplicate":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"rejected":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"unmatched":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"required":["bucket","pending","received","filtered","duplicate","rejected","unmatched"],"additionalProperties":false}},"breakdown":{"type":"object","properties":{"pending":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"received":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"filtered":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"duplicate":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"rejected":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"unmatched":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"required":["pending","received","filtered","duplicate","rejected","unmatched"],"additionalProperties":false},"acceptRate":{"description":"received / total (excludes pending); null if total = 0","type":["number","null"]}},"required":["total","prev","byBucket","breakdown","acceptRate"],"additionalProperties":false},"deliveries":{"type":"object","properties":{"total":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Total delivery attempts in the window"},"prev":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Total delivery attempts in the prior window of equal length"},"byBucket":{"type":"array","items":{"type":"object","properties":{"bucket":{"type":"string","format":"date-time","x-native-type":"date","description":"Start of the time bucket (UTC)"},"pending":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"success":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"failed":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"skipped":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Deliveries deliberately not sent: a disabled route, or an event type no longer in the allowlist"}},"required":["bucket","pending","success","failed","skipped"],"additionalProperties":false}},"breakdown":{"type":"object","properties":{"pending":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"success":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"failed":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"skipped":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Deliveries deliberately not sent — never attempted, so never a failure"}},"required":["pending","success","failed","skipped"],"additionalProperties":false},"successRate":{"description":"success / attempted, where attempted excludes pending, throttled and the deliberately-skipped. An intentional skip is neither a success nor a failure, so counting it either way misreports the rate. Null when nothing was attempted","type":["number","null"]},"latency":{"anyOf":[{"type":"object","properties":{"p50Ms":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"p95Ms":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"required":["p50Ms","p95Ms"],"additionalProperties":false},{"type":"null"}],"description":"Latency percentiles across successful deliveries in the window; null if no successes"}},"required":["total","prev","byBucket","breakdown","successRate","latency"],"additionalProperties":false},"destinationHealth":{"maxItems":10,"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"volume":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},"successRate":{"type":"number","description":"Success rate over the window (0–1)"},"p95Ms":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}]}},"required":["id","name","volume","successRate","p95Ms"],"additionalProperties":false},"description":"Top 10 destinations with traffic in the window, ordered worst-success-first then by volume desc"},"recentFailures":{"maxItems":10,"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"destinationId":{"type":"string"},"destinationName":{"type":"string"},"responseStatus":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}]},"error":{"type":["string","null"]},"lastAttemptAt":{"anyOf":[{"type":"string","format":"date-time","x-native-type":"date"},{"type":"null"}]}},"required":["id","destinationId","destinationName","responseStatus","error","lastAttemptAt"],"additionalProperties":false},"description":"Last 10 failed delivery attempts, newest first"}},"required":["range","bucketCount","events","deliveries","destinationHealth","recentFailures"],"additionalProperties":false}}}}}}},"/billing/plan":{"get":{"operationId":"billing.plan","summary":"Get Current Plan","description":"Get the active billing plan and effective entitlements (after per-organization overrides) for the organization.","tags":["Billing"],"responses":{"200":{"description":"The request has succeeded.","content":{"application/json":{"schema":{"type":"object","properties":{"planKey":{"type":"string","description":"Stable identifier of the active plan"},"planName":{"type":"string","description":"Display name of the active plan"},"currency":{"type":"string","enum":["gbp","usd","eur"],"description":"The organization's billing currency"},"priceCents":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}],"description":"Monthly price in `currency`'s minor unit. null = custom/sales-led."},"prices":{"type":"object","propertyNames":{"type":"string","enum":["gbp","usd","eur"],"description":"Presentment currency, lowercase ISO 4217. An organization bills in exactly one."},"additionalProperties":{"type":"object","properties":{"amountCents":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Monthly price in the currency's minor unit (pence/cents). 0 = free."},"overagePer100kCents":{"description":"Pay-as-you-go price per 100,000 overage deliveries, in the currency's minor unit. Absent on capped plans.","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"required":["amountCents"],"additionalProperties":false},"description":"Prices per currency. Empty = custom/sales-led (contact sales). A currency absent means the plan cannot be bought in it."},"source":{"type":"string","enum":["override","subscription","default"],"description":"How the plan was assigned: per-org override, Stripe subscription, or the default plan"},"entitlements":{"type":"object","properties":{"includedDeliveriesPerMonth":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}],"description":"Deliveries included per usage window — the billing period for subscribed orgs, the UTC calendar month otherwise. A delivery is one event routed to one connection. null = unlimited/custom."},"retentionDays":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}],"description":"Days of event history retained and replayable; null = unlimited (negotiated)"},"maxSeats":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}],"description":"Included organization seats. null = unlimited."},"maxRequestsPerSecond":{"description":"Sustained org-wide ingest requests per second. null = unlimited/negotiated.","type":["number","null"]},"burstRequestsPerSecond":{"description":"Burst capacity above the sustained ingest rate. null = unlimited.","type":["number","null"]},"maxDeliveriesPerSecond":{"description":"Sustained org-wide outbound deliveries per second, enforced in the delivery worker by re-scheduling (never failing) jobs at the cap. Absent or null = unlimited (a rate limiter fails open).","type":["number","null"]},"burstDeliveriesPerSecond":{"description":"Token-bucket burst capacity above the sustained delivery rate. Absent/null = same as the sustained rate.","type":["number","null"]},"overage":{"description":"Pay-as-you-go overage switch. Absent/null = the plan hard-caps at the included quota. The overage PRICE lives in the plan's `prices` map, per currency.","anyOf":[{"type":"object","properties":{"enabled":{"type":"boolean","description":"Whether deliveries beyond the included quota are billed instead of blocked"}},"required":["enabled"],"additionalProperties":false},{"type":"null"}]},"maxConnectors":{"description":"Private-delivery connectors (SolidHooks Connect) the org may create. null = unlimited/custom; absent = feature off (the connectors UI reads absent as locked). Enforced at create only, level-triggered.","anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}]},"maxReplicasPerConnector":{"description":"Replica identities a single connector may enroll (SolidHooks Connect HA fan-out). null = unlimited/custom; absent = feature off. Enforced at POST /connectors/bootstrap, level-triggered.","anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}]},"maxDeliveryAttempts":{"description":"Total outbound delivery attempts, first attempt included. absent = the default schedule (NOT off, NOT unlimited); null = unlimited/custom.","anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}]},"maxRetryHours":{"description":"Wall-clock ceiling on a delivery retry lifetime, in hours from the first attempt. Bounds repeated Retry-After parking, which consumes no attempt. absent = the default schedule; null = unlimited/custom. Clamped by retentionDays.","type":["number","null"]},"maxOrganizations":{"description":"Organizations a user may own. null = unlimited/custom; absent = unlimited (the create gate fails open, unlike maxConnectors). Enforced at organization.create only, level-triggered.","anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}]}},"required":["includedDeliveriesPerMonth","retentionDays","maxSeats","maxRequestsPerSecond","burstRequestsPerSecond"],"additionalProperties":false,"description":"Effective entitlements after per-org overrides are applied"}},"required":["planKey","planName","currency","priceCents","prices","source","entitlements"],"additionalProperties":false}}}}}}},"/billing/usage":{"get":{"operationId":"billing.usage","summary":"Get Usage","description":"Current-window delivery usage against the plan quota. A delivery is one event routed to one connection; the window is the subscription billing period for subscribed organizations, else the UTC calendar month.","tags":["Billing"],"responses":{"200":{"description":"The request has succeeded.","content":{"application/json":{"schema":{"type":"object","properties":{"limit":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}],"description":"Deliveries included in the current plan per usage window. null = unlimited/custom."},"current":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Deliveries counted so far in the current usage window"},"remaining":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}],"description":"Deliveries left before the quota is exhausted. null = unlimited."},"resetAt":{"type":"string","format":"date-time","x-native-type":"date","description":"When the usage window resets — the billing period end for subscribed orgs, else the first instant of the next UTC month"},"overage":{"anyOf":[{"type":"object","properties":{"currency":{"type":"string","enum":["gbp","usd","eur"],"description":"Currency the overage prices and estimate below are expressed in"},"pricePer100kCents":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Overage price per 100,000 deliveries, in `currency`'s minor unit"},"units":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Deliveries beyond the included quota so far this period"},"estimatedCents":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Estimated overage charge so far this period, in `currency`'s minor unit"}},"required":["currency","pricePer100kCents","units","estimatedCents"],"additionalProperties":false},{"type":"null"}],"description":"Pay-as-you-go overage state. null = the plan hard-caps at the included quota."}},"required":["limit","current","remaining","resetAt","overage"],"additionalProperties":false}}}}}}},"/billing/cancel":{"post":{"operationId":"billing.cancel","summary":"Cancel Subscription","description":"Schedule the organization subscription to cancel at the end of the current billing period. The plan, quota, and usage window stay unchanged until then; afterwards the organization falls back to the free tier. Owner/admin only. Undo with billing.resume before the period ends.","tags":["Billing"],"responses":{"200":{"description":"The request has succeeded.","content":{"application/json":{"schema":{"type":"object","properties":{"subscription":{"anyOf":[{"type":"object","properties":{"plan":{"type":"string","description":"Plan key of the subscription"},"status":{"type":"string","description":"Stripe subscription status (active, trialing, ...)"},"periodStart":{"anyOf":[{"type":"string","format":"date-time","x-native-type":"date"},{"type":"null"}],"description":"Start of the current billing period"},"periodEnd":{"anyOf":[{"type":"string","format":"date-time","x-native-type":"date"},{"type":"null"}],"description":"End of the current billing period (renewal date)"},"cancelAtPeriodEnd":{"type":"boolean","description":"Whether the subscription ends (rather than renews) at period end"},"seats":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}],"description":"Seat count, when the plan is seat-based"},"billingInterval":{"description":"Billing interval (month or year)","type":["string","null"]},"scheduledChange":{"type":"object","properties":{"hasScheduledChange":{"type":"boolean","description":"Whether a plan change (typically a downgrade) is scheduled to take effect at period end. The target plan is not exposed here — resolving it needs a Stripe schedule lookup this read deliberately avoids; the effective date is periodEnd. Revert with billing.revertScheduledChange before period end."}},"required":["hasScheduledChange"],"additionalProperties":false,"description":"Pending scheduled plan change on the subscription, if any."}},"required":["plan","status","periodStart","periodEnd","cancelAtPeriodEnd","seats","billingInterval","scheduledChange"],"additionalProperties":false},{"type":"null"}],"description":"The entitled (active/trialing) subscription, if any"},"hasStripeCustomer":{"type":"boolean","description":"Whether the organization has a Stripe customer — required before the billing portal can open"}},"required":["subscription","hasStripeCustomer"],"additionalProperties":false}}}}}}},"/billing/resume":{"post":{"operationId":"billing.resume","summary":"Resume Subscription","description":"Undo a scheduled cancellation before the billing period ends. Owner/admin only.","tags":["Billing"],"responses":{"200":{"description":"The request has succeeded.","content":{"application/json":{"schema":{"type":"object","properties":{"subscription":{"anyOf":[{"type":"object","properties":{"plan":{"type":"string","description":"Plan key of the subscription"},"status":{"type":"string","description":"Stripe subscription status (active, trialing, ...)"},"periodStart":{"anyOf":[{"type":"string","format":"date-time","x-native-type":"date"},{"type":"null"}],"description":"Start of the current billing period"},"periodEnd":{"anyOf":[{"type":"string","format":"date-time","x-native-type":"date"},{"type":"null"}],"description":"End of the current billing period (renewal date)"},"cancelAtPeriodEnd":{"type":"boolean","description":"Whether the subscription ends (rather than renews) at period end"},"seats":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}],"description":"Seat count, when the plan is seat-based"},"billingInterval":{"description":"Billing interval (month or year)","type":["string","null"]},"scheduledChange":{"type":"object","properties":{"hasScheduledChange":{"type":"boolean","description":"Whether a plan change (typically a downgrade) is scheduled to take effect at period end. The target plan is not exposed here — resolving it needs a Stripe schedule lookup this read deliberately avoids; the effective date is periodEnd. Revert with billing.revertScheduledChange before period end."}},"required":["hasScheduledChange"],"additionalProperties":false,"description":"Pending scheduled plan change on the subscription, if any."}},"required":["plan","status","periodStart","periodEnd","cancelAtPeriodEnd","seats","billingInterval","scheduledChange"],"additionalProperties":false},{"type":"null"}],"description":"The entitled (active/trialing) subscription, if any"},"hasStripeCustomer":{"type":"boolean","description":"Whether the organization has a Stripe customer — required before the billing portal can open"}},"required":["subscription","hasStripeCustomer"],"additionalProperties":false}}}}}}},"/billing/change-plan":{"post":{"operationId":"billing.changePlan","summary":"Change Plan","description":"Change an existing organization subscription between paid tiers. Upgrades apply immediately and invoice the prorated difference now; downgrades (scheduleAtPeriodEnd=true) take effect at the end of the current billing period. Only for organizations that already have a subscription — the FIRST subscription is a Stripe Checkout browser step done from the dashboard (owner/admin), not via this endpoint. Moving to the free tier is a cancellation (use billing.cancel); enterprise plans are sales-led. Owner/admin only. Call billing.refresh afterwards, or read the returned state, to pick up the new entitlements.","tags":["Billing"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"planKey":{"type":"string","description":"Target plan key from GET /v1/billing/plans (a self-serve paid tier, e.g. \"launch\" or \"scale\"). Must differ from the current plan; the free tier and enterprise plans are rejected."},"scheduleAtPeriodEnd":{"description":"When true, apply the change at the end of the current billing period (the convention for downgrades) via a Stripe subscription schedule. When false/omitted, apply immediately and invoice the proration now (the convention for upgrades).","type":"boolean"}},"required":["planKey"]}}}},"responses":{"200":{"description":"The request has succeeded.","content":{"application/json":{"schema":{"type":"object","properties":{"subscription":{"anyOf":[{"type":"object","properties":{"plan":{"type":"string","description":"Plan key of the subscription"},"status":{"type":"string","description":"Stripe subscription status (active, trialing, ...)"},"periodStart":{"anyOf":[{"type":"string","format":"date-time","x-native-type":"date"},{"type":"null"}],"description":"Start of the current billing period"},"periodEnd":{"anyOf":[{"type":"string","format":"date-time","x-native-type":"date"},{"type":"null"}],"description":"End of the current billing period (renewal date)"},"cancelAtPeriodEnd":{"type":"boolean","description":"Whether the subscription ends (rather than renews) at period end"},"seats":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}],"description":"Seat count, when the plan is seat-based"},"billingInterval":{"description":"Billing interval (month or year)","type":["string","null"]},"scheduledChange":{"type":"object","properties":{"hasScheduledChange":{"type":"boolean","description":"Whether a plan change (typically a downgrade) is scheduled to take effect at period end. The target plan is not exposed here — resolving it needs a Stripe schedule lookup this read deliberately avoids; the effective date is periodEnd. Revert with billing.revertScheduledChange before period end."}},"required":["hasScheduledChange"],"additionalProperties":false,"description":"Pending scheduled plan change on the subscription, if any."}},"required":["plan","status","periodStart","periodEnd","cancelAtPeriodEnd","seats","billingInterval","scheduledChange"],"additionalProperties":false},{"type":"null"}],"description":"The entitled (active/trialing) subscription, if any"},"hasStripeCustomer":{"type":"boolean","description":"Whether the organization has a Stripe customer — required before the billing portal can open"}},"required":["subscription","hasStripeCustomer"],"additionalProperties":false}}}}}}},"/billing/preview-plan-change":{"post":{"operationId":"billing.previewPlanChange","summary":"Preview Plan Change","description":"Price an immediate change to another paid tier WITHOUT making it, using Stripe's invoice preview — the amount due now, the proration lines behind it, and the recurring price that follows. Read-only: unlike billing.changePlan it does not release a pending scheduled change. Only prices the immediate direction; a downgrade scheduled to the period end charges nothing today. When Stripe cannot price the change the response carries available=false rather than an error, so a preview outage never blocks the upgrade itself. It also REFUSES to price — same available=false — while a scheduled change is pending: billing.changePlan releases that schedule before charging, so any figure Stripe returns for the scheduled state is one we would not honour. Revert the queued change (billing.revertScheduledChange) to get a quote. Same refusals as billing.changePlan otherwise. Owner/admin only.","tags":["Billing"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"planKey":{"type":"string","description":"The plan key to price a change to. Must be a paid, self-serve plan."}},"required":["planKey"]}}}},"responses":{"200":{"description":"The request has succeeded.","content":{"application/json":{"schema":{"type":"object","properties":{"currency":{"type":"string","enum":["gbp","usd","eur"],"description":"The subscription's presentment currency, which pins at first checkout."},"dueTodayCents":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"What confirming actually charges (Stripe's amount_due). Never negative — it clamps at 0 when account credit covers the invoice — and NOT necessarily the sum of `lines`, since it is computed after tax while the lines are net."},"lines":{"type":"array","items":{"type":"object","properties":{"description":{"type":"string"},"amountCents":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"required":["description","amountCents"],"additionalProperties":false},"description":"The proration lines, net and pre-tax, in the order Stripe returns them. Reconcile any remainder against dueTodayCents rather than assuming they add up."},"nextAmountCents":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}],"description":"The target plan's recurring price in this currency. null when the plan carries no price here."},"nextChargeAt":{"anyOf":[{"type":"string","format":"date-time","x-native-type":"date"},{"type":"null"}],"description":"When the next full invoice lands — the current period end."},"available":{"type":"boolean","description":"False when Stripe could not price the change; the amounts are then not usable."}},"required":["currency","dueTodayCents","lines","nextAmountCents","nextChargeAt","available"],"additionalProperties":false}}}}}}},"/billing/revert-scheduled-change":{"post":{"operationId":"billing.revertScheduledChange","summary":"Revert Scheduled Plan Change","description":"Revert a plan change (typically a downgrade) that was scheduled to take effect at the end of the current billing period, releasing its Stripe subscription schedule so the organization keeps its current plan. No-op error if nothing is scheduled. Owner/admin only.","tags":["Billing"],"responses":{"200":{"description":"The request has succeeded.","content":{"application/json":{"schema":{"type":"object","properties":{"subscription":{"anyOf":[{"type":"object","properties":{"plan":{"type":"string","description":"Plan key of the subscription"},"status":{"type":"string","description":"Stripe subscription status (active, trialing, ...)"},"periodStart":{"anyOf":[{"type":"string","format":"date-time","x-native-type":"date"},{"type":"null"}],"description":"Start of the current billing period"},"periodEnd":{"anyOf":[{"type":"string","format":"date-time","x-native-type":"date"},{"type":"null"}],"description":"End of the current billing period (renewal date)"},"cancelAtPeriodEnd":{"type":"boolean","description":"Whether the subscription ends (rather than renews) at period end"},"seats":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}],"description":"Seat count, when the plan is seat-based"},"billingInterval":{"description":"Billing interval (month or year)","type":["string","null"]},"scheduledChange":{"type":"object","properties":{"hasScheduledChange":{"type":"boolean","description":"Whether a plan change (typically a downgrade) is scheduled to take effect at period end. The target plan is not exposed here — resolving it needs a Stripe schedule lookup this read deliberately avoids; the effective date is periodEnd. Revert with billing.revertScheduledChange before period end."}},"required":["hasScheduledChange"],"additionalProperties":false,"description":"Pending scheduled plan change on the subscription, if any."}},"required":["plan","status","periodStart","periodEnd","cancelAtPeriodEnd","seats","billingInterval","scheduledChange"],"additionalProperties":false},{"type":"null"}],"description":"The entitled (active/trialing) subscription, if any"},"hasStripeCustomer":{"type":"boolean","description":"Whether the organization has a Stripe customer — required before the billing portal can open"}},"required":["subscription","hasStripeCustomer"],"additionalProperties":false}}}}}}},"/billing/refresh":{"post":{"operationId":"billing.refresh","summary":"Refresh Plan Cache","description":"Invalidate the cached plan resolution for the organization and return the freshly resolved plan. Call after changing the subscription so entitlements update without waiting for the cache TTL or webhook.","tags":["Billing"],"responses":{"200":{"description":"The request has succeeded.","content":{"application/json":{"schema":{"type":"object","properties":{"planKey":{"type":"string","description":"Stable identifier of the active plan"},"planName":{"type":"string","description":"Display name of the active plan"},"currency":{"type":"string","enum":["gbp","usd","eur"],"description":"The organization's billing currency"},"priceCents":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}],"description":"Monthly price in `currency`'s minor unit. null = custom/sales-led."},"prices":{"type":"object","propertyNames":{"type":"string","enum":["gbp","usd","eur"],"description":"Presentment currency, lowercase ISO 4217. An organization bills in exactly one."},"additionalProperties":{"type":"object","properties":{"amountCents":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Monthly price in the currency's minor unit (pence/cents). 0 = free."},"overagePer100kCents":{"description":"Pay-as-you-go price per 100,000 overage deliveries, in the currency's minor unit. Absent on capped plans.","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"required":["amountCents"],"additionalProperties":false},"description":"Prices per currency. Empty = custom/sales-led (contact sales). A currency absent means the plan cannot be bought in it."},"source":{"type":"string","enum":["override","subscription","default"],"description":"How the plan was assigned: per-org override, Stripe subscription, or the default plan"},"entitlements":{"type":"object","properties":{"includedDeliveriesPerMonth":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}],"description":"Deliveries included per usage window — the billing period for subscribed orgs, the UTC calendar month otherwise. A delivery is one event routed to one connection. null = unlimited/custom."},"retentionDays":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}],"description":"Days of event history retained and replayable; null = unlimited (negotiated)"},"maxSeats":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}],"description":"Included organization seats. null = unlimited."},"maxRequestsPerSecond":{"description":"Sustained org-wide ingest requests per second. null = unlimited/negotiated.","type":["number","null"]},"burstRequestsPerSecond":{"description":"Burst capacity above the sustained ingest rate. null = unlimited.","type":["number","null"]},"maxDeliveriesPerSecond":{"description":"Sustained org-wide outbound deliveries per second, enforced in the delivery worker by re-scheduling (never failing) jobs at the cap. Absent or null = unlimited (a rate limiter fails open).","type":["number","null"]},"burstDeliveriesPerSecond":{"description":"Token-bucket burst capacity above the sustained delivery rate. Absent/null = same as the sustained rate.","type":["number","null"]},"overage":{"description":"Pay-as-you-go overage switch. Absent/null = the plan hard-caps at the included quota. The overage PRICE lives in the plan's `prices` map, per currency.","anyOf":[{"type":"object","properties":{"enabled":{"type":"boolean","description":"Whether deliveries beyond the included quota are billed instead of blocked"}},"required":["enabled"],"additionalProperties":false},{"type":"null"}]},"maxConnectors":{"description":"Private-delivery connectors (SolidHooks Connect) the org may create. null = unlimited/custom; absent = feature off (the connectors UI reads absent as locked). Enforced at create only, level-triggered.","anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}]},"maxReplicasPerConnector":{"description":"Replica identities a single connector may enroll (SolidHooks Connect HA fan-out). null = unlimited/custom; absent = feature off. Enforced at POST /connectors/bootstrap, level-triggered.","anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}]},"maxDeliveryAttempts":{"description":"Total outbound delivery attempts, first attempt included. absent = the default schedule (NOT off, NOT unlimited); null = unlimited/custom.","anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}]},"maxRetryHours":{"description":"Wall-clock ceiling on a delivery retry lifetime, in hours from the first attempt. Bounds repeated Retry-After parking, which consumes no attempt. absent = the default schedule; null = unlimited/custom. Clamped by retentionDays.","type":["number","null"]},"maxOrganizations":{"description":"Organizations a user may own. null = unlimited/custom; absent = unlimited (the create gate fails open, unlike maxConnectors). Enforced at organization.create only, level-triggered.","anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}]}},"required":["includedDeliveriesPerMonth","retentionDays","maxSeats","maxRequestsPerSecond","burstRequestsPerSecond"],"additionalProperties":false,"description":"Effective entitlements after per-org overrides are applied"}},"required":["planKey","planName","currency","priceCents","prices","source","entitlements"],"additionalProperties":false}}}}}}},"/billing/subscription":{"get":{"operationId":"billing.subscription","summary":"Get Subscription State","description":"Get the organization’s entitled Stripe subscription (if any) and whether a Stripe customer exists yet.","tags":["Billing"],"responses":{"200":{"description":"The request has succeeded.","content":{"application/json":{"schema":{"type":"object","properties":{"subscription":{"anyOf":[{"type":"object","properties":{"plan":{"type":"string","description":"Plan key of the subscription"},"status":{"type":"string","description":"Stripe subscription status (active, trialing, ...)"},"periodStart":{"anyOf":[{"type":"string","format":"date-time","x-native-type":"date"},{"type":"null"}],"description":"Start of the current billing period"},"periodEnd":{"anyOf":[{"type":"string","format":"date-time","x-native-type":"date"},{"type":"null"}],"description":"End of the current billing period (renewal date)"},"cancelAtPeriodEnd":{"type":"boolean","description":"Whether the subscription ends (rather than renews) at period end"},"seats":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}],"description":"Seat count, when the plan is seat-based"},"billingInterval":{"description":"Billing interval (month or year)","type":["string","null"]},"scheduledChange":{"type":"object","properties":{"hasScheduledChange":{"type":"boolean","description":"Whether a plan change (typically a downgrade) is scheduled to take effect at period end. The target plan is not exposed here — resolving it needs a Stripe schedule lookup this read deliberately avoids; the effective date is periodEnd. Revert with billing.revertScheduledChange before period end."}},"required":["hasScheduledChange"],"additionalProperties":false,"description":"Pending scheduled plan change on the subscription, if any."}},"required":["plan","status","periodStart","periodEnd","cancelAtPeriodEnd","seats","billingInterval","scheduledChange"],"additionalProperties":false},{"type":"null"}],"description":"The entitled (active/trialing) subscription, if any"},"hasStripeCustomer":{"type":"boolean","description":"Whether the organization has a Stripe customer — required before the billing portal can open"}},"required":["subscription","hasStripeCustomer"],"additionalProperties":false}}}}}}},"/billing/plans":{"get":{"operationId":"billing.plans","summary":"List Plans","description":"List all active billing plans with pricing and entitlements, ordered for display.","tags":["Billing"],"responses":{"200":{"description":"The request has succeeded.","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string","description":"Stable plan identifier (e.g. \"hobby\", \"launch\")"},"name":{"type":"string","description":"Plan display name"},"currency":{"type":"string","enum":["gbp","usd","eur"],"description":"Currency this response is priced in — the organization's billing currency"},"priceCents":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}],"description":"Monthly price in `currency`'s minor unit — what this organization would actually be charged. null = custom/sales-led."},"prices":{"type":"object","propertyNames":{"type":"string","enum":["gbp","usd","eur"],"description":"Presentment currency, lowercase ISO 4217. An organization bills in exactly one."},"additionalProperties":{"type":"object","properties":{"amountCents":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Monthly price in the currency's minor unit (pence/cents). 0 = free."},"overagePer100kCents":{"description":"Pay-as-you-go price per 100,000 overage deliveries, in the currency's minor unit. Absent on capped plans.","type":"integer","minimum":-9007199254740991,"maximum":9007199254740991}},"required":["amountCents"],"additionalProperties":false},"description":"Prices per currency. Empty = custom/sales-led (contact sales). A currency absent means the plan cannot be bought in it."},"selfServe":{"type":"boolean","description":"Whether the plan is purchasable via self-serve checkout"},"displayOrder":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Sort order for plan comparison pages"},"entitlements":{"type":"object","properties":{"includedDeliveriesPerMonth":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}],"description":"Deliveries included per usage window — the billing period for subscribed orgs, the UTC calendar month otherwise. A delivery is one event routed to one connection. null = unlimited/custom."},"retentionDays":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}],"description":"Days of event history retained and replayable; null = unlimited (negotiated)"},"maxSeats":{"anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}],"description":"Included organization seats. null = unlimited."},"maxRequestsPerSecond":{"description":"Sustained org-wide ingest requests per second. null = unlimited/negotiated.","type":["number","null"]},"burstRequestsPerSecond":{"description":"Burst capacity above the sustained ingest rate. null = unlimited.","type":["number","null"]},"maxDeliveriesPerSecond":{"description":"Sustained org-wide outbound deliveries per second, enforced in the delivery worker by re-scheduling (never failing) jobs at the cap. Absent or null = unlimited (a rate limiter fails open).","type":["number","null"]},"burstDeliveriesPerSecond":{"description":"Token-bucket burst capacity above the sustained delivery rate. Absent/null = same as the sustained rate.","type":["number","null"]},"overage":{"description":"Pay-as-you-go overage switch. Absent/null = the plan hard-caps at the included quota. The overage PRICE lives in the plan's `prices` map, per currency.","anyOf":[{"type":"object","properties":{"enabled":{"type":"boolean","description":"Whether deliveries beyond the included quota are billed instead of blocked"}},"required":["enabled"],"additionalProperties":false},{"type":"null"}]},"maxConnectors":{"description":"Private-delivery connectors (SolidHooks Connect) the org may create. null = unlimited/custom; absent = feature off (the connectors UI reads absent as locked). Enforced at create only, level-triggered.","anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}]},"maxReplicasPerConnector":{"description":"Replica identities a single connector may enroll (SolidHooks Connect HA fan-out). null = unlimited/custom; absent = feature off. Enforced at POST /connectors/bootstrap, level-triggered.","anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}]},"maxDeliveryAttempts":{"description":"Total outbound delivery attempts, first attempt included. absent = the default schedule (NOT off, NOT unlimited); null = unlimited/custom.","anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}]},"maxRetryHours":{"description":"Wall-clock ceiling on a delivery retry lifetime, in hours from the first attempt. Bounds repeated Retry-After parking, which consumes no attempt. absent = the default schedule; null = unlimited/custom. Clamped by retentionDays.","type":["number","null"]},"maxOrganizations":{"description":"Organizations a user may own. null = unlimited/custom; absent = unlimited (the create gate fails open, unlike maxConnectors). Enforced at organization.create only, level-triggered.","anyOf":[{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991},{"type":"null"}]}},"required":["includedDeliveriesPerMonth","retentionDays","maxSeats","maxRequestsPerSecond","burstRequestsPerSecond"],"additionalProperties":false}},"required":["key","name","currency","priceCents","prices","selfServe","displayOrder","entitlements"],"additionalProperties":false}}}}}}}},"/countries":{"get":{"operationId":"countries.list","summary":"List Countries","description":"List countries available for sign-up, ordered for display. Public: the sign-up form needs this before a session exists. Each row carries the billing currency and default locale an organization created from that country inherits. Countries we cannot serve are omitted, and submitting one is rejected server-side.","tags":["Countries"],"responses":{"200":{"description":"The request has succeeded.","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"code":{"type":"string","minLength":2,"maxLength":2,"description":"ISO 3166-1 alpha-2 country code, uppercase (e.g. \"GB\")"},"name":{"type":"string","description":"English display name"},"billingCurrency":{"type":"string","enum":["gbp","usd","eur"],"description":"The currency an organization created from this country is billed in. Not the country's own currency — we settle in three presentment currencies, so e.g. NG bills in USD."},"defaultLanguage":{"type":"string","description":"Locale pre-selected for a user signing up from this country, which they can change at onboarding (e.g. \"en_us\")"}},"required":["code","name","billingCurrency","defaultLanguage"],"additionalProperties":false}}}}}}}},"/providers":{"get":{"operationId":"providers.list","summary":"List Providers","description":"The catalog of webhook providers a source can be created against, ordered for display. Public: it is reference data with no tenant content — the install count is a global aggregate across all organizations. Each entry carries the `family` that decides how the source is set up: `preset` providers have their signature scheme fixed server-side and need only a secret, `generic` hands you the whole verifier, and `email` has nothing to sign and takes an inbound address instead of an ingest URL.","tags":["Providers"],"parameters":[{"in":"query","name":"search","schema":{"description":"Case-insensitive substring match against the provider name and description.","type":"string","maxLength":100},"allowEmptyValue":true,"allowReserved":true},{"in":"query","name":"category","schema":{"description":"Exact category match, e.g. \"Payments\".","type":"string","maxLength":60},"allowEmptyValue":true,"allowReserved":true},{"in":"query","name":"sort","schema":{"description":"Ordering. \"installs\" (default) is most-installed first — a global, cross-tenant popularity count. \"name\" is alphabetical.","type":"string","enum":["installs","name"]},"allowEmptyValue":true,"allowReserved":true}],"responses":{"200":{"description":"The request has succeeded.","content":{"application/json":{"schema":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string","description":"Stable catalog identifier. Matches the `provider` field on a source exactly."},"name":{"type":"string","description":"Display name"},"category":{"type":"string","description":"Grouping shown on the tile and in the category filter"},"description":{"type":"string","description":"One-line outcome-oriented summary of what this provider sends"},"family":{"type":"string","enum":["preset","generic","email"],"description":"Setup shape. \"preset\" fixes the signature scheme server-side and asks only for a secret; \"generic\" hands the verifier to you; \"email\" has nothing to sign and receives an inbound address instead of an ingest URL."},"iconKey":{"type":"string","description":"Identifier for the provider mark; resolved to an image by the client"},"installCount":{"type":"integer","minimum":-9007199254740991,"maximum":9007199254740991,"description":"Sources created against this provider across all organizations"}},"required":["key","name","category","description","family","iconKey","installCount"],"additionalProperties":false}}}}}}}},"/providers/categories":{"get":{"operationId":"providers.categories","summary":"List Provider Categories","description":"The distinct categories present in the provider catalog, alphabetically. Backs the category filter on the provider picker so it offers only values that match something.","tags":["Providers"],"responses":{"200":{"description":"The request has succeeded.","content":{"application/json":{"schema":{"type":"array","items":{"type":"string"}}}}}}}}},"servers":[{"url":"https://api.solidhooks.com/v1"}]}