{"openapi":"3.1.0","info":{"title":"Bitelio API","description":"The Bitelio API: send transactional email, run campaigns, and automate your marketing from one place","version":"1.0.0","contact":{"name":"Bitelio Support","url":"https://www.bitelio.com"}},"servers":[{"url":"https://api.bitelio.com","description":"Production server"}],"security":[{"ApiKeyAuth":[]}],"components":{"securitySchemes":{"ApiKeyAuth":{"type":"http","scheme":"bearer","bearerFormat":"API Key","description":"API Key authentication. **Secret keys (sk_*)** are required for all endpoints except `/v1/track`. **Public keys (pk_*)** only work with the `/v1/track` endpoint for client-side event tracking. The project is automatically derived from the key."},"MobileSurveySession":{"type":"apiKey","in":"header","name":"X-Bitelio-Mobile-Session","description":"Short-lived token created by the authenticated merchant backend; also requires X-Bitelio-Device."}},"schemas":{"Contact":{"type":"object","properties":{"id":{"type":"string","description":"Unique contact identifier"},"email":{"type":"string","format":"email","description":"Contact email address"},"subscribed":{"type":"boolean","description":"Subscription status"},"data":{"type":"object","description":"Custom contact data fields","additionalProperties":true},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"Template":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"subject":{"type":"string"},"body":{"type":"string"},"type":{"type":"string","enum":["TRANSACTIONAL","MARKETING"]},"createdAt":{"type":"string","format":"date-time"}}},"Campaign":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"subject":{"type":"string"},"type":{"type":"string","enum":["ALL","SEGMENT","FILTERED"]},"status":{"type":"string","enum":["DRAFT","SCHEDULED","SENDING","SENT"]},"scheduledAt":{"type":"string","format":"date-time","nullable":true}}},"Segment":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"filters":{"type":"object"},"trackMembership":{"type":"boolean"},"memberCount":{"type":"integer"}}},"Error":{"type":"object","properties":{"code":{"type":"integer"},"error":{"type":"string"},"message":{"type":"string"},"time":{"type":"integer"}}},"SurveyDelivery":{"type":"object","properties":{"id":{"type":"string"},"channel":{"type":"string","enum":["EMAIL","WHATSAPP","WEB_PUSH","MOBILE_API"]},"status":{"type":"string","enum":["QUEUED","SENDING","ACCEPTED","DELIVERED","FAILED","UNKNOWN","BLOCKED"]},"lastError":{"type":["string","null"]},"attempts":{"type":"integer"},"acceptedAt":{"type":["string","null"],"format":"date-time"},"deliveredAt":{"type":["string","null"],"format":"date-time"},"createdAt":{"type":"string","format":"date-time"},"openedAt":{"type":["string","null"],"format":"date-time"},"clickedAt":{"type":["string","null"],"format":"date-time"},"repliedAt":{"type":["string","null"],"format":"date-time"},"kind":{"type":"string","enum":["SURVEY","HELP","ALTERNATIVE","REPLENISHMENT","RECOMMEND"]}},"required":["id","channel","status","attempts","createdAt"],"additionalProperties":false},"MobileSurveyClaim":{"type":"object","properties":{"deliveryId":{"type":"string"},"receiptToken":{"type":"string"},"surveyUrl":{"type":"string"},"surveyToken":{"type":"string"},"contactId":{"type":"string"},"locale":{"type":"string"},"title":{"type":"string"},"body":{"type":"string"},"expiresAt":{"type":"string","format":"date-time"},"claimExpiresAt":{"type":"string","format":"date-time"}},"required":["deliveryId","receiptToken","surveyUrl","surveyToken","contactId","locale","title","body","expiresAt","claimExpiresAt"],"additionalProperties":false},"SurveyPushSubscription":{"type":"object","properties":{"endpoint":{"type":"string","format":"uri","maxLength":2048},"expirationTime":{"type":["integer","null"]},"keys":{"type":"object","properties":{"p256dh":{"type":"string","pattern":"^[A-Za-z0-9_-]{87}$"},"auth":{"type":"string","pattern":"^[A-Za-z0-9_-]{22}$"}},"required":["p256dh","auth"],"additionalProperties":false}},"required":["endpoint","keys"],"additionalProperties":false},"RetentionMessageSnapshot":{"type":"object","properties":{"schemaVersion":{"type":"integer","const":1},"kind":{"type":"string","enum":["HELP","ALTERNATIVE","REPLENISHMENT","RECOMMEND"]},"brandName":{"type":"string"},"locale":{"type":"string"},"expiresAt":{"type":"string","format":"date-time"},"message":{"type":"object","properties":{"subject":{"type":"string"},"text":{"type":"string"},"url":{"type":["string","null"],"format":"uri"}},"required":["subject","text","url"],"additionalProperties":false}},"required":["schemaVersion","kind","brandName","locale","expiresAt","message"],"additionalProperties":false},"MobileRetentionClaim":{"oneOf":[{"type":"object","properties":{"deliveryId":{"type":"string"},"receiptToken":{"type":"string"},"surveyUrl":{"type":"string"},"surveyToken":{"type":"string"},"contactId":{"type":"string"},"locale":{"type":"string"},"title":{"type":"string"},"body":{"type":"string"},"expiresAt":{"type":"string","format":"date-time"},"claimExpiresAt":{"type":"string","format":"date-time"},"kind":{"const":"SURVEY","type":"string"},"url":{"type":"string","format":"uri"}},"required":["deliveryId","receiptToken","surveyUrl","surveyToken","contactId","locale","title","body","expiresAt","claimExpiresAt","kind","url"],"additionalProperties":false},{"type":"object","properties":{"deliveryId":{"type":"string"},"receiptToken":{"type":"string"},"contactId":{"type":"string"},"locale":{"type":"string"},"title":{"type":"string"},"body":{"type":"string"},"expiresAt":{"type":"string","format":"date-time"},"claimExpiresAt":{"type":"string","format":"date-time"},"kind":{"type":"string","enum":["HELP","ALTERNATIVE","REPLENISHMENT","RECOMMEND"]},"url":{"type":"string","format":"uri"},"message":{"$ref":"#/components/schemas/RetentionMessageSnapshot"}},"required":["deliveryId","receiptToken","contactId","locale","title","body","expiresAt","claimExpiresAt","kind","url","message"],"additionalProperties":false}],"discriminator":{"propertyName":"kind"}}},"parameters":{"Limit":{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":20},"description":"Maximum items per page"},"Cursor":{"name":"cursor","in":"query","schema":{"type":"string"},"description":"Pagination cursor"},"IdempotencyKey":{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string","maxLength":255},"description":"Optional key that guarantees this request runs at most once. If the key was already used by your project, the request is refused with `409` instead of being performed a second time. Keys are scoped to your project, expire after 24 hours, and must be 1-255 printable ASCII characters."}}},"paths":{"/v1/back-in-stock":{"post":{"tags":["Public API"],"summary":"Back-in-stock request","description":"Record that a storefront visitor wants to be told when an out-of-stock Shopify variant returns.\n\nAuthenticated with your **public** key, because it is meant to be called from a storefront widget. Rate-limited to 20 requests a minute per caller.\n\nA back-in-stock request is **not** marketing consent: the contact is created unsubscribed, exactly as a contact created by the transactional API is. Ask for consent separately.\n\nReturns **404** when the variant is not one of your project's.","operationId":"backInStock","security":[{"ApiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email","shopifyVariantId"],"properties":{"email":{"type":"string","format":"email","description":"The visitor's email address. Created as a contact if new, unsubscribed."},"shopifyVariantId":{"type":"string","example":"45123456789012","description":"Shopify variant, either the bare numeric id or the full `gid://shopify/ProductVariant/…`."}}}}}},"responses":{"202":{"description":"Request recorded","content":{"application/json":{"schema":{"type":"object","properties":{"subscribed":{"type":"boolean"}}}}}},"400":{"description":"The body failed validation"},"404":{"description":"No such variant in this project"}}}},"/v1/campaigns":{"get":{"tags":["Campaigns"],"summary":"List campaigns","description":"Get a paginated list of email campaigns\n\n---\n\n**Path versioning.** This resource is also reachable at the unversioned `/campaigns`, which is where it has always lived and which keeps working indefinitely — `bitelio@0.1.0` calls it. Prefer `/v1/campaigns`: the `/v1` prefix is the path that carries a version promise, and a future `/v2` will be able to change shape without breaking anything written against this one.","operationId":"listCampaigns","parameters":[{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/Cursor"},{"name":"status","in":"query","schema":{"type":"string","enum":["DRAFT","SCHEDULED","SENDING","SENT"]}}],"responses":{"200":{"description":"List of campaigns","content":{"application/json":{"schema":{"type":"object","properties":{"campaigns":{"type":"array","items":{"$ref":"#/components/schemas/Campaign"}},"page":{"type":"integer"},"pageSize":{"type":"integer"},"total":{"type":"integer"},"totalPages":{"type":"integer"}}}}}}}},"post":{"tags":["Campaigns"],"summary":"Create campaign","description":"Create a new email campaign\n\n---\n\n**Path versioning.** This resource is also reachable at the unversioned `/campaigns`, which is where it has always lived and which keeps working indefinitely — `bitelio@0.1.0` calls it. Prefer `/v1/campaigns`: the `/v1` prefix is the path that carries a version promise, and a future `/v2` will be able to change shape without breaking anything written against this one.","operationId":"createCampaign","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","subject","body","from","audienceType"],"properties":{"name":{"type":"string","description":"Campaign name"},"description":{"type":"string","description":"Campaign description"},"subject":{"type":"string","description":"Email subject line"},"body":{"type":"string","description":"HTML email body"},"from":{"type":"string","format":"email","description":"Sender email address (must be from verified domain)"},"fromName":{"type":"string","description":"Sender name"},"replyTo":{"type":"string","format":"email","description":"Reply-to email address"},"audienceType":{"type":"string","enum":["ALL","SEGMENT","FILTERED"],"description":"Target audience type"},"segmentId":{"type":"string","description":"Segment ID (required if audienceType is SEGMENT)"},"audienceFilter":{"type":"object","description":"Filter conditions (required if audienceType is FILTERED)"}}}}}},"responses":{"200":{"description":"Campaign created","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"$ref":"#/components/schemas/Campaign"}}}}}}}}},"/v1/campaigns/{id}/send":{"post":{"tags":["Campaigns"],"summary":"Send or schedule campaign","description":"Send a campaign immediately or schedule for future delivery\n\n---\n\n**Path versioning.** This resource is also reachable at the unversioned `/campaigns/{id}/send`, which is where it has always lived and which keeps working indefinitely — `bitelio@0.1.0` calls it. Prefer `/v1/campaigns/{id}/send`: the `/v1` prefix is the path that carries a version promise, and a future `/v2` will be able to change shape without breaking anything written against this one.","operationId":"sendCampaign","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"scheduledFor":{"type":"string","format":"date-time","nullable":true,"description":"ISO 8601 timestamp for when to send the campaign. Omit or set to null for immediate send."}}}}}},"responses":{"200":{"description":"Campaign scheduled/sending"}}}},"/v1/contacts":{"get":{"tags":["Contacts"],"summary":"List contacts","description":"Get a paginated list of contacts with cursor-based pagination.\n\n---\n\n**Path versioning.** This resource is also reachable at the unversioned `/contacts`, which is where it has always lived and which keeps working indefinitely — `bitelio@0.1.0` calls it. Prefer `/v1/contacts`: the `/v1` prefix is the path that carries a version promise, and a future `/v2` will be able to change shape without breaking anything written against this one.","operationId":"listContacts","parameters":[{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/Cursor"},{"name":"search","in":"query","schema":{"type":"string"},"description":"Case-insensitive substring match on email."}],"responses":{"200":{"description":"List of contacts","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Contact"}},"cursor":{"type":"string","nullable":true,"description":"Cursor for the next page. Pass this back as the `cursor` query parameter to fetch the next page."},"hasMore":{"type":"boolean"},"total":{"type":"integer","description":"Total count. Only populated on the first page (when no `cursor` is supplied); subsequent pages return `0` to avoid the recount cost."}}}}}}}},"post":{"tags":["Contacts"],"summary":"Create or update contact","description":"Create a new contact or update existing (upsert by email)\n\n---\n\n**Path versioning.** This resource is also reachable at the unversioned `/contacts`, which is where it has always lived and which keeps working indefinitely — `bitelio@0.1.0` calls it. Prefer `/v1/contacts`: the `/v1` prefix is the path that carries a version promise, and a future `/v2` will be able to change shape without breaking anything written against this one.","operationId":"createContact","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email"],"properties":{"email":{"type":"string","format":"email"},"subscribed":{"type":"boolean","default":true},"data":{"type":"object","additionalProperties":true}}},"example":{"email":"user@example.com","subscribed":true,"data":{"firstName":"John","lastName":"Doe","plan":"premium"}}}}},"responses":{"200":{"description":"Contact updated","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Contact"},{"type":"object","properties":{"_meta":{"type":"object","properties":{"isNew":{"type":"boolean"},"isUpdate":{"type":"boolean"}}}}}]}}}},"201":{"description":"Contact created","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Contact"},{"type":"object","properties":{"_meta":{"type":"object","properties":{"isNew":{"type":"boolean"},"isUpdate":{"type":"boolean"}}}}}]}}}}}}},"/v1/contacts/{id}":{"get":{"tags":["Contacts"],"summary":"Get contact","description":"Get a single contact by ID\n\n---\n\n**Path versioning.** This resource is also reachable at the unversioned `/contacts/{id}`, which is where it has always lived and which keeps working indefinitely — `bitelio@0.1.0` calls it. Prefer `/v1/contacts/{id}`: the `/v1` prefix is the path that carries a version promise, and a future `/v2` will be able to change shape without breaking anything written against this one.","operationId":"getContact","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Contact details","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Contact"}}}},"404":{"description":"Contact not found"}}},"patch":{"tags":["Contacts"],"summary":"Update contact","description":"Update an existing contact's email, subscription state, or `data` fields.\n\n---\n\n**Path versioning.** This resource is also reachable at the unversioned `/contacts/{id}`, which is where it has always lived and which keeps working indefinitely — `bitelio@0.1.0` calls it. Prefer `/v1/contacts/{id}`: the `/v1` prefix is the path that carries a version promise, and a future `/v2` will be able to change shape without breaking anything written against this one.","operationId":"updateContact","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"email":{"type":"string","format":"email","description":"Change the contact's email address. Returns 409 if another contact in the project already uses this email."},"subscribed":{"type":"boolean","description":"Update subscription state. Flipping this fires `contact.subscribed` or `contact.unsubscribed`."},"data":{"type":"object","additionalProperties":true,"description":"Patch contact data. `null` deletes a key, empty strings are ignored, primitives are stored. Reserved keys are silently filtered out."}}}}}},"responses":{"200":{"description":"Contact updated","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Contact"}}}},"409":{"description":"An existing contact already uses the email you're trying to set."}}},"delete":{"tags":["Contacts"],"summary":"Delete contact","description":"Permanently delete a contact\n\n---\n\n**Path versioning.** This resource is also reachable at the unversioned `/contacts/{id}`, which is where it has always lived and which keeps working indefinitely — `bitelio@0.1.0` calls it. Prefer `/v1/contacts/{id}`: the `/v1` prefix is the path that carries a version promise, and a future `/v2` will be able to change shape without breaking anything written against this one.","operationId":"deleteContact","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Contact deleted successfully (no content returned)"},"404":{"description":"Contact not found"}}}},"/v1/emails":{"get":{"tags":["Public API"],"summary":"List emails","description":"The project's send history, newest first, **cursor-paginated**. Shows live sends only by default; use `mode` to see test sends.\n\nThere is deliberately no search by subject or body: over millions of rows that is a scan, and saying so is better than offering it and being slow.\n\nThe rendered body is **not** in the listing — that is hundreds of kilobytes a page. It is on `GET /v1/emails/{id}`.","operationId":"listEmails","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"limit","in":"query","description":"Page size.","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":25}},{"name":"cursor","in":"query","description":"The previous page's `nextCursor`. Opaque — return it verbatim.","required":false,"schema":{"type":"string"}},{"name":"mode","in":"query","description":"Which sends to show.","required":false,"schema":{"type":"string","enum":["live","test","all"],"default":"live"}},{"name":"status","in":"query","description":"Filter by delivery status.","required":false,"schema":{"type":"string","enum":["PENDING","SENDING","HELD","SENT","DELIVERED","RECEIVED","OPENED","CLICKED","BOUNCED","COMPLAINED","FAILED"]}},{"name":"to","in":"query","description":"Recipient address. Empty if they are not a contact of this project.","required":false,"schema":{"type":"string","format":"email"}},{"name":"since","in":"query","description":"ISO 8601.","required":false,"schema":{"type":"string","format":"date-time"}},{"name":"until","in":"query","description":"ISO 8601.","required":false,"schema":{"type":"string","format":"date-time"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object"}},"nextCursor":{"type":"string","nullable":true,"description":"null on the last page."}}}}}},"400":{"description":"Invalid limit, mode, status or date"},"403":{"description":"The key lacks the emails:view scope"}}}},"/v1/emails/{id}":{"get":{"tags":["Public API"],"summary":"Get an email","description":"One email, **including the rendered HTML body**. That body is why this endpoint exists: the question you cannot answer from code is not \"did it send\", it is \"what did it send\".\n\nReturns **404** for another project's email, not 403 — a 403 would confirm the id exists.","operationId":"getEmail","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object"}}}}}},"403":{"description":"The key lacks the emails:view scope"},"404":{"description":"No such email in this project"}}}},"/v1/segments":{"get":{"tags":["Segments"],"summary":"List segments","description":"Get all audience segments\n\n---\n\n**Path versioning.** This resource is also reachable at the unversioned `/segments`, which is where it has always lived and which keeps working indefinitely — `bitelio@0.1.0` calls it. Prefer `/v1/segments`: the `/v1` prefix is the path that carries a version promise, and a future `/v2` will be able to change shape without breaking anything written against this one.","operationId":"listSegments","responses":{"200":{"description":"List of segments","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Segment"}}}}}}},"post":{"tags":["Segments"],"summary":"Create segment","description":"Create a new audience segment\n\n---\n\n**Path versioning.** This resource is also reachable at the unversioned `/segments`, which is where it has always lived and which keeps working indefinitely — `bitelio@0.1.0` calls it. Prefer `/v1/segments`: the `/v1` prefix is the path that carries a version promise, and a future `/v2` will be able to change shape without breaking anything written against this one.","operationId":"createSegment","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","filters"],"properties":{"name":{"type":"string"},"filters":{"type":"object","description":"Filter conditions"},"trackMembership":{"type":"boolean","default":false}}},"example":{"name":"Premium Users","filters":{"operator":"AND","conditions":[{"field":"data.plan","operator":"equals","value":"premium"}]},"trackMembership":true}}}},"responses":{"201":{"description":"Segment created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Segment"}}}}}}},"/v1/send":{"post":{"tags":["Public API"],"summary":"Send transactional email","description":"Send a transactional email via the public API. Automatically creates or updates the recipient contact.\n\n**Required content:** either a `template` ID, **or** both `subject` and `body`. Template fields can be overridden by explicit request fields.\n\n**Sender:** `from` is required unless using a template that already has a `from` configured. The sender's domain must be verified.\n\n**Multiple recipients:** when `to` is an array, each recipient is processed sequentially with its own contact upsert and rendered email — there is no batch-send semantics. Sending is always immediate; for scheduled sends, use a Campaign.\n\n**Attachments:** up to 10 attachments per email and 10 MB total by default. The total message size cannot exceed 40 MB.","operationId":"sendEmail","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["to"],"properties":{"to":{"oneOf":[{"type":"string","format":"email","description":"Simple email address"},{"type":"object","required":["email"],"properties":{"name":{"type":"string","description":"Recipient display name"},"email":{"type":"string","format":"email","description":"Recipient email address"}},"description":"Recipient with name and email"},{"type":"array","items":{"oneOf":[{"type":"string","format":"email"},{"type":"object","required":["email"],"properties":{"name":{"type":"string","description":"Recipient display name"},"email":{"type":"string","format":"email","description":"Recipient email address"}}}]},"description":"Array of recipients (strings or objects)"}],"description":"Recipient email(s). Can be a string, an object with {name, email}, or an array of either."},"subject":{"type":"string","minLength":1,"maxLength":998,"description":"Email subject. Required if no `template` is provided. Cannot contain newline characters."},"body":{"type":"string","minLength":1,"description":"Email body (HTML). Required if no `template` is provided."},"template":{"type":"string","description":"Template ID to use for this email. When provided, uses the template's subject, body, from, and reply-to settings. You can override these by explicitly providing subject, body, from, or reply fields in the request. Template variables are populated from the data field."},"from":{"oneOf":[{"type":"string","format":"email","description":"Simple email address"},{"type":"object","required":["email"],"properties":{"name":{"type":"string","description":"Sender display name"},"email":{"type":"string","format":"email","description":"Sender email address"}},"description":"Sender with name and email"}],"description":"Sender email address (requires verified domain). Required unless using a template that has a 'from' address configured. Can be a string (e.g., 'hello@example.com') or an object with {name, email} (e.g., {name: 'My App', email: 'hello@example.com'})."},"name":{"type":"string","description":"**Deprecated.** Sender display name. Prefer `from: { name, email }`. Used only as a fallback when `from` is a string and no name is set there."},"subscribed":{"type":"boolean","description":"Subscription state to apply to the recipient. For **new** contacts, defaults to `false` on `/v1/send`. For **existing** contacts, omitting this preserves their current state — pass `true` or `false` to explicitly change it. A change emits `contact.subscribed` or `contact.unsubscribed`."},"data":{"type":"object","additionalProperties":true,"description":"Variables for template rendering and contact data updates. Each value can be:\n- A primitive (string, number, boolean) — saved on the contact and available as a template variable.\n- `null` — deletes the field from the contact.\n- An empty string — skipped (does not overwrite existing data).\n- An object `{ value, persistent: false }` — used for this send only, not stored on the contact (good for one-shot password reset codes, magic links).\n\nReserved keys (`id`, `bitelio_id`, `bitelio_email`, `email`, `unsubscribeUrl`, `subscribeUrl`, `manageUrl`) are silently filtered out."},"headers":{"type":"object","additionalProperties":{"type":"string"},"description":"Custom email headers. Header names cannot contain `\\r\\n`. Header values are limited to 998 characters and cannot contain `\\r\\n` (header injection is rejected)."},"reply":{"type":"string","format":"email","description":"Reply-to address."},"attachments":{"type":"array","description":"Email attachments. Default cap: 10 attachments and 10 MB total. The full message size cannot exceed 40 MB.","maxItems":10,"items":{"type":"object","required":["filename","content","contentType"],"properties":{"filename":{"type":"string","maxLength":255,"description":"Attachment filename. Cannot contain newline or quote characters."},"content":{"type":"string","description":"Base64-encoded file content."},"contentType":{"type":"string","maxLength":255,"description":"MIME type (e.g., `application/pdf`, `image/png`)."},"contentId":{"type":"string","description":"Content-ID for inline images. Required when `disposition` is `inline`. Reference the image in the email body via `<img src=\"cid:yourContentId\">`."},"disposition":{"type":"string","enum":["attachment","inline"],"default":"attachment","description":"Use `inline` together with `contentId` to embed images in the body. Use `attachment` (the default) for downloadable files."}}}}}},"examples":{"simple":{"summary":"Simple transactional email","value":{"to":"user@example.com","subject":"Password Reset Request","body":"<h1>Reset Your Password</h1><p>Click the link to reset: {{resetLink}}</p>","data":{"resetLink":"https://example.com/reset/abc123"}}},"withNames":{"summary":"Email with recipient and sender names","value":{"to":{"name":"Jane Doe","email":"jane@example.com"},"from":{"name":"My Company","email":"hello@mycompany.com"},"subject":"Welcome to Our Service","body":"<h1>Welcome {{name}}!</h1><p>We're glad to have you.</p>","data":{"name":"Jane"}}},"multipleRecipients":{"summary":"Multiple recipients with names","value":{"to":[{"name":"Jane Doe","email":"jane@example.com"},{"name":"John Smith","email":"john@example.com"}],"from":{"name":"Newsletter","email":"news@mycompany.com"},"subject":"Monthly Update","body":"<h1>Hello {{name}}!</h1>"}},"withTemplate":{"summary":"Using a template","description":"Send email using a template. Provide the template ID and any data for template variables. The template's subject, body, from address, and reply-to will be used automatically.","value":{"to":"user@example.com","template":"clx123abc456","data":{"firstName":"John","lastName":"Doe","resetCode":{"value":"ABC123","persistent":false}}}},"withTemplateOverride":{"summary":"Using template with overrides","description":"You can override template values by providing subject, body, from, or reply fields. This example overrides the template's subject line.","value":{"to":"user@example.com","template":"clx123abc456","subject":"Custom Subject Override","data":{"firstName":"Jane"}}},"marketingEmail":{"summary":"Marketing email (set subscribed: true)","value":{"to":"user@example.com","subject":"Weekly Newsletter","body":"<h1>This Week's Updates</h1>","subscribed":true}},"withAttachment":{"summary":"Email with PDF attachment","value":{"to":"user@example.com","subject":"Your Invoice","body":"<h1>Invoice Attached</h1><p>Please find your invoice attached.</p>","attachments":[{"filename":"invoice.pdf","content":"JVBERi0xLjQKJeLjz9MKMSAwIG9iago8PC9UeXBlL...","contentType":"application/pdf"}]}}}}}},"responses":{"200":{"description":"Email queued successfully","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"type":"object","properties":{"emails":{"type":"array","items":{"type":"object","properties":{"contact":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string"}}},"email":{"type":"string","description":"Bitelio email record ID. Use this to correlate webhook events (which include this ID as 'emailId' in the event data) with your send requests."}}}},"timestamp":{"type":"string","format":"date-time"}}}}},"example":{"success":true,"data":{"emails":[{"contact":{"id":"cnt_abc123","email":"user@example.com"},"email":"ac32f08e-c6b9-45d3-9824-a73dff1e3bbf"}],"timestamp":"2025-01-15T10:30:00.000Z"}}}}},"400":{"description":"Bad request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"Idempotency-Key already used. The request was refused, not performed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"success":false,"error":{"code":"IDEMPOTENCY_KEY_REUSED","message":"Idempotency-Key \"order-1234-receipt\" has already been used","statusCode":409,"requestId":"8f14e45f-ceea-467a-9575-1f0f38e0b1c2","details":{"key":"order-1234-receipt","originalRequest":"POST /v1/send","originalRequestAt":"2025-01-15T10:30:00.000Z","originalStatusCode":200},"suggestion":"This Idempotency-Key was already used, so the request was refused rather than performed twice. Generate a new key for a genuinely new request."},"timestamp":"2025-01-15T10:31:00.000Z"}}}}},"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}]}},"/v1/sms":{"get":{"tags":["Public API"],"summary":"List SMS messages","description":"Your project's SMS history, newest first, **cursor-paginated** — the same shape as `GET /v1/emails`, so one client pages both the same way.\n\n**Every row carries `segments` and `encoding`, and that is the point.** SMS is billed per segment: a GSM-7 message splits every 160 characters, a Unicode one every 70. A single accented character in a template flips the whole message to Unicode and can double its cost. This is the only place that number is readable after the send.\n\nThe body is not in the listing — it is on `GET /v1/sms/{id}`.","operationId":"listSms","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"limit","in":"query","description":"Page size.","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":25}},{"name":"cursor","in":"query","description":"The previous page's `nextCursor`. Opaque — return it verbatim.","required":false,"schema":{"type":"string"}},{"name":"status","in":"query","description":"Filter by delivery status.","required":false,"schema":{"type":"string","enum":["PENDING","QUEUED","SENT","DELIVERED","UNDELIVERED","FAILED"]}},{"name":"to","in":"query","description":"Destination in E.164, matched exactly.","required":false,"schema":{"type":"string","example":"+34600000000"}},{"name":"since","in":"query","description":"ISO 8601.","required":false,"schema":{"type":"string","format":"date-time"}},{"name":"until","in":"query","description":"ISO 8601.","required":false,"schema":{"type":"string","format":"date-time"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"to":{"type":"string"},"from":{"type":"string"},"country":{"type":"string"},"encoding":{"type":"string","enum":["GSM7","UCS2"]},"segments":{"type":"integer"},"status":{"type":"string","enum":["PENDING","QUEUED","SENT","DELIVERED","UNDELIVERED","FAILED"]},"error":{"type":"string","nullable":true}}}},"nextCursor":{"type":"string","nullable":true}}}}}},"400":{"description":"Invalid limit, status or date"},"403":{"description":"The key lacks the sms:view scope"}}}},"/v1/sms/send":{"post":{"tags":["Public API"],"summary":"Send an SMS","description":"Send a single transactional SMS. Mirrors `POST /v1/send`: a secret key with the `sms:send` scope, and an `Idempotency-Key` header if you want a retry to be safe.\n\n**You do not choose the sender.** It is resolved from the destination's country and the senders your project has registered, because what counts as a valid sender differs per country — an alphanumeric sender ID works in Spain and is rejected in the United States.\n\n**Segments, not messages, are what you pay for.** A GSM-7 message splits every 160 characters, a Unicode one every 70. The response tells you how many this became before you are billed for them.\n\nReturns **202**: accepted for delivery, not delivered. Delivery status arrives by webhook.","operationId":"sendSms","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"Idempotency-Key","in":"header","required":false,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["to","body"],"properties":{"to":{"type":"string","example":"+34600000000","description":"Destination in E.164 form, e.g. `+34600000000`."},"body":{"type":"string","minLength":1,"maxLength":1600,"description":"Message text. Up to 1600 characters; longer bodies are refused rather than truncated."},"contactId":{"type":"string","format":"uuid","description":"Attach the message to a contact you already have, instead of matching by number."}}}}}},"responses":{"202":{"description":"Accepted for delivery","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"to":{"type":"string"},"from":{"type":"string"},"country":{"type":"string"},"encoding":{"type":"string","enum":["GSM7","UCS2"]},"segments":{"type":"integer"},"status":{"type":"string"}}}}}},"400":{"description":"The body failed validation"},"403":{"description":"The key lacks the sms:send scope, or SMS is not enabled for this deployment"},"409":{"description":"This Idempotency-Key was already used by this project"}}}},"/v1/sms/{id}":{"get":{"tags":["Public API"],"summary":"Get an SMS message","description":"One message, including the text that was actually sent.\n\nReturns **404** for a message belonging to another project, never 403 — a 403 would confirm the id is real.","operationId":"getSms","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object"}}}}}},"403":{"description":"The key lacks the sms:view scope"},"404":{"description":"No such message in this project"}}}},"/v1/templates":{"get":{"tags":["Templates"],"summary":"List templates","description":"Get a paginated list of email templates\n\n---\n\n**Path versioning.** This resource is also reachable at the unversioned `/templates`, which is where it has always lived and which keeps working indefinitely — `bitelio@0.1.0` calls it. Prefer `/v1/templates`: the `/v1` prefix is the path that carries a version promise, and a future `/v2` will be able to change shape without breaking anything written against this one.","operationId":"listTemplates","parameters":[{"$ref":"#/components/parameters/Limit"},{"$ref":"#/components/parameters/Cursor"},{"name":"type","in":"query","schema":{"type":"string","enum":["TRANSACTIONAL","MARKETING"]}},{"name":"search","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"List of templates","content":{"application/json":{"schema":{"type":"object","properties":{"templates":{"type":"array","items":{"$ref":"#/components/schemas/Template"}},"total":{"type":"integer"},"page":{"type":"integer"},"pageSize":{"type":"integer"},"totalPages":{"type":"integer"}}}}}}}},"post":{"tags":["Templates"],"summary":"Create template","description":"Create a new email template\n\n---\n\n**Path versioning.** This resource is also reachable at the unversioned `/templates`, which is where it has always lived and which keeps working indefinitely — `bitelio@0.1.0` calls it. Prefer `/v1/templates`: the `/v1` prefix is the path that carries a version promise, and a future `/v2` will be able to change shape without breaking anything written against this one.","operationId":"createTemplate","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","subject","body","type"],"properties":{"name":{"type":"string"},"subject":{"type":"string"},"body":{"type":"string"},"type":{"type":"string","enum":["TRANSACTIONAL","MARKETING"]}}}}}},"responses":{"201":{"description":"Template created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Template"}}}}}}},"/v1/track":{"post":{"tags":["Public API"],"summary":"Track event","description":"Track an event for a contact. Automatically creates or upserts the contact, then records the event. Tracked events can be used as workflow triggers, segment filters, and audience filters.\n\n**Reserved event names** (rejected with `VALIDATION_ERROR` and code `reserved_event`): anything matching `email.*`, `contact.subscribed`, `contact.unsubscribed`, `segment.<slug>.entry`, `segment.<slug>.exit`. These are emitted by Bitelio itself.\n\n**System-sourced namespaces** (rejected with `VALIDATION_ERROR` and code `system_sourced_event`): anything starting with `pixel.` or `shopify.`. Bitelio writes these itself — `pixel.*` from the storefront Web Pixel via `POST /v1/pixel`, `shopify.*` from Shopify's signed webhooks — and the prebuilt flows render their fields into email bodies and links. This endpoint accepts a **public** key, so it may not write them: send storefront events to `POST /v1/pixel`, and choose any name outside those two prefixes for your own events.\n\n**Idempotency**: re-tracking the same event creates a new event record. Send an `Idempotency-Key` header to have a repeated request refused with `409` instead.","operationId":"trackEvent","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email","event"],"properties":{"email":{"type":"string","format":"email","description":"Contact email. The contact is auto-created if it doesn't exist."},"event":{"type":"string","description":"Event name. Cannot match the reserved patterns or the system-sourced namespaces above."},"subscribed":{"type":"boolean","description":"Subscription state to apply to the contact. **New** contacts default to subscribed (`true`). **Existing** contacts keep their current state unless you pass an explicit value here. Pass `false` to track an event without resubscribing an unsubscribed contact."},"data":{"type":"object","additionalProperties":true,"description":"Contact data and one-off event variables. Persistent values (primitives, plain objects) are saved on the contact and become available as template variables. Pass `{ value, persistent: false }` for one-shot variables that should not be stored on the contact (e.g. order IDs, transaction details). `null` deletes a field. Empty strings are ignored. Reserved keys are filtered out — see the contacts concept page."}}},"example":{"email":"user@example.com","event":"purchase","data":{"product":"Premium Plan","amount":99}}}}},"responses":{"200":{"description":"Event tracked successfully","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"data":{"type":"object","properties":{"contact":{"type":"string","description":"Contact ID"},"event":{"type":"string","description":"Event ID"},"timestamp":{"type":"string","format":"date-time"}}}}}}}},"409":{"description":"Idempotency-Key already used. The request was refused, not performed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"success":false,"error":{"code":"IDEMPOTENCY_KEY_REUSED","message":"Idempotency-Key \"order-1234-receipt\" has already been used","statusCode":409,"requestId":"8f14e45f-ceea-467a-9575-1f0f38e0b1c2","details":{"key":"order-1234-receipt","originalRequest":"POST /v1/send","originalRequestAt":"2025-01-15T10:30:00.000Z","originalStatusCode":200},"suggestion":"This Idempotency-Key was already used, so the request was refused rather than performed twice. Generate a new key for a genuinely new request."},"timestamp":"2025-01-15T10:31:00.000Z"}}}}},"parameters":[{"$ref":"#/components/parameters/IdempotencyKey"}]}},"/v1/verify":{"post":{"tags":["Public API"],"summary":"Verify email address","description":"Verify an email address for validity, check if it's from a disposable domain or personal email provider, verify MX records, and detect potential typos with suggestions.","operationId":"verifyEmail","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["email"],"properties":{"email":{"type":"string","format":"email","description":"Email address to verify"}}},"examples":{"validEmail":{"summary":"Valid email address","value":{"email":"user@gmail.com"}},"typoEmail":{"summary":"Email with potential typo","value":{"email":"user@gmial.com"}},"disposableEmail":{"summary":"Disposable email address","value":{"email":"user@tempmail.com"}}}}}},"responses":{"200":{"description":"Email verification completed successfully","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean","description":"Always true for successful requests"},"data":{"type":"object","properties":{"email":{"type":"string","format":"email","description":"Email address that was verified"},"valid":{"type":"boolean","description":"Whether the email appears to be valid overall"},"isDisposable":{"type":"boolean","description":"Whether the email is from a disposable/temporary email domain"},"isAlias":{"type":"boolean","description":"Whether the email is from a forwarding/alias service"},"isTypo":{"type":"boolean","description":"Whether a potential typo was detected in the email address"},"isPlusAddressed":{"type":"boolean","description":"Whether the email uses plus addressing (contains a + in the local part)"},"isPersonalEmail":{"type":"boolean","description":"Whether the email is from a personal/free email provider (Gmail, Hotmail, Yahoo, etc.)"},"domainExists":{"type":"boolean","description":"Whether the domain exists in DNS (has NS records)"},"hasWebsite":{"type":"boolean","description":"Whether the domain has a website (has DNS A or AAAA records) - informational only"},"hasMxRecords":{"type":"boolean","description":"Whether the domain has MX records configured for email delivery"},"suggestedEmail":{"type":"string","format":"email","description":"Suggested correction if a typo was detected (optional)","nullable":true},"reasons":{"type":"array","items":{"type":"string"},"description":"Array of human-readable reasons describing the verification results"}},"required":["email","valid","isDisposable","isAlias","isTypo","isPlusAddressed","isPersonalEmail","domainExists","hasWebsite","hasMxRecords","reasons"]}}},"examples":{"validEmail":{"summary":"Valid email","value":{"success":true,"data":{"email":"user@gmail.com","valid":true,"isDisposable":false,"isAlias":false,"isTypo":false,"isPlusAddressed":false,"isPersonalEmail":true,"domainExists":true,"hasWebsite":true,"hasMxRecords":true,"reasons":["Email appears to be valid"]}}},"typoDetected":{"summary":"Email with typo detected","value":{"success":true,"data":{"email":"user@gmial.com","valid":false,"isDisposable":false,"isAlias":false,"isTypo":true,"isPlusAddressed":false,"isPersonalEmail":false,"domainExists":false,"hasWebsite":false,"hasMxRecords":false,"suggestedEmail":"user@gmail.com","reasons":["Possible typo detected, did you mean gmail.com?","Domain does not exist (no nameservers found)"]}}},"disposableEmail":{"summary":"Disposable email detected","value":{"success":true,"data":{"email":"user@tempmail.com","valid":true,"isDisposable":true,"isAlias":false,"isTypo":false,"isPlusAddressed":false,"isPersonalEmail":false,"domainExists":true,"hasWebsite":true,"hasMxRecords":true,"reasons":["Email appears to be valid"]}}}}}}},"400":{"description":"Bad request - invalid email format","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Unauthorized - invalid or missing API key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/retention/survey-deliveries":{"get":{"operationId":"listSurveyDeliveries","tags":["Survey channels"],"summary":"List survey delivery history","description":"Merchant server only, secret key with retention:view. Newest first, at most20 records, opaque nextCursor. Project-scoped limit120 requests/minute shared with survey channel API. ACCEPTED is not evidence of device delivery; UNKNOWN must not trigger a resend. This compatibility endpoint returns survey invitations only; use /v1/retention/messages for all message kinds.","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"after","in":"query","schema":{"type":"string","maxLength":400}}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/SurveyDelivery"}},"nextCursor":{"type":["string","null"]}},"required":["items","nextCursor"],"additionalProperties":false}}}},"400":{"description":"Invalid input or unavailable channel"},"401":{"description":"Secret API key required"},"403":{"description":"Required project permission is missing"},"429":{"description":"Project rate limit exceeded. Retry-After identifies the wait in seconds."},"503":{"description":"Rate limiter temporarily unavailable; retry later."}}}},"/v1/retention/survey-deliveries/claim":{"post":{"operationId":"claimMobileSurveyDeliveries","tags":["Survey channels"],"summary":"Claim prepared surveys for a mobile backend","description":"Requires retention:view, retention:edit and retention:activate. Mobile API must be enabled. Claims up to20 current programme invitations after current programme, withdrawal, actor and subsequent-purchase checks. Each invitation is handed off once. Deliver before claimExpiresAt and report an actual result; do not send an expired claim. The merchant must verify its native notification consent and use its own APNs/FCM provider. Store capability and receipt tokens securely; never put an sk_ key in the mobile app. Limit120 requests/minute shared perproject. Only survey invitations are returned. Use /v1/retention/messages/claim to handle other retention messages.","security":[{"ApiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"after":{"type":"string","maxLength":100}},"additionalProperties":false}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","maxItems":20,"items":{"$ref":"#/components/schemas/MobileSurveyClaim"}}},"required":["items"],"additionalProperties":false}}}},"400":{"description":"Invalid input or unavailable channel"},"401":{"description":"Secret API key required"},"403":{"description":"Required project permission is missing"},"429":{"description":"Project rate limit exceeded. Retry-After identifies the wait in seconds."},"503":{"description":"Rate limiter temporarily unavailable; retry later."}}}},"/v1/retention/survey-deliveries/{id}/receipt":{"post":{"operationId":"acknowledgeMobileSurveyDelivery","tags":["Survey channels"],"summary":"Acknowledge a mobile survey handoff","description":"Requires retention:view and retention:edit. Idempotent and scoped to this project, delivery and receiptToken. Report ACCEPTED when your notification provider accepts, DELIVERED only with provider/device evidence, FAILED when definitely not delivered, OPENED only after a real user opens the notification, and CLICKED after a deliberate interaction. Send receipts from your merchant server, never expose the secret API key or receiptToken in an app binary. Opening and clicking imply delivery; repeated or out-of-order receipts preserve the first timestamps and cannot downgrade DELIVERED. Bitelio never automatically reissues an uncertain handoff. Tracking-disabled projects and customers who withdraw from follow-up do not collect interaction timestamps. Limit 120 requests/minute shared per project.","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","maxLength":100}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"receiptToken":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["ACCEPTED","DELIVERED","FAILED","OPENED","CLICKED"]}},"required":["receiptToken","status"],"additionalProperties":false}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"acknowledged":{"type":"boolean"}},"required":["acknowledged"],"additionalProperties":false}}}},"400":{"description":"Invalid input or unavailable channel"},"401":{"description":"Secret API key required"},"403":{"description":"Required project permission is missing"},"429":{"description":"Project rate limit exceeded. Retry-After identifies the wait in seconds."},"503":{"description":"Rate limiter temporarily unavailable; retry later."}}}},"/v1/retention/push-subscriptions":{"post":{"operationId":"registerSurveyPushSubscription","tags":["Survey channels"],"summary":"Register explicit storefront notification permission","description":"Merchant backend only, retention:view and retention:edit. Authenticate your customer and derive contactId on the server; never trust a browser-supplied contactId. Requires explicit permission, an allowed exact HTTPS origin, and a browser subscription to a supported push service. The endpoint and keys are encrypted. Maximum10 active subscriptions/contact. Existing endpoints cannot be transferred between customers. Limit120 requests/minute shared perproject. Scope defaults to SURVEY. Only an explicit new customer opt-in may pass RETENTION, permitting help and offers in addition to surveys. Existing subscriptions retain SURVEY scope until a new consent is registered. Optional timeZone is the IANA zone reported by the authenticated customer device, for example America/New_York; never infer it from IP or country. It adjusts configured contact hours for 90 days, then falls back to the store zone. It does not expand consent or create a schedule.","security":[{"ApiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"contactId":{"type":"string","maxLength":100},"origin":{"type":"string","format":"uri","maxLength":250},"subscription":{"$ref":"#/components/schemas/SurveyPushSubscription"},"consent":{"const":true},"scope":{"type":"string","enum":["SURVEY","RETENTION"],"default":"SURVEY"},"timeZone":{"type":"string","maxLength":100,"description":"Optional device-declared IANA time zone, captured only after explicit permission and tenant/customer verification.","example":"America/New_York"}},"required":["contactId","origin","subscription","consent"],"additionalProperties":false}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string"}},"required":["id","status"],"additionalProperties":false}}}},"400":{"description":"Invalid input or unavailable channel"},"401":{"description":"Secret API key required"},"403":{"description":"Required project permission is missing"},"429":{"description":"Project rate limit exceeded. Retry-After identifies the wait in seconds."},"503":{"description":"Rate limiter temporarily unavailable; retry later."}}}},"/v1/retention/push-subscriptions/{id}":{"delete":{"operationId":"revokeSurveyPushSubscription","tags":["Survey channels"],"summary":"Revoke a storefront notification subscription","description":"Merchant backend only, retention:view and retention:edit. Scoped to the authenticated project, contactId and subscription id; idempotent. Limit120 requests/minute shared perproject.","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","maxLength":100}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"contactId":{"type":"string","maxLength":100}},"required":["contactId"],"additionalProperties":false}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"revoked":{"type":"boolean"}},"required":["revoked"],"additionalProperties":false}}}},"400":{"description":"Invalid input or unavailable channel"},"401":{"description":"Secret API key required"},"403":{"description":"Required project permission is missing"},"429":{"description":"Project rate limit exceeded. Retry-After identifies the wait in seconds."},"503":{"description":"Rate limiter temporarily unavailable; retry later."}}}},"/v1/retention/messages":{"get":{"operationId":"listRetentionMessages","tags":["Survey channels"],"summary":"List retention message delivery history","description":"Merchant server only, secret key with retention:view. Newest first, at most20 records, opaque nextCursor. Project-scoped limit120 requests/minute shared with survey channel API. ACCEPTED is not evidence of device delivery; UNKNOWN must not trigger a resend.","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"after","in":"query","schema":{"type":"string","maxLength":400}}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"$ref":"#/components/schemas/SurveyDelivery"}},"nextCursor":{"type":["string","null"]}},"required":["items","nextCursor"],"additionalProperties":false}}}},"400":{"description":"Invalid input or unavailable channel"},"401":{"description":"Secret API key required"},"403":{"description":"Required project permission is missing"},"429":{"description":"Project rate limit exceeded. Retry-After identifies the wait in seconds."},"503":{"description":"Rate limiter temporarily unavailable; retry later."}}}},"/v1/retention/messages/claim":{"post":{"operationId":"claimMobileRetentionMessages","tags":["Survey channels"],"summary":"Claim surveys, help and offers for a mobile backend","description":"Requires retention:view, retention:edit and retention:activate. Mobile API must be enabled. Claims up to20 current programme messages after current programme, withdrawal, actor and subsequent-purchase checks. Each message is handed off once. Deliver before claimExpiresAt and report an actual result; do not send an expired claim. The merchant must verify its native notification consent and use its own APNs/FCM provider. Store capability and receipt tokens securely; never put an sk_ key in the mobile app. Limit120 requests/minute shared perproject. The kind discriminator distinguishes SURVEY, HELP, ALTERNATIVE, REPLENISHMENT and RECOMMEND. Open url for the complete content; never render a help or offer message as a survey. Non-survey messages carry a pinned message snapshot and use a separate message capability. The legacy survey-deliveries claim endpoint remains invitation-only. Non-survey mobile messages additionally require an ACTIVE RETENTION permission for that customer, registered through /v1/retention/mobile-permissions. A revoked permission blocks every mobile claim, including legacy surveys.","security":[{"ApiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"after":{"type":"string","maxLength":100}},"additionalProperties":false}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","maxItems":20,"items":{"$ref":"#/components/schemas/MobileRetentionClaim"}}},"required":["items"],"additionalProperties":false}}}},"400":{"description":"Invalid input or unavailable channel"},"401":{"description":"Secret API key required"},"403":{"description":"Required project permission is missing"},"429":{"description":"Project rate limit exceeded. Retry-After identifies the wait in seconds."},"503":{"description":"Rate limiter temporarily unavailable; retry later."}}}},"/v1/retention/messages/{id}/receipt":{"post":{"operationId":"acknowledgeMobileRetentionMessage","tags":["Survey channels"],"summary":"Acknowledge a mobile retention message handoff","description":"Requires retention:view and retention:edit. Idempotent and scoped to this project, delivery and receiptToken. Report ACCEPTED when your notification provider accepts, DELIVERED only with provider/device evidence, FAILED when definitely not delivered, OPENED only after a real user opens the notification, and CLICKED after a deliberate interaction. Send receipts from your merchant server, never expose the secret API key or receiptToken in an app binary. Opening and clicking imply delivery; repeated or out-of-order receipts preserve the first timestamps and cannot downgrade DELIVERED. Bitelio never automatically reissues an uncertain handoff. Tracking-disabled projects and customers who withdraw from follow-up do not collect interaction timestamps. Limit 120 requests/minute shared per project.","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","maxLength":100}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"receiptToken":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["ACCEPTED","DELIVERED","FAILED","OPENED","CLICKED"]}},"required":["receiptToken","status"],"additionalProperties":false}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"acknowledged":{"type":"boolean"}},"required":["acknowledged"],"additionalProperties":false}}}},"400":{"description":"Invalid input or unavailable channel"},"401":{"description":"Secret API key required"},"403":{"description":"Required project permission is missing"},"429":{"description":"Project rate limit exceeded. Retry-After identifies the wait in seconds."},"503":{"description":"Rate limiter temporarily unavailable; retry later."}}}},"/v1/retention/mobile-permissions":{"post":{"operationId":"registerRetentionMobilePermission","tags":["Survey channels"],"summary":"Register explicit customer permission for mobile follow-up","description":"Merchant server only: derive contactId from the authenticated app customer, verify device permission and retain their explicit consent. Scope SURVEY permits surveys; RETENTION also permits help, recommendations and offers. No existing survey permission is silently expanded. Requires retention:view and retention:edit; shares the project channel API rate limit. Optional timeZone is the IANA zone reported by the authenticated customer device, for example America/New_York; never infer it from IP or country. It adjusts configured contact hours for 90 days, then falls back to the store zone. It does not expand consent or create a schedule.","security":[{"ApiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"contactId":{"type":"string","minLength":1,"maxLength":100},"scope":{"type":"string","enum":["SURVEY","RETENTION"]},"consent":{"type":"boolean","const":true},"timeZone":{"type":"string","maxLength":100,"description":"Optional device-declared IANA time zone, captured only after explicit permission and tenant/customer verification.","example":"America/New_York"}},"required":["contactId","scope","consent"],"additionalProperties":false}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string","const":"ACTIVE"},"scope":{"type":"string","enum":["SURVEY","RETENTION"]}},"required":["id","status","scope"],"additionalProperties":false}}}},"400":{"description":"Invalid input or unavailable channel"},"401":{"description":"Secret API key required"},"403":{"description":"Required project permission is missing"},"429":{"description":"Project rate limit exceeded. Retry-After identifies the wait in seconds."},"503":{"description":"Rate limiter temporarily unavailable; retry later."}}}},"/v1/retention/mobile-permissions/{contactId}":{"delete":{"operationId":"revokeRetentionMobilePermission","tags":["Survey channels"],"summary":"Revoke customer mobile follow-up permission","description":"Revoke all mobile follow-up for this project customer. Future claims recheck the permission. Requires retention:view and retention:edit; merchant server only, same project rate limit.","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"contactId","in":"path","required":true,"schema":{"type":"string","minLength":1,"maxLength":100}}],"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"revoked":{"type":"boolean"}},"required":["revoked"],"additionalProperties":false}}}},"400":{"description":"Invalid input or unavailable channel"},"401":{"description":"Secret API key required"},"403":{"description":"Required project permission is missing"},"429":{"description":"Project rate limit exceeded. Retry-After identifies the wait in seconds."},"503":{"description":"Rate limiter temporarily unavailable; retry later."}}}},"/v1/retention/messages/cost-receipts":{"post":{"operationId":"importRetentionDeliveryCost","tags":["Survey channels"],"summary":"Import a communication cost receipt","description":"Merchant server only, secret key with retention:view, retention:edit and settings:manage. Imports one provider invoice line for an attempted external delivery. Identify an initial delivery by deliveryId, a native WhatsApp question/confirmation turn by turnId, or either by providerMessageId. Do not combine deliveryId with turnId. If a providerMessageId is included it must match that exact row. Native turns count as separate provider attempts and are not assumed free. amountMinor is an integer string in currency minor units. Zero requires explicit cost evidence. Replaying an identical receipt is idempotent; changing a recorded amount or reusing its reference returns COST_RECEIPT_CONFLICT (400). Evidence is labelled MERCHANT_PROVIDER_RECEIPT, not an automatically verified provider bill. A delivery acknowledgement does not establish its cost. No currency conversion is performed. Missing costs remain pending and cached results update in the background. Limit120 requests/minute shared per project.","security":[{"ApiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"receiptId":{"type":"string","minLength":1,"maxLength":200,"example":"invoice-2026-10:line-14"},"currency":{"type":"string","pattern":"^[A-Z]{3}$","example":"EUR"},"amountMinor":{"type":"string","pattern":"^(0|[1-9]\\d{0,13})$","example":"7"},"providerMessageId":{"type":"string","minLength":1,"maxLength":500},"deliveryId":{"type":"string","minLength":1,"maxLength":100},"turnId":{"type":"string","maxLength":100,"description":"Native WhatsApp turn ID, instead of deliveryId. Use providerMessageId when reconciling a provider invoice."}},"required":["receiptId","currency","amountMinor"],"additionalProperties":false,"anyOf":[{"required":["deliveryId"]},{"required":["providerMessageId"]},{"required":["turnId"]}]}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"deliveryId":{"type":"string"},"recorded":{"type":"boolean","enum":[true]},"turnId":{"type":"string"}},"required":["recorded"],"additionalProperties":false,"oneOf":[{"required":["deliveryId"]},{"required":["turnId"]}]}}}},"400":{"description":"Invalid input, unsupported currency or conflicting receipt"},"401":{"description":"Secret API key required"},"403":{"description":"Required project permission is missing"},"404":{"description":"No attempted external delivery matches in this project, or retention is unavailable"},"429":{"description":"Project rate limit exceeded. Retry-After identifies the wait in seconds."},"503":{"description":"Rate limiter temporarily unavailable; retry later."}}}},"/v1/retention/messages/{id}/cost-receipt":{"post":{"operationId":"recordRetentionDeliveryCost","tags":["Survey channels"],"summary":"Record a delivery cost receipt","description":"Merchant server only, secret key with retention:view, retention:edit and settings:manage. Imports one provider invoice line for an attempted external delivery. Identify it by deliveryId or providerMessageId; if both are provided they must match. amountMinor is an integer string in currency minor units. Zero requires explicit cost evidence. Replaying an identical receipt is idempotent; changing a recorded amount or reusing its reference returns COST_RECEIPT_CONFLICT (400). Evidence is labelled MERCHANT_PROVIDER_RECEIPT, not an automatically verified provider bill. A delivery acknowledgement does not establish its cost. No currency conversion is performed. Missing costs remain pending and cached results update in the background. Limit120 requests/minute shared per project. The path id identifies the delivery; it need not be repeated in the body.","security":[{"ApiKeyAuth":[]}],"parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string","maxLength":100}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"receiptId":{"type":"string","minLength":1,"maxLength":200,"example":"invoice-2026-10:line-14"},"currency":{"type":"string","pattern":"^[A-Z]{3}$","example":"EUR"},"amountMinor":{"type":"string","pattern":"^(0|[1-9]\\d{0,13})$","example":"7"},"providerMessageId":{"type":"string","minLength":1,"maxLength":500}},"required":["receiptId","currency","amountMinor"],"additionalProperties":false}}}},"responses":{"200":{"description":"Success","content":{"application/json":{"schema":{"type":"object","properties":{"deliveryId":{"type":"string"},"recorded":{"type":"boolean","enum":[true]}},"required":["deliveryId","recorded"],"additionalProperties":false}}}},"400":{"description":"Invalid input, unsupported currency or conflicting receipt"},"401":{"description":"Secret API key required"},"403":{"description":"Required project permission is missing"},"404":{"description":"No attempted external delivery matches in this project, or retention is unavailable"},"429":{"description":"Project rate limit exceeded. Retry-After identifies the wait in seconds."},"503":{"description":"Rate limiter temporarily unavailable; retry later."}}}},"/v1/retention/mobile-survey-sessions":{"post":{"operationId":"createMobileSurveySession","tags":["Survey channels"],"summary":"Create a native mobile survey session","description":"The merchant backend must authenticate the customer and bind their device before requesting a session. Requires retention:view and retention:edit, active mobile permission and an accepted MOBILE_API delivery. contactId must match that exact delivery. receiptToken stays on the server. The returned token expires after 15 minutes (or invitation expiry), is bound to deviceId and is revoked by permission updates. Never put a secret key in the app. 120 requests/project/minute. See /guides/native-surveys.","security":[{"ApiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"deliveryId":{"type":"string","format":"uuid"},"contactId":{"type":"string","maxLength":100},"receiptToken":{"type":"string","format":"uuid"},"deviceId":{"type":"string","pattern":"^[A-Za-z0-9_-]{16,128}$"}},"required":["deliveryId","contactId","receiptToken","deviceId"]}}}},"responses":{"200":{"description":"Device-scoped session","content":{"application/json":{"schema":{"type":"object","required":["sessionToken","expiresAt","capabilities"],"properties":{"sessionToken":{"type":"string","maxLength":160},"expiresAt":{"type":"string","format":"date-time"},"capabilities":{"type":"object","properties":{"nativeQuestions":{"type":"boolean","enum":[true]},"conditionalQuestions":{"type":"boolean","enum":[true]},"resume":{"type":"boolean","enum":[true]},"choice":{"type":"boolean","enum":[true]},"multipleChoice":{"type":"boolean","enum":[true]},"rating":{"type":"boolean","enum":[true]},"explicitSubmission":{"type":"boolean","enum":[true]}}}}}}}},"400":{"description":"Invalid input or unavailable survey"},"401":{"description":"Secret API key required"},"403":{"description":"Required project permission missing"},"404":{"description":"Session, permission or survey unavailable"},"429":{"description":"Rate limit exceeded; Retry-After gives seconds"},"503":{"description":"Rate limiter temporarily unavailable"}}}},"/public/retention/mobile-survey":{"get":{"tags":["Survey channels"],"security":[{"MobileSurveySession":[]}],"parameters":[{"name":"X-Bitelio-Device","in":"header","required":true,"schema":{"type":"string","pattern":"^[A-Za-z0-9_-]{16,128}$"}}],"operationId":"readNativeMobileSurvey","summary":"Read native survey and saved progress","description":"Read-only. Returns the current BuyerSurveyView: state, brand, locale, published closed-choice questions and conditional showWhen rules, draft revision/answers/throughQuestionId, or completion/help/nextStep after submission. Does not record an answer or click. Render only questions whose showWhen condition matches visible prior answers. 60 requests/session/minute; no-store.","responses":{"200":{"description":"Current published survey and progress","content":{"application/json":{"schema":{"type":"object","properties":{"state":{"type":"string","enum":["OPEN","PAUSED","EXPIRED","COMPLETED"]},"questions":{"type":"array","items":{"type":"object"}},"draft":{"type":"object"}}}}}},"400":{"description":"Invalid input or unavailable survey"},"404":{"description":"Session, permission or survey unavailable"},"429":{"description":"Rate limit exceeded; Retry-After gives seconds"},"503":{"description":"Rate limiter temporarily unavailable"}}}},"/public/retention/mobile-survey/progress":{"post":{"tags":["Survey channels"],"security":[{"MobileSurveySession":[]}],"parameters":[{"name":"X-Bitelio-Device","in":"header","required":true,"schema":{"type":"string","pattern":"^[A-Za-z0-9_-]{16,128}$"}}],"operationId":"saveNativeMobileSurveyProgress","summary":"Save a completed native survey step","description":"Explicit user continuation only. Uses the same invitation draft as the browser and WhatsApp. Submit all answers in the completed visible prefix and the current revision. Optional skipped steps may omit their answer. Hidden/unknown options and missing required preceding answers are rejected. A successful save is progress, not a completed survey. On 409 reload and let the customer reconcile their changes. Never silently overwrite a newer device.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"revision":{"type":"integer","minimum":0},"throughQuestionId":{"type":"string","maxLength":80},"answers":{"type":"array","maxItems":100,"items":{"type":"object","required":["questionId","value"],"additionalProperties":false,"properties":{"questionId":{"type":"string","maxLength":80},"value":{"oneOf":[{"type":"string"},{"type":"number"},{"type":"array","items":{"type":"string"}}]}}}}},"required":["revision","throughQuestionId","answers"]}}}},"responses":{"200":{"description":"Saved draft with new revision","content":{"application/json":{"schema":{"type":"object","properties":{"revision":{"type":"integer"},"throughQuestionId":{"type":"string"},"answers":{"type":"array","maxItems":100,"items":{"type":"object","required":["questionId","value"],"additionalProperties":false,"properties":{"questionId":{"type":"string","maxLength":80},"value":{"oneOf":[{"type":"string"},{"type":"number"},{"type":"array","items":{"type":"string"}}]}}}}}}}}},"400":{"description":"Invalid input or unavailable survey"},"404":{"description":"Session, permission or survey unavailable"},"409":{"description":"SURVEY_DRAFT_CONFLICT: reload before continuing"},"429":{"description":"Rate limit exceeded; Retry-After gives seconds"},"503":{"description":"Rate limiter temporarily unavailable"}}}},"/public/retention/mobile-survey/submit":{"post":{"tags":["Survey channels"],"security":[{"MobileSurveySession":[]}],"parameters":[{"name":"X-Bitelio-Device","in":"header","required":true,"schema":{"type":"string","pattern":"^[A-Za-z0-9_-]{16,128}$"}}],"operationId":"submitNativeMobileSurvey","summary":"Submit native survey answers","description":"Requires explicit final customer confirmation, the current revision and only visible answers. Uses the existing atomic survey validation, submission and customer insight pipeline. Exact retries are idempotent. Returns completed, replayed and available completion/help/nextStep views. Rendering or opening a notification never submits answers.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"revision":{"type":"integer","minimum":0},"answers":{"type":"array","maxItems":100,"items":{"type":"object","required":["questionId","value"],"additionalProperties":false,"properties":{"questionId":{"type":"string","maxLength":80},"value":{"oneOf":[{"type":"string"},{"type":"number"},{"type":"array","items":{"type":"string"}}]}}}}},"required":["revision","answers"]}}}},"responses":{"200":{"description":"Completed survey and available next step","content":{"application/json":{"schema":{"type":"object","properties":{"completed":{"type":"boolean"},"replayed":{"type":"boolean"},"completion":{"type":"object"},"help":{"type":"object"},"nextStep":{"type":"object"}}}}}},"400":{"description":"Invalid input or unavailable survey"},"404":{"description":"Session, permission or survey unavailable"},"409":{"description":"SURVEY_DRAFT_CONFLICT: reload before continuing"},"429":{"description":"Rate limit exceeded; Retry-After gives seconds"},"503":{"description":"Rate limiter temporarily unavailable"}}}},"/v1/retention/commerce-intents":{"post":{"operationId":"recordRetentionCommerceIntent","tags":["Survey channels"],"summary":"Record authenticated commerce intent","description":"Merchant server only, secret API key with retention:edit AND contacts:edit. Verify the authenticated buyer before mapping Bitelio integrationId/contactId/orderId. Requires enabled tracking and an active order-scoped program with no withdrawal or erased feedback. Shopify product/variant GIDs must belong to that integration. occurredAt must be within the last 24 hours and not in the future. Immutable eventId deduplication rejects changed payloads; 120 requests/project/minute. Public pixel events do not establish trusted purchase intent. Does not create consent or answer surveys. Decision evidence expires in 2–48 hours; the cleanup worker then removes temporary diagnostic history.","security":[{"ApiKeyAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"integrationId":{"type":"string","maxLength":100,"minLength":1},"contactId":{"type":"string","maxLength":100,"minLength":1},"orderId":{"type":"string","maxLength":100,"minLength":1},"eventId":{"type":"string","pattern":"^[a-zA-Z0-9._:-]+$","maxLength":128,"minLength":1},"kind":{"type":"string","enum":["VIEWED_PRODUCT","ADDED_TO_CART","CHECKOUT_STARTED","CHECKOUT_ABANDONED","CHECKOUT_COMPLETED"]},"occurredAt":{"type":"string","format":"date-time"},"products":{"type":"array","minItems":1,"maxItems":20,"items":{"type":"object","additionalProperties":false,"required":["productId"],"properties":{"productId":{"type":"string","pattern":"^gid://shopify/Product/[0-9]+$"},"variantId":{"type":"string","pattern":"^gid://shopify/ProductVariant/[0-9]+$"}}}}},"required":["integrationId","contactId","orderId","eventId","kind","occurredAt","products"]}}}},"responses":{"202":{"description":"Accepted intent","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"accepted":{"type":"boolean","enum":[true]},"duplicate":{"type":"boolean"},"expiresAt":{"type":"string","format":"date-time"}}}}}},"400":{"description":"Invalid scope, expired event or idempotency conflict"},"401":{"description":"Secret API key required"},"403":{"description":"Required project permissions missing"},"429":{"description":"Rate limit exceeded; Retry-After gives seconds"},"503":{"description":"Rate limiter temporarily unavailable"}}}},"/public/retention/mobile-survey/actions":{"get":{"tags":["Survey channels"],"security":[{"MobileSurveySession":[]}],"parameters":[{"name":"X-Bitelio-Device","in":"header","required":true,"schema":{"type":"string","pattern":"^[A-Za-z0-9_-]{16,128}$"}}],"operationId":"readNativeMobileBuyerActions","summary":"Read available direct buyer actions","description":"Uses the same short-lived device session. Read-only: returns allowed REPEAT, DISCOVER and HELP actions without requiring a survey response. No buyer capability token is returned.","responses":{"200":{"description":"Available actions and support state","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"Invalid input or unavailable survey"},"404":{"description":"Session, permission or survey unavailable"},"429":{"description":"Rate limit exceeded; Retry-After gives seconds"},"503":{"description":"Rate limiter temporarily unavailable"}}},"post":{"tags":["Survey channels"],"security":[{"MobileSurveySession":[]}],"parameters":[{"name":"X-Bitelio-Device","in":"header","required":true,"schema":{"type":"string","pattern":"^[A-Za-z0-9_-]{16,128}$"}}],"operationId":"actNativeMobileBuyer","summary":"Choose products or request help inside the app","description":"Revalidates device session and mobile permission inside the action transaction. REQUESTED returns up to three eligible product choices; VIEWED records deliberate viewing; SELECTED/CHECKOUT require an exact returned product and variant and recheck current stock and policy. HELP REQUESTED shows reviewed guidance; HELP_REQUESTED creates support only if canContact, RESOLVED confirms resolution. No survey submission is required. Never invent product ids or a checkout URL. HELP/RESOLVED must echo caseId and updatedAt from the displayed helpRevision; a changed case is rejected with HELP_REPLY_CONFLICT. Never generate these values locally.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["action","stage"],"additionalProperties":false,"properties":{"action":{"type":"string","enum":["REPEAT","DISCOVER","HELP"]},"stage":{"type":"string","enum":["REQUESTED","VIEWED","SELECTED","CHECKOUT","HELP_REQUESTED","RESOLVED"]},"productId":{"type":"string"},"variantId":{"type":"string"},"caseId":{"type":"string","description":"Exact helpRevision.caseId returned for the case shown to the buyer; required only for HELP/RESOLVED."},"updatedAt":{"type":"string","format":"date-time","description":"Exact helpRevision.updatedAt returned by the API; required only for HELP/RESOLVED."}}}}}},"responses":{"200":{"description":"Current choices, help, support state or verified checkout URL","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"Invalid input or unavailable survey"},"404":{"description":"Session, permission or survey unavailable"},"429":{"description":"Rate limit exceeded; Retry-After gives seconds"},"503":{"description":"Rate limiter temporarily unavailable"}}}}},"tags":[{"name":"Public API","description":"Public API endpoints for sending emails and tracking events"},{"name":"Contacts","description":"Contact management operations"},{"name":"Templates","description":"Email template management"},{"name":"Campaigns","description":"Email campaign management"},{"name":"Segments","description":"Audience segmentation"}],"x-ext-urls":{}}