{ "openapi": "3.0.3", "info": { "title": "Notifuse API", "description": "API for Notifuse - a transactional email and newsletter management platform", "version": "1.0.0", "contact": { "name": "Notifuse Support", "url": "https://www.notifuse.com/support", "email": "hello@notifuse.com" } }, "servers": [ { "url": "https://{notifuseDomain}", "description": "Customer-specific Notifuse API server", "variables": { "notifuseDomain": { "description": "Your unique Notifuse domain", "default": "demo.notifuse.com" } } } ], "paths": { "/track": { "post": { "summary": "Ingest a web analytics beat", "description": "Public collect endpoint used by the Notifuse Analytics browser SDK. Each beat carries the full cumulative session state (all pageviews and goals), so the server can rebuild the session from any single payload. Beats are typically sent as text/plain to avoid CORS preflights; the body is JSON regardless of Content-Type. Silently-dropped traffic (disabled feature, disallowed origin, bot user agents) still receives success responses.\n\nThe endpoint is public and unauthenticated, so the identity fields (contact_email + contact_email_hmac, or identify_token) are claims until they verify against the workspace secret. One that does not verify is not an error: the beat is recorded anonymously. A single malformed action is dropped the same way, leaving the rest of the beat intact.\n\nVerifying the credential is the FIRST of three gates, not the only one. An address that clears all three is attached to the session; failing any of them costs the identity alone, silently — the beat is still stored and the response is still a 200 with no error. Success and every failure are therefore indistinguishable on the wire:\n\n1. Signature (or token decryption) must verify against the workspace secret. The recipe is in the contact_email_hmac schema; getting the domain-separation prefix wrong is the usual cause.\n\n2. Rate limits, applied to the IDENTIFIED path only (anonymous traffic is the normal firehose and stays unthrottled) and before the contact lookup, so an abusive caller cannot spend database reads: 120 identified beats per minute per workspace + address, and 600 per minute per client IP (that one counted across all workspaces, since an office shares one IP) — both sized around the SDK's 10-30s heartbeat. Exceeding either is deliberately never a 429, which the SDK would only queue for a retry that cannot fix it.\n\n3. The address must ALREADY be a contact in the workspace. A signature proves who the caller is, never that the address belongs to anyone, so without this gate a workspace's own signing key could store the email of people who are not contacts, and erasure would be unenforceable — a deleted contact's next beat would re-stamp the address. The answer is cached for 60 seconds, so a freshly created contact can stay unidentified and a deleted one keep resolving for about that long.\n\nThis is why a correctly-signed beat for an address that is not a contact is indistinguishable from a bad signature: both return 200 and record the session anonymously.\n", "operationId": "trackWebAnalyticsBeat", "tags": [ "Web Analytics" ], "security": [], "requestBody": { "required": true, "content": { "text/plain": { "schema": { "$ref": "#/components/schemas/WebTrackPayload" } }, "application/json": { "schema": { "$ref": "#/components/schemas/WebTrackPayload" } } } }, "responses": { "200": { "description": "Beat accepted (or silently dropped)", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" } } } } } }, "400": { "description": "Malformed payload. The full set of causes: an unparseable body; a missing or blank workspace_id; a session_id that is not a UUIDv7 or whose embedded timestamp falls outside [now-48h, now+24h]; an updated_at more than 24h from server time; a negative seq; more actions than the schema's maxItems; a custom_1..custom_10 dimension value over 256 characters; or more than 50 dimension entries. Row building re-derives the session date from session_id and surfaces any failure the same way, which today can only be that same session_id rule.\n\nNever returned for an unverifiable, throttled or unknown identity, and never for a single out-of-range ACTION — that one action is dropped and the beat is kept. An oversized actions ARRAY is a different rule and IS a 400: see maxItems on the actions property.", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "error": { "type": "string" } } } } } }, "413": { "description": "Body over 1 MB. Distinguished from a 400 because it is the one failure the client can act on: actions[] only grows, so every later beat of that session fails too until it trims its oldest actions or rotates the session.", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean" }, "error": { "type": "string" } } } } } } } } }, "/api/workspaces.setWebAnalyticsSettings": { "post": { "summary": "Replace the workspace's web analytics settings", "description": "Gated by the web_analytics write permission (like blog settings, members manage the feature without workspace write access). The attribution filters version is recomputed server-side. Passing null settings clears the configuration.\n\nWriting an identified visitor's goals and navigation to their contact timeline is not configured here and has no setting: calling identify() with an HMAC is the opt-in, because minting that credential requires the workspace secret.\n", "operationId": "setWebAnalyticsSettings", "tags": [ "Web Analytics" ], "security": [ { "BearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "type": "object", "required": [ "workspace_id" ], "properties": { "workspace_id": { "type": "string" }, "settings": { "$ref": "#/components/schemas/WebAnalyticsSettings" } } } } } }, "responses": { "200": { "description": "Settings updated" }, "400": { "description": "Validation failed" }, "403": { "description": "The caller lacks web_analytics write, returned with the missing resource named in the error message." } } } }, "/api/webAnalytics.backfillStart": { "post": { "summary": "Start an attribution backfill", "description": "Rewrites historical web_sessions and web_goals rows with the current attribution rules, one monthly partition per step. Fails while a run is already pending or running.\n", "operationId": "webAnalyticsBackfillStart", "tags": [ "Web Analytics" ], "security": [ { "BearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WebAnalyticsBackfillRequest" } } } }, "responses": { "200": { "description": "Backfill task created", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WebAnalyticsBackfillResponse" } } } }, "400": { "description": "A backfill is already in progress" }, "403": { "description": "Missing web_analytics write permission" } } } }, "/api/webAnalytics.backfillStatus": { "post": { "summary": "Get the latest backfill run", "operationId": "webAnalyticsBackfillStatus", "tags": [ "Web Analytics" ], "security": [ { "BearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WebAnalyticsBackfillRequest" } } } }, "responses": { "200": { "description": "Latest run (null when none exists)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WebAnalyticsBackfillResponse" } } } } } } }, "/api/webAnalytics.backfillCancel": { "post": { "summary": "Cancel the in-flight backfill run", "operationId": "webAnalyticsBackfillCancel", "tags": [ "Web Analytics" ], "security": [ { "BearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/WebAnalyticsBackfillRequest" } } } }, "responses": { "200": { "description": "Run cancelled" }, "400": { "description": "No backfill in progress" } } } }, "/api/annotations.list": { "get": { "summary": "List annotations", "description": "Returns the workspace's annotations, most recent moment first. Every filter is optional; with none the most recent 100 rows come back.\n\nGated by the web_analytics read permission, like the rest of the web analytics surface.\n", "operationId": "listAnnotations", "tags": [ "Web Analytics" ], "security": [ { "BearerAuth": [] } ], "parameters": [ { "name": "workspace_id", "in": "query", "required": true, "schema": { "type": "string" }, "description": "The ID of the workspace", "example": "ws_1234567890" }, { "name": "start", "in": "query", "required": false, "schema": { "type": "string", "format": "date-time" }, "description": "Only annotations at or after this instant. Must be RFC3339; a malformed value is a 400 rather than a dropped filter.", "example": "2026-08-01T00:00:00Z" }, { "name": "end", "in": "query", "required": false, "schema": { "type": "string", "format": "date-time" }, "description": "Only annotations at or before this instant. Must be RFC3339.", "example": "2026-08-31T23:59:59Z" }, { "name": "sources", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Comma-separated list of sources to keep. Any value outside `manual`/`broadcast` is a 400.", "example": "manual,broadcast" }, { "name": "limit", "in": "query", "required": false, "schema": { "type": "integer", "default": 100, "maximum": 1000 }, "description": "Maximum rows to return. Values above the maximum are clamped to it rather than refused.", "example": 100 } ], "responses": { "200": { "description": "Annotations matching the filters", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ListAnnotationsResponse" } } } }, "400": { "description": "Bad request - invalid query parameters", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "examples": { "missingWorkspaceId": { "value": { "error": "workspace_id is required" } }, "malformedStart": { "value": { "error": "start must be an RFC3339 timestamp" } }, "endBeforeStart": { "value": { "error": "invalid request: end must not be before start" } } } } } }, "401": { "description": "Unauthorized - invalid or missing authentication token", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "403": { "description": "Forbidden - missing required permission", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "error": "Insufficient permissions: read access to web analytics required" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "error": "Failed to list annotations" } } } } } } }, "/api/annotations.get": { "get": { "summary": "Get an annotation", "description": "Retrieves a single annotation by ID. Requires the web_analytics read permission.", "operationId": "getAnnotation", "tags": [ "Web Analytics" ], "security": [ { "BearerAuth": [] } ], "parameters": [ { "name": "workspace_id", "in": "query", "required": true, "schema": { "type": "string" }, "description": "The ID of the workspace", "example": "ws_1234567890" }, { "name": "id", "in": "query", "required": true, "schema": { "type": "string" }, "description": "The ID of the annotation", "example": "3f2504e04f8911d39a0c0305e82c3301" } ], "responses": { "200": { "description": "The annotation", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AnnotationResponse" } } } }, "400": { "description": "Bad request - missing query parameters", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "error": "id is required" } } } }, "401": { "description": "Unauthorized - invalid or missing authentication token", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "403": { "description": "Forbidden - missing required permission", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "error": "Insufficient permissions: read access to web analytics required" } } } }, "404": { "description": "No annotation with that ID in this workspace", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "error": "annotation not found with ID: 3f2504e04f8911d39a0c0305e82c3301" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "error": "Failed to get annotation" } } } } } } }, "/api/annotations.create": { "post": { "summary": "Create an annotation", "description": "Creates a manual annotation. Requires the web_analytics write permission.\n\n`source` is always stored as `manual` and `source_id` is always empty: only the platform writes automatic rows, so there is no way to attach a caller-supplied key to one. **This endpoint is therefore not idempotent** — a retried call (a CI job re-running a deploy marker, for instance) writes a second annotation. De-duplicate on the caller's side, or delete the extra row.\n\nOmitted `color` and `timezone` are filled in server-side: the default colour, and the workspace timezone falling back to UTC.\n", "operationId": "createAnnotation", "tags": [ "Web Analytics" ], "security": [ { "BearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateAnnotationRequest" } } } }, "responses": { "201": { "description": "Annotation created", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AnnotationResponse" } } } }, "400": { "description": "Bad request - unparseable body, validation failed, or the instance runs in demo mode, where every mutating endpoint is closed.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "examples": { "invalidBody": { "value": { "error": "Invalid request body" } }, "missingTitle": { "value": { "error": "invalid request: title is required" } }, "invalidColor": { "value": { "error": "invalid request: color must be a hex color like #3b82f6" } }, "systemSource": { "value": { "error": "invalid request: source must be \"manual\"" } }, "demoMode": { "value": { "error": "This operation is not allowed in demo mode" } } } } } }, "401": { "description": "Unauthorized - invalid or missing authentication token", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "403": { "description": "Forbidden - missing required permission", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "error": "Insufficient permissions: write access to web analytics required" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "error": "Failed to create annotation" } } } } } } }, "/api/annotations.update": { "post": { "summary": "Update an annotation", "description": "Edits an annotation's moment, timezone and presentation. Requires the web_analytics write permission.\n\nAutomatic annotations are editable — an operator may want to reword a broadcast's title — but their origin is not: `source` and `source_id` are reloaded from storage and carried forward, so an edit can neither promote a manual row to a system one nor take over another broadcast's slot.\n", "operationId": "updateAnnotation", "tags": [ "Web Analytics" ], "security": [ { "BearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpdateAnnotationRequest" } } } }, "responses": { "200": { "description": "Annotation updated", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/AnnotationResponse" } } } }, "400": { "description": "Bad request - unparseable body, validation failed, or the instance runs in demo mode.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "examples": { "missingId": { "value": { "error": "invalid request: id is required" } }, "missingAnnotatedAt": { "value": { "error": "invalid request: annotated_at is required" } }, "demoMode": { "value": { "error": "This operation is not allowed in demo mode" } } } } } }, "401": { "description": "Unauthorized - invalid or missing authentication token", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "403": { "description": "Forbidden - missing required permission", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "error": "Insufficient permissions: write access to web analytics required" } } } }, "404": { "description": "No annotation with that ID in this workspace", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "error": "annotation not found with ID: 3f2504e04f8911d39a0c0305e82c3301" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "error": "Failed to update annotation" } } } } } } }, "/api/annotations.delete": { "post": { "summary": "Delete an annotation", "description": "Deletes an annotation, automatic ones included — a broadcast annotation cannot come back by accident, since its broadcast has already started. Requires the web_analytics write permission.\n", "operationId": "deleteAnnotation", "tags": [ "Web Analytics" ], "security": [ { "BearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DeleteAnnotationRequest" } } } }, "responses": { "200": { "description": "Annotation deleted. A body rather than a 204, so a delete is never indistinguishable from a proxy swallowing the response.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DeleteAnnotationResponse" } } } }, "400": { "description": "Bad request - unparseable body, missing id, or the instance runs in demo mode.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "examples": { "missingId": { "value": { "error": "invalid request: id is required" } }, "demoMode": { "value": { "error": "This operation is not allowed in demo mode" } } } } } }, "401": { "description": "Unauthorized - invalid or missing authentication token", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "403": { "description": "Forbidden - missing required permission", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "error": "Insufficient permissions: write access to web analytics required" } } } }, "404": { "description": "No annotation with that ID in this workspace", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "error": "annotation not found with ID: 3f2504e04f8911d39a0c0305e82c3301" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "error": "Failed to delete annotation" } } } } } } }, "/api/transactional.send": { "post": { "summary": "Send a transactional notification", "description": "Sends a transactional notification to a contact through specified channels.\nRequires authentication.\n", "operationId": "sendTransactionalNotification", "security": [ { "BearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SendTransactionalRequest" } } } }, "responses": { "200": { "description": "Notification sent successfully", "content": { "application/json": { "schema": { "type": "object", "properties": { "message_id": { "type": "string", "description": "Unique identifier for the sent message", "example": "msg_1234567890abcdef" }, "success": { "type": "boolean", "description": "Whether the notification was sent successfully", "example": true } } } } } }, "400": { "description": "Bad request - validation failed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "examples": { "notFoundExample": { "value": { "error": "Notification not found" } }, "inactiveExample": { "value": { "error": "Notification is not active" } }, "noChannelsExample": { "value": { "error": "No valid channels configured" } }, "validationExample": { "value": { "error": "notification.contact is required" } } } } } }, "401": { "description": "Unauthorized - invalid or missing authentication token", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "error": "Unauthorized" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "error": "Failed to send notification" } } } } } } }, "/api/contacts.list": { "get": { "summary": "List contacts with filtering and pagination", "description": "Retrieves a paginated list of contacts with optional filtering. All contact fields are always returned.\n\n**Filtering**: Use filters to search for contacts. Text filters (email, external_id, first_name, last_name, full_name, phone, country, language) use case-insensitive partial matching (ILIKE).\n\n**List filtering**: Use `list_id` and/or `contact_list_status` to filter contacts by list membership.\n\n**Segment filtering**: Use `segments[]` to filter contacts that belong to specific segments.\n\n**Contact lists**: By default, `contact_lists` is not included in the response. Set `with_contact_lists=true` to include the contact's list subscriptions.\n\n**Pagination**: Uses cursor-based pagination. Use the `next_cursor` from the response to fetch the next page.\n", "operationId": "listContacts", "security": [ { "BearerAuth": [] } ], "parameters": [ { "name": "workspace_id", "in": "query", "required": true, "schema": { "type": "string" }, "description": "The ID of the workspace", "example": "ws_1234567890" }, { "name": "email", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Filter by email (case-insensitive partial match)", "example": "user@example" }, { "name": "external_id", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Filter by external ID (case-insensitive partial match)", "example": "user_123" }, { "name": "first_name", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Filter by first name (case-insensitive partial match)", "example": "John" }, { "name": "last_name", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Filter by last name (case-insensitive partial match)", "example": "Doe" }, { "name": "full_name", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Filter by full name (case-insensitive partial match)", "example": "John Doe" }, { "name": "phone", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Filter by phone number (case-insensitive partial match)", "example": "+1555" }, { "name": "country", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Filter by country (case-insensitive partial match)", "example": "US" }, { "name": "language", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Filter by language (case-insensitive partial match)", "example": "en" }, { "name": "list_id", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Filter by list membership (contacts subscribed to this list)", "example": "newsletter" }, { "name": "contact_list_status", "in": "query", "required": false, "schema": { "type": "string", "enum": [ "active", "pending", "unsubscribed", "bounced", "complained" ] }, "description": "Filter by subscription status within the list", "example": "active" }, { "name": "segments[]", "in": "query", "required": false, "schema": { "type": "array", "items": { "type": "string" } }, "description": "Filter by segment membership (contacts in any of these segments)", "example": [ "premium_users", "active_buyers" ] }, { "name": "with_contact_lists", "in": "query", "required": false, "schema": { "type": "boolean", "default": false }, "description": "Include contact list subscriptions in the response", "example": true }, { "name": "limit", "in": "query", "required": false, "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 20 }, "description": "Maximum number of contacts to return (1-100)", "example": 50 }, { "name": "cursor", "in": "query", "required": false, "schema": { "type": "string" }, "description": "Pagination cursor from previous response", "example": "MjAyMy0wMS0xNVQxMDozMDowMFp+dXNlckBleGFtcGxlLmNvbQ==" } ], "responses": { "200": { "description": "Contacts retrieved successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ListContactsResponse" } } } }, "400": { "description": "Bad request - invalid parameters", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "examples": { "missingWorkspaceId": { "value": { "error": "workspace_id is required" } }, "invalidLimit": { "value": { "error": "limit must be between 1 and 100" } }, "invalidCursor": { "value": { "error": "invalid cursor encoding" } } } } } }, "401": { "description": "Unauthorized - invalid or missing authentication token", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } } } } }, "/api/contacts.count": { "get": { "summary": "Count contacts in a workspace", "description": "Returns the total number of contacts in a workspace.", "operationId": "countContacts", "security": [ { "BearerAuth": [] } ], "parameters": [ { "name": "workspace_id", "in": "query", "required": true, "schema": { "type": "string" }, "description": "The ID of the workspace", "example": "ws_1234567890" } ], "responses": { "200": { "description": "Contact count retrieved successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CountContactsResponse" } } } }, "400": { "description": "Bad request - missing workspace ID", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "error": "Missing workspace ID" } } } }, "401": { "description": "Unauthorized - invalid or missing authentication token", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } } } } }, "/api/contacts.upsert": { "post": { "summary": "Create or update a contact", "description": "Creates a new contact or updates an existing one. Contacts are matched **by email address only** — the email is the contact's unique identifier. `external_id` is stored but never used for matching, so sending an existing `external_id` with a new email address creates a second contact.\n\nTo change a contact's email address: upsert a contact with the new email and the full profile, re-create its list subscriptions (preserving unsubscribed/bounced/complained statuses — do not reset opt-outs), then delete the contact with the old email using `contacts.delete`. Message history and segment memberships are keyed to the email address and are not transferred.\n\nReturns information about whether the contact was created or updated.\n", "operationId": "upsertContact", "security": [ { "BearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpsertContactRequest" } } } }, "responses": { "200": { "description": "Contact upserted successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpsertContactResponse" } } } }, "400": { "description": "Bad request - validation failed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "examples": { "missingWorkspaceId": { "value": { "error": "workspace_id is required" } }, "invalidEmail": { "value": { "error": "invalid email format" } }, "missingContact": { "value": { "error": "contact is required" } } } } } }, "401": { "description": "Unauthorized - invalid or missing authentication token", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } } } } }, "/api/contacts.getByEmail": { "get": { "summary": "Get a contact by email address", "description": "Retrieves a contact by their email address within a specific workspace. The response always includes the contact's list subscriptions with their status (active, pending, unsubscribed, bounced, complained).", "operationId": "getContactByEmail", "security": [ { "BearerAuth": [] } ], "parameters": [ { "name": "workspace_id", "in": "query", "required": true, "schema": { "type": "string" }, "description": "The ID of the workspace", "example": "ws_1234567890" }, { "name": "email", "in": "query", "required": true, "schema": { "type": "string", "format": "email" }, "description": "The email address of the contact", "example": "user@example.com" } ], "responses": { "200": { "description": "Contact found successfully", "content": { "application/json": { "schema": { "type": "object", "properties": { "contact": { "$ref": "#/components/schemas/Contact" } } } } } }, "400": { "description": "Bad request - missing parameters", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "examples": { "missingWorkspaceId": { "value": { "error": "Missing workspace ID" } }, "missingEmail": { "value": { "error": "Missing email" } } } } } }, "401": { "description": "Unauthorized - invalid or missing authentication token", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "404": { "description": "Contact not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "error": "Contact not found" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } } } } }, "/api/contacts.getByExternalID": { "get": { "summary": "Get a contact by external ID", "description": "Retrieves a contact by their external ID within a specific workspace. The response always includes the contact's list subscriptions with their status (active, pending, unsubscribed, bounced, complained).\n\nNote that `external_id` is not unique — if several contacts share the same external ID, only one of them is returned (which one is not defined). Contacts are uniquely identified by email address.\n", "operationId": "getContactByExternalID", "security": [ { "BearerAuth": [] } ], "parameters": [ { "name": "workspace_id", "in": "query", "required": true, "schema": { "type": "string" }, "description": "The ID of the workspace", "example": "ws_1234567890" }, { "name": "external_id", "in": "query", "required": true, "schema": { "type": "string" }, "description": "The external ID of the contact", "example": "user_12345" } ], "responses": { "200": { "description": "Contact found successfully", "content": { "application/json": { "schema": { "type": "object", "properties": { "contact": { "$ref": "#/components/schemas/Contact" } } } } } }, "400": { "description": "Bad request - missing parameters", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "examples": { "missingWorkspaceId": { "value": { "error": "Missing workspace ID" } }, "missingExternalId": { "value": { "error": "Missing external ID" } } } } } }, "401": { "description": "Unauthorized - invalid or missing authentication token", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "404": { "description": "Contact not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "error": "Contact not found" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } } } } }, "/api/contacts.import": { "post": { "summary": "Batch import contacts", "description": "Creates or updates multiple contacts in a single batch operation. This is significantly more efficient than individual upsert operations. Optionally subscribes all contacts to specified lists.", "operationId": "batchImportContacts", "security": [ { "BearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BatchImportContactsRequest" } } } }, "responses": { "200": { "description": "Batch import completed (may include partial failures)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BatchImportContactsResponse" } } } }, "400": { "description": "Bad request - validation failed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "examples": { "missingWorkspaceId": { "value": { "error": "workspace_id is required" } }, "missingContacts": { "value": { "error": "contacts is required" } }, "emptyContacts": { "value": { "error": "contacts array cannot be empty" } }, "insufficientPermissions": { "value": { "error": "write access to lists required when subscribe_to_lists is provided" } } } } } }, "401": { "description": "Unauthorized - invalid or missing authentication token", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } } } } }, "/api/contacts.delete": { "post": { "summary": "Delete a contact", "description": "Deletes a contact from the workspace by email address. This action is permanent and cannot be undone.", "operationId": "deleteContact", "security": [ { "BearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DeleteContactRequest" } } } }, "responses": { "200": { "description": "Contact deleted successfully", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean", "description": "Whether the deletion was successful", "example": true } } } } } }, "400": { "description": "Bad request - validation failed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "examples": { "missingWorkspaceId": { "value": { "error": "workspace_id is required" } }, "missingEmail": { "value": { "error": "email is required" } }, "invalidEmail": { "value": { "error": "invalid email format" } } } } } }, "401": { "description": "Unauthorized - invalid or missing authentication token", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "404": { "description": "Contact not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "error": "Contact not found" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "error": "Failed to delete contact" } } } } } } }, "/api/contactLists.updateStatus": { "post": { "summary": "Update contact list subscription status", "description": "Updates the subscription status of a contact in a specific list. Can set status to active, pending, unsubscribed, bounced, or complained.\n\n**Note:** This endpoint only modifies **existing** list memberships. It does not add a contact to a list. To add a contact to a list, use the `/subscribe` endpoint (for public lists) or the `/api/contacts.import` endpoint (for any list, requires authentication).\n", "operationId": "updateContactListStatus", "security": [ { "BearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpdateContactListStatusRequest" } } } }, "responses": { "200": { "description": "Status updated successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpdateContactListStatusResponse" } } } }, "400": { "description": "Bad request - validation failed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "examples": { "missingWorkspaceId": { "value": { "error": "workspace_id is required" } }, "missingEmail": { "value": { "error": "email is required" } }, "missingListId": { "value": { "error": "list_id is required" } }, "invalidStatus": { "value": { "error": "invalid status value" } } } } } }, "401": { "description": "Unauthorized - invalid or missing authentication token", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "404": { "description": "Contact list relationship not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "error": "contact list not found" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } } } } }, "/api/broadcasts.list": { "get": { "summary": "List broadcasts", "description": "Retrieves a list of broadcasts with pagination and optional filtering by status. Supports fetching template details for each variation.", "operationId": "listBroadcasts", "security": [ { "BearerAuth": [] } ], "parameters": [ { "name": "workspace_id", "in": "query", "required": true, "schema": { "type": "string" }, "description": "The ID of the workspace", "example": "ws_1234567890" }, { "name": "status", "in": "query", "required": false, "schema": { "type": "string", "enum": [ "draft", "scheduled", "sending", "paused", "sent", "cancelled", "failed", "testing", "test_completed", "winner_selected" ] }, "description": "Filter broadcasts by status" }, { "name": "limit", "in": "query", "required": false, "schema": { "type": "integer", "default": 50 }, "description": "Maximum number of broadcasts to return", "example": 50 }, { "name": "offset", "in": "query", "required": false, "schema": { "type": "integer", "default": 0 }, "description": "Number of broadcasts to skip for pagination", "example": 0 }, { "name": "with_templates", "in": "query", "required": false, "schema": { "type": "boolean", "default": false }, "description": "Include full template details for each variation", "example": false } ], "responses": { "200": { "description": "List of broadcasts retrieved successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/BroadcastListResponse" } } } }, "400": { "description": "Bad request - validation failed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "error": "workspace_id is required" } } } }, "401": { "description": "Unauthorized - invalid or missing authentication token", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "error": "Failed to list broadcasts" } } } } } } }, "/api/broadcasts.get": { "get": { "summary": "Get a broadcast", "description": "Retrieves a single broadcast by ID. Optionally includes template details for each variation.", "operationId": "getBroadcast", "security": [ { "BearerAuth": [] } ], "parameters": [ { "name": "workspace_id", "in": "query", "required": true, "schema": { "type": "string" }, "description": "The ID of the workspace", "example": "ws_1234567890" }, { "name": "id", "in": "query", "required": true, "schema": { "type": "string" }, "description": "The ID of the broadcast", "example": "broadcast_12345" }, { "name": "with_templates", "in": "query", "required": false, "schema": { "type": "boolean", "default": false }, "description": "Include full template details for each variation", "example": false } ], "responses": { "200": { "description": "Broadcast retrieved successfully", "content": { "application/json": { "schema": { "type": "object", "properties": { "broadcast": { "$ref": "#/components/schemas/Broadcast" } } } } } }, "400": { "description": "Bad request - validation failed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "401": { "description": "Unauthorized - invalid or missing authentication token", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "404": { "description": "Broadcast not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "error": "Broadcast not found" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } } } } }, "/api/broadcasts.create": { "post": { "summary": "Create a broadcast", "description": "Creates a new broadcast campaign in draft status.\n\n**Important:** Creating a broadcast does not schedule or send it. After creating the broadcast,\nyou must call the `/api/broadcasts.schedule` endpoint to schedule or send it immediately.\n", "operationId": "createBroadcast", "security": [ { "BearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateBroadcastRequest" } } } }, "responses": { "201": { "description": "Broadcast created successfully", "content": { "application/json": { "schema": { "type": "object", "properties": { "broadcast": { "$ref": "#/components/schemas/Broadcast" } } } } } }, "400": { "description": "Bad request - validation failed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "examples": { "missingName": { "value": { "error": "name is required" } }, "missingList": { "value": { "error": "list is required" } }, "invalidTestSettings": { "value": { "error": "at least 2 variations are required for A/B testing" } } } } } }, "401": { "description": "Unauthorized - invalid or missing authentication token", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "error": "Failed to create broadcast" } } } } } } }, "/api/broadcasts.update": { "post": { "summary": "Update a broadcast", "description": "Updates an existing broadcast. Only broadcasts in draft, scheduled, or paused status can be updated.", "operationId": "updateBroadcast", "security": [ { "BearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpdateBroadcastRequest" } } } }, "responses": { "200": { "description": "Broadcast updated successfully", "content": { "application/json": { "schema": { "type": "object", "properties": { "broadcast": { "$ref": "#/components/schemas/Broadcast" } } } } } }, "400": { "description": "Bad request - validation failed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "examples": { "invalidStatus": { "value": { "error": "cannot update broadcast with status: sending" } } } } } }, "401": { "description": "Unauthorized - invalid or missing authentication token", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "404": { "description": "Broadcast not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "error": "Broadcast not found" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } } } } }, "/api/broadcasts.schedule": { "post": { "summary": "Schedule a broadcast", "description": "Schedules a broadcast for sending either immediately or at a specified time. This endpoint is restricted in demo mode.", "operationId": "scheduleBroadcast", "security": [ { "BearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ScheduleBroadcastRequest" } } } }, "responses": { "200": { "description": "Broadcast scheduled successfully", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean", "example": true } } } } } }, "400": { "description": "Bad request - validation failed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "examples": { "missingDateTime": { "value": { "error": "scheduled_date and scheduled_time are required when not sending immediately" } } } } } }, "401": { "description": "Unauthorized - invalid or missing authentication token", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "404": { "description": "Broadcast not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } } } } }, "/api/broadcasts.pause": { "post": { "summary": "Pause a broadcast", "description": "Pauses a broadcast that is currently sending.", "operationId": "pauseBroadcast", "security": [ { "BearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/PauseBroadcastRequest" } } } }, "responses": { "200": { "description": "Broadcast paused successfully", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean", "example": true } } } } } }, "400": { "description": "Bad request - validation failed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "401": { "description": "Unauthorized - invalid or missing authentication token", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "404": { "description": "Broadcast not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } } } } }, "/api/broadcasts.resume": { "post": { "summary": "Resume a broadcast", "description": "Resumes a paused broadcast.", "operationId": "resumeBroadcast", "security": [ { "BearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ResumeBroadcastRequest" } } } }, "responses": { "200": { "description": "Broadcast resumed successfully", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean", "example": true } } } } } }, "400": { "description": "Bad request - validation failed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "401": { "description": "Unauthorized - invalid or missing authentication token", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "404": { "description": "Broadcast not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } } } } }, "/api/broadcasts.cancel": { "post": { "summary": "Cancel a broadcast", "description": "Cancels a scheduled broadcast.", "operationId": "cancelBroadcast", "security": [ { "BearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CancelBroadcastRequest" } } } }, "responses": { "200": { "description": "Broadcast cancelled successfully", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean", "example": true } } } } } }, "400": { "description": "Bad request - validation failed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "401": { "description": "Unauthorized - invalid or missing authentication token", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "404": { "description": "Broadcast not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } } } } }, "/api/broadcasts.sendToIndividual": { "post": { "summary": "Send broadcast to individual", "description": "Sends a broadcast to a single recipient for testing purposes. Optionally specify which template variation to send.", "operationId": "sendBroadcastToIndividual", "security": [ { "BearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SendToIndividualRequest" } } } }, "responses": { "200": { "description": "Broadcast sent to individual successfully", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean", "example": true } } } } } }, "400": { "description": "Bad request - validation failed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "401": { "description": "Unauthorized - invalid or missing authentication token", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "404": { "description": "Broadcast not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } } } } }, "/api/broadcasts.delete": { "post": { "summary": "Delete a broadcast", "description": "Deletes a broadcast. This action is permanent and cannot be undone.", "operationId": "deleteBroadcast", "security": [ { "BearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DeleteBroadcastRequest" } } } }, "responses": { "200": { "description": "Broadcast deleted successfully", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean", "example": true } } } } } }, "400": { "description": "Bad request - validation failed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "401": { "description": "Unauthorized - invalid or missing authentication token", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "404": { "description": "Broadcast not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } } } } }, "/api/broadcasts.getTestResults": { "get": { "summary": "Get A/B test results", "description": "Retrieves the performance metrics for all variations in an A/B test broadcast.", "operationId": "getBroadcastTestResults", "security": [ { "BearerAuth": [] } ], "parameters": [ { "name": "workspace_id", "in": "query", "required": true, "schema": { "type": "string" }, "description": "The ID of the workspace", "example": "ws_1234567890" }, { "name": "id", "in": "query", "required": true, "schema": { "type": "string" }, "description": "The ID of the broadcast", "example": "broadcast_12345" } ], "responses": { "200": { "description": "Test results retrieved successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TestResultsResponse" } } } }, "400": { "description": "Bad request - validation failed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "401": { "description": "Unauthorized - invalid or missing authentication token", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "error": "Failed to get test results" } } } } } } }, "/api/broadcasts.selectWinner": { "post": { "summary": "Select winning A/B test variation", "description": "Manually selects the winning variation for an A/B test broadcast. The winning variation will be sent to the remaining audience. This endpoint is restricted in demo mode.", "operationId": "selectBroadcastWinner", "security": [ { "BearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SelectWinnerRequest" } } } }, "responses": { "200": { "description": "Winner selected successfully", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean", "example": true } } } } } }, "400": { "description": "Bad request - validation failed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "401": { "description": "Unauthorized - invalid or missing authentication token", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "error": "Failed to select winner" } } } } } } }, "/api/broadcasts.refreshGlobalFeed": { "post": { "summary": "Refresh global feed data", "description": "Fetches fresh data from the configured global feed URL and stores it in the broadcast.\nThe global feed must be enabled for the broadcast before calling this endpoint.\n", "operationId": "refreshGlobalFeed", "security": [ { "BearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/RefreshGlobalFeedRequest" } } } }, "responses": { "200": { "description": "Global feed data refreshed successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/RefreshGlobalFeedResponse" } } } }, "400": { "description": "Bad request - global feed not enabled or validation failed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "examples": { "notEnabled": { "value": { "error": "global feed is not enabled for this broadcast" } }, "validation": { "value": { "error": "workspace_id is required" } } } } } }, "401": { "description": "Unauthorized - invalid or missing authentication token", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "404": { "description": "Broadcast not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "error": "Broadcast not found" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "error": "Failed to refresh global feed" } } } } } } }, "/api/broadcasts.testRecipientFeed": { "post": { "summary": "Test recipient feed configuration", "description": "Tests the per-recipient feed configuration by fetching data for a specific contact or a random contact from the broadcast audience.\nThe recipient feed must be enabled for the broadcast before calling this endpoint.\n", "operationId": "testRecipientFeed", "security": [ { "BearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TestRecipientFeedRequest" } } } }, "responses": { "200": { "description": "Recipient feed test completed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TestRecipientFeedResponse" } } } }, "400": { "description": "Bad request - recipient feed not enabled or validation failed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "examples": { "notEnabled": { "value": { "error": "recipient feed is not enabled for this broadcast" } }, "validation": { "value": { "error": "workspace_id is required" } } } } } }, "401": { "description": "Unauthorized - invalid or missing authentication token", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "404": { "description": "Broadcast or contact not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "examples": { "broadcastNotFound": { "value": { "error": "Broadcast not found" } }, "contactNotFound": { "value": { "error": "Contact not found" } } } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "error": "Failed to test recipient feed" } } } } } } }, "/api/templates.list": { "get": { "summary": "List templates", "description": "Retrieves a list of all templates in the workspace. Supports optional filtering by category and channel.", "operationId": "listTemplates", "security": [ { "BearerAuth": [] } ], "parameters": [ { "name": "workspace_id", "in": "query", "required": true, "schema": { "type": "string" }, "description": "The ID of the workspace", "example": "ws_1234567890" }, { "name": "category", "in": "query", "required": false, "schema": { "type": "string", "enum": [ "marketing", "transactional", "welcome", "opt_in", "unsubscribe", "bounce", "blocklist", "blog", "other" ] }, "description": "Filter templates by category" }, { "name": "channel", "in": "query", "required": false, "schema": { "type": "string", "enum": [ "email", "web" ] }, "description": "Filter templates by channel" } ], "responses": { "200": { "description": "List of templates retrieved successfully", "content": { "application/json": { "schema": { "type": "object", "properties": { "templates": { "type": "array", "items": { "$ref": "#/components/schemas/Template" } } } } } } }, "400": { "description": "Bad request - validation failed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "error": "workspace_id is required" } } } }, "401": { "description": "Unauthorized - invalid or missing authentication token", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "error": "Failed to get templates" } } } } } } }, "/api/templates.get": { "get": { "summary": "Get a template", "description": "Retrieves a single template by ID. Optionally specify a version number to retrieve a specific version.", "operationId": "getTemplate", "security": [ { "BearerAuth": [] } ], "parameters": [ { "name": "workspace_id", "in": "query", "required": true, "schema": { "type": "string" }, "description": "The ID of the workspace", "example": "ws_1234567890" }, { "name": "id", "in": "query", "required": true, "schema": { "type": "string" }, "description": "The ID of the template", "example": "welcome_email" }, { "name": "version", "in": "query", "required": false, "schema": { "type": "integer", "format": "int64" }, "description": "Specific version number (defaults to latest)", "example": 1 } ], "responses": { "200": { "description": "Template retrieved successfully", "content": { "application/json": { "schema": { "type": "object", "properties": { "template": { "$ref": "#/components/schemas/Template" } } } } } }, "400": { "description": "Bad request - validation failed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "401": { "description": "Unauthorized - invalid or missing authentication token", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "404": { "description": "Template not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "error": "Template not found" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } } } } }, "/api/templates.create": { "post": { "summary": "Create a template", "description": "Creates a new template. Each template must have a channel (email or web) with corresponding channel-specific content.", "operationId": "createTemplate", "security": [ { "BearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateTemplateRequest" } } } }, "responses": { "201": { "description": "Template created successfully", "content": { "application/json": { "schema": { "type": "object", "properties": { "template": { "$ref": "#/components/schemas/Template" } } } } } }, "400": { "description": "Bad request - validation failed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "examples": { "duplicateId": { "value": { "error": "Template id already exists" } }, "missingEmail": { "value": { "error": "email is required for channel 'email'" } }, "invalidId": { "value": { "error": "id must contain only letters, numbers, underscores, and hyphens" } } } } } }, "401": { "description": "Unauthorized - invalid or missing authentication token", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "error": "Failed to create template" } } } } } } }, "/api/templates.update": { "post": { "summary": "Update a template", "description": "Updates an existing template. Creates a new version of the template.", "operationId": "updateTemplate", "security": [ { "BearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpdateTemplateRequest" } } } }, "responses": { "200": { "description": "Template updated successfully", "content": { "application/json": { "schema": { "type": "object", "properties": { "template": { "$ref": "#/components/schemas/Template" } } } } } }, "400": { "description": "Bad request - validation failed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "401": { "description": "Unauthorized - invalid or missing authentication token", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "404": { "description": "Template not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "error": "Template not found" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "error": "Failed to update template" } } } } } } }, "/api/templates.delete": { "post": { "summary": "Delete a template", "description": "Soft-deletes a template. The template will be marked as deleted but not removed from the database.", "operationId": "deleteTemplate", "security": [ { "BearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DeleteTemplateRequest" } } } }, "responses": { "200": { "description": "Template deleted successfully", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean", "example": true } } } } } }, "400": { "description": "Bad request - validation failed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "401": { "description": "Unauthorized - invalid or missing authentication token", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "404": { "description": "Template not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "error": "Template not found" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "error": "Failed to delete template" } } } } } } }, "/api/templates.compile": { "post": { "summary": "Compile template", "description": "Compiles an MJML template with provided data, returning the rendered HTML.\nWhen `subject` and/or `subject_preview` are provided, they are rendered\nthrough the same Liquid engine used at send time and returned alongside\nthe HTML so callers do not need to run a separate Liquid pass to preview\nthe subject line. Useful for previewing templates before saving.\n", "operationId": "compileTemplate", "security": [ { "BearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CompileTemplateRequest" } } } }, "responses": { "200": { "description": "Template compiled successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CompileTemplateResponse" } } } }, "400": { "description": "Bad request - compilation failed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "error": "Compilation failed: Invalid MJML syntax" } } } }, "401": { "description": "Unauthorized - invalid or missing authentication token", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } } } } }, "/api/customEvents.import": { "post": { "summary": "Import custom events", "description": "Imports multiple custom events in a single batch operation. Maximum 50 events per request. Auto-creates contacts if they don't exist.", "operationId": "importCustomEvents", "security": [ { "BearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ImportCustomEventsRequest" } } } }, "responses": { "201": { "description": "Custom events imported successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ImportCustomEventsResponse" } } } }, "400": { "description": "Bad request - validation failed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "examples": { "missingWorkspaceId": { "value": { "error": "workspace_id is required" } }, "emptyEvents": { "value": { "error": "events array cannot be empty" } }, "tooManyEvents": { "value": { "error": "maximum 50 events allowed per batch" } }, "invalidEventName": { "value": { "error": "invalid event name: must contain only lowercase letters, numbers, underscores, dots, and slashes" } }, "missingRequiredFields": { "value": { "error": "event_name, external_id, and email are required for each event" } } } } } }, "401": { "description": "Unauthorized - invalid or missing authentication token", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "403": { "description": "Forbidden - missing required permission", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "error": "contacts:write permission required" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "error": "Failed to create custom events" } } } } } } }, "/api/webhookSubscriptions.create": { "post": { "summary": "Create a webhook subscription", "description": "Creates a new webhook subscription to receive HTTP callbacks when specified events occur in the workspace.", "operationId": "createWebhookSubscription", "security": [ { "BearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateWebhookSubscriptionRequest" } } } }, "responses": { "201": { "description": "Webhook subscription created successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/CreateWebhookSubscriptionResponse" } } } }, "400": { "description": "Bad request - validation failed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "examples": { "missingWorkspaceId": { "value": { "error": "workspace_id is required" } }, "invalidUrl": { "value": { "error": "url must be a valid HTTPS URL" } }, "emptyEventTypes": { "value": { "error": "at least one event type is required" } } } } } }, "401": { "description": "Unauthorized - invalid or missing authentication token", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } } } } }, "/api/webhookSubscriptions.list": { "get": { "summary": "List webhook subscriptions", "description": "Returns all webhook subscriptions for the specified workspace.", "operationId": "listWebhookSubscriptions", "security": [ { "BearerAuth": [] } ], "parameters": [ { "name": "workspace_id", "in": "query", "required": true, "schema": { "type": "string" }, "description": "The ID of the workspace", "example": "ws_1234567890" } ], "responses": { "200": { "description": "List of webhook subscriptions", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ListWebhookSubscriptionsResponse" } } } }, "400": { "description": "Bad request - missing workspace_id", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "error": "workspace_id is required" } } } }, "401": { "description": "Unauthorized - invalid or missing authentication token", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } } } } }, "/api/webhookSubscriptions.get": { "get": { "summary": "Get a webhook subscription", "description": "Retrieves a specific webhook subscription by ID.", "operationId": "getWebhookSubscription", "security": [ { "BearerAuth": [] } ], "parameters": [ { "name": "workspace_id", "in": "query", "required": true, "schema": { "type": "string" }, "description": "The ID of the workspace", "example": "ws_1234567890" }, { "name": "id", "in": "query", "required": true, "schema": { "type": "string" }, "description": "The ID of the webhook subscription", "example": "whsub_a1b2c3d4e5f6" } ], "responses": { "200": { "description": "Webhook subscription found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/GetWebhookSubscriptionResponse" } } } }, "400": { "description": "Bad request - missing parameters", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "examples": { "missingWorkspaceId": { "value": { "error": "workspace_id is required" } }, "missingId": { "value": { "error": "id is required" } } } } } }, "401": { "description": "Unauthorized - invalid or missing authentication token", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "404": { "description": "Webhook subscription not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "error": "Webhook subscription not found" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } } } } }, "/api/webhookSubscriptions.update": { "post": { "summary": "Update a webhook subscription", "description": "Updates an existing webhook subscription with new configuration.", "operationId": "updateWebhookSubscription", "security": [ { "BearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpdateWebhookSubscriptionRequest" } } } }, "responses": { "200": { "description": "Webhook subscription updated successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UpdateWebhookSubscriptionResponse" } } } }, "400": { "description": "Bad request - validation failed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "examples": { "missingWorkspaceId": { "value": { "error": "workspace_id is required" } }, "missingId": { "value": { "error": "id is required" } }, "invalidUrl": { "value": { "error": "url must be a valid HTTPS URL" } } } } } }, "401": { "description": "Unauthorized - invalid or missing authentication token", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "404": { "description": "Webhook subscription not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } } } } }, "/api/webhookSubscriptions.delete": { "post": { "summary": "Delete a webhook subscription", "description": "Permanently deletes a webhook subscription. This action cannot be undone.", "operationId": "deleteWebhookSubscription", "security": [ { "BearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/DeleteWebhookSubscriptionRequest" } } } }, "responses": { "200": { "description": "Webhook subscription deleted successfully", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean", "example": true } } } } } }, "400": { "description": "Bad request - missing parameters", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "examples": { "missingWorkspaceId": { "value": { "error": "workspace_id is required" } }, "missingId": { "value": { "error": "id is required" } } } } } }, "401": { "description": "Unauthorized - invalid or missing authentication token", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } } } } }, "/api/webhookSubscriptions.toggle": { "post": { "summary": "Toggle webhook subscription enabled state", "description": "Enables or disables a webhook subscription. Disabled subscriptions will not receive events.", "operationId": "toggleWebhookSubscription", "security": [ { "BearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ToggleWebhookSubscriptionRequest" } } } }, "responses": { "200": { "description": "Webhook subscription toggled successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ToggleWebhookSubscriptionResponse" } } } }, "400": { "description": "Bad request - missing parameters", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "examples": { "missingWorkspaceId": { "value": { "error": "workspace_id is required" } }, "missingId": { "value": { "error": "id is required" } } } } } }, "401": { "description": "Unauthorized - invalid or missing authentication token", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } } } } }, "/api/webhookSubscriptions.regenerateSecret": { "post": { "summary": "Regenerate webhook secret", "description": "Generates a new signing secret for the webhook subscription. The old secret will immediately become invalid.", "operationId": "regenerateWebhookSecret", "security": [ { "BearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/RegenerateWebhookSecretRequest" } } } }, "responses": { "200": { "description": "Webhook secret regenerated successfully", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/RegenerateWebhookSecretResponse" } } } }, "400": { "description": "Bad request - missing parameters", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "examples": { "missingWorkspaceId": { "value": { "error": "workspace_id is required" } }, "missingId": { "value": { "error": "id is required" } } } } } }, "401": { "description": "Unauthorized - invalid or missing authentication token", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } } } } }, "/api/webhookSubscriptions.deliveries": { "get": { "summary": "Get webhook delivery history", "description": "Returns the delivery history for a webhook subscription, including status and response information.", "operationId": "getWebhookDeliveries", "security": [ { "BearerAuth": [] } ], "parameters": [ { "name": "workspace_id", "in": "query", "required": true, "schema": { "type": "string" }, "description": "The ID of the workspace", "example": "ws_1234567890" }, { "name": "subscription_id", "in": "query", "required": false, "schema": { "type": "string" }, "description": "The ID of the webhook subscription (optional - if not provided, returns all deliveries for the workspace)", "example": "whsub_a1b2c3d4e5f6" }, { "name": "limit", "in": "query", "required": false, "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 20 }, "description": "Number of deliveries to return (1-100)", "example": 20 }, { "name": "offset", "in": "query", "required": false, "schema": { "type": "integer", "minimum": 0, "default": 0 }, "description": "Offset for pagination", "example": 0 } ], "responses": { "200": { "description": "List of webhook deliveries", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ListWebhookDeliveriesResponse" } } } }, "400": { "description": "Bad request - missing parameters", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "error": "workspace_id is required" } } } }, "401": { "description": "Unauthorized - invalid or missing authentication token", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } } } } }, "/api/webhookSubscriptions.test": { "post": { "summary": "Send a test webhook", "description": "Sends a test webhook to the subscription's URL to verify connectivity and configuration.", "operationId": "testWebhookSubscription", "security": [ { "BearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TestWebhookRequest" } } } }, "responses": { "200": { "description": "Test webhook sent (check response for success/failure)", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/TestWebhookResponse" } } } }, "400": { "description": "Bad request - missing parameters", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "examples": { "missingWorkspaceId": { "value": { "error": "workspace_id is required" } }, "missingId": { "value": { "error": "id is required" } } } } } }, "401": { "description": "Unauthorized - invalid or missing authentication token", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "404": { "description": "Webhook subscription not found", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "error": "Webhook subscription not found" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } } } } }, "/api/webhookSubscriptions.eventTypes": { "get": { "summary": "List available event types", "description": "Returns a list of all available webhook event types that can be subscribed to.", "operationId": "listWebhookEventTypes", "security": [ { "BearerAuth": [] } ], "responses": { "200": { "description": "List of available event types", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ListWebhookEventTypesResponse" } } } }, "401": { "description": "Unauthorized - invalid or missing authentication token", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } } } } }, "/subscribe": { "post": { "summary": "Subscribe to email lists", "description": "Subscribe a contact to one or more email lists. This is a public endpoint that doesn't require authentication.\n\n**Important:** For unauthenticated requests, only **public lists** can be subscribed to. To subscribe to private lists, you can either:\n- Provide a valid `email_hmac` in the contact object (used by the notification center for re-subscribing after unsubscribing)\n- Use the authenticated `/api/lists.subscribe` endpoint with a bearer token\n\nIf the list has double opt-in enabled, a confirmation email will be sent.\n", "operationId": "subscribeToLists", "security": [], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SubscribeToListsRequest" } } } }, "responses": { "200": { "description": "Subscription successful", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean", "description": "Whether the subscription was successful", "example": true } } } } } }, "400": { "description": "Bad request - validation failed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "examples": { "missingWorkspaceId": { "value": { "error": "workspace_id is required" } }, "invalidEmail": { "value": { "error": "invalid contact: invalid email format" } }, "missingListIds": { "value": { "error": "list_ids is required" } }, "listNotPublic": { "value": { "error": "list is not public" } } } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "error": "Failed to subscribe to lists" } } } } } } }, "/api/lists.subscribe": { "post": { "summary": "Subscribe to email lists (authenticated)", "description": "Subscribe a contact to one or more email lists. This is an authenticated endpoint that requires a bearer token.\n\nUnlike the public `/subscribe` endpoint, this endpoint can subscribe contacts to **any list** (public or private) as long as the authenticated user has write permission on lists.\n\n**Note:** Double opt-in is bypassed for authenticated requests. Contacts are immediately set to `active` status regardless of the list's double opt-in setting. This allows administrators to manually add contacts without requiring email confirmation.\n", "operationId": "subscribeToListsAuthenticated", "security": [ { "BearerAuth": [] } ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/SubscribeToListsRequest" } } } }, "responses": { "200": { "description": "Subscription successful", "content": { "application/json": { "schema": { "type": "object", "properties": { "success": { "type": "boolean", "description": "Whether the subscription was successful", "example": true } } } } } }, "400": { "description": "Bad request - validation failed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "examples": { "missingWorkspaceId": { "value": { "error": "workspace_id is required" } }, "invalidEmail": { "value": { "error": "invalid contact: invalid email format" } }, "missingListIds": { "value": { "error": "list_ids is required" } } } } } }, "401": { "description": "Unauthorized - invalid or missing authentication token", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" } } } }, "500": { "description": "Internal server error", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "error": "Failed to subscribe to lists" } } } } } } }, "/api/user.rootSignin": { "post": { "summary": "Root user programmatic signin", "security": [], "description": "Authenticates the root user using HMAC signature for programmatic access.\nThis endpoint is designed for automation scenarios like Infrastructure-as-Code\ndeployments, CI/CD pipelines, and automated testing where magic link\nauthentication is impractical.\n\n**Security Features:**\n- HMAC-SHA256 signature verification using the application's secret key\n- 60-second timestamp window to prevent replay attacks\n- Rate limited to 5 attempts per 5 minutes per email\n- Only works for a configured root email address (ROOT_EMAIL may list several, comma/semicolon-separated)\n\n**How to generate the signature:**\n```bash\nSECRET_KEY=\"your-notifuse-secret-key\"\nROOT_EMAIL=\"admin@example.com\"\nTIMESTAMP=$(date +%s)\nMESSAGE=\"${ROOT_EMAIL}:${TIMESTAMP}\"\nSIGNATURE=$(echo -n \"$MESSAGE\" | openssl dgst -sha256 -hmac \"$SECRET_KEY\" | awk '{print $2}')\n```\n", "operationId": "rootSignin", "tags": [ "Authentication" ], "requestBody": { "required": true, "content": { "application/json": { "schema": { "$ref": "#/components/schemas/RootSigninRequest" }, "example": { "email": "admin@example.com", "timestamp": 1735600000, "signature": "a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456" } } } }, "responses": { "200": { "description": "Authentication successful", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/RootSigninResponse" }, "example": { "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoidXNyXzEyMzQ1Njc4OTAiLCJzZXNzaW9uX2lkIjoic2VzXzEyMzQ1Njc4OTAiLCJleHAiOjE3MzU2ODY0MDB9.signature", "user": { "id": "usr_1234567890", "email": "admin@example.com", "name": "Admin User", "created_at": "2025-01-01T00:00:00Z", "updated_at": "2025-01-01T00:00:00Z" }, "expires_at": "2025-01-01T12:00:00Z" } } } }, "400": { "description": "Bad request - missing required fields", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "examples": { "missingFields": { "summary": "Missing required fields", "value": { "error": "Missing required fields: email, timestamp, signature" } }, "invalidBody": { "summary": "Invalid JSON body", "value": { "error": "Invalid request body" } } } } } }, "401": { "description": "Unauthorized - invalid credentials", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "examples": { "invalidCredentials": { "summary": "Invalid email, timestamp, or signature", "value": { "error": "Invalid credentials" } }, "rateLimited": { "summary": "Too many failed attempts", "value": { "error": "Invalid credentials" } } } } } }, "405": { "description": "Method not allowed", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ErrorResponse" }, "example": { "error": "Method not allowed" } } } } } } } }, "components": { "securitySchemes": { "BearerAuth": { "type": "http", "scheme": "bearer", "description": "API token for authentication" } }, "schemas": { "ErrorResponse": { "type": "object", "properties": { "error": { "type": "string", "description": "Error message" } }, "required": [ "error" ] }, "SendTransactionalRequest": { "type": "object", "required": [ "workspace_id", "notification" ], "properties": { "workspace_id": { "type": "string", "description": "The ID of the workspace", "example": "ws_1234567890" }, "notification": { "$ref": "#/components/schemas/TransactionalNotificationSendParams" } } }, "TransactionalNotificationSendParams": { "type": "object", "required": [ "id", "contact", "channels" ], "properties": { "id": { "type": "string", "description": "ID of the notification to send", "example": "welcome_email" }, "external_id": { "type": "string", "nullable": true, "description": "External ID for idempotency checks", "example": "txn_12345" }, "contact": { "$ref": "#/components/schemas/Contact" }, "channels": { "type": "array", "description": "Channels to send through (at least one required)", "minItems": 1, "items": { "type": "string", "enum": [ "email" ] }, "example": [ "email" ] }, "data": { "type": "object", "additionalProperties": true, "description": "Data to populate the template with", "example": { "user_name": "John Doe", "activation_link": "https://example.com/activate/abc123" } }, "metadata": { "type": "object", "additionalProperties": true, "description": "Additional metadata for tracking", "example": { "campaign_id": "spring_promo_2023", "source": "user_signup" } }, "email_options": { "type": "object", "description": "Email-specific options", "properties": { "from_name": { "type": "string", "nullable": true, "description": "Override default sender from name", "example": "Support Team" }, "subject": { "type": "string", "nullable": true, "description": "Override template subject line. Supports Liquid templating variables.", "maxLength": 255, "example": "Welcome, {{ name }}!" }, "subject_preview": { "type": "string", "nullable": true, "description": "Override template preheader/preview text. Supports Liquid templating variables.", "maxLength": 255, "example": "Your order has been confirmed" }, "cc": { "type": "array", "description": "CC email addresses", "items": { "type": "string", "format": "email" }, "example": [ "manager@example.com" ] }, "bcc": { "type": "array", "description": "BCC email addresses", "items": { "type": "string", "format": "email" }, "example": [ "archive@example.com" ] }, "reply_to": { "type": "string", "format": "email", "description": "Reply-To email address", "example": "support@example.com" }, "attachments": { "type": "array", "description": "Email attachments (max 20 files, 3MB per file, 10MB total)", "items": { "$ref": "#/components/schemas/EmailAttachment" }, "maxItems": 20, "example": [ { "filename": "invoice.pdf", "content": "JVBERi0xLjQKJeLjz9MKMy4uLg==", "content_type": "application/pdf", "disposition": "attachment" } ] } } } } }, "Contact": { "type": "object", "required": [ "email" ], "properties": { "email": { "type": "string", "format": "email", "description": "Email address of the contact. This is the contact's unique identifier.", "example": "user@example.com" }, "external_id": { "type": "string", "nullable": true, "description": "Identifier for the contact in your own system. Not unique — several contacts may share the same external_id.", "example": "user_12345" }, "timezone": { "type": "string", "nullable": true, "description": "Timezone of the contact", "example": "America/New_York" }, "language": { "type": "string", "nullable": true, "description": "Preferred language of the contact", "example": "en-US" }, "first_name": { "type": "string", "nullable": true, "description": "First name of the contact", "example": "John" }, "last_name": { "type": "string", "nullable": true, "description": "Last name of the contact", "example": "Doe" }, "full_name": { "type": "string", "nullable": true, "description": "Full name of the contact (for systems that don't have separate first/last names)", "example": "John Doe" }, "phone": { "type": "string", "nullable": true, "description": "Phone number of the contact", "example": "+15551234567" }, "address_line_1": { "type": "string", "nullable": true, "description": "First line of address", "example": "123 Main St" }, "address_line_2": { "type": "string", "nullable": true, "description": "Second line of address", "example": "Apt 4B" }, "country": { "type": "string", "nullable": true, "description": "Country of the contact", "example": "US" }, "postcode": { "type": "string", "nullable": true, "description": "Postal code", "example": "10001" }, "state": { "type": "string", "nullable": true, "description": "State or province", "example": "NY" }, "job_title": { "type": "string", "nullable": true, "description": "Job title of the contact", "example": "Software Engineer" }, "custom_string_1": { "type": "string", "nullable": true, "description": "Custom string field 1", "example": "Premium tier" }, "custom_string_2": { "type": "string", "nullable": true, "description": "Custom string field 2" }, "custom_string_3": { "type": "string", "nullable": true, "description": "Custom string field 3" }, "custom_string_4": { "type": "string", "nullable": true, "description": "Custom string field 4" }, "custom_string_5": { "type": "string", "nullable": true, "description": "Custom string field 5" }, "custom_number_1": { "type": "number", "format": "float", "nullable": true, "description": "Custom number field 1", "example": 42 }, "custom_number_2": { "type": "number", "format": "float", "nullable": true, "description": "Custom number field 2" }, "custom_number_3": { "type": "number", "format": "float", "nullable": true, "description": "Custom number field 3" }, "custom_number_4": { "type": "number", "format": "float", "nullable": true, "description": "Custom number field 4" }, "custom_number_5": { "type": "number", "format": "float", "nullable": true, "description": "Custom number field 5" }, "custom_datetime_1": { "type": "string", "format": "date-time", "nullable": true, "description": "Custom datetime field 1", "example": "2023-06-01T09:00:00Z" }, "custom_datetime_2": { "type": "string", "format": "date-time", "nullable": true, "description": "Custom datetime field 2" }, "custom_datetime_3": { "type": "string", "format": "date-time", "nullable": true, "description": "Custom datetime field 3" }, "custom_datetime_4": { "type": "string", "format": "date-time", "nullable": true, "description": "Custom datetime field 4" }, "custom_datetime_5": { "type": "string", "format": "date-time", "nullable": true, "description": "Custom datetime field 5" }, "custom_json_1": { "type": "object", "nullable": true, "description": "Custom JSON field 1", "example": { "preferences": { "theme": "dark", "notifications": true } } }, "custom_json_2": { "type": "object", "nullable": true, "description": "Custom JSON field 2" }, "custom_json_3": { "type": "object", "nullable": true, "description": "Custom JSON field 3" }, "custom_json_4": { "type": "object", "nullable": true, "description": "Custom JSON field 4" }, "custom_json_5": { "type": "object", "nullable": true, "description": "Custom JSON field 5" }, "created_at": { "type": "string", "format": "date-time", "readOnly": true, "description": "When the contact was created (read-only, set by server)", "example": "2023-01-15T10:30:00Z" }, "updated_at": { "type": "string", "format": "date-time", "readOnly": true, "description": "When the contact was last updated (read-only, set by server)", "example": "2023-04-22T15:45:00Z" }, "contact_lists": { "type": "array", "readOnly": true, "description": "Lists the contact is subscribed to (read-only, included in GET responses only)", "items": { "$ref": "#/components/schemas/ContactList" }, "example": [ { "email": "user@example.com", "list_id": "newsletter", "list_name": "Newsletter", "status": "active", "created_at": "2023-01-15T10:30:00Z", "updated_at": "2023-01-15T10:30:00Z", "deleted_at": null } ] }, "contact_segments": { "type": "array", "readOnly": true, "description": "Segments the contact belongs to (read-only, included in GET responses only)", "items": { "$ref": "#/components/schemas/ContactSegment" } } } }, "ContactInput": { "type": "object", "required": [ "email" ], "properties": { "email": { "type": "string", "format": "email", "description": "Email address of the contact. This is the contact's unique identifier — upserts always match contacts by email address.", "example": "user@example.com" }, "external_id": { "type": "string", "nullable": true, "description": "Optional identifier for the contact in your own system (e.g. your user ID). Stored and searchable, but not unique and never used to match contacts during upserts — sending an existing external_id with a new email address creates a second contact.", "example": "user_12345" }, "timezone": { "type": "string", "nullable": true, "description": "Timezone of the contact", "example": "America/New_York" }, "language": { "type": "string", "nullable": true, "description": "Preferred language of the contact", "example": "en-US" }, "first_name": { "type": "string", "nullable": true, "description": "First name of the contact", "example": "John" }, "last_name": { "type": "string", "nullable": true, "description": "Last name of the contact", "example": "Doe" }, "full_name": { "type": "string", "nullable": true, "description": "Full name of the contact (for systems that don't have separate first/last names)", "example": "John Doe" }, "phone": { "type": "string", "nullable": true, "description": "Phone number of the contact", "example": "+15551234567" }, "address_line_1": { "type": "string", "nullable": true, "description": "First line of address", "example": "123 Main St" }, "address_line_2": { "type": "string", "nullable": true, "description": "Second line of address", "example": "Apt 4B" }, "country": { "type": "string", "nullable": true, "description": "Country of the contact", "example": "US" }, "postcode": { "type": "string", "nullable": true, "description": "Postal code", "example": "10001" }, "state": { "type": "string", "nullable": true, "description": "State or province", "example": "NY" }, "job_title": { "type": "string", "nullable": true, "description": "Job title of the contact", "example": "Software Engineer" }, "custom_string_1": { "type": "string", "nullable": true, "description": "Custom string field 1", "example": "Premium tier" }, "custom_string_2": { "type": "string", "nullable": true, "description": "Custom string field 2" }, "custom_string_3": { "type": "string", "nullable": true, "description": "Custom string field 3" }, "custom_string_4": { "type": "string", "nullable": true, "description": "Custom string field 4" }, "custom_string_5": { "type": "string", "nullable": true, "description": "Custom string field 5" }, "custom_number_1": { "type": "number", "format": "float", "nullable": true, "description": "Custom number field 1", "example": 42 }, "custom_number_2": { "type": "number", "format": "float", "nullable": true, "description": "Custom number field 2" }, "custom_number_3": { "type": "number", "format": "float", "nullable": true, "description": "Custom number field 3" }, "custom_number_4": { "type": "number", "format": "float", "nullable": true, "description": "Custom number field 4" }, "custom_number_5": { "type": "number", "format": "float", "nullable": true, "description": "Custom number field 5" }, "custom_datetime_1": { "type": "string", "format": "date-time", "nullable": true, "description": "Custom datetime field 1", "example": "2023-06-01T09:00:00Z" }, "custom_datetime_2": { "type": "string", "format": "date-time", "nullable": true, "description": "Custom datetime field 2" }, "custom_datetime_3": { "type": "string", "format": "date-time", "nullable": true, "description": "Custom datetime field 3" }, "custom_datetime_4": { "type": "string", "format": "date-time", "nullable": true, "description": "Custom datetime field 4" }, "custom_datetime_5": { "type": "string", "format": "date-time", "nullable": true, "description": "Custom datetime field 5" }, "custom_json_1": { "type": "object", "nullable": true, "description": "Custom JSON field 1 (must be a JSON object or array)", "example": { "preferences": { "theme": "dark", "notifications": true } } }, "custom_json_2": { "type": "object", "nullable": true, "description": "Custom JSON field 2 (must be a JSON object or array)" }, "custom_json_3": { "type": "object", "nullable": true, "description": "Custom JSON field 3 (must be a JSON object or array)" }, "custom_json_4": { "type": "object", "nullable": true, "description": "Custom JSON field 4 (must be a JSON object or array)" }, "custom_json_5": { "type": "object", "nullable": true, "description": "Custom JSON field 5 (must be a JSON object or array)" } } }, "ContactList": { "type": "object", "properties": { "email": { "type": "string", "format": "email", "description": "Email address of the contact", "example": "user@example.com" }, "list_id": { "type": "string", "description": "ID of the list", "example": "newsletter" }, "list_name": { "type": "string", "description": "Name of the list", "example": "Newsletter" }, "status": { "type": "string", "enum": [ "active", "pending", "unsubscribed", "bounced", "complained" ], "description": "Subscription status", "example": "active" }, "created_at": { "type": "string", "format": "date-time", "description": "When the contact was subscribed to this list", "example": "2023-01-15T10:30:00Z" }, "updated_at": { "type": "string", "format": "date-time", "description": "When the subscription was last updated", "example": "2023-01-15T10:30:00Z" }, "deleted_at": { "type": "string", "format": "date-time", "nullable": true, "description": "When the subscription was deleted (null if active)", "example": null } } }, "ContactSegment": { "type": "object", "properties": { "email": { "type": "string", "format": "email", "description": "Email address of the contact", "example": "user@example.com" }, "segment_id": { "type": "string", "description": "ID of the segment", "example": "premium_users" }, "version": { "type": "integer", "format": "int64", "description": "Version of the segment computation", "example": 1 }, "matched_at": { "type": "string", "format": "date-time", "description": "When the contact was matched to this segment", "example": "2023-01-15T10:30:00Z" }, "computed_at": { "type": "string", "format": "date-time", "description": "When the segment was last computed", "example": "2023-01-15T10:30:00Z" } } }, "SubscribeToListsRequest": { "type": "object", "required": [ "workspace_id", "contact", "list_ids" ], "properties": { "workspace_id": { "type": "string", "description": "The ID of the workspace", "example": "ws_1234567890" }, "contact": { "$ref": "#/components/schemas/SubscriptionContact" }, "list_ids": { "type": "array", "description": "Array of list IDs to subscribe the contact to", "items": { "type": "string" }, "example": [ "newsletter", "product_updates" ], "minItems": 1 } } }, "SubscriptionContact": { "type": "object", "required": [ "email" ], "properties": { "email": { "type": "string", "format": "email", "description": "Email address of the contact", "example": "user@example.com" }, "email_hmac": { "type": "string", "description": "HMAC of the email for authenticated subscription (optional)", "example": "a1b2c3d4e5f6..." }, "external_id": { "type": "string", "nullable": true, "description": "Optional identifier for the contact in your own system. Not unique — contacts are identified by email address.", "example": "user_12345" }, "timezone": { "type": "string", "nullable": true, "description": "Timezone of the contact", "example": "America/New_York" }, "language": { "type": "string", "nullable": true, "description": "Preferred language of the contact", "example": "en-US" }, "first_name": { "type": "string", "nullable": true, "description": "First name of the contact", "example": "John" }, "last_name": { "type": "string", "nullable": true, "description": "Last name of the contact", "example": "Doe" }, "full_name": { "type": "string", "nullable": true, "description": "Full name of the contact (for systems that don't have separate first/last names)", "example": "John Doe" }, "phone": { "type": "string", "nullable": true, "description": "Phone number of the contact", "example": "+15551234567" }, "address_line_1": { "type": "string", "nullable": true, "description": "First line of address", "example": "123 Main St" }, "address_line_2": { "type": "string", "nullable": true, "description": "Second line of address", "example": "Apt 4B" }, "country": { "type": "string", "nullable": true, "description": "Country of the contact", "example": "US" }, "postcode": { "type": "string", "nullable": true, "description": "Postal code", "example": "10001" }, "state": { "type": "string", "nullable": true, "description": "State or province", "example": "NY" }, "job_title": { "type": "string", "nullable": true, "description": "Job title of the contact", "example": "Software Engineer" }, "custom_string_1": { "type": "string", "nullable": true, "description": "Custom string field 1", "example": "Premium tier" }, "custom_string_2": { "type": "string", "nullable": true, "description": "Custom string field 2" }, "custom_string_3": { "type": "string", "nullable": true, "description": "Custom string field 3" }, "custom_string_4": { "type": "string", "nullable": true, "description": "Custom string field 4" }, "custom_string_5": { "type": "string", "nullable": true, "description": "Custom string field 5" }, "custom_number_1": { "type": "number", "format": "float", "nullable": true, "description": "Custom number field 1", "example": 42 }, "custom_number_2": { "type": "number", "format": "float", "nullable": true, "description": "Custom number field 2" }, "custom_number_3": { "type": "number", "format": "float", "nullable": true, "description": "Custom number field 3" }, "custom_number_4": { "type": "number", "format": "float", "nullable": true, "description": "Custom number field 4" }, "custom_number_5": { "type": "number", "format": "float", "nullable": true, "description": "Custom number field 5" }, "custom_datetime_1": { "type": "string", "format": "date-time", "nullable": true, "description": "Custom datetime field 1", "example": "2023-06-01T09:00:00Z" }, "custom_datetime_2": { "type": "string", "format": "date-time", "nullable": true, "description": "Custom datetime field 2" }, "custom_datetime_3": { "type": "string", "format": "date-time", "nullable": true, "description": "Custom datetime field 3" }, "custom_datetime_4": { "type": "string", "format": "date-time", "nullable": true, "description": "Custom datetime field 4" }, "custom_datetime_5": { "type": "string", "format": "date-time", "nullable": true, "description": "Custom datetime field 5" }, "custom_json_1": { "type": "object", "nullable": true, "description": "Custom JSON field 1", "example": { "preferences": { "theme": "dark", "notifications": true } } }, "custom_json_2": { "type": "object", "nullable": true, "description": "Custom JSON field 2" }, "custom_json_3": { "type": "object", "nullable": true, "description": "Custom JSON field 3" }, "custom_json_4": { "type": "object", "nullable": true, "description": "Custom JSON field 4" }, "custom_json_5": { "type": "object", "nullable": true, "description": "Custom JSON field 5" } } }, "UpsertContactRequest": { "type": "object", "required": [ "workspace_id", "contact" ], "properties": { "workspace_id": { "type": "string", "description": "The ID of the workspace", "example": "ws_1234567890" }, "contact": { "$ref": "#/components/schemas/ContactInput" } } }, "UpsertContactResponse": { "type": "object", "properties": { "email": { "type": "string", "format": "email", "description": "Email address of the contact", "example": "user@example.com" }, "action": { "type": "string", "enum": [ "create", "update", "error" ], "description": "The action that was performed: 'create' for new contacts, 'update' for existing contacts, 'error' if validation failed", "example": "create" }, "error": { "type": "string", "nullable": true, "description": "Error message if the operation failed", "example": null } } }, "DeleteContactRequest": { "type": "object", "required": [ "workspace_id", "email" ], "properties": { "workspace_id": { "type": "string", "description": "The ID of the workspace", "example": "ws_1234567890" }, "email": { "type": "string", "format": "email", "description": "Email address of the contact to delete", "example": "user@example.com" } } }, "BatchImportContactsRequest": { "type": "object", "required": [ "workspace_id", "contacts" ], "properties": { "workspace_id": { "type": "string", "description": "The ID of the workspace", "example": "ws_1234567890" }, "contacts": { "type": "array", "description": "Array of contacts to import (create or update)", "items": { "$ref": "#/components/schemas/ContactInput" }, "minItems": 1, "example": [ { "email": "user1@example.com", "first_name": "John", "last_name": "Doe" }, { "email": "user2@example.com", "first_name": "Jane", "last_name": "Smith" } ] }, "subscribe_to_lists": { "type": "array", "description": "Optional array of list IDs to subscribe all contacts to", "items": { "type": "string" }, "example": [ "newsletter", "product_updates" ] } } }, "BatchImportContactsResponse": { "type": "object", "properties": { "operations": { "type": "array", "description": "Array of operation results, one for each contact in the request", "items": { "$ref": "#/components/schemas/UpsertContactOperation" } }, "error": { "type": "string", "nullable": true, "description": "Global error message if the entire operation failed", "example": null } } }, "UpsertContactOperation": { "type": "object", "properties": { "email": { "type": "string", "format": "email", "description": "Email address of the contact", "example": "user@example.com" }, "action": { "type": "string", "enum": [ "create", "update", "error" ], "description": "The action that was performed: 'create' for new contacts, 'update' for existing contacts, 'error' for failed operations", "example": "create" }, "error": { "type": "string", "nullable": true, "description": "Error message if this specific contact operation failed", "example": null } } }, "UpdateContactListStatusRequest": { "type": "object", "required": [ "workspace_id", "email", "list_id", "status" ], "properties": { "workspace_id": { "type": "string", "description": "The ID of the workspace", "example": "ws_1234567890" }, "email": { "type": "string", "format": "email", "description": "Email address of the contact", "example": "user@example.com" }, "list_id": { "type": "string", "description": "ID of the list", "example": "newsletter" }, "status": { "type": "string", "enum": [ "active", "pending", "unsubscribed", "bounced", "complained" ], "description": "New subscription status", "example": "active" } } }, "UpdateContactListStatusResponse": { "type": "object", "properties": { "success": { "type": "boolean", "description": "Whether the operation was successful", "example": true }, "message": { "type": "string", "description": "Status message", "example": "status updated successfully" }, "found": { "type": "boolean", "description": "Whether the contact was found in the list", "example": true } } }, "ListContactsResponse": { "type": "object", "properties": { "contacts": { "type": "array", "description": "Array of contacts matching the query", "items": { "$ref": "#/components/schemas/Contact" } }, "next_cursor": { "type": "string", "nullable": true, "description": "Cursor for fetching the next page of results. Null if no more results.", "example": "MjAyMy0wMS0xNVQxMDozMDowMFp+dXNlckBleGFtcGxlLmNvbQ==" } } }, "CountContactsResponse": { "type": "object", "properties": { "total_contacts": { "type": "integer", "description": "Total number of contacts in the workspace", "example": 1523 } } }, "EmailAttachment": { "type": "object", "required": [ "filename", "content" ], "properties": { "filename": { "type": "string", "description": "Name of the file (max 255 characters, no path separators)", "example": "invoice.pdf", "maxLength": 255 }, "content": { "type": "string", "format": "byte", "description": "Base64-encoded file content (max 3MB per file)", "example": "JVBERi0xLjQKJeLjz9MKMy4uLg==" }, "content_type": { "type": "string", "description": "MIME type of the file (auto-detected if not provided)", "example": "application/pdf" }, "disposition": { "type": "string", "enum": [ "attachment", "inline" ], "description": "How the attachment should be displayed (defaults to 'attachment')", "example": "attachment", "default": "attachment" }, "content_id": { "type": "string", "description": "Content-ID for inline attachments, referenced from the HTML body as . Only allowed when disposition is 'inline'. May only contain letters, digits, and the characters . _ - @ (max 255 characters). Defaults to the filename when omitted.", "example": "checkInQr", "maxLength": 255 } } }, "Broadcast": { "type": "object", "properties": { "id": { "type": "string", "description": "Unique identifier for the broadcast", "example": "broadcast_12345" }, "workspace_id": { "type": "string", "description": "The ID of the workspace", "example": "ws_1234567890" }, "name": { "type": "string", "description": "Name of the broadcast", "example": "Spring Newsletter 2024", "maxLength": 255 }, "channel_type": { "type": "string", "description": "Communication channel type", "example": "email" }, "status": { "type": "string", "enum": [ "draft", "scheduled", "processing", "paused", "processed", "cancelled", "failed", "testing", "test_completed", "winner_selected" ], "description": "Current status of the broadcast", "example": "draft" }, "audience": { "$ref": "#/components/schemas/AudienceSettings" }, "schedule": { "$ref": "#/components/schemas/ScheduleSettings" }, "test_settings": { "$ref": "#/components/schemas/BroadcastTestSettings" }, "utm_parameters": { "$ref": "#/components/schemas/UTMParameters" }, "data_feed": { "$ref": "#/components/schemas/DataFeedSettings" }, "metadata": { "type": "object", "additionalProperties": true, "description": "Custom metadata for the broadcast" }, "winning_template": { "type": "string", "nullable": true, "description": "ID of the winning template variation", "example": "template_winner" }, "test_sent_at": { "type": "string", "format": "date-time", "nullable": true, "description": "When the A/B test was sent" }, "winner_sent_at": { "type": "string", "format": "date-time", "nullable": true, "description": "When the winning variation was sent" }, "enqueued_count": { "type": "integer", "description": "Number of emails added to the queue by orchestrator", "example": 1000 }, "test_phase_recipient_count": { "type": "integer", "description": "Number of recipients in test phase" }, "winner_phase_recipient_count": { "type": "integer", "description": "Number of recipients in winner phase" }, "created_at": { "type": "string", "format": "date-time", "description": "When the broadcast was created" }, "updated_at": { "type": "string", "format": "date-time", "description": "When the broadcast was last updated" }, "started_at": { "type": "string", "format": "date-time", "nullable": true, "description": "When the broadcast started sending" }, "completed_at": { "type": "string", "format": "date-time", "nullable": true, "description": "When the broadcast completed sending" }, "cancelled_at": { "type": "string", "format": "date-time", "nullable": true, "description": "When the broadcast was cancelled" }, "paused_at": { "type": "string", "format": "date-time", "nullable": true, "description": "When the broadcast was paused" }, "pause_reason": { "type": "string", "nullable": true, "description": "Reason for pausing the broadcast" } } }, "BroadcastTestSettings": { "type": "object", "properties": { "enabled": { "type": "boolean", "description": "Whether A/B testing is enabled", "example": true }, "sample_percentage": { "type": "integer", "minimum": 1, "maximum": 100, "description": "Percentage of audience to use for testing", "example": 20 }, "auto_send_winner": { "type": "boolean", "description": "Automatically send winning variation to remaining audience", "example": true }, "auto_send_winner_metric": { "type": "string", "enum": [ "open_rate", "click_rate" ], "description": "Metric used to determine the winner", "example": "open_rate" }, "test_duration_hours": { "type": "integer", "minimum": 1, "maximum": 168, "description": "Duration of the test in hours (max 7 days)", "example": 24 }, "variations": { "type": "array", "description": "Test variations (2-8 variations allowed)", "minItems": 2, "maxItems": 8, "items": { "$ref": "#/components/schemas/BroadcastVariation" } } } }, "BroadcastVariation": { "type": "object", "required": [ "template_id" ], "properties": { "variation_name": { "type": "string", "description": "Name of this variation", "example": "Variation A" }, "template_id": { "type": "string", "description": "Template ID for this variation", "example": "template_variant_a" }, "metrics": { "$ref": "#/components/schemas/VariationMetrics" }, "template": { "type": "object", "nullable": true, "description": "Full template object (populated when with_templates=true)" } } }, "VariationMetrics": { "type": "object", "properties": { "recipients": { "type": "integer", "description": "Number of recipients" }, "delivered": { "type": "integer", "description": "Number of delivered messages" }, "opens": { "type": "integer", "description": "Number of opens" }, "clicks": { "type": "integer", "description": "Number of clicks" }, "bounced": { "type": "integer", "description": "Number of bounces" }, "complained": { "type": "integer", "description": "Number of complaints" }, "unsubscribed": { "type": "integer", "description": "Number of unsubscribes" } } }, "AudienceSettings": { "type": "object", "required": [ "list" ], "properties": { "list": { "type": "string", "description": "List ID to send to", "example": "newsletter" }, "segments": { "type": "array", "description": "Optional segment IDs to filter recipients", "items": { "type": "string" }, "example": [ "premium_users" ] }, "exclude_unsubscribed": { "type": "boolean", "description": "Whether to exclude unsubscribed contacts", "example": true } } }, "ScheduleSettings": { "type": "object", "description": "Schedule settings for a broadcast. Note: When a broadcast is first created,\nthese fields are empty/false. Use the /api/broadcasts.schedule endpoint to\nconfigure scheduling.\n", "properties": { "is_scheduled": { "type": "boolean", "description": "Whether the broadcast is scheduled (false after creation, set via broadcasts.schedule)", "example": false }, "scheduled_date": { "type": "string", "pattern": "^\\d{4}-\\d{2}-\\d{2}$", "description": "Scheduled date in YYYY-MM-DD format", "example": "2024-03-15" }, "scheduled_time": { "type": "string", "pattern": "^\\d{2}:\\d{2}$", "description": "Scheduled time in HH:MM format", "example": "10:30" }, "timezone": { "type": "string", "description": "IANA timezone", "example": "America/New_York" }, "use_recipient_timezone": { "type": "boolean", "description": "Send at scheduled time in each recipient's timezone", "example": false } } }, "UTMParameters": { "type": "object", "properties": { "source": { "type": "string", "description": "UTM source parameter", "example": "newsletter" }, "medium": { "type": "string", "description": "UTM medium parameter", "example": "email" }, "campaign": { "type": "string", "description": "UTM campaign parameter", "example": "spring_2024" }, "term": { "type": "string", "description": "UTM term parameter" }, "content": { "type": "string", "description": "UTM content parameter" } } }, "CreateBroadcastRequest": { "type": "object", "required": [ "workspace_id", "name", "audience" ], "properties": { "workspace_id": { "type": "string", "description": "The ID of the workspace", "example": "ws_1234567890" }, "name": { "type": "string", "description": "Name of the broadcast", "example": "Spring Newsletter 2024", "maxLength": 255 }, "audience": { "$ref": "#/components/schemas/AudienceSettings" }, "test_settings": { "$ref": "#/components/schemas/BroadcastTestSettings" }, "tracking_enabled": { "type": "boolean", "description": "Enable click and open tracking", "example": true }, "utm_parameters": { "$ref": "#/components/schemas/UTMParameters" }, "data_feed": { "$ref": "#/components/schemas/DataFeedSettings" }, "metadata": { "type": "object", "additionalProperties": true, "description": "Custom metadata for the broadcast" } } }, "UpdateBroadcastRequest": { "type": "object", "required": [ "workspace_id", "id", "name", "audience" ], "properties": { "workspace_id": { "type": "string", "description": "The ID of the workspace", "example": "ws_1234567890" }, "id": { "type": "string", "description": "ID of the broadcast to update", "example": "broadcast_12345" }, "name": { "type": "string", "description": "Name of the broadcast", "example": "Spring Newsletter 2024", "maxLength": 255 }, "audience": { "$ref": "#/components/schemas/AudienceSettings" }, "schedule": { "$ref": "#/components/schemas/ScheduleSettings" }, "test_settings": { "$ref": "#/components/schemas/BroadcastTestSettings" }, "tracking_enabled": { "type": "boolean", "description": "Enable click and open tracking", "example": true }, "utm_parameters": { "$ref": "#/components/schemas/UTMParameters" }, "data_feed": { "$ref": "#/components/schemas/DataFeedSettings" }, "metadata": { "type": "object", "additionalProperties": true, "description": "Custom metadata for the broadcast" } } }, "ScheduleBroadcastRequest": { "type": "object", "required": [ "workspace_id", "id" ], "properties": { "workspace_id": { "type": "string", "description": "The ID of the workspace", "example": "ws_1234567890" }, "id": { "type": "string", "description": "ID of the broadcast to schedule", "example": "broadcast_12345" }, "send_now": { "type": "boolean", "description": "Send immediately instead of scheduling", "example": false }, "scheduled_date": { "type": "string", "pattern": "^\\d{4}-\\d{2}-\\d{2}$", "description": "Scheduled date in YYYY-MM-DD format (required if send_now=false)", "example": "2024-03-15" }, "scheduled_time": { "type": "string", "pattern": "^\\d{2}:\\d{2}$", "description": "Scheduled time in HH:MM format (required if send_now=false)", "example": "10:30" }, "timezone": { "type": "string", "description": "IANA timezone", "example": "America/New_York" }, "use_recipient_timezone": { "type": "boolean", "description": "Send at scheduled time in each recipient's timezone", "example": false } } }, "PauseBroadcastRequest": { "type": "object", "required": [ "workspace_id", "id" ], "properties": { "workspace_id": { "type": "string", "description": "The ID of the workspace", "example": "ws_1234567890" }, "id": { "type": "string", "description": "ID of the broadcast to pause", "example": "broadcast_12345" } } }, "ResumeBroadcastRequest": { "type": "object", "required": [ "workspace_id", "id" ], "properties": { "workspace_id": { "type": "string", "description": "The ID of the workspace", "example": "ws_1234567890" }, "id": { "type": "string", "description": "ID of the broadcast to resume", "example": "broadcast_12345" } } }, "CancelBroadcastRequest": { "type": "object", "required": [ "workspace_id", "id" ], "properties": { "workspace_id": { "type": "string", "description": "The ID of the workspace", "example": "ws_1234567890" }, "id": { "type": "string", "description": "ID of the broadcast to cancel", "example": "broadcast_12345" } } }, "DeleteBroadcastRequest": { "type": "object", "required": [ "workspace_id", "id" ], "properties": { "workspace_id": { "type": "string", "description": "The ID of the workspace", "example": "ws_1234567890" }, "id": { "type": "string", "description": "ID of the broadcast to delete", "example": "broadcast_12345" } } }, "SendToIndividualRequest": { "type": "object", "required": [ "workspace_id", "broadcast_id", "recipient_email" ], "properties": { "workspace_id": { "type": "string", "description": "The ID of the workspace", "example": "ws_1234567890" }, "broadcast_id": { "type": "string", "description": "ID of the broadcast", "example": "broadcast_12345" }, "recipient_email": { "type": "string", "format": "email", "description": "Email address to send test to", "example": "test@example.com" }, "template_id": { "type": "string", "description": "Optional specific template variation to send", "example": "template_variant_a" } } }, "SelectWinnerRequest": { "type": "object", "required": [ "workspace_id", "id", "template_id" ], "properties": { "workspace_id": { "type": "string", "description": "The ID of the workspace", "example": "ws_1234567890" }, "id": { "type": "string", "description": "ID of the broadcast", "example": "broadcast_12345" }, "template_id": { "type": "string", "description": "Template ID of the winning variation", "example": "template_variant_a" } } }, "BroadcastListResponse": { "type": "object", "properties": { "broadcasts": { "type": "array", "description": "List of broadcasts", "items": { "$ref": "#/components/schemas/Broadcast" } }, "total_count": { "type": "integer", "description": "Total number of broadcasts matching the query", "example": 45 } } }, "VariationResult": { "type": "object", "properties": { "template_id": { "type": "string", "description": "Template ID for this variation", "example": "template_variant_a" }, "template_name": { "type": "string", "description": "Template name", "example": "Variation A" }, "recipients": { "type": "integer", "description": "Total sent emails", "example": 1000 }, "delivered": { "type": "integer", "description": "Total delivered emails", "example": 980 }, "opens": { "type": "integer", "description": "Number of opens", "example": 450 }, "clicks": { "type": "integer", "description": "Number of clicks", "example": 120 }, "open_rate": { "type": "number", "format": "float", "description": "Open rate (opens / recipients)", "example": 0.45 }, "click_rate": { "type": "number", "format": "float", "description": "Click rate (clicks / recipients)", "example": 0.12 } } }, "TestResultsResponse": { "type": "object", "properties": { "broadcast_id": { "type": "string", "description": "ID of the broadcast", "example": "broadcast_12345" }, "status": { "type": "string", "description": "Current broadcast status", "example": "testing" }, "test_started_at": { "type": "string", "format": "date-time", "nullable": true, "description": "When the test started" }, "test_completed_at": { "type": "string", "format": "date-time", "nullable": true, "description": "When the test completed" }, "variation_results": { "type": "object", "additionalProperties": { "$ref": "#/components/schemas/VariationResult" }, "description": "Results for each variation, keyed by template_id" }, "recommended_winner": { "type": "string", "nullable": true, "description": "Recommended winning template ID based on metrics", "example": "template_variant_a" }, "winning_template": { "type": "string", "nullable": true, "description": "Selected winning template ID", "example": "template_variant_a" }, "is_auto_send_winner": { "type": "boolean", "description": "Whether winner will be automatically sent", "example": true } } }, "DataFeedHeader": { "type": "object", "required": [ "name", "value" ], "properties": { "name": { "type": "string", "description": "HTTP header name", "example": "Authorization" }, "value": { "type": "string", "description": "HTTP header value", "example": "Bearer token123" } } }, "GlobalFeedSettings": { "type": "object", "properties": { "enabled": { "type": "boolean", "description": "Whether the global data feed is enabled", "example": true }, "url": { "type": "string", "description": "URL endpoint to fetch global data from (must be HTTPS)", "example": "https://api.example.com/broadcast-data" }, "headers": { "type": "array", "description": "Custom HTTP headers to include in the request", "items": { "$ref": "#/components/schemas/DataFeedHeader" } } } }, "RecipientFeedSettings": { "type": "object", "properties": { "enabled": { "type": "boolean", "description": "Whether the per-recipient data feed is enabled", "example": true }, "url": { "type": "string", "description": "URL endpoint to fetch per-recipient data from (must be HTTPS)", "example": "https://api.example.com/recipient-data" }, "headers": { "type": "array", "description": "Custom HTTP headers to include in the request", "items": { "$ref": "#/components/schemas/DataFeedHeader" } } } }, "DataFeedSettings": { "type": "object", "description": "Configuration for external data feeds", "properties": { "global_feed": { "$ref": "#/components/schemas/GlobalFeedSettings" }, "global_feed_data": { "type": "object", "additionalProperties": true, "description": "Cached data from the global feed (populated after fetch)" }, "global_feed_fetched_at": { "type": "string", "format": "date-time", "nullable": true, "description": "When the global feed was last fetched" }, "recipient_feed": { "$ref": "#/components/schemas/RecipientFeedSettings" } } }, "RefreshGlobalFeedRequest": { "type": "object", "required": [ "workspace_id", "broadcast_id", "url" ], "properties": { "workspace_id": { "type": "string", "description": "The ID of the workspace", "example": "ws_1234567890" }, "broadcast_id": { "type": "string", "description": "ID of the broadcast", "example": "broadcast_12345" }, "url": { "type": "string", "format": "uri", "description": "The feed URL to fetch data from", "example": "https://api.example.com/feed/global" }, "headers": { "type": "array", "description": "Optional custom headers to send with the request", "items": { "$ref": "#/components/schemas/DataFeedHeader" } } } }, "RefreshGlobalFeedResponse": { "type": "object", "properties": { "success": { "type": "boolean", "description": "Whether the refresh was successful", "example": true }, "data": { "type": "object", "additionalProperties": true, "description": "The fetched data from the global feed" }, "fetched_at": { "type": "string", "format": "date-time", "description": "When the data was fetched" }, "error": { "type": "string", "description": "Error message if the refresh failed", "example": null } } }, "TestRecipientFeedRequest": { "type": "object", "required": [ "workspace_id", "broadcast_id", "url" ], "properties": { "workspace_id": { "type": "string", "description": "The ID of the workspace", "example": "ws_1234567890" }, "broadcast_id": { "type": "string", "description": "ID of the broadcast", "example": "broadcast_12345" }, "contact_email": { "type": "string", "format": "email", "description": "Optional email of a specific contact to test with. If not provided, uses a random contact from the audience.", "example": "john@example.com" }, "url": { "type": "string", "format": "uri", "description": "The feed URL to fetch data from (must use HTTPS)", "example": "https://api.example.com/feed/recipient" }, "headers": { "type": "array", "description": "Optional custom headers to send with the request", "items": { "$ref": "#/components/schemas/DataFeedHeader" } } } }, "TestRecipientFeedResponse": { "type": "object", "properties": { "success": { "type": "boolean", "description": "Whether the test was successful", "example": true }, "data": { "type": "object", "additionalProperties": true, "description": "The fetched data from the recipient feed" }, "fetched_at": { "type": "string", "format": "date-time", "description": "When the data was fetched" }, "error": { "type": "string", "description": "Error message if the test failed", "example": null }, "contact_email": { "type": "string", "format": "email", "description": "Email of the contact used for the test", "example": "john@example.com" } } }, "Template": { "type": "object", "properties": { "id": { "type": "string", "description": "Unique identifier for the template (alphanumeric, underscores, and hyphens only)", "example": "welcome_email", "maxLength": 32, "pattern": "^[a-zA-Z0-9_-]+$" }, "name": { "type": "string", "description": "Name of the template", "example": "Welcome Email", "maxLength": 32 }, "version": { "type": "integer", "format": "int64", "description": "Version number of the template", "example": 1 }, "channel": { "type": "string", "enum": [ "email", "web" ], "description": "Communication channel", "example": "email" }, "email": { "$ref": "#/components/schemas/EmailTemplate" }, "web": { "$ref": "#/components/schemas/WebTemplate" }, "category": { "type": "string", "enum": [ "marketing", "transactional", "welcome", "opt_in", "unsubscribe", "bounce", "blocklist", "blog", "other" ], "description": "Template category", "example": "transactional", "maxLength": 20 }, "template_macro_id": { "type": "string", "nullable": true, "description": "ID of the template macro (layout) to use" }, "integration_id": { "type": "string", "nullable": true, "description": "ID of the integration managing this template (e.g., Supabase)" }, "test_data": { "type": "object", "additionalProperties": true, "description": "Test data for template preview", "example": { "user_name": "John Doe", "action_url": "https://example.com/action" } }, "settings": { "type": "object", "additionalProperties": true, "description": "Channel-specific third-party settings" }, "created_at": { "type": "string", "format": "date-time", "description": "When the template was created" }, "updated_at": { "type": "string", "format": "date-time", "description": "When the template was last updated" }, "deleted_at": { "type": "string", "format": "date-time", "nullable": true, "description": "When the template was deleted (null if active)" } }, "required": [ "id", "name", "channel", "category" ] }, "EmailTemplate": { "type": "object", "properties": { "sender_id": { "type": "string", "description": "ID of the sender configuration to use", "example": "sender_123" }, "reply_to": { "type": "string", "format": "email", "description": "Reply-To email address", "example": "support@example.com" }, "subject": { "type": "string", "description": "Email subject line (supports Liquid templating)", "example": "Welcome, {{user_name}}!" }, "subject_preview": { "type": "string", "nullable": true, "description": "Preview text shown in email clients", "example": "Get started with your account" }, "compiled_preview": { "type": "string", "description": "Compiled HTML preview of the email" }, "visual_editor_tree": { "type": "object", "description": "MJML visual editor tree structure", "example": { "type": "mjml", "children": [] } }, "text": { "type": "string", "nullable": true, "description": "Plain text version of the email" } }, "required": [ "subject", "compiled_preview", "visual_editor_tree" ] }, "WebTemplate": { "type": "object", "properties": { "content": { "type": "object", "additionalProperties": true, "description": "Tiptap JSON content (source of truth for web templates)" }, "html": { "type": "string", "description": "Pre-rendered HTML for display" }, "plain_text": { "type": "string", "description": "Extracted text for search indexing" } }, "required": [ "content" ] }, "CreateTemplateRequest": { "type": "object", "required": [ "workspace_id", "id", "name", "channel", "category" ], "properties": { "workspace_id": { "type": "string", "description": "The ID of the workspace", "example": "ws_1234567890" }, "id": { "type": "string", "description": "Unique identifier for the template (alphanumeric, underscores, and hyphens only)", "example": "welcome_email", "maxLength": 32, "pattern": "^[a-zA-Z0-9_-]+$" }, "name": { "type": "string", "description": "Name of the template", "example": "Welcome Email", "maxLength": 32 }, "channel": { "type": "string", "enum": [ "email", "web" ], "description": "Communication channel", "example": "email" }, "email": { "$ref": "#/components/schemas/EmailTemplate" }, "web": { "$ref": "#/components/schemas/WebTemplate" }, "category": { "type": "string", "enum": [ "marketing", "transactional", "welcome", "opt_in", "unsubscribe", "bounce", "blocklist", "blog", "other" ], "description": "Template category", "example": "transactional" }, "template_macro_id": { "type": "string", "nullable": true, "description": "ID of the template macro (layout) to use" }, "test_data": { "type": "object", "additionalProperties": true, "description": "Test data for template preview" }, "settings": { "type": "object", "additionalProperties": true, "description": "Channel-specific settings" } } }, "UpdateTemplateRequest": { "type": "object", "required": [ "workspace_id", "id", "name", "channel", "category" ], "properties": { "workspace_id": { "type": "string", "description": "The ID of the workspace", "example": "ws_1234567890" }, "id": { "type": "string", "description": "ID of the template to update", "example": "welcome_email", "maxLength": 32 }, "name": { "type": "string", "description": "Name of the template", "example": "Welcome Email", "maxLength": 32 }, "channel": { "type": "string", "enum": [ "email", "web" ], "description": "Communication channel", "example": "email" }, "email": { "$ref": "#/components/schemas/EmailTemplate" }, "web": { "$ref": "#/components/schemas/WebTemplate" }, "category": { "type": "string", "enum": [ "marketing", "transactional", "welcome", "opt_in", "unsubscribe", "bounce", "blocklist", "blog", "other" ], "description": "Template category", "example": "transactional" }, "template_macro_id": { "type": "string", "nullable": true, "description": "ID of the template macro (layout) to use" }, "test_data": { "type": "object", "additionalProperties": true, "description": "Test data for template preview" }, "settings": { "type": "object", "additionalProperties": true, "description": "Channel-specific settings" } } }, "DeleteTemplateRequest": { "type": "object", "required": [ "workspace_id", "id" ], "properties": { "workspace_id": { "type": "string", "description": "The ID of the workspace", "example": "ws_1234567890" }, "id": { "type": "string", "description": "ID of the template to delete", "example": "welcome_email" } } }, "CompileTemplateRequest": { "type": "object", "required": [ "workspace_id", "message_id", "visual_editor_tree" ], "properties": { "workspace_id": { "type": "string", "description": "The ID of the workspace", "example": "ws_1234567890" }, "message_id": { "type": "string", "description": "Unique message ID for tracking", "example": "msg_abc123" }, "visual_editor_tree": { "type": "object", "description": "MJML visual editor tree structure (must have type 'mjml')", "additionalProperties": true }, "subject": { "type": "string", "description": "Optional email subject. When provided, it is rendered through the same\nLiquid engine the send path uses (with `test_data`) and the rendered\nresult is returned as `subject` in the response. Liquid processing is\nskipped when `preserve_liquid` is true, `channel` is `web`, or\n`test_data` is empty.\n", "example": "Hi {{ contact.first_name }}" }, "subject_preview": { "type": "string", "description": "Optional inbox preview text (the snippet shown after the subject in\nmost clients). Rendered through Liquid like `subject` and returned as\n`subject_preview` in the response.\n", "example": "Welcome {{ contact.first_name }}" }, "test_data": { "type": "object", "description": "Data to use for Liquid templating", "additionalProperties": true, "example": { "user_name": "John Doe", "action_url": "https://example.com/action" } }, "tracking_settings": { "$ref": "#/components/schemas/TrackingSettings" }, "channel": { "type": "string", "enum": [ "email", "web" ], "description": "Which channel this template targets. It selects the content object the template must carry: an `email` template supplies `email` and omits `web`, and a `web` template does the reverse." } } }, "CompileTemplateResponse": { "type": "object", "properties": { "success": { "type": "boolean", "description": "Whether compilation was successful" }, "mjml": { "type": "string", "description": "Generated MJML markup" }, "html": { "type": "string", "description": "Compiled HTML output" }, "subject": { "type": "string", "description": "Rendered email subject. Present only when the request included a\nnon-empty `subject`. Returned on both success and error paths so the\ncaller can display the rendered subject alongside any compilation\nerror.\n", "example": "Hi Pierre" }, "subject_preview": { "type": "string", "description": "Rendered inbox preview text. Present only when the request included a\nnon-empty `subject_preview`. Returned on both success and error paths.\n", "example": "Welcome Pierre" }, "error": { "type": "object", "description": "MJML compilation error details, if any", "properties": { "message": { "type": "string" } } } } }, "TrackingSettings": { "type": "object", "properties": { "enable_tracking": { "type": "boolean", "description": "Whether to enable click/open tracking" }, "tracking_mode": { "type": "string", "enum": [ "inherit", "disabled" ], "description": "Per-notification tri-state tracking preference. Absent or \"inherit\"\nfollows the workspace tracking flag (the workspace flag always acts\nas a kill-switch and cannot be overridden). \"disabled\" suppresses all\nrewriting for this notification — no click-tracking redirect, no open\npixel, and no UTM parameters — used for auth emails (e.g. Supabase\nmagic links) whose single-use URLs must never be modified. Update\nsemantics: an ABSENT tracking_mode field keeps the stored value (so\npartial edits cannot wipe an opt-out; an empty string is treated as\nabsent); an explicit \"inherit\" resets the notification to follow the\nworkspace flag; an explicit \"disabled\" opts it out. Note that the\nother tracking_settings fields (utm_*) are replaced wholesale on\nupdate — only tracking_mode has keep-when-absent semantics.\n" }, "endpoint": { "type": "string", "description": "API endpoint for tracking redirects" }, "utm_source": { "type": "string", "description": "UTM source parameter" }, "utm_medium": { "type": "string", "description": "UTM medium parameter" }, "utm_campaign": { "type": "string", "description": "UTM campaign parameter" }, "utm_content": { "type": "string", "description": "UTM content parameter" }, "utm_term": { "type": "string", "description": "UTM term parameter" }, "workspace_id": { "type": "string", "description": "Workspace ID for tracking" }, "message_id": { "type": "string", "description": "Message ID for tracking" } } }, "CustomEvent": { "type": "object", "required": [ "event_name", "external_id", "email" ], "properties": { "event_name": { "type": "string", "description": "Event type name (lowercase letters, numbers, underscores, dots, and slashes only)", "example": "orders/fulfilled", "maxLength": 100, "pattern": "^[a-z0-9_./-]+$" }, "external_id": { "type": "string", "description": "External system identifier for this event", "example": "shopify_order_12345", "maxLength": 255 }, "email": { "type": "string", "format": "email", "description": "Contact email address", "example": "customer@example.com" }, "properties": { "type": "object", "additionalProperties": true, "description": "JSONB event data with arbitrary structure", "example": { "order_total": 129.99, "product_name": "Premium Subscription", "quantity": 1 } }, "occurred_at": { "type": "string", "format": "date-time", "nullable": true, "description": "When the event occurred (allows backdating)", "example": "2023-05-15T14:30:00Z" }, "source": { "type": "string", "enum": [ "api", "integration", "import" ], "description": "Origin of the event (set automatically by server)", "example": "api", "default": "api" }, "integration_id": { "type": "string", "nullable": true, "description": "Optional reference to integration that created the event", "example": "shopify_integration_123" }, "goal_type": { "type": "string", "enum": [ "purchase", "subscription", "lead", "signup", "booking", "trial", "other" ], "nullable": true, "description": "Type of goal for conversion tracking. Required for goal_value to have meaning.", "example": "purchase" }, "goal_name": { "type": "string", "nullable": true, "maxLength": 100, "description": "Optional goal name for categorization (e.g., 'first_purchase', 'monthly_subscription')", "example": "first_purchase" }, "goal_value": { "type": "number", "format": "double", "nullable": true, "description": "Monetary value of the goal. Required for 'purchase' and 'subscription' goal types. Can be negative for refunds/chargebacks.", "example": 99.99 }, "deleted_at": { "type": "string", "format": "date-time", "nullable": true, "description": "Soft delete timestamp. Set to delete an event, set to null to restore.", "example": null }, "created_at": { "type": "string", "format": "date-time", "description": "When the event was created in the system", "example": "2023-05-15T14:30:00Z" }, "updated_at": { "type": "string", "format": "date-time", "description": "When the event was last updated", "example": "2023-05-15T14:30:00Z" } } }, "ImportCustomEventsRequest": { "type": "object", "required": [ "workspace_id", "events" ], "properties": { "workspace_id": { "type": "string", "description": "The ID of the workspace", "example": "ws_1234567890" }, "events": { "type": "array", "description": "Array of custom events to import (1-50 events)", "minItems": 1, "maxItems": 50, "items": { "type": "object", "required": [ "event_name", "external_id", "email" ], "properties": { "event_name": { "type": "string", "description": "Event type name", "example": "orders/fulfilled", "maxLength": 100, "pattern": "^[a-z0-9_./-]+$" }, "external_id": { "type": "string", "description": "External system identifier", "example": "shopify_order_12345", "maxLength": 255 }, "email": { "type": "string", "format": "email", "description": "Contact email address", "example": "customer@example.com" }, "properties": { "type": "object", "additionalProperties": true, "description": "Event data", "example": { "order_total": 129.99, "product_name": "Premium Subscription" } }, "occurred_at": { "type": "string", "format": "date-time", "description": "When the event occurred", "example": "2023-05-15T14:30:00Z" }, "source": { "type": "string", "enum": [ "api", "integration", "import" ], "description": "Origin of the event", "example": "api" }, "integration_id": { "type": "string", "description": "Integration reference", "example": "shopify_integration_123" }, "goal_type": { "type": "string", "enum": [ "purchase", "subscription", "lead", "signup", "booking", "trial", "other" ], "description": "Type of goal for conversion tracking", "example": "purchase" }, "goal_name": { "type": "string", "maxLength": 100, "description": "Optional goal name for categorization", "example": "first_purchase" }, "goal_value": { "type": "number", "format": "double", "description": "Monetary value (required for purchase/subscription, can be negative for refunds)", "example": 99.99 }, "deleted_at": { "type": "string", "format": "date-time", "description": "Soft delete timestamp", "example": null } } }, "example": [ { "event_name": "orders/fulfilled", "external_id": "shopify_order_12345", "email": "customer@example.com", "properties": { "order_total": 129.99, "product_name": "Premium Subscription" }, "occurred_at": "2023-05-15T14:30:00Z" }, { "event_name": "payment/succeeded", "external_id": "stripe_payment_67890", "email": "customer@example.com", "properties": { "amount": 129.99, "currency": "USD" }, "occurred_at": "2023-05-15T14:29:00Z" } ] } } }, "ImportCustomEventsResponse": { "type": "object", "properties": { "event_ids": { "type": "array", "description": "External IDs of successfully imported events", "items": { "type": "string" }, "example": [ "shopify_order_12345", "stripe_payment_67890" ] }, "count": { "type": "integer", "description": "Number of events created", "example": 2 } } }, "WebhookSubscription": { "type": "object", "description": "A webhook subscription that receives HTTP callbacks when events occur", "properties": { "id": { "type": "string", "description": "Unique identifier for the subscription", "example": "whsub_a1b2c3d4e5f6" }, "name": { "type": "string", "description": "Human-readable name for the subscription", "example": "Production Webhook" }, "url": { "type": "string", "format": "uri", "description": "The HTTPS URL to receive webhook events", "example": "https://api.example.com/webhooks/notifuse" }, "secret": { "type": "string", "description": "Secret key for signing webhook payloads (Standard Webhooks spec)", "example": "whsec_a1b2c3d4e5f6g7h8i9j0" }, "settings": { "$ref": "#/components/schemas/WebhookSubscriptionSettings" }, "event_types": { "type": "array", "description": "List of event types this subscription is subscribed to (flattened from settings for convenience)", "items": { "type": "string" }, "example": [ "contact.created", "contact.updated", "email.sent" ] }, "custom_event_filters": { "$ref": "#/components/schemas/CustomEventFilters", "description": "Optional filters for custom_event.* event types (flattened from settings for convenience)" }, "enabled": { "type": "boolean", "description": "Whether the subscription is currently active", "example": true }, "last_delivery_at": { "type": "string", "format": "date-time", "nullable": true, "description": "Timestamp of the last delivery attempt", "example": "2024-01-15T10:30:00Z" }, "created_at": { "type": "string", "format": "date-time", "description": "When the subscription was created", "example": "2024-01-01T00:00:00Z" }, "updated_at": { "type": "string", "format": "date-time", "description": "When the subscription was last updated", "example": "2024-01-15T10:30:00Z" } } }, "WebhookDelivery": { "type": "object", "description": "A record of a webhook delivery attempt", "properties": { "id": { "type": "string", "description": "Unique identifier for the delivery", "example": "whdel_x1y2z3" }, "subscription_id": { "type": "string", "description": "ID of the subscription this delivery belongs to", "example": "whsub_a1b2c3d4e5f6" }, "event_type": { "type": "string", "description": "Type of event that triggered the delivery", "example": "contact.created" }, "payload": { "type": "object", "description": "The JSON payload sent in the webhook", "additionalProperties": true }, "status": { "type": "string", "enum": [ "pending", "delivering", "delivered", "failed" ], "description": "Current status of the delivery", "example": "delivered" }, "attempts": { "type": "integer", "description": "Number of delivery attempts made", "example": 1 }, "max_attempts": { "type": "integer", "description": "Maximum number of retry attempts", "example": 10 }, "next_attempt_at": { "type": "string", "format": "date-time", "nullable": true, "description": "When the next retry will be attempted", "example": "2024-01-15T10:35:00Z" }, "last_attempt_at": { "type": "string", "format": "date-time", "nullable": true, "description": "When the last attempt was made", "example": "2024-01-15T10:30:00Z" }, "delivered_at": { "type": "string", "format": "date-time", "nullable": true, "description": "When the delivery was successfully completed", "example": "2024-01-15T10:30:00Z" }, "last_response_status": { "type": "integer", "nullable": true, "description": "HTTP status code from the last attempt", "example": 200 }, "last_response_body": { "type": "string", "nullable": true, "description": "Response body from the last attempt (truncated)", "example": "{\"received\": true}" }, "last_error": { "type": "string", "nullable": true, "description": "Error message from the last failed attempt", "example": null }, "created_at": { "type": "string", "format": "date-time", "description": "When the delivery was created", "example": "2024-01-15T10:30:00Z" } } }, "WebhookEventType": { "type": "string", "description": "Available webhook event types", "enum": [ "contact.created", "contact.updated", "contact.deleted", "list.subscribed", "list.unsubscribed", "list.confirmed", "list.resubscribed", "list.bounced", "list.complained", "list.pending", "list.removed", "segment.joined", "segment.left", "email.sent", "email.delivered", "email.opened", "email.clicked", "email.bounced", "email.complained", "email.unsubscribed", "custom_event.created", "custom_event.updated", "custom_event.deleted" ] }, "CustomEventFilters": { "type": "object", "nullable": true, "description": "Optional filters for custom_event.* event types", "properties": { "goal_types": { "type": "array", "nullable": true, "description": "Filter by goal type (e.g., purchase, subscription, lead)", "items": { "type": "string" }, "example": [ "purchase", "subscription" ] }, "event_names": { "type": "array", "nullable": true, "description": "Filter by event name (e.g., orders/fulfilled, payment.succeeded)", "items": { "type": "string" }, "example": [ "orders/fulfilled", "payment.succeeded" ] } } }, "WebhookSubscriptionSettings": { "type": "object", "description": "Settings for webhook subscription", "properties": { "event_types": { "type": "array", "description": "List of event types this subscription is subscribed to", "items": { "type": "string" }, "example": [ "contact.created", "contact.updated", "email.sent" ] }, "custom_event_filters": { "$ref": "#/components/schemas/CustomEventFilters" } } }, "CreateWebhookSubscriptionRequest": { "type": "object", "required": [ "workspace_id", "name", "url", "event_types" ], "properties": { "workspace_id": { "type": "string", "description": "The ID of the workspace", "example": "ws_1234567890" }, "name": { "type": "string", "description": "Human-readable name for the subscription", "example": "Production Webhook", "minLength": 1, "maxLength": 255 }, "url": { "type": "string", "format": "uri", "description": "The HTTPS URL to receive webhook events", "example": "https://api.example.com/webhooks/notifuse" }, "event_types": { "type": "array", "description": "List of event types to subscribe to", "items": { "type": "string" }, "minItems": 1, "example": [ "contact.created", "contact.updated" ] }, "custom_event_filters": { "$ref": "#/components/schemas/CustomEventFilters" } } }, "CreateWebhookSubscriptionResponse": { "type": "object", "properties": { "subscription": { "$ref": "#/components/schemas/WebhookSubscription" } } }, "UpdateWebhookSubscriptionRequest": { "type": "object", "required": [ "workspace_id", "id", "name", "url", "event_types" ], "properties": { "workspace_id": { "type": "string", "description": "The ID of the workspace", "example": "ws_1234567890" }, "id": { "type": "string", "description": "The ID of the subscription to update", "example": "whsub_a1b2c3d4e5f6" }, "name": { "type": "string", "description": "Updated name", "example": "Production Webhook Updated" }, "url": { "type": "string", "format": "uri", "description": "Updated URL", "example": "https://api.example.com/webhooks/notifuse/v2" }, "event_types": { "type": "array", "description": "Updated list of event types", "items": { "type": "string" }, "example": [ "contact.created", "contact.updated", "contact.deleted" ] }, "custom_event_filters": { "$ref": "#/components/schemas/CustomEventFilters" }, "enabled": { "type": "boolean", "description": "Whether the subscription is enabled", "example": true } } }, "UpdateWebhookSubscriptionResponse": { "type": "object", "properties": { "subscription": { "$ref": "#/components/schemas/WebhookSubscription" } } }, "DeleteWebhookSubscriptionRequest": { "type": "object", "required": [ "workspace_id", "id" ], "properties": { "workspace_id": { "type": "string", "description": "The ID of the workspace", "example": "ws_1234567890" }, "id": { "type": "string", "description": "The ID of the subscription to delete", "example": "whsub_a1b2c3d4e5f6" } } }, "ToggleWebhookSubscriptionRequest": { "type": "object", "required": [ "workspace_id", "id", "enabled" ], "properties": { "workspace_id": { "type": "string", "description": "The ID of the workspace", "example": "ws_1234567890" }, "id": { "type": "string", "description": "The ID of the subscription", "example": "whsub_a1b2c3d4e5f6" }, "enabled": { "type": "boolean", "description": "Whether to enable or disable the subscription", "example": true } } }, "ToggleWebhookSubscriptionResponse": { "type": "object", "properties": { "subscription": { "$ref": "#/components/schemas/WebhookSubscription" } } }, "RegenerateWebhookSecretRequest": { "type": "object", "required": [ "workspace_id", "id" ], "properties": { "workspace_id": { "type": "string", "description": "The ID of the workspace", "example": "ws_1234567890" }, "id": { "type": "string", "description": "The ID of the subscription", "example": "whsub_a1b2c3d4e5f6" } } }, "RegenerateWebhookSecretResponse": { "type": "object", "properties": { "subscription": { "$ref": "#/components/schemas/WebhookSubscription" } } }, "ListWebhookSubscriptionsResponse": { "type": "object", "properties": { "subscriptions": { "type": "array", "items": { "$ref": "#/components/schemas/WebhookSubscription" } } } }, "GetWebhookSubscriptionResponse": { "type": "object", "properties": { "subscription": { "$ref": "#/components/schemas/WebhookSubscription" } } }, "ListWebhookDeliveriesResponse": { "type": "object", "properties": { "deliveries": { "type": "array", "items": { "$ref": "#/components/schemas/WebhookDelivery" } }, "total": { "type": "integer", "description": "Total number of deliveries", "example": 150 }, "limit": { "type": "integer", "description": "Number of deliveries per page", "example": 20 }, "offset": { "type": "integer", "description": "Offset for pagination", "example": 0 } } }, "TestWebhookRequest": { "type": "object", "required": [ "workspace_id", "id" ], "properties": { "workspace_id": { "type": "string", "description": "The ID of the workspace", "example": "ws_1234567890" }, "id": { "type": "string", "description": "The ID of the subscription to test", "example": "whsub_a1b2c3d4e5f6" }, "event_type": { "type": "string", "description": "Optional event type to use for the test webhook payload", "example": "contact.created" } } }, "TestWebhookResponse": { "type": "object", "properties": { "success": { "type": "boolean", "description": "Whether the test webhook was delivered successfully", "example": true }, "status_code": { "type": "integer", "description": "HTTP status code returned by the webhook endpoint", "example": 200 }, "response_body": { "type": "string", "description": "Response body from the webhook endpoint", "example": "{\"received\": true}" }, "error": { "type": "string", "nullable": true, "description": "Error message if the test failed", "example": null } } }, "ListWebhookEventTypesResponse": { "type": "object", "properties": { "event_types": { "type": "array", "items": { "type": "string" }, "example": [ "contact.created", "contact.updated", "contact.deleted", "list.subscribed", "list.unsubscribed", "email.sent", "email.delivered" ] } } }, "RootSigninRequest": { "type": "object", "description": "Request payload for root user programmatic signin using HMAC signature", "properties": { "email": { "type": "string", "format": "email", "description": "The root user's email address (must match one of the configured ROOT_EMAIL addresses)", "example": "admin@example.com" }, "timestamp": { "type": "integer", "format": "int64", "description": "Unix timestamp (seconds since epoch). Must be within 60 seconds of server time.", "example": 1735600000 }, "signature": { "type": "string", "description": "HMAC-SHA256 signature computed as: HMAC-SHA256(email + \":\" + timestamp, SECRET_KEY)\nThe signature should be hex-encoded.\n", "example": "a1b2c3d4e5f6..." } }, "required": [ "email", "timestamp", "signature" ] }, "RootSigninResponse": { "type": "object", "description": "Successful authentication response containing JWT token and user details", "properties": { "token": { "type": "string", "description": "JWT authentication token for subsequent API requests", "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." }, "user": { "$ref": "#/components/schemas/User" }, "expires_at": { "type": "string", "format": "date-time", "description": "Token expiration timestamp", "example": "2025-01-01T12:00:00Z" } }, "required": [ "token", "user", "expires_at" ] }, "User": { "type": "object", "description": "User account information", "properties": { "id": { "type": "string", "description": "Unique user identifier", "example": "usr_1234567890" }, "email": { "type": "string", "format": "email", "description": "User's email address", "example": "admin@example.com" }, "name": { "type": "string", "description": "User's display name", "example": "Admin User" }, "language": { "type": "string", "description": "User's preferred locale for the console UI and system emails", "example": "en" }, "created_at": { "type": "string", "format": "date-time", "description": "Account creation timestamp", "example": "2025-01-01T00:00:00Z" }, "updated_at": { "type": "string", "format": "date-time", "description": "Last update timestamp", "example": "2025-01-01T00:00:00Z" } }, "required": [ "id", "email" ] }, "WebPageviewAction": { "type": "object", "description": "One pageview of the cumulative action list. Breaking any bound below drops this action alone; the rest of the beat is still accepted.", "required": [ "type", "path", "page_number" ], "properties": { "type": { "type": "string", "enum": [ "pageview" ] }, "path": { "type": "string", "maxLength": 2048 }, "page_number": { "type": "integer", "minimum": 1, "maximum": 1000 }, "duration": { "type": "integer", "format": "int64", "minimum": 0, "maximum": 86400000, "description": "Engaged (visible and focused) time on the page, ms. Capped at a day, which is more engaged time than one page can plausibly collect." }, "scroll": { "type": "integer", "minimum": 0, "maximum": 100 }, "entered_at": { "type": "integer", "format": "int64", "minimum": 0, "maximum": 100000000000000, "description": "Epoch ms, bounded to what a timestamp column can hold rather than to a window: a replayed offline beat legitimately carries an old timestamp." }, "exited_at": { "type": "integer", "format": "int64", "minimum": 0, "maximum": 100000000000000, "description": "Epoch ms; when both are set, must be >= entered_at." } } }, "WebGoalAction": { "type": "object", "description": "One conversion of the cumulative action list. Breaking any bound below drops this action alone; the rest of the beat is still accepted.", "required": [ "type", "name", "path", "page_number", "timestamp" ], "properties": { "type": { "type": "string", "enum": [ "goal" ] }, "name": { "type": "string", "maxLength": 100 }, "path": { "type": "string", "maxLength": 2048 }, "page_number": { "type": "integer", "minimum": 1, "maximum": 1000 }, "timestamp": { "type": "integer", "format": "int64", "minimum": 1, "maximum": 100000000000000, "description": "Original client timestamp in epoch ms; part of the goal's dedup key, so a retried beat lands on the same row. Bounded to what a timestamp column can hold, not to a window." }, "value": { "type": "number", "minimum": 0, "maximum": 1000000000000 }, "properties": { "type": "object", "maxProperties": 50, "additionalProperties": { "type": "string", "maxLength": 1024 }, "description": "At most 50 keys, 1024 characters per value and 8 KB in total: the action list is cumulative, so an unbounded map is re-sent forever and would eventually push the body past the 1 MB limit." } } }, "WebSessionAttributes": { "type": "object", "required": [ "landing_page" ], "properties": { "referrer": { "type": "string" }, "landing_page": { "type": "string" }, "utm_source": { "type": "string" }, "utm_medium": { "type": "string" }, "utm_campaign": { "type": "string" }, "utm_term": { "type": "string" }, "utm_content": { "type": "string" }, "utm_id": { "type": "string" }, "utm_id_from": { "type": "string" }, "screen_width": { "type": "integer" }, "screen_height": { "type": "integer" }, "viewport_width": { "type": "integer" }, "viewport_height": { "type": "integer" }, "device": { "type": "string", "description": "Device class, one of desktop, mobile or tablet. Detected IN THE BROWSER by the SDK, not re-parsed from user_agent server-side: modern browsers freeze the UA string and expose the real device, OS and version only through the Client Hints API, which never reaches the server. Client input, so it is trimmed to 200 characters and stored as sent. Defaults to \"desktop\" when absent or blank." }, "browser": { "type": "string", "description": "Browser name from the same in-browser detection (\"Chrome\", \"Safari\", …). Defaults to \"Unknown\" when absent or blank." }, "browser_type": { "type": "string", "description": "Qualifier for non-ordinary clients the SDK recognises — crawler, inapp, email, fetcher, cli — and empty for a normal browser. Note the server's own bot filtering works off the request User-Agent header, not this field, which is client input like the rest." }, "os": { "type": "string", "description": "Operating system from the same in-browser detection (\"macOS\", \"Windows\", \"iPadOS\", …). Defaults to \"Unknown\" when absent or blank." }, "user_agent": { "type": "string", "description": "Raw user agent, stored as sent and usable as an attribution filter dimension. It is NOT parsed into device/browser/os — those arrive already resolved in the fields above. Falls back to the request's User-Agent header when the attribute is absent." }, "connection_type": { "type": "string" }, "language": { "type": "string" }, "timezone": { "type": "string" } } }, "WebTrackPayload": { "type": "object", "required": [ "workspace_id", "session_id", "actions", "updated_at" ], "properties": { "workspace_id": { "type": "string" }, "session_id": { "type": "string", "description": "Client-generated UUIDv7. Its embedded timestamp is the single source of truth for the session start and for the partition the rows land in, and must fall within [now-48h, now+24h]. The past bound is the wider one because a session can keep beating for 24h after it started and the SDK offline queue holds beats for another 24h; the future bound is the same 24h as the other timestamps, since the id is minted from the device clock and a visitor whose clock runs fast inherits the whole skew." }, "actions": { "type": "array", "maxItems": 1000, "description": "The FULL cumulative list of session actions (re-sent on every beat).\n\nTwo rules apply here and they are easy to conflate. One ACTION that breaks its own bounds is DROPPED on its own and the rest of the beat is still recorded — never a 400, because the list is cumulative and rejecting the beat would reject every later beat of that session too, permanently. The LIST exceeding maxItems is the opposite: it is a 400 for the whole beat, and because actions[] only ever grows, every later beat fails the same way until the client trims its oldest actions or rotates the session — the same remedy as the 413.", "items": { "oneOf": [ { "$ref": "#/components/schemas/WebPageviewAction" }, { "$ref": "#/components/schemas/WebGoalAction" } ] } }, "attributes": { "$ref": "#/components/schemas/WebSessionAttributes" }, "created_at": { "type": "integer", "format": "int64", "description": "Session creation, epoch ms. Optional, and never read: the session start comes from the UUIDv7 session_id instead, so a stored session start can never disagree with the partition its rows landed in. Accepted for wire compatibility with SDKs that still send it, discarded on arrival — not listed as required, so a generated client is not forced to invent a value the server throws away." }, "updated_at": { "type": "integer", "format": "int64", "description": "Beat timestamp, epoch ms; must be within 24h of server time." }, "sdk_version": { "type": "string" }, "tab_id": { "type": "integer", "format": "int64", "description": "The writing tab. Tabs share a session id but keep their own cumulative actions and their own seq, so they are disjoint writers. Absent (or 0) from an older SDK." }, "sent_at": { "type": "integer", "format": "int64", "description": "Stamped at each HTTP attempt; drives clock-skew correction beyond 5s." }, "contact_email": { "type": "string", "maxLength": 255, "description": "Contact address claimed for this session, signed by contact_email_hmac. /track is public and unauthenticated, so no identity field is believed until it verifies against the workspace secret. Send either this pair or identify_token, never both.\n\nA valid signature is necessary but NOT sufficient: the identified path is rate limited, and the address must already be a contact in the workspace. Both extra gates are described under the /track operation. Every rejection along the way — bad signature, throttled, unknown address — costs the identity only: the beat is still recorded, just anonymously, and the response is the same 200 with no error. There is nothing on the wire distinguishing a working integration from a broken one, so verify against a contact that exists and check that the session actually shows up identified." }, "contact_email_hmac": { "type": "string", "maxLength": 64, "description": "HMAC-SHA-256 of a DOMAIN-SEPARATED string, minted on the customer's server (never in the browser, which would hand the workspace secret to every visitor). The exact recipe, which must be followed literally:\n\nsignature = lowercase_hex( HMAC_SHA256( key = workspace secret key, message = \"wa_identify:\" + contact_email ) )\n\nThe message is the ASCII prefix \"wa_identify:\" concatenated with the address — signing bare \"alice@example.com\" instead of \"wa_identify:alice@example.com\" is the failure mode to check first. The result is the 64 lowercase hex characters of the digest, compared as bytes, so uppercase hex or base64 will not match.\n\nThe prefix exists to keep this credential from being interchangeable with the email HMAC that authorizes subscription changes (notification center, unsubscribe, one-click), which is computed over the bare address with the same workspace secret and is printed into every email Notifuse sends. Without the prefix, an unsubscribe HMAC scraped from a forwarded email would silently identify a visitor, and this analytics credential lifted out of page JS by any third-party script would let its holder change that contact's subscriptions.\n\nSigned and verified over the RAW address exactly as it appears in contact_email, before any normalization: sign the same bytes you send. Normalization (trim, lowercase) happens only after the signature checks out, to match the stored contact." }, "identify_token": { "type": "string", "maxLength": 3174, "description": "Opaque encrypted credential carried by an email-click link (the nf_id parameter), and the alternative to the contact_email / contact_email_hmac pair rather than a companion to it. Expired or undecryptable tokens cost the identity only, not the beat. Once decrypted, the address it carries passes through the same two further gates as a signed contact_email (rate limit, then contact-must-exist).\n\nThe length limit is a ceiling on what the server will look at, not a size to aim for: it is derived from the longest address contacts can store, so that no token Notifuse mints can ever exceed it. Send the nf_id value back exactly as it arrived — a longer one is discarded before decryption, which costs the identity silently." }, "dimensions": { "type": "object", "additionalProperties": { "type": "string" }, "description": "Custom dimensions custom_1..custom_10." }, "seq": { "type": "integer", "format": "int64", "minimum": 0, "description": "Monotonic per-session beat counter; the server only applies strictly newer beats." } } }, "WebFilterCondition": { "type": "object", "required": [ "field", "operator" ], "properties": { "field": { "type": "string" }, "operator": { "type": "string", "enum": [ "equals", "not_equals", "contains", "not_contains", "is_empty", "is_not_empty", "regex" ] }, "value": { "type": "string" } } }, "WebFilterOperation": { "type": "object", "required": [ "dimension", "action" ], "properties": { "dimension": { "type": "string" }, "action": { "type": "string", "enum": [ "set_value", "unset_value", "set_default_value" ] }, "value": { "type": "string" } } }, "WebFilter": { "type": "object", "required": [ "id", "name", "priority", "conditions", "operations", "enabled" ], "properties": { "id": { "type": "string" }, "name": { "type": "string" }, "priority": { "type": "integer", "minimum": 0, "maximum": 1000 }, "order": { "type": "integer" }, "tags": { "type": "array", "items": { "type": "string" } }, "conditions": { "type": "array", "items": { "$ref": "#/components/schemas/WebFilterCondition" } }, "operations": { "type": "array", "items": { "$ref": "#/components/schemas/WebFilterOperation" } }, "enabled": { "type": "boolean" }, "version": { "type": "string" } } }, "WebAnalyticsSettings": { "type": "object", "nullable": true, "properties": { "enabled": { "type": "boolean" }, "allowed_domains": { "type": "array", "items": { "type": "string" }, "description": "Origins allowed to send beats (\"example.com\", \"*.example.com\"); empty allows all." }, "bounce_threshold_seconds": { "type": "integer", "description": "Sessions with less engaged time count as bounces (default 10)." }, "filters": { "type": "array", "items": { "$ref": "#/components/schemas/WebFilter" } }, "filters_version": { "type": "string", "readOnly": true }, "custom_dimension_labels": { "type": "object", "additionalProperties": { "type": "string" } }, "identify_from_email_links": { "type": "boolean", "description": "Adds a signed identity to the links of tracked emails, so a recipient who clicks one is recognised on landing without any code on the customer's site, and their visit is tied to their contact record. Off by default: unlike identify(), which the customer's own server calls with their own secret, this credential is minted by Notifuse for every recipient of every tracked send, so it is theirs to ask for." }, "geo_enabled": { "type": "boolean" }, "geo_store_city": { "type": "boolean" }, "geo_store_region": { "type": "boolean" }, "geo_coordinates_precision": { "type": "integer", "minimum": 0, "maximum": 2 } } }, "WebAnalyticsBackfillRequest": { "type": "object", "required": [ "workspace_id" ], "properties": { "workspace_id": { "type": "string" } } }, "WebAnalyticsBackfillResponse": { "type": "object", "properties": { "backfill": { "type": "object", "nullable": true, "properties": { "task_id": { "type": "string" }, "status": { "type": "string", "enum": [ "pending", "running", "completed", "failed", "paused" ] }, "progress": { "type": "number" }, "state": { "type": "object", "properties": { "filters_version": { "type": "string" }, "partition_index": { "type": "integer" }, "rows_updated": { "type": "integer", "format": "int64" } } }, "error_message": { "type": "string" } } } } }, "Annotation": { "type": "object", "required": [ "id", "annotated_at", "timezone", "title", "color", "source", "created_at", "updated_at" ], "properties": { "id": { "type": "string", "description": "Server-generated identifier (a UUID with its dashes removed).", "example": "3f2504e04f8911d39a0c0305e82c3301" }, "annotated_at": { "type": "string", "format": "date-time", "description": "The moment the annotation marks, as a real instant.", "example": "2026-08-15T09:00:00Z" }, "timezone": { "type": "string", "description": "IANA timezone the moment was entered in. This is display intent only — annotated_at already fixes the instant, and filtering never uses this field. It is what lets \"9am in Tokyo\" render back as 9am instead of the reader's local equivalent.", "example": "Asia/Tokyo" }, "title": { "type": "string", "maxLength": 100, "description": "Counted in characters, not bytes.", "example": "Pricing page redesign" }, "description": { "type": "string", "maxLength": 500, "description": "Optional longer note. Counted in characters, not bytes.", "example": "Rolled out to all visitors after the A/B test." }, "color": { "type": "string", "pattern": "^#[0-9A-Fa-f]{6}$", "description": "Six-digit hex colour used for the marker on the charts.", "example": "#3b82f6" }, "source": { "type": "string", "enum": [ "manual", "broadcast" ], "description": "Who wrote the row. `manual` is anything typed by an operator or posted to this API; `broadcast` rows are written automatically when a broadcast starts sending. There is deliberately no API source — see the note on annotations.create.", "example": "manual" }, "source_id": { "type": "string", "description": "The entity that caused an automatic annotation (a broadcast id today). Always absent on manual rows, and never settable through the API. An annotation outlives the entity it points at: deleting a broadcast leaves its annotation, because the send did happen.", "example": "bcast_1234567890" }, "created_at": { "type": "string", "format": "date-time" }, "updated_at": { "type": "string", "format": "date-time" } } }, "ListAnnotationsResponse": { "type": "object", "properties": { "annotations": { "type": "array", "items": { "$ref": "#/components/schemas/Annotation" } } } }, "AnnotationResponse": { "type": "object", "properties": { "annotation": { "$ref": "#/components/schemas/Annotation" } } }, "CreateAnnotationRequest": { "type": "object", "required": [ "workspace_id", "annotated_at", "title" ], "properties": { "workspace_id": { "type": "string", "example": "ws_1234567890" }, "annotated_at": { "type": "string", "format": "date-time", "description": "The moment to mark, as an RFC3339 instant.", "example": "2026-08-15T09:00:00Z" }, "timezone": { "type": "string", "description": "IANA timezone the moment was entered in, kept for display. Defaults to the workspace timezone, then to UTC.", "example": "Asia/Tokyo" }, "title": { "type": "string", "maxLength": 100, "example": "Pricing page redesign" }, "description": { "type": "string", "maxLength": 500, "example": "Rolled out to all visitors after the A/B test." }, "color": { "type": "string", "pattern": "^#[0-9A-Fa-f]{6}$", "default": "#3b82f6", "example": "#3b82f6" }, "source": { "type": "string", "enum": [ "manual" ], "description": "Only `manual` is accepted. Sending `broadcast` is rejected rather than quietly downgraded: it would be claiming the slot of an automatic row.", "default": "manual" } } }, "UpdateAnnotationRequest": { "type": "object", "required": [ "workspace_id", "id", "annotated_at", "title" ], "properties": { "workspace_id": { "type": "string", "example": "ws_1234567890" }, "id": { "type": "string", "example": "3f2504e04f8911d39a0c0305e82c3301" }, "annotated_at": { "type": "string", "format": "date-time", "example": "2026-08-15T09:00:00Z" }, "timezone": { "type": "string", "description": "Falls back to the annotation's stored timezone when omitted.", "example": "Asia/Tokyo" }, "title": { "type": "string", "maxLength": 100, "example": "Pricing page redesign" }, "description": { "type": "string", "maxLength": 500 }, "color": { "type": "string", "pattern": "^#[0-9A-Fa-f]{6}$", "description": "Falls back to the annotation's stored colour when omitted.", "example": "#3b82f6" } } }, "DeleteAnnotationRequest": { "type": "object", "required": [ "workspace_id", "id" ], "properties": { "workspace_id": { "type": "string", "example": "ws_1234567890" }, "id": { "type": "string", "example": "3f2504e04f8911d39a0c0305e82c3301" } } }, "DeleteAnnotationResponse": { "type": "object", "properties": { "success": { "type": "boolean", "example": true } } } } } }