openapi: 3.1.0
info:
  title: SendRCS API
  version: 2.0.1
  description: |
    # SendRCS API

    Send rich, interactive messages to mobile users via RCS (Rich Communication Services).
    This API supports text messages, rich cards, carousels, media, and interactive buttons
    with SMS fallback for devices without RCS support.

    ## Key Features
    - **Rich Message Types**: Text, Rich Cards, Carousels, Media, File, Image, Video, Audio
    - **Interactive Buttons**: Up to 11 chip suggestions, 4 card actions per card
    - **SMS Fallback**: Automatic fallback when RCS is unavailable
    - **Standalone SMS**: Send SMS messages independently of RCS via `/send-sms`
    - **Scheduling**: Timezone-aware message scheduling
    - **Conversation Billing**: 24-hour conversation windows for cost optimization
    - **Batch Sending**: Send to up to 10,000 recipients in one request
    - **Webhooks**: Receive button clicks, incoming RCS/SMS messages, and delivery status callbacks in real-time
    - **Contact Management**: Full CRUD for contacts, custom merge fields, and per-contact field values
    - **MCP Integration**: Connect SendRCS to AI assistants via Model Context Protocol

    ## Authentication
    All endpoints require an API key passed in the `X-API-Key` header, except the public **Sandbox**
    endpoints under `/sandbox/*`. The sandbox validates and previews a message body without an account
    and never sends anything.

    ## Try it without an account (Sandbox)
    You can validate and preview any message body before you have an API key. The sandbox runs the
    same validator as `POST /send`, returns the same error shape, and gives you a phone-mockup preview
    link. Nothing is sent, no credits are used, and no auth header is needed.

    - `POST /sandbox/validate` - validation only
    - `POST /sandbox/preview` - validation plus a `share_url` (expires after 24 hours)
    - `GET /sandbox/samples` - sample images, a video and a PDF you may reference

    Send the exact body you would send to `/send` or `/send-batch`. Recipient and sender fields
    (`phoneNumber`, `sendernameId`, `scheduleAt`, ...) are accepted and listed under
    `sandbox.ignored_fields`. Only media from `/sandbox/samples` is rendered; other `https` URLs still
    pass validation but show as a placeholder in the preview. Rate limit: 20 requests per minute and
    100 per day per IP. Sandbox preview ids cannot be used to send.


    ## Base URLs
    - **RCS/SMS endpoints:** `https://api.sendrcs.eu/api/rcs`
    - **Message Log:** `https://api.sendrcs.eu/api/messages`
    - **Contacts API:** `https://api.sendrcs.eu/api/v1/contacts`
    - **Groups API:** `https://api.sendrcs.eu/api/v1/groups`
    - **Templates API:** `https://api.sendrcs.eu/api/v1/templates`

    ## Merge Fields & Unsubscribe Link

    Message text supports `{{merge_field}}` placeholders, replaced per recipient at send time.

    - **RCS** (`/send`, `/send-batch`): full set — contact fields (`{{contact_first_name}}`,
      `{{contact_last_name}}`, `{{contact_phone}}`, `{{contact_email}}`), company fields
      (`{{company_name}}`, `{{company_phone}}`, `{{company_website}}`, …), `{{today}}`,
      custom field tags (`{{your_tag}}`), and `{{unsubscribe_link}}`. Placeholders work in text,
      card titles/descriptions, and button/chip URLs.
    - **Standalone SMS** (`/send-sms`): full set - same contact/company/custom fields, `{{today}}`,
      and `{{unsubscribe_link}}` as RCS. Contact and custom fields require the recipient to be an
      existing contact (or one is auto-created unless `skipContactCreation: true`); unknown
      placeholders are replaced with an empty string.
    - **SMS fallback text** (`smsFallback.message` on RCS sends): same set as the RCS body -
      contact, company and custom fields, `{{today}}` and `{{unsubscribe_link}}`. Substituted
      per recipient at the moment the fallback is sent, whichever reason triggered it. Contact,
      custom and unsubscribe fields need a real contact; without one they resolve to an empty
      string (unsubscribe placeholder stays as written) while company fields still work.

    **`{{unsubscribe_link}}`** is replaced with a unique, per-recipient opt-out URL
    (`https://stoprcs.eu/u/<token>`, or your configured custom unsubscribe URL). A fresh token is
    issued on every send. Because the link is bound to a specific contact, it resolves only when the
    message is sent to a contact — it is **not** replaced when `skipContactCreation: true` is used for
    a phone number that is not already a contact (the placeholder is left as-is). Recommended on
    marketing/promotional messages to provide a compliant opt-out.

  x-logo:
    url: ./images/apple-touch-icon.png
    altText: SendRCS Logo
  contact:
    name: SendRCS Support
    email: support@sendrcs.eu
  license:
    name: Proprietary
    url: https://sendrcs.eu/terms-and-conditions/

servers:
  - url: https://api.sendrcs.eu/api/rcs
    description: Production server

tags:
  - name: Messaging
    description: Send RCS messages to single or multiple recipients
  - name: SMS
    description: |
      Send standalone SMS messages independently of RCS.
      Specify your SMS sender ID and message content. Billed at standard SMS rate per message segment.
  - name: Scheduling
    description: Timezone-aware message scheduling
  - name: Status
    description: Check service and job status
  - name: Validation
    description: Validate messages before sending
  - name: Sandbox
    description: |
      Validate and preview messages without an account. Public, rate limited (20 requests per minute
      and 100 per day per IP), nothing is ever sent. Post the exact body you would send to `/send` and
      get the same validation result plus a phone-mockup preview link that expires after 24 hours.
      Only sample media from `/sandbox/samples` is rendered; other URLs pass validation but show as a
      placeholder. Sandbox preview ids cannot be used to send.
  - name: Sender Management
    description: List and inspect available sender identities (SMS and RCS)
  - name: Conversations
    description: Conversation billing and analytics
  - name: Message Log
    description: |
      Retrieve your message history with filtering and cursor-based pagination.

      **Base URL:** `https://api.sendrcs.eu/api/messages`
  - name: Contacts
    description: |
      Full CRUD for contacts with cursor-based pagination, phone number lookup, search, and bulk operations.
      Includes soft-delete (disable/enable) and group management.

      **Base URL:** `https://api.sendrcs.eu/api/v1/contacts`

      **Required permissions:** `contacts:read` for read operations, `contacts:write` for create/update/delete.
  - name: Custom Fields
    description: |
      Manage custom merge field definitions and per-contact custom field values.
      Custom fields use tags (e.g., `customer_status`) that can be used as merge fields in messages via `{{customer_status}}`.

      **Base URL:** `https://api.sendrcs.eu/api/v1/contacts`

      **Required permissions:** `custom-fields:read` for read operations, `custom-fields:write` for create/update/delete.
  - name: Groups
    description: |
      Manage contact groups with full CRUD and membership operations.
      Groups organize contacts and can be linked to campaigns and message flows.
      Uses bidirectional sync to maintain consistency between Contact.groups and Group.contacts.

      **Base URL:** `https://api.sendrcs.eu/api/v1/groups`

      **Required permissions:** `groups:read` for read operations, `groups:write` for create/update/delete and membership management.
  - name: Templates
    description: |
      Manage message templates for SMS and RCS.
      Templates support categories, contexts (event/flow/general), default designation, and RCS-specific content types.

      **Base URL:** `https://api.sendrcs.eu/api/v1/templates`

      **Required permissions:** `templates:read` for read operations, `templates:write` for create/update/delete.
  - name: Template Groups
    description: |
      Manage template groups — folders for organizing message templates.
      Each template belongs to at most one template group (or is ungrouped).
      Deleting a template group never deletes its templates; they become ungrouped.

      **Template groups are NOT contact groups.** Contact groups (see the Groups tag) organize contacts;
      template groups organize message templates. All template-group fields are explicitly named
      (`templateGroupId`, `templateCount`, `ungroupedTemplateCount`) to avoid any confusion.

      **Base URL:** `https://api.sendrcs.eu/api/v1/templates/groups`

      **Required permissions:** `templates:read` for read operations, `templates:write` for create/update/delete/assign.
  - name: MCP Integration
    description: |
      ## What is MCP?

      MCP (Model Context Protocol) is an open standard that lets AI assistants connect to external tools and services. With the SendRCS MCP integration, you can send RCS messages, check delivery status, manage conversations, and more — all through natural language commands in any MCP-compatible AI client.

      ## Supported AI Clients

      The SendRCS MCP server works with any MCP-compatible client, including:

      - **Claude Desktop** (Anthropic) — full OAuth and API key support
      - **Claude Code** (Anthropic CLI) — API key or OAuth
      - **Cursor** (AI code editor) — supports MCP servers via settings
      - **ChatGPT Desktop** (OpenAI) — MCP support available
      - **Any other MCP-compatible tool** — the protocol is an open standard

      The setup is similar across clients: add the SendRCS MCP server URL and authenticate via OAuth or API key.

      ## Connection Methods

      There are two ways to connect:

      ### Method 1: OAuth 2.0 (Recommended for End Users)

      OAuth lets you connect securely through your browser — no API key copying needed. You log in with your SendRCS email and password (including 2FA), and your AI client receives a secure token that lasts 30 days.

      **Setup (works in Claude Desktop, Cursor, ChatGPT Desktop, etc.):**

      In your AI client's MCP server settings, add a new "Streamable HTTP" server:

      - Name: SendRCS
      - URL: `https://api.sendrcs.eu/api/mcp`

      The client will automatically discover the OAuth configuration via the standard `.well-known/oauth-authorization-server` endpoint and guide you through login in your browser.

      **OAuth flow overview:**
      1. Your AI client opens a browser window to the SendRCS login page
      2. You enter your email and password
      3. You enter your 2FA verification code
      4. After successful login, your client receives an access token (valid 30 days)
      5. All subsequent MCP calls use this token automatically

      **Security features:**
      - PKCE (Proof Key for Code Exchange) prevents token interception
      - 2FA support (email codes or authenticator app)
      - Tokens can be revoked at any time
      - 30-day expiration (re-login required after)

      ### Method 2: API Key (For Developers / Automation)

      You can also connect using an API key from your SendRCS account. This is simpler but less secure (keys don't expire automatically).

      **Setup (API Key):**

      Add this to your AI client's MCP server configuration (Claude Desktop, Cursor, ChatGPT Desktop, etc.):

      ```json
      {
        "mcpServers": {
          "sendrcs": {
            "url": "https://api.sendrcs.eu/api/mcp",
            "headers": {
              "X-API-Key": "rk_your_api_key_here"
            }
          }
        }
      }
      ```

      API keys are found in the SendRCS web app under [Settings > API Keys](https://app.sendrcs.eu/settings/api).

      ### Comparison

      | Feature | OAuth | API Key |
      |---------|-------|---------|
      | Setup | Login via browser | Copy-paste key |
      | Expiration | 30 days | Never (manual revocation) |
      | 2FA | Yes | No |
      | Best for | End users in AI clients | Automated systems, developers |

      ## Endpoint

      - **URL**: `https://api.sendrcs.eu/api/mcp`
      - **Protocol**: Streamable HTTP (JSON-RPC 2.0)

      ## Available Tools (51 tools)

      ### RCS Messaging (4 tools)

      | Tool | Description |
      |------|-------------|
      | `rcs_send` | Send a single RCS message (text, rich card, carousel, media) to a phone number. Supports SMS fallback, timezone-aware scheduling, and merge fields. Requires a `preview_id` from `rcs_preview`. The result reports `success`, `method`, `messageRecordId`, `countrySupported`/`countryError`, `fallbackUsed`/`fallbackReason` and `error`; `SMS_COUNTRY_NOT_SUPPORTED` means neither channel can reach the number and nothing was sent or charged. |
      | `rcs_send_batch` | Send RCS messages to multiple recipients (up to 10,000). Messages are queued for background processing. Returns immediately with queue information. Requires a `preview_id` from `rcs_preview`. |
      | `rcs_validate` | Validate an RCS message structure without sending it. Returns validation errors, warnings, and suggestion limits. |
      | `rcs_validate_schedule_time` | Check if a scheduled send time is valid and in the future. Returns timezone resolution and time-until information. |

      ### SMS Messaging (2 tools)

      | Tool | Description |
      |------|-------------|
      | `sms_preview` | Create a preview of an SMS message before sending. Returns a `share_url` for user review and a `preview_id` required by `sms_send`. Idempotent: calling with the same payload returns the same preview. SMS previews cannot be edited — only approved or rejected. |
      | `sms_send` | Send a single SMS message. Requires a valid `preview_id` from `sms_preview`. If the user has approval enabled (the default), the preview must also be approved via the preview page. Supports the full merge field set (`{{contact_first_name}}`, `{{company_name}}`, custom tags, `{{today}}`, `{{unsubscribe_link}}`) and timezone-aware scheduling via `scheduleAt` and `timeZone`. Reports SMS segment count and encoding (GSM-7 or Unicode). With a connected Shopify store, links to that store become short tracked links at send time (clicks and purchases show up in statistics). |

      ### Preview & Approval (1 tool)

      | Tool | Description |
      |------|-------------|
      | `rcs_preview` | Create a visual preview of an RCS message before sending. Supports two modes: (1) provide `messageType` + `content` directly, or (2) provide `templateId` to load content from a saved RCS template. Returns a `preview_id` (required by `rcs_send` / `rcs_send_batch`), a `share_url` (preview page link), and `approval_required` (whether you must approve before sending). See "Preview & Approval Workflow" below. The SMS equivalent is `sms_preview`, listed under SMS Messaging. |

      ### Sender Management (1 tool)

      | Tool | Description |
      |------|-------------|
      | `list_sendernames` | List all available SMS and RCS sender names, their status (APPROVED, PENDING, REJECTED, DRAFT), and capabilities. Use the returned `id` as `sendernameId` when sending. If `isTest=true`, sender can only send free test messages to numbers in `testPhones`. |

      ### Status & Configuration (4 tools)

      | Tool | Description |
      |------|-------------|
      | `rcs_health` | Quick health check for the RCS service. Returns healthy or down. |
      | `rcs_limits` | Get all RCS message limits and constraints. Use this BEFORE composing a message to know character limits, max cards, media sizes, and suggestion limits. |
      | `rcs_button_guidelines` | Get guidelines for using card actions vs chip suggestions in RCS messages, including best practices, limits, and examples. |
      | `rcs_timezones` | Get available timezones for scheduling, including the user's configured timezone and current local times. |

      ### Conversations (1 tool)

      | Tool | Description |
      |------|-------------|
      | `rcs_conversation_status` | Check if a phone number has an active RCS session. If `hasActiveConversation` is true, follow-up messages within the 24-hour window are free (no credit charge). |

      ### Queue & Jobs (3 tools)

      | Tool | Description |
      |------|-------------|
      | `rcs_batch_status` | Get the status and progress of a specific batch RCS sending job. |
      | `rcs_batch_cancel` | Cancel a batch RCS sending job. Only cancels messages that have not yet been sent. |
      | `rcs_jobs` | List all RCS jobs (scheduled, processing, completed, failed, cancelled) with filtering and pagination. |

      ### Interactions & Analytics (3 tools)

      | Tool | Description |
      |------|-------------|
      | `list_rcs_interactions` | Campaign engagement: button clicks, user replies. Filter by campaign, phone, interaction type (`button_click`, `reply_received`), and date range. Each result includes a `messageId` for looking up the full message. |
      | `rcs_message_log` | Query message history with filtering. Look up a single message by ID, or filter by date range (defaults to last 7 days), phone number, campaign, status, direction, and message type. Failed messages include the failure reason (`error`). Max 31-day range, max 200 results. |
      | `rcs_message_content` | Get the full, reusable content of a single message by its ID — to resend or adapt a previous RCS/SMS. Returns the structured RCS `messageType` + `content` (ready for `rcs_preview`) and any SMS fallback, or the SMS text (ready for `sms_preview`). |

      ### Contacts (9 tools)

      | Tool | Description |
      |------|-------------|
      | `list_contacts` | List contacts with cursor-based pagination, search (partial match on name/phone/email), exact phone lookup (country code required, `+`/`00` prefix accepted), filtering by disabled status or group, and sorting. Supports `includeCustomFields` to return custom field values. |
      | `count_contacts` | Count contacts with optional filters (disabled status, group, search). Returns just the count without loading contact data. |
      | `get_contact` | Get a single contact by ID or by phone number with country code (`45...`, `+45...` or `0045...`). Always includes group membership and custom field values. |
      | `create_contact` | Create a new contact. Phone number is required and must be unique per user (6-20 digits with country code, optional `+`/`00` prefix - stored without the prefix). Can assign to groups and set custom field values in the same call. Numbers whose country is outside the account's price list are refused (`COUNTRY_NOT_SUPPORTED`) and never stored. |
      | `update_contact` | Update an existing contact (partial update). Can change name, phone, email, disabled status, group memberships, and custom field values. |
      | `delete_contact` | Permanently delete a contact with full cascading cleanup: cancels scheduled messages, unenrolls from flows, removes from events and groups, deletes custom field values. |
      | `bulk_create_contacts` | Create or upsert up to 1,000 contacts at once. In upsert mode (`upsert: true`), existing contacts matched by phone are updated. Supports custom fields (by fieldId or tag) and group assignment. Numbers whose country is outside the account's price list are never stored; they are counted in `failed` and listed in `errors` with the reason. |
      | `bulk_disable_contacts` | Soft-disable multiple contacts by ID. Cancels their scheduled messages and deactivates flow enrollments. Disabled contacts are skipped when sending RCS messages. |
      | `bulk_enable_contacts` | Re-enable multiple previously disabled contacts. |

      ### Groups (7 tools)

      | Tool | Description |
      |------|-------------|
      | `list_groups` | List all contact groups with their contact counts. Returns a lightweight summary sorted by newest first. |
      | `get_group` | Get a single group by ID. Optionally includes a paginated list of contacts in the group (id, name, phone, email). |
      | `create_group` | Create a new contact group with a name and optional description. Groups are used to organize contacts and can be linked to campaigns and message flows. |
      | `update_group` | Update a group's name and/or description. Does not modify group membership — use `add_contacts_to_group` / `remove_contacts_from_group` for that. |
      | `delete_group` | Delete a group. Does NOT delete the contacts — only removes the group and clears group references from all contacts that belonged to it. |
      | `add_contacts_to_group` | Add contacts to a group with bidirectional sync. Automatically cascades to linked campaigns/flows. Duplicates are silently ignored. |
      | `remove_contacts_from_group` | Remove contacts from a group with bidirectional sync. Automatically cascades removal from linked campaigns/flows. Contacts themselves are NOT deleted. |

      ### Custom Fields (4 tools)

      | Tool | Description |
      |------|-------------|
      | `list_custom_fields` | List all custom merge field definitions. Tags become `{{tag}}` merge field placeholders in RCS messages. |
      | `create_custom_field` | Create a new custom field definition with a name and tag. Tag must be unique (letters, numbers, underscores only) and is used as `{{tag}}` in messages. |
      | `delete_custom_field` | Delete a custom field definition. If the field is in use by contacts, reports usage count and requires `force: true` to proceed. |
      | `set_contact_custom_fields` | Set or delete custom field values on a specific contact. Identify fields by fieldId or tag. Pass `null` as value to delete a field value. |

      ### Templates (5 tools)

      | Tool | Description |
      |------|-------------|
      | `list_templates` | List message templates with page-based pagination. Filter by type (SMS/RCS), RCS message subtype, template group, context, and name search. RCS templates include `parsedContent` with the structured message content. |
      | `get_template` | Get a single template by ID. RCS templates include `parsedContent` with the structured message content (same format as `rcs_send` content). |
      | `create_template` | Create a new message template. For RCS: pass content as an object (same format as `rcs_send`), the tool handles JSON serialization. Requires `rcsMessageType` for RCS templates. |
      | `update_template` | Partial update of an existing template (name, content, template group, context, isDefault, etc.). Same RCS content handling as `create_template`. |
      | `delete_template` | Permanently delete a message template. |

      ### Template Groups (5 tools)

      | Tool | Description |
      |------|-------------|
      | `list_template_groups` | List all template groups (folders for message templates) with template counts, plus the count of ungrouped templates. |
      | `create_template_group` | Create a new template group (a folder for message templates). Group names must be unique. |
      | `update_template_group` | Rename a template group (folder for message templates). |
      | `delete_template_group` | Delete a template group (folder for message templates). Templates inside the group are NOT deleted — they become ungrouped. |
      | `assign_templates_to_template_group` | Move one or more templates into a template group (folder), or remove them from any group by passing `templateGroupId: null`. Each template belongs to at most one template group. |

      ### Guides & Reference (2 tools)

      | Tool | Description |
      |------|-------------|
      | `rcs_workflow_guide` | Get recommended workflows and step-by-step guides for common RCS and SMS messaging tasks, including contact, group, and template management. Call this first to understand how to use the available tools together. Optional `task` parameter (send, batch, schedule, sms, template, engagement, contacts, all) returns only that section. |
      | `rcs_api_reference` | Authoritative reference for the SendRCS REST HTTP API, compiled from the published OpenAPI spec so it cannot drift from these docs. Call with no arguments for the complete index of every REST endpoint that exists, grouped by tag with the correct base URL for each. Pass `endpoint` (e.g. `POST /send`) for one operation's full detail: request-body fields, types, required flags, enum values, an example request and the response shape. Pass `query` to search by description, `schema` for an object shape, or `section` for a documentation topic. An endpoint missing from the index does not exist. Documents the REST API, not the MCP tools on this server. |

      ## Message Types

      ### RCS
      - **text** — Rich text with optional suggestion chips (reply/action buttons)
      - **textBasic** — Plain text (GSM-7 compatible, wider device support)
      - **richCard** — Card with title, description, image/video, and up to 4 action buttons
      - **carousel** — Multiple swipeable cards (up to 10 cards)
      - **media** / **image** / **video** / **audio** / **file** — Media with optional caption

      ### SMS
      - **sms** — Plain text SMS message (max 1530 characters). Messages over 160 characters are split into multiple segments. Encoding is auto-detected as GSM-7 (standard) or Unicode (when special characters are used).

      ## Key Limits

      ### RCS Limits

      | Limit | Value |
      |-------|-------|
      | Text | max 3072 characters |
      | Rich card title | max 200 characters |
      | Rich card description | max 2000 characters |
      | Card actions (buttons ON the card) | max 4, max 25 characters each |
      | Suggestion chips (below the card) | max 11, max 25 characters each |
      | Carousel | max 10 cards |
      | Batch | max 10,000 recipients |
      | Media | max 100 MB |
      | Campaign name | max 100 characters |

      ### SMS Limits

      | Limit | Value |
      |-------|-------|
      | SMS message | max 1530 characters |
      | Single segment (GSM-7) | 160 characters |
      | Single segment (Unicode) | 70 characters |
      | Multi-part segment (GSM-7) | 153 characters per segment |
      | Multi-part segment (Unicode) | 67 characters per segment |

      ## Scheduling

      Messages can be scheduled for future delivery using `scheduleAt` (ISO 8601 datetime) and `timeZone` (IANA format, e.g., `Europe/Copenhagen`). If no timezone is given, the user's configured timezone applies.

      ## SMS Fallback

      Any RCS message can include `smsFallback` for devices without RCS support:
      - `message` — SMS text (max 1530 characters)
      - `sendernameId` — ID of an approved SMS sender

      ## Conversation-Based Billing

      Some senders use conversation-based billing: the first message opens a 24-hour window, and all subsequent messages within that window are free. Use `rcs_conversation_status` to check if a phone number has an active session before sending follow-ups.

      ## Preview & Approval Workflow

      Every message sent via MCP — both RCS and SMS — must go through a preview step. The flow is: **validate → preview → (approve if required) → send**.

      ### How it works (RCS)

      1. **`rcs_validate`** — validates the message structure. Free, catches errors before creating a preview.
      2. **`rcs_preview`** — creates a visual preview page (phone mockup showing exactly how the message will look). Returns:
         - `preview_id` — required when calling `rcs_send` or `rcs_send_batch`.
         - `share_url` — a link to the preview page where you can review the message.
         - `approval_required` — whether you must approve the preview before sending.
      3. **Approval** — if `approval_required` is `true`, you must open the `share_url` and click **"Approve for sending"** on the preview page. Until you do, `rcs_send` / `rcs_send_batch` will be rejected.
      4. **Send** — call `rcs_send` or `rcs_send_batch` with the `preview_id`. The server verifies the payload matches the preview and checks approval status.

      ### How it works (SMS)

      1. **`sms_preview`** — creates a preview of the SMS message. Returns the same fields as `rcs_preview`: `preview_id`, `share_url`, and `approval_required`. SMS previews cannot be edited on the preview page — only approved or rejected.
      2. **Approval** — same as RCS. If `approval_required` is `true`, approve on the preview page before sending.
      3. **Send** — call `sms_send` with the `preview_id`. Supports `scheduleAt` and `timeZone` for scheduled delivery. The response includes `smsSegments` (number of SMS segments) and `smsEncoding` (GSM-7 or Unicode). Merge fields (`{{contact_first_name}}`, `{{company_name}}`, custom tags, `{{today}}`, `{{unsubscribe_link}}`) are replaced per recipient at send time - the billed segment count reflects the resolved text.

      ### Approval setting

      The **"Approve before sending"** setting is a checkbox in the web app under **Settings → API & MCP → MCP Settings** ([app.sendrcs.eu/settings/api](https://app.sendrcs.eu/settings/api)).

      - **Enabled (default, recommended)**: you must approve every message on the preview page before the AI can send it. This is a safety measure to prevent accidental sends.
      - **Disabled**: the AI can send messages immediately after creating a preview — no manual approval needed. The preview step and payload hash guard are still enforced (the AI cannot skip the preview).

      ### Save as template

      On the preview page, you can also click **"Save as template"** to save the previewed message as a reusable template. This lets you compose a message with AI assistance, preview it, and save it for later use in the web interface (campaigns, send-message, etc.) — without sending it immediately.

      When saving as template, you provide:
      - **Template name** (required) — e.g. "Welcome message"
      - **Template group** (optional) — the folder to save it in

      The preview page also shows the SMS fallback text (if configured) in a collapsible section below the phone mockup.

      ## Troubleshooting

      | Problem | Solution |
      |---------|----------|
      | "Authentication required" | Token expired (OAuth: re-login required after 30 days) or API key invalid |
      | "Sender not approved" | The sendername must have APPROVED status before it can send messages. Use `list_sendernames` to check. |
      | Rich card not delivering | The `media.height` field is required (TALL, MEDIUM, or SHORT). The system defaults to TALL if omitted. |
      | Batch not processing | Check `rcs_batch_status` with the job ID for progress, or use `rcs_jobs` to list all jobs and their statuses. |

      ## Example Use Cases

      You can give natural-language instructions like:
      - "Send an RCS message to +4512345678 saying 'Your order is ready for pickup'"
      - "Send a rich card with a photo and two buttons to my Danish customers"
      - "Send an SMS to +4512345678 saying 'Your appointment is confirmed for tomorrow at 10am'"
      - "Schedule a reminder for tomorrow at 9am Copenhagen time"
      - "Show me which senders I have and which are approved"
      - "What's the status of my batch job?"
      - "Show me click analytics for my 'Summer Campaign'"
      - "Show me all failed messages from the last week"
      - "What message did this person click on?"
      - "Check if sending to +4587654321 will cost credits or is within the free conversation window"

  - name: Webhooks
    description: |
      Button click, incoming RCS message, incoming SMS message, delivery status,
      and form submission callbacks.
      Configure your webhook URLs in [Settings > Webhooks](https://app.sendrcs.eu/settings/webhooks) after logging in.

      ## Verifying webhook signatures (optional)

      Every outbound webhook request includes an `X-RCS-Signature` header so you
      can verify it genuinely came from SendRCS and hasn't been tampered with.
      **Signature verification is optional** — if you don't need it, you can
      safely ignore the header and process the JSON body directly. It's
      recommended for production integrations handling sensitive actions.

      **Header format (Stripe-style):**

      ```
      X-RCS-Signature: t=<unix-seconds>,v1=<hex-hmac>
      ```

      **To verify:**

      1. Retrieve your per-webhook signing secret: go to
         [Settings > Webhooks](https://app.sendrcs.eu/settings/webhooks), open
         **Edit** on the webhook you want to verify, switch to its **Settings**
         tab and click **Reveal signing secret**. The full secret is also shown
         once when you create the webhook.
      2. Parse `t` and `v1` from the header.
      3. Compute `HMAC_SHA256(secret, t + "." + rawBody)` where `rawBody` is the
         exact bytes of the request body (do not re-serialize the parsed JSON —
         character escape drift will break the signature).
      4. Compare the hex result to the `v1=` portion using a constant-time
         comparison.

      If the signature matches, the request came from SendRCS and the body was
      not tampered with. That is all you need for verification.

      **Optional extra layer, replay protection:** the `t` timestamp is signed
      along with the body, so you can also reject requests where `|now - t|`
      exceeds a window of your choosing, for example 300 seconds. This stops a
      captured request from being replayed later. It is not required, and
      it needs your server clock to be reasonably accurate.

      **Node.js example (with the optional replay check):**

      ```js
      const crypto = require('crypto');

      function verifyWebhook(rawBody, header, secret) {
        const parts = Object.fromEntries(
          header.split(',').map(p => p.split('='))
        );
        const t = parts.t;
        const v1 = parts.v1;
        if (!t || !v1) return false;

        // Optional: reject stale requests (replay protection).
        // Remove this block if you only want signature verification.
        if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;

        const expected = crypto
          .createHmac('sha256', secret)
          .update(`${t}.${rawBody}`)
          .digest('hex');

        const a = Buffer.from(expected, 'hex');
        const b = Buffer.from(v1, 'hex');
        return a.length === b.length && crypto.timingSafeEqual(a, b);
      }
      ```

      Each webhook in your account has its own independent signing secret.

security:
  - ApiKeyAuth: []

paths:
  /send:
    post:
      tags:
        - Messaging
      summary: Send single RCS message
      description: |
        Send an RCS message to a single recipient. Supports all message types
        (text, richCard, carousel, media) with optional SMS fallback and scheduling.

        **New Features:**
        - `skipContactCreation`: Send without creating a contact record
        - `skipDisabled`: Skip sending to disabled contacts (default: true)
        - Test phone detection (no charge for test sender phones)
        - Interactive buttons with webhook callbacks
      operationId: sendMessage
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SendMessageRequest'
            examples:
              richCard:
                summary: Rich card with image and buttons
                value:
                  phoneNumber: "+4512345678"
                  messageType: "richCard"
                  sendernameId: "60f1b2c3d4e5f6a7b8c9d0e1"
                  content:
                    title: "Special Offer - 50% Off!"
                    description: "Limited time offer on premium products."
                    cardOrientation: "VERTICAL"
                    media:
                      height: "MEDIUM"
                      contentInfo:
                        fileUrl: "https://sendrcs.eu/images/rcsexamples/tech.jpg"
                      thumbnailUrl: "https://sendrcs.eu/images/rcsexamples/tech.jpg"
                    cardActions:
                      - text: "Shop Now"
                        type: "openUrl"
                        openUrlAction:
                          url: "https://shop.example.com"
                      - text: "Get Details"
                        type: "postback"
                        postbackData: "offer_details"
                        webhookUrl: "https://yourapp.com/webhooks/rcs"
                  smsFallback:
                    message: "50% Off! Shop: shop.example.com"
                    sendernameId: "60f1b2c3d4e5f6a7b8c9d0e2"
              richCardWithUnsubscribe:
                summary: Rich card with unsubscribe button (marketing)
                value:
                  phoneNumber: "+4512345678"
                  messageType: "richCard"
                  sendernameId: "60f1b2c3d4e5f6a7b8c9d0e1"
                  content:
                    title: "Special Offer - 50% Off!"
                    description: "Limited time offer on premium products."
                    cardOrientation: "VERTICAL"
                    media:
                      height: "MEDIUM"
                      contentInfo:
                        fileUrl: "https://sendrcs.eu/images/rcsexamples/tech.jpg"
                      thumbnailUrl: "https://sendrcs.eu/images/rcsexamples/tech.jpg"
                    cardActions:
                      - text: "Shop Now"
                        type: "openUrl"
                        openUrlAction:
                          url: "https://shop.example.com"
                      # Unsubscribe button: type MUST be "unsubscribeLink" (not "openUrl"),
                      # and the URL MUST be exactly {{unsubscribe_link}} — resolved per recipient at send time.
                      - text: "Unsubscribe"
                        type: "unsubscribeLink"
                        openUrlAction:
                          url: "{{unsubscribe_link}}"
              textMessage:
                summary: Text message with reply buttons
                value:
                  phoneNumber: "+4512345678"
                  messageType: "text"
                  sendernameId: "60f1b2c3d4e5f6a7b8c9d0e1"
                  content:
                    text: "Hello! How can we help you today?"
                    suggestions:
                      - reply:
                          text: "Book Appointment"
                          postbackData: "book_appointment"
                          webhookUrl: "https://yourapp.com/webhooks/rcs"
                      - reply:
                          text: "Get Support"
                          postbackData: "get_support"
                          webhookUrl: "https://yourapp.com/webhooks/rcs"
                      - action:
                          text: "Call Us"
                          type: "dial"
                          dialAction:
                            phoneNumber: "+4512345678"                  
              textWithUnsubscribe:
                summary: Text message with unsubscribe link inline (marketing)
                value:
                  phoneNumber: "+4512345678"
                  messageType: "text"
                  sendernameId: "60f1b2c3d4e5f6a7b8c9d0e1"
                  content:
                    text: "Summer sale! 20% off all items this weekend only.\n\nTo opt out: {{unsubscribe_link}}"
              carousel:
                summary: Carousel with multiple cards
                value:
                  phoneNumber: "+4512345678"
                  messageType: "carousel"
                  sendernameId: "60f1b2c3d4e5f6a7b8c9d0e1"
                  content:
                    cardContents:
                      - title: "Product 1"
                        description: "Description of product 1"
                        media:
                          height: "MEDIUM"
                          contentInfo:
                            fileUrl: "https://sendrcs.eu/images/rcsexamples/tech-1.jpg"
                        cardActions:
                          - text: "Buy Now"
                            type: "openUrl"
                            openUrlAction:
                              url: "https://shop.example.com/product1"
                      - title: "Product 2"
                        description: "Description of product 2"
                        media:
                          height: "MEDIUM"
                          contentInfo:
                            fileUrl: "https://sendrcs.eu/images/rcsexamples/tech-2.jpg"
                        cardActions:
                          - text: "Buy Now"
                            type: "openUrl"
                            openUrlAction:
                              url: "https://shop.example.com/product2"
                    suggestions:
                      - reply:
                          text: "View All"
                          postbackData: "view_all"
                          webhookUrl: "https://yourapp.com/webhooks/rcs"
              scheduledMessage:
                summary: Scheduled message with timezone
                value:
                  phoneNumber: "+4512345678"
                  messageType: "text"
                  sendernameId: "60f1b2c3d4e5f6a7b8c9d0e1"
                  content:
                    text: "Your appointment reminder for tomorrow at 10:00 AM"
                  scheduleAt: "2024-02-15T09:00:00"
                  timeZone: "Europe/Copenhagen"
              skipContactCreation:
                summary: Send without creating contact
                value:
                  phoneNumber: "+4512345678"
                  messageType: "text"
                  sendernameId: "60f1b2c3d4e5f6a7b8c9d0e1"
                  content:
                    text: "One-time notification message"
                  skipContactCreation: true
              fileMessage:
                summary: Send a file (PDF, document, etc.)
                value:
                  phoneNumber: "+4512345678"
                  messageType: "file"
                  sendernameId: "60f1b2c3d4e5f6a7b8c9d0e1"
                  content:
                    media:
                      contentInfo:
                        fileUrl: "https://example.com/document.pdf"
              imageMessage:
                summary: Send an image
                value:
                  phoneNumber: "+4512345678"
                  messageType: "image"
                  sendernameId: "60f1b2c3d4e5f6a7b8c9d0e1"
                  content:
                    url: "https://sendrcs.eu/images/rcsexamples/tech.jpg"
              imageWithCaption:
                summary: Send an image with caption (sent as rich card)
                value:
                  phoneNumber: "+4512345678"
                  messageType: "media"
                  sendernameId: "60f1b2c3d4e5f6a7b8c9d0e1"
                  content:
                    url: "https://sendrcs.eu/images/rcsexamples/tech.jpg"
                    caption: "Your travel documents for Barcelona are ready!"
              videoMessage:
                summary: Send a video
                value:
                  phoneNumber: "+4512345678"
                  messageType: "video"
                  sendernameId: "60f1b2c3d4e5f6a7b8c9d0e1"
                  content:
                    media:
                      contentInfo:
                        fileUrl: "https://example.com/video.mp4"
              audioMessage:
                summary: Send an audio file
                value:
                  phoneNumber: "+4512345678"
                  messageType: "audio"
                  sendernameId: "60f1b2c3d4e5f6a7b8c9d0e1"
                  content:
                    media:
                      contentInfo:
                        fileUrl: "https://example.com/audio.mp3"
      responses:
        '200':
          description: Message sent or scheduled successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SendMessageResponse'
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Unauthorized
        '500':
          description: Server error

  /send-batch:
    post:
      tags:
        - Messaging
      summary: Send batch RCS messages
      description: |
        Send the same RCS message to multiple recipients (up to 10,000).

        **Features:**
        - Automatic disabled contact filtering
        - Test phone detection (no charge)
        - Bulk contact lookup optimization
        - Progress tracking via job ID
      operationId: sendBatch
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SendBatchRequest'
            examples:
              basicBatch:
                summary: Basic batch send
                value:
                  phoneNumbers:
                    - "+4512345678"
                    - "+4587654321"
                    - "+4511223344"
                  messageType: "text"
                  sendernameId: "60f1b2c3d4e5f6a7b8c9d0e1"
                  content:
                    text: "Hello! This is a batch message."
                  campaignName: "February Campaign"
              batchWithFallback:
                summary: Batch with SMS fallback
                value:
                  phoneNumbers:
                    - "+4512345678"
                    - "+4587654321"
                  messageType: "richCard"
                  sendernameId: "60f1b2c3d4e5f6a7b8c9d0e1"
                  content:
                    title: "Special Offer"
                    description: "Limited time discount"
                    media:
                      height: "MEDIUM"
                      contentInfo:
                        fileUrl: "https://sendrcs.eu/images/rcsexamples/tech-3.jpg"
                    cardActions:
                      - text: "Shop Now"
                        type: "openUrl"
                        openUrlAction:
                          url: "https://shop.example.com"
                  smsFallback:
                    message: "Special offer! Visit shop.example.com"
                    sendernameId: "60f1b2c3d4e5f6a7b8c9d0e2"
                  skipDisabled: true
                  batchSize: 100
                  delayMs: 50
      responses:
        '200':
          description: Batch scheduled for later delivery (returned when scheduleAt is provided)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SendBatchScheduledResponse'
        '202':
          description: Batch queued for immediate background processing
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SendBatchResponse'
        '400':
          description: Validation error
        '401':
          description: Unauthorized

  /send-sms:
    post:
      tags:
        - SMS
      summary: Send standalone SMS message
      description: |
        Send an SMS message to a single recipient, independently of RCS.
        Specify your SMS sender ID and message content. Billed at standard SMS rate per message segment.

        **Features:**
        - Standalone SMS — no RCS sending or fallback involved
        - Full merge field support: `{{contact_first_name}}`, `{{company_name}}`, custom field tags, `{{today}}`, and `{{unsubscribe_link}}` - replaced per recipient before segments are calculated
        - Tracked store links: when a Shopify store is connected, links to that store are replaced by short tracked links at send time, so clicks and purchases show up in statistics. The billed segment count reflects the final text. Requires a contact (not with `skipContactCreation`); can be turned off on the Shopify settings page.
        - Timezone-aware scheduling
        - `skipContactCreation`: Send without creating a contact record
        - `skipDisabled`: Skip sending to disabled contacts (default: true)
      operationId: sendSms
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SendSmsRequest'
            examples:
              basicSms:
                summary: Basic SMS message
                value:
                  phoneNumber: "+4512345678"
                  message: "Hello! How can we help you today? Visit our website or call +4593700401"
                  sendernameId: "60f1b2c3d4e5f6a7b8c9d0e2"
                  campaignName: "My SMS Campaign"
              marketingSms:
                summary: Marketing SMS with unsubscribe link
                value:
                  phoneNumber: "+4512345678"
                  message: "Summer sale! 20% off all items this weekend only. Unsubscribe: {{unsubscribe_link}}"
                  sendernameId: "60f1b2c3d4e5f6a7b8c9d0e2"
                  campaignName: "Summer Sale"
              personalizedSms:
                summary: Personalized SMS with merge fields
                value:
                  phoneNumber: "+4512345678"
                  message: "Hi {{contact_first_name}}! Your order from {{company_name}} is ready for pickup."
                  sendernameId: "60f1b2c3d4e5f6a7b8c9d0e2"
                  campaignName: "Order Ready"
              smsNoContact:
                summary: SMS without creating contact
                value:
                  phoneNumber: "+4512345678"
                  message: "Your verification code is 123456"
                  sendernameId: "60f1b2c3d4e5f6a7b8c9d0e2"
                  skipContactCreation: true
              scheduledSms:
                summary: Scheduled SMS with timezone
                value:
                  phoneNumber: "+4512345678"
                  message: "Reminder: Your appointment is tomorrow at 10:00 AM"
                  sendernameId: "60f1b2c3d4e5f6a7b8c9d0e2"
                  campaignName: "Appointment Reminders"
                  scheduleAt: "2026-06-15T09:00:00"
                  timeZone: "Europe/Copenhagen"
      responses:
        '200':
          description: SMS sent or scheduled successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SendSmsResponse'
              examples:
                immediateSend:
                  summary: Immediate send (queued)
                  value:
                    success: true
                    messageId: "42"
                    scheduled: false
                    queued: true
                    smsSegments: 1
                    smsEncoding: "GSM-7"
                    creditsEstimated: 1
                scheduledSend:
                  summary: Scheduled send
                  value:
                    success: true
                    scheduled: true
                    jobId: "sms_1718445600_a1b2c3"
                    messageId: "507f1f77bcf86cd799439011"
                    scheduleTime: "2026-06-15T07:00:00.000Z"
                    localScheduleTime: "2026-06-15T09:00:00+02:00"
                    effectiveTimezone: "Europe/Copenhagen"
                    timezoneSource: "request"
                    smsSegments: 1
                    smsEncoding: "GSM-7"
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Unauthorized
        '500':
          description: Server error

  /timezones:
    get:
      tags:
        - Scheduling
      summary: Get available timezones
      description: Returns list of supported timezones with current times and user's configured timezone.
      operationId: getTimezones
      responses:
        '200':
          description: Timezone information
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TimezonesResponse'

  /validate-schedule-time:
    post:
      tags:
        - Scheduling
      summary: Validate schedule time
      description: Validate a schedule time before sending. Returns parsed UTC time and timezone information.
      operationId: validateScheduleTime
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - scheduleAt
              properties:
                scheduleAt:
                  type: string
                  description: Schedule time (ISO 8601 format)
                  example: "2024-02-15T14:00:00"
                timeZone:
                  type: string
                  description: IANA timezone
                  example: "Europe/Copenhagen"
      responses:
        '200':
          description: Validation result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidateScheduleResponse'

  /status:
    get:
      tags:
        - Status
      summary: Get RCS service status
      description: |
        Returns current status of the RCS messaging service and validation limits
        for message content (text lengths, card limits, supported media formats, etc.).
      operationId: getStatus
      responses:
        '200':
          description: Service status and validation limits
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum: [healthy, down]
                    description: Overall service status
                  validationLimits:
                    type: object
                    description: Content validation limits for building RCS messages
                    properties:
                      cardActions:
                        type: object
                        properties:
                          maxPerCard:
                            type: integer
                            example: 4
                          textMaxLength:
                            type: integer
                            example: 25
                      chipSuggestions:
                        type: object
                        properties:
                          total:
                            type: integer
                            example: 11
                            description: Max 11 chips total (any mix of reply + action)
                          textMaxLength:
                            type: integer
                            example: 25
                      postbackData:
                        type: object
                        properties:
                          maxLength:
                            type: integer
                            example: 2048
                      text:
                        type: object
                        properties:
                          maxLength:
                            type: integer
                            example: 3072
                      richCard:
                        type: object
                        properties:
                          titleMaxLength:
                            type: integer
                            example: 200
                          descriptionMaxLength:
                            type: integer
                            example: 2000
                      carousel:
                        type: object
                        properties:
                          minCards:
                            type: integer
                            example: 2
                          maxCards:
                            type: integer
                            example: 10
                      media:
                        type: object
                        properties:
                          maxFileSizeBytes:
                            type: integer
                            example: 104857600
                          supportedFormats:
                            type: array
                            items:
                              type: string
                            example: ["image/jpeg", "image/png", "video/mp4"]
                  timestamp:
                    type: string
                    format: date-time

  /health:
    get:
      tags:
        - Status
      summary: Health check
      description: |
        Simple health check endpoint for monitoring services like statuspage.io.
        Returns HTTP 200 when healthy, HTTP 503 when down.
      operationId: healthCheck
      responses:
        '200':
          description: Service is healthy
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum: [healthy, down]
                  timestamp:
                    type: string
                    format: date-time
        '503':
          description: Service is down
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum: [down]
                  timestamp:
                    type: string
                    format: date-time

  /senders:
    get:
      tags:
        - Sender Management
      summary: List sender names
      description: |
        List all SMS and RCS sender names available to the authenticated user.
        Use the returned `id` as `sendernameId` when calling `/send`, `/send-batch`, or `/send-sms`.

        Status meanings:
        - **APPROVED** — ready to send. If `isTest` is true, can only send free test messages to numbers in `testPhones`.
        - **PENDING** — waiting for approval, cannot send yet.
        - **REJECTED** — not approved. Check `rejectionReason`.
        - **DRAFT** — not submitted yet.
      operationId: listSenders
      parameters:
        - name: type
          in: query
          required: false
          schema:
            type: string
            enum: [SMS, RCS]
          description: Filter by sender type. Omit to list all.
      responses:
        '200':
          description: List of sender names
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  count:
                    type: integer
                  sendernames:
                    type: array
                    items:
                      $ref: '#/components/schemas/Sendername'
              example:
                success: true
                count: 2
                sendernames:
                  - id: "60f1b2c3d4e5f6a7b8c9d0e1"
                    name: "MyBrand RCS"
                    type: "RCS"
                    status: "APPROVED"
                    isDefault: true
                    isTest: false
                    supportsConversationBilling: true
                  - id: "60f1b2c3d4e5f6a7b8c9d0e2"
                    name: "MyBrand SMS"
                    type: "SMS"
                    status: "APPROVED"
                    isDefault: true
                    isTest: false
                    supportsConversationBilling: false

  /queue/status:
    get:
      tags:
        - Status
      summary: Get queue status
      description: Returns current message queue status including pending jobs.
      operationId: getQueueStatus
      responses:
        '200':
          description: Queue status

  /batch/{jobId}/status:
    get:
      tags:
        - Status
      summary: Get batch job status
      description: Returns detailed status of a batch sending job.
      operationId: getBatchStatus
      parameters:
        - name: jobId
          in: path
          required: true
          schema:
            type: string
          description: Batch job ID
      responses:
        '200':
          description: Job status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BatchJobStatus'
        '404':
          description: Job not found

  /batch/{jobId}:
    delete:
      tags:
        - Status
      summary: Cancel batch job
      description: Cancel a pending or in-progress batch job.
      operationId: cancelBatch
      parameters:
        - name: jobId
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Job cancelled
        '404':
          description: Job not found

  /jobs:
    get:
      tags:
        - Status
      summary: List jobs
      description: Returns list of batch and scheduled RCS jobs with filtering and pagination.
      operationId: listJobs
      parameters:
        - name: status
          in: query
          schema:
            type: string
            enum: [SCHEDULED, PROCESSING, COMPLETED, FAILED, CANCELLED]
          description: Filter by job status
        - name: type
          in: query
          schema:
            type: string
            enum: [RCS_MESSAGE, RCS_BATCH]
          description: Filter by job type
        - name: page
          in: query
          schema:
            type: integer
            minimum: 1
            default: 1
          description: Page number
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
          description: Results per page
        - name: startDate
          in: query
          schema:
            type: string
            format: date-time
          description: Filter jobs created after this date (ISO 8601)
        - name: endDate
          in: query
          schema:
            type: string
            format: date-time
          description: Filter jobs created before this date (ISO 8601)
      responses:
        '200':
          description: List of jobs
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  jobs:
                    type: array
                    items:
                      type: object
                      properties:
                        jobId:
                          type: string
                        type:
                          type: string
                          enum: [RCS_MESSAGE, RCS_BATCH]
                        status:
                          type: string
                          enum: [SCHEDULED, PROCESSING, COMPLETED, FAILED, CANCELLED]
                        totalItems:
                          type: integer
                          description: Total recipients in the job
                        processedItems:
                          type: integer
                          description: Number of recipients processed so far
                        progress:
                          type: number
                          description: Progress percentage (0-100)
                        results:
                          type: object
                          nullable: true
                          description: Job results summary (when completed)
                        createdAt:
                          type: string
                          format: date-time
                        targetDate:
                          type: string
                          format: date-time
                          nullable: true
                          description: Scheduled send time (null if sent immediately)
                        completedAt:
                          type: string
                          format: date-time
                          nullable: true
                        error:
                          type: string
                          nullable: true
                  pagination:
                    type: object
                    properties:
                      page:
                        type: integer
                      limit:
                        type: integer
                      total:
                        type: integer
                      pages:
                        type: integer

  /stats:
    get:
      tags:
        - Status
      summary: Get messaging statistics
      description: Returns RCS messaging statistics for the authenticated user, filtered by time period.
      operationId: getStats
      parameters:
        - name: period
          in: query
          schema:
            type: string
            enum: [today, week, month, year]
            default: month
          description: Predefined time period
        - name: startDate
          in: query
          schema:
            type: string
            format: date-time
          description: Custom start date (overrides period)
        - name: endDate
          in: query
          schema:
            type: string
            format: date-time
          description: Custom end date (overrides period)
      responses:
        '200':
          description: Messaging statistics
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  stats:
                    type: object
                    properties:
                      period:
                        type: string
                        enum: [today, week, month, year]
                      dateRange:
                        type: object
                        properties:
                          start:
                            type: string
                            format: date-time
                            nullable: true
                          end:
                            type: string
                            format: date-time
                            nullable: true
                      messages:
                        type: object
                        properties:
                          total:
                            type: integer
                          sent:
                            type: integer
                          failed:
                            type: integer
                          scheduled:
                            type: integer
                          fallbackUsed:
                            type: integer
                            description: Number of messages that fell back to SMS
                          successRate:
                            type: integer
                            description: Percentage of successfully sent messages
                          fallbackRate:
                            type: integer
                            description: Percentage of messages that used SMS fallback
                          typeDistribution:
                            type: object
                            additionalProperties:
                              type: integer
                            description: Message count by type (e.g. TEXT, RICHCARD, CAROUSEL)
                      jobs:
                        type: object
                        properties:
                          individual:
                            type: object
                            description: Scheduled single-message jobs
                            properties:
                              total:
                                type: integer
                              completed:
                                type: integer
                              failed:
                                type: integer
                              processing:
                                type: integer
                          batch:
                            type: object
                            description: Batch sending jobs
                            properties:
                              total:
                                type: integer
                              completed:
                                type: integer
                              failed:
                                type: integer
                              processing:
                                type: integer

  /validate:
    post:
      tags:
        - Validation
      summary: Validate message
      description: Validate a message structure before sending. Returns validation errors and warnings.
      operationId: validateMessage
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ValidateMessageRequest'
      responses:
        '200':
          description: Validation result
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ValidationResult'

  /button-guidelines:
    get:
      tags:
        - Validation
      summary: Get button guidelines
      description: Returns guidelines and limits for interactive buttons.
      operationId: getButtonGuidelines
      responses:
        '200':
          description: Button guidelines
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ButtonGuidelines'

  # ───────────────────────────────────────────────────────────────
  # Sandbox (public, no API key, nothing is sent)
  # ───────────────────────────────────────────────────────────────

  /sandbox/validate:
    post:
      tags:
        - Sandbox
      summary: Validate a message without an account
      description: |
        Runs the exact validator behind `POST /send` on the body you post, with no API key and no
        account. Always returns `200` once the body is structurally valid; read `valid`, `errors` and
        `warnings`. Fields the real send needs (`phoneNumber`, `sendernameId`, `scheduleAt`, ...) are
        accepted and echoed under `sandbox.ignored_fields`. Nothing is sent.

        Rate limit per IP: 20 requests per minute, 100 per day (`429` with code `SANDBOX_RATE_LIMITED`).
      operationId: sandboxValidate
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SandboxRequest'
            examples:
              richCard:
                summary: Rich card with a sample image
                value:
                  messageType: richCard
                  content:
                    title: Your order is on its way
                    description: Track the parcel or talk to us.
                    media:
                      height: TALL
                      contentInfo:
                        fileUrl: https://sendrcs.eu/images/rcsexamples/shoes-1.jpg
                        contentType: image/jpeg
                    cardActions:
                      - type: openUrl
                        text: Track order
                        openUrlAction:
                          url: https://example.com/track
                      - type: dial
                        text: Call us
                        dialAction:
                          phoneNumber: "+4512345678"
      responses:
        '200':
          description: Validation result (check `valid`)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SandboxValidateResponse'
        '400':
          description: Structurally invalid body (missing `messageType` or `content`)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '413':
          description: '`content` larger than 256000 bytes'
        '429':
          description: Sandbox rate limit reached
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SandboxRateLimited'

  /sandbox/preview:
    post:
      tags:
        - Sandbox
      summary: Validate and preview a message without an account
      description: |
        Same validation as `/sandbox/validate`, plus a stored anonymous preview with a `share_url`
        that shows the message on a phone mockup (Android and iOS) with SendRCS branding. The preview
        expires after 24 hours. Identical input returns the same link.

        Invalid content returns `400` with the same body as `POST /send`, so you learn the real error
        shape before you have an account.

        Only media listed by `GET /sandbox/samples` is rendered. Any other `https` URL passes
        validation but is replaced by a placeholder on the preview page; each replacement is reported
        in `sandbox.media`. Sandbox preview ids (`sbx_...`) cannot be used to send.
      operationId: sandboxPreview
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SandboxRequest'
            examples:
              richCard:
                summary: Rich card with a sample image
                value:
                  messageType: richCard
                  content:
                    title: Your order is on its way
                    description: Track the parcel or talk to us.
                    media:
                      height: TALL
                      contentInfo:
                        fileUrl: https://sendrcs.eu/images/rcsexamples/shoes-1.jpg
                        contentType: image/jpeg
                    cardActions:
                      - type: openUrl
                        text: Track order
                        openUrlAction:
                          url: https://example.com/track
                      - type: dial
                        text: Call us
                        dialAction:
                          phoneNumber: "+4512345678"
              carouselWithSendFields:
                summary: Carousel posted with the fields a real send would carry
                value:
                  phoneNumber: "+4512345678"
                  sendernameId: 507f1f77bcf86cd799439011
                  messageType: carousel
                  content:
                    cardContents:
                      - title: Trainers
                        description: Light and fast.
                        media:
                          height: MEDIUM
                          contentInfo:
                            fileUrl: https://sendrcs.eu/images/rcsexamples/shoes-1.jpg
                        cardActions:
                          - type: openUrl
                            text: Shop trainers
                            openUrlAction:
                              url: https://example.com/trainers
                      - title: Road trip
                        description: Book your next weekend.
                        media:
                          height: MEDIUM
                          contentInfo:
                            fileUrl: https://sendrcs.eu/images/rcsexamples/travel-1.jpg
                        cardActions:
                          - type: openUrl
                            text: See offers
                            openUrlAction:
                              url: https://example.com/travel
      responses:
        '200':
          description: Valid message with a preview link
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SandboxPreviewResponse'
        '400':
          description: Structurally invalid body, or content that failed validation (same body as `POST /send`)
          content:
            application/json:
              schema:
                oneOf:
                  - $ref: '#/components/schemas/ErrorResponse'
                  - $ref: '#/components/schemas/SandboxValidationFailedResponse'
        '413':
          description: '`content` larger than 256000 bytes'
        '429':
          description: Sandbox rate limit reached
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SandboxRateLimited'

  /sandbox/samples:
    get:
      tags:
        - Sandbox
      summary: List the sample media the sandbox renders
      description: |
        Images, a video and a PDF you may reference in sandbox requests. These are the only media
        URLs rendered on sandbox preview pages. `usage` shows where to put a URL for each message type.
      operationId: sandboxSamples
      security: []
      responses:
        '200':
          description: Sample media list
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SandboxSamplesResponse'

  /sandbox/preview/{slug}:
    get:
      tags:
        - Sandbox
      summary: Read a sandbox preview
      description: |
        The JSON behind a sandbox `share_url`. The slug is the last path segment of the `share_url`.
        Returns the rendered content (placeholders already applied), the validation warnings and the
        expiry. Foreign media URLs are never returned, only their host.
      operationId: sandboxGetPreview
      security: []
      parameters:
        - name: slug
          in: path
          required: true
          schema:
            type: string
            minLength: 20
            maxLength: 30
      responses:
        '200':
          description: Sandbox preview
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SandboxSharedPreview'
        '404':
          description: Unknown or expired preview
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /conversations:
    get:
      tags:
        - Conversations
      summary: List conversations
      description: |
        Returns your conversations with billing information, newest activity first.
        Without a `status` filter, both active and expired conversations are returned.

        A conversation is `ACTIVE` while its 24-hour window is open and `EXPIRED` once
        the window has passed. Status is derived from `sessionExpires` at request time.
      operationId: listConversations
      parameters:
        - name: status
          in: query
          description: Only return conversations with this status. Omit to return all.
          schema:
            type: string
            enum: [ACTIVE, EXPIRED]
        - name: page
          in: query
          schema:
            type: integer
            minimum: 1
            default: 1
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
      responses:
        '200':
          description: List of conversations
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  conversations:
                    type: array
                    items:
                      $ref: '#/components/schemas/ConversationListItem'
                  pagination:
                    type: object
                    properties:
                      page:
                        type: integer
                      limit:
                        type: integer
                      total:
                        type: integer
                      pages:
                        type: integer

  /conversations/{phoneNumber}/status:
    get:
      tags:
        - Conversations
      summary: Get conversation status
      description: Returns conversation status for a specific phone number.
      operationId: getConversationStatus
      parameters:
        - name: phoneNumber
          in: path
          required: true
          schema:
            type: string
          example: "+4512345678"
      responses:
        '200':
          description: Conversation status
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ConversationStatus'

  /conversations/{phoneNumber}/rate-limit:
    get:
      tags:
        - Conversations
      summary: Get rate limit for conversation
      description: Returns rate limit information for a specific conversation.
      operationId: getConversationRateLimit
      parameters:
        - name: phoneNumber
          in: path
          required: true
          schema:
            type: string
      responses:
        '200':
          description: Rate limit info

  /conversations/billing-preview:
    post:
      tags:
        - Conversations
      summary: Preview billing for multiple recipients
      description: |
        Check, before sending, which recipients already have an open 24-hour conversation
        window (free) and which will be charged. Read-only - nothing is sent and no credits
        are consumed.

        Up to 1,000 phone numbers per call. Recipients whose status cannot be determined are
        reported with `reason: "ERROR_CHECKING"` and counted as chargeable, so the estimate
        stays conservative.
      operationId: previewConversationBilling
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required:
                - phoneNumbers
              properties:
                phoneNumbers:
                  type: array
                  minItems: 1
                  maxItems: 1000
                  items:
                    type: string
                  description: Recipient phone numbers to price.
                  example: ["+4512345678", "+4587654321"]
            examples:
              basic:
                summary: Price three recipients
                value:
                  phoneNumbers: ["+4512345678", "+4587654321", "+4593700401"]
      responses:
        '200':
          description: Billing preview
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                  preview:
                    type: object
                    properties:
                      totalRecipients:
                        type: integer
                      creditsRequired:
                        type: integer
                        description: Estimated credits for the chargeable recipients.
                      freeMessages:
                        type: integer
                        description: Recipients covered by an open conversation window.
                      chargedMessages:
                        type: integer
                      currentCredits:
                        type: number
                        description: The account's credit balance at the time of the call.
                      hasEnoughCredits:
                        type: boolean
                      creditEfficiency:
                        type: integer
                        description: Percentage of recipients that are free.
                      estimatedSavings:
                        type: string
                  recipients:
                    type: array
                    items:
                      type: object
                      properties:
                        phoneNumber:
                          type: string
                        willCharge:
                          type: boolean
                        reason:
                          type: string
                        remainingTime:
                          type: string
                          nullable: true
                          description: Time left on the open conversation window, when there is one.
                        conversationActive:
                          type: boolean
        '400':
          description: Missing phoneNumbers, or more than 1,000 numbers
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /messages/log:
    servers:
      - url: https://api.sendrcs.eu/api
        description: Messages API
    get:
      tags:
        - Message Log
      summary: Get message log
      description: |
        Retrieve your message history with filtering and cursor-based pagination.
        Supports filtering by phone number, campaign name, direction, status, and message type.

        **Single message lookup:** Pass `messageId` to retrieve a specific message by ID (no date range needed).

        **Date range:** Defaults to last 7 days if not provided. Maximum range is 31 days per request.

        **Pagination:** Uses cursor-based pagination. Follow the `nextCursor` value from each response
        to fetch the next page. When `hasNext` is `false`, you've reached the end.

        **Base URL:** `https://api.sendrcs.eu/api/messages/log`

        **Example request:**
        ```
        GET https://api.sendrcs.eu/api/messages/log?startDate=2026-05-01T00:00:00Z&endDate=2026-05-07T23:59:59Z&status=DELIVERED,READ&limit=200
        ```
      operationId: getMessageLog
      parameters:
        - name: messageId
          in: query
          schema:
            type: string
          description: Look up a single message by ID. When provided, date range is not required. Matches the `messageRecordId` from `/send` responses and webhook payloads.
          example: "507f1f77bcf86cd799439011"
        - name: startDate
          in: query
          schema:
            type: string
            format: date-time
          description: Start of date range (ISO 8601). Defaults to 7 days ago if omitted.
          example: "2026-05-01T00:00:00Z"
        - name: endDate
          in: query
          schema:
            type: string
            format: date-time
          description: End of date range (ISO 8601). Max 31 days from startDate. Defaults to now if omitted.
          example: "2026-05-07T23:59:59Z"
        - name: phoneNumber
          in: query
          schema:
            type: string
            maxLength: 30
          description: Filter by exact phone number. Accepts with or without `+` prefix (e.g. `+4512345678` or `4512345678`)
          example: "4512345678"
        - name: campaignName
          in: query
          schema:
            type: string
            maxLength: 200
          description: Filter by exact campaign name
          example: "Summer Sale 2026"
        - name: direction
          in: query
          schema:
            type: string
            enum: [inbound, outbound]
          description: Filter by message direction
        - name: status
          in: query
          schema:
            type: string
          description: "Comma-separated statuses: SCHEDULED, SENDING, SENT, DELIVERED, READ, RECEIVED, FAILED, BLOCKED, REJECTED"
          example: "DELIVERED,READ"
        - name: type
          in: query
          schema:
            type: string
          description: "Comma-separated message types: sms, rcs, mms"
          example: "rcs"
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 500
            default: 100
          description: Number of messages per page (1-500)
        - name: cursor
          in: query
          schema:
            type: string
          description: Pagination cursor from previous response's `pagination.nextCursor`
      responses:
        '200':
          description: Message log page
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MessageLogResponse'
              example:
                messages:
                  - id: "507f1f77bcf86cd799439011"
                    type: "RCS"
                    status: "DELIVERED"
                    direction: "OUTBOUND"
                    source: "API_MESSAGE"
                    phoneNumber: "+4512345678"
                    content: "Hello! Your order #1234 has been shipped."
                    campaignName: "Order Updates"
                    sendernameId: "663f1a2b4e1c8d0012a45678"
                    smsFallback: null
                    creditsCost: 0.025
                    createdAt: "2026-05-01T10:00:00Z"
                    sentAt: "2026-05-01T10:00:01Z"
                    deliveredAt: "2026-05-01T10:00:03Z"
                    readAt: null
                    scheduledAt: null
                    failedAt: null
                    error: null
                  - id: "507f1f77bcf86cd799439012"
                    type: "SMS"
                    status: "FAILED"
                    direction: "OUTBOUND"
                    source: "DIRECT_MESSAGE"
                    phoneNumber: "+1601234567890"
                    content: "Reminder: appointment tomorrow at 10:00"
                    campaignName: null
                    sendernameId: "663f1a2b4e1c8d0012a45679"
                    smsFallback: null
                    creditsCost: null
                    createdAt: "2026-05-01T09:30:00Z"
                    sentAt: "2026-05-01T09:30:02Z"
                    deliveredAt: null
                    readAt: null
                    scheduledAt: null
                    failedAt: "2026-05-01T09:30:02Z"
                    error: "Country not supported"
                pagination:
                  nextCursor: "eyJzZW50QXQiOiIyMDI2LTA1LTAxVDA5OjMwOjAyWiIsIl9pZCI6IjUwN2YxZjc3YmNmODZjZDc5OTQzOTAxMiJ9"
                  hasNext: true
                  limit: 100
                  count: 100
        '400':
          description: Validation error (missing dates, invalid range, bad cursor, etc.)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Unauthorized — missing or invalid API key / JWT token

  /messages/{id}/content:
    servers:
      - url: https://api.sendrcs.eu/api
        description: Messages API
    get:
      tags:
        - Message Log
      summary: Get message content
      description: |
        Retrieve the full, reusable content of a single message by its ID — so you can
        resend or adapt a previously sent RCS or SMS message.

        Unlike the [message log](#operation/getMessageLog) (which returns a flat text
        summary), this returns the complete, structured, re-sendable content:

        - **RCS:** returns `messageType` (text, textBasic, richCard, carousel, …) and the
          full `content` object — pass them straight to the RCS send/preview endpoints to
          recreate the message. The SMS fallback configuration is included when one was set.
        - **SMS:** returns the message text in `content`, ready to resend.

        The `content` field is a structured object for RCS and a plain string for SMS;
        `text` always holds the flat text representation (the SMS body, or the RCS
        fallback/display text).

        **Base URL:** `https://api.sendrcs.eu/api/messages/{id}/content`

        **Example request:**
        ```
        GET https://api.sendrcs.eu/api/messages/507f1f77bcf86cd799439011/content
        ```
      operationId: getMessageContent
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: The message record ID. Matches the `messageRecordId` from `/send` responses, the `id` from the message log, and webhook payloads.
          example: "507f1f77bcf86cd799439011"
      responses:
        '200':
          description: Message content
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/MessageContentResponse'
              example:
                message:
                  id: "507f1f77bcf86cd799439011"
                  type: "RCS"
                  status: "DELIVERED"
                  direction: "OUTBOUND"
                  source: "API_MESSAGE"
                  phoneNumber: "+4512345678"
                  messageType: "richCard"
                  content:
                    title: "Your order shipped"
                    description: "Order #1234 is on its way"
                    media:
                      height: "TALL"
                      contentInfo:
                        fileUrl: "https://example.com/box.jpg"
                    suggestions:
                      - reply:
                          text: "Track"
                          postbackData: "track_1234"
                  text: "Your order #1234 has been shipped."
                  error: null
                  smsFallback:
                    message: "Your order #1234 has been shipped."
                    sendernameId: "663f1a2b4e1c8d0012a45679"
                  campaignName: "Order Updates"
                  sendernameId: "663f1a2b4e1c8d0012a45678"
                  createdAt: "2026-05-01T10:00:00Z"
                  sentAt: "2026-05-01T10:00:01Z"
        '400':
          description: Invalid message ID format
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Unauthorized — missing or invalid API key / JWT token
        '404':
          description: Message not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /messages/{id}:
    servers:
      - url: https://api.sendrcs.eu/api
        description: Messages API
    delete:
      tags:
        - Message Log
      summary: Cancel a scheduled message
      description: |
        Cancel one scheduled message before it is sent, by its message ID.
        Works the same way for RCS and SMS - a scheduled `/send` and a scheduled
        `/send-sms` both produce one cancellable message record.

        Only messages still in `SCHEDULED` status can be cancelled. Anything already
        sending or sent returns `404` - once a message has left, it cannot be called back.
        The scheduled job is cancelled and the message record is removed, so the message
        no longer appears in the message log.

        **Cancelling one recipient of a scheduled batch** works too: pass that recipient's
        message ID and only their message is dropped. The rest of the batch is untouched
        and still goes out on schedule. To cancel the whole batch instead, use
        [`DELETE /batch/{jobId}`](#operation/cancelBatch) - do not loop this endpoint
        over every recipient.

        **Finding the ID:** a scheduled send (`/send` or `/send-sms` with `scheduleAt`)
        returns `{ "scheduled": true, "jobId": "...", "messageId": "..." }` - `messageId`
        is the ID to pass here. Note that scheduled responses do not carry
        `messageRecordId`; that field appears only on immediate sends. You can also list
        everything still pending with
        [`GET /messages/log?status=SCHEDULED`](#operation/getMessageLog) and use the `id`
        of a row.

        Requires the `messages:write` permission.
      operationId: cancelScheduledMessage
      parameters:
        - name: id
          in: path
          required: true
          description: The message ID - `messageId` from a scheduled send response, or `id` from the message log.
          schema:
            type: string
            example: "60f1b2c3d4e5f6a7b8c9d0e1"
      responses:
        '200':
          description: The scheduled message was cancelled
          content:
            application/json:
              schema:
                type: object
                properties:
                  success:
                    type: boolean
                    example: true
                  message:
                    type: string
                    example: Scheduled message cancelled successfully
        '400':
          description: Invalid message ID format
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '403':
          description: The API key lacks the `messages:write` permission
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: No scheduled message with that ID on this account (already sent, already cancelled, or not yours)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  # ───────────────────────────────────────────────────────────────
  # Contacts API  (base URL: https://api.sendrcs.eu/api/v1/contacts)
  # ───────────────────────────────────────────────────────────────

  /v1/contacts:
    servers:
      - url: https://api.sendrcs.eu/api
        description: Contacts API
    get:
      tags:
        - Contacts
      summary: List contacts
      description: |
        List contacts with cursor-based pagination, search, and filtering.

        **Phone lookup:** Pass `?phone=4512345678` for an exact-match lookup on the unique
        `{user, phone}` index. Returns 0 or 1 results - pagination is bypassed.
        The number must include the country code and is accepted with or without a prefix:
        `4512345678`, `+4512345678` or `004512345678`. In query strings, URL-encode `+`
        as `%2B` (an unencoded `+` is tolerated too).

        **Search:** Pass `?search=peter` to search across firstName, lastName, phone, and email (case-insensitive regex).

        **Custom fields:** Pass `?include=customFields` to embed each contact's custom field values
        in the response. This uses a single batch query — no N+1 overhead.

        **Pagination:** Uses cursor-based pagination (like the Message Log). Follow `nextCursor`
        from each response to fetch the next page. When `hasNext` is `false`, you've reached the end.
        Use the separate `/count` endpoint if you need a total count.

        **Example requests:**
        ```
        GET /api/v1/contacts?limit=50&search=peter&include=customFields
        GET /api/v1/contacts?phone=4512345678
        GET /api/v1/contacts?disabled=false&group=60f1b2c3d4e5f6a7b8c9d0e1&sort=lastName&order=asc
        ```
      operationId: listContacts
      parameters:
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 200
            default: 50
          description: Number of contacts per page (1-200)
        - name: cursor
          in: query
          schema:
            type: string
          description: Pagination cursor from previous response's `pagination.nextCursor`
        - name: search
          in: query
          schema:
            type: string
            maxLength: 200
          description: Search across firstName, lastName, phone, email (case-insensitive)
          example: "peter"
        - name: phone
          in: query
          schema:
            type: string
            maxLength: 30
          description: Exact phone number lookup (country code required, `+`/`00` prefix accepted) - bypasses pagination, returns 0 or 1 results
          example: "4512345678"
        - name: disabled
          in: query
          schema:
            type: string
            enum: ["true", "false"]
          description: Filter by disabled status. Omit to include all.
        - name: group
          in: query
          schema:
            type: string
          description: Filter by group ID (MongoDB ObjectId)
          example: "60f1b2c3d4e5f6a7b8c9d0e1"
        - name: sort
          in: query
          schema:
            type: string
            enum: [createdAt, firstName, lastName, phone]
            default: createdAt
          description: Sort field
        - name: order
          in: query
          schema:
            type: string
            enum: [asc, desc]
            default: desc
          description: Sort order
        - name: include
          in: query
          schema:
            type: string
            enum: [customFields]
          description: "Pass `customFields` to embed custom field values in each contact"
      responses:
        '200':
          description: Paginated list of contacts
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ContactListResponse'
              example:
                data:
                  - id: "665abc123def456ghi789jkl"
                    firstName: "Peter"
                    lastName: "Thomsen"
                    phone: "4512345678"
                    email: "peter@example.com"
                    disabled: false
                    groups:
                      - id: "60f1b2c3d4e5f6a7b8c9d0e1"
                        name: "VIP Customers"
                    createdAt: "2025-01-15T10:00:00Z"
                    customFields:
                      - fieldId: "665ghi789jkl012mno345pqr"
                        tag: "customer_status"
                        name: "Customer Status"
                        value: "Premium"
                pagination:
                  nextCursor: "eyJjcmVhdGVkQXQiOiIyMDI1LTAxLTE1VDEwOjAwOjAwWiIsIl9pZCI6IjY2NWFiYzEyM2RlZjQ1NmdoaTc4OWprbCJ9"
                  hasNext: true
                  limit: 50
                  count: 50
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Unauthorized
    post:
      tags:
        - Contacts
      summary: Create contact
      description: |
        Create a new contact. The phone number must be unique per user account.

        Optionally assign to groups and set custom field values in the same request.
        Groups are managed via bidirectional relationships — group contact counts are
        updated automatically.

        Returns `409 Conflict` if a contact with the same phone number already exists.
      operationId: createContact
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateContactRequest'
            example:
              firstName: "Peter"
              lastName: "Thomsen"
              phone: "4512345678"
              email: "peter@example.com"
              groups: ["60f1b2c3d4e5f6a7b8c9d0e1"]
              customFields:
                - fieldId: "665ghi789jkl012mno345pqr"
                  value: "Premium"
      responses:
        '201':
          description: Contact created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Contact'
        '400':
          description: |
            Validation error, or the phone's country is outside your price list
            (`code: COUNTRY_NOT_SUPPORTED`). Numbers you cannot message are never stored
            as contacts; this applies to every way a contact can be created.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Unauthorized
        '409':
          description: A contact with this phone number already exists
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                  existingId:
                    type: string

  /v1/contacts/count:
    servers:
      - url: https://api.sendrcs.eu/api
        description: Contacts API
    get:
      tags:
        - Contacts
      summary: Count contacts
      description: |
        Get the total number of contacts matching optional filters.
        More efficient than listing all contacts when you only need the count.
      operationId: countContacts
      parameters:
        - name: disabled
          in: query
          schema:
            type: string
            enum: ["true", "false"]
          description: Filter by disabled status
        - name: group
          in: query
          schema:
            type: string
          description: Filter by group ID
        - name: search
          in: query
          schema:
            type: string
            maxLength: 200
          description: Search filter (same as list endpoint)
      responses:
        '200':
          description: Contact count
          content:
            application/json:
              schema:
                type: object
                properties:
                  count:
                    type: integer
              example:
                count: 1542
        '401':
          description: Unauthorized

  /v1/contacts/bulk:
    servers:
      - url: https://api.sendrcs.eu/api
        description: Contacts API
    post:
      tags:
        - Contacts
      summary: Bulk create or upsert contacts
      description: |
        Create or upsert up to 1,000 contacts in a single request.

        **Create mode** (`upsert: false`, default): Creates new contacts only.
        Contacts with duplicate phone numbers are skipped and reported in the `errors` array.

        **Upsert mode** (`upsert: true`): Matches existing contacts by phone number.
        If found, updates firstName/lastName/email. If not found, creates a new contact.
        Uses MongoDB `bulkWrite` for efficient processing.

        Custom fields can be referenced by `fieldId` or by `tag` (the tag on the custom field definition).

        Optionally assign all contacts to one or more groups.

        Numbers whose country is outside your price list are never stored (created or
        upserted). They are counted in `failed` and listed in `errors` with the reason,
        e.g. "Country not supported in your price list".
      operationId: bulkCreateContacts
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/BulkContactRequest'
            example:
              contacts:
                - phone: "4512345678"
                  firstName: "Peter"
                  lastName: "Thomsen"
                  email: "peter@example.com"
                  customFields:
                    - tag: "customer_status"
                      value: "Premium"
                - phone: "4587654321"
                  firstName: "Anna"
                  lastName: "Jensen"
              upsert: true
              groups: ["60f1b2c3d4e5f6a7b8c9d0e1"]
      responses:
        '200':
          description: Bulk operation results
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BulkContactResponse'
              example:
                created: 1
                updated: 1
                failed: 1
                errors:
                  - phone: "99912345678"
                    error: "Country not supported in your price list"
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Unauthorized

  /v1/contacts/bulk/disable:
    servers:
      - url: https://api.sendrcs.eu/api
        description: Contacts API
    patch:
      tags:
        - Contacts
      summary: Bulk disable contacts
      description: |
        Soft-delete multiple contacts by setting `disabled: true`.
        Disabled contacts cannot receive messages. All scheduled messages
        for these contacts are cancelled, and active flow enrollments are deactivated.
      operationId: bulkDisableContacts
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [contactIds]
              properties:
                contactIds:
                  type: array
                  items:
                    type: string
                  description: Array of contact IDs to disable
                reason:
                  type: string
                  maxLength: 500
                  description: Optional reason for disabling
            example:
              contactIds: ["665abc123def456ghi789jkl", "665abc123def456ghi789jkm"]
              reason: "Unsubscribed via API"
      responses:
        '200':
          description: Disable results
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                  disabledCount:
                    type: integer
                  requestedCount:
                    type: integer
                  cancelledMessages:
                    type: integer
              example:
                message: "2 contacts disabled"
                disabledCount: 2
                requestedCount: 2
                cancelledMessages: 3
        '400':
          description: No valid contacts to disable
        '401':
          description: Unauthorized

  /v1/contacts/bulk/enable:
    servers:
      - url: https://api.sendrcs.eu/api
        description: Contacts API
    patch:
      tags:
        - Contacts
      summary: Bulk enable contacts
      description: |
        Re-enable previously disabled contacts so they can receive messages again.
      operationId: bulkEnableContacts
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [contactIds]
              properties:
                contactIds:
                  type: array
                  items:
                    type: string
                  description: Array of contact IDs to enable
            example:
              contactIds: ["665abc123def456ghi789jkl"]
      responses:
        '200':
          description: Enable results
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                  enabledCount:
                    type: integer
                  requestedCount:
                    type: integer
              example:
                message: "1 contacts enabled"
                enabledCount: 1
                requestedCount: 1
        '400':
          description: No valid disabled contacts to enable
        '401':
          description: Unauthorized

  /v1/contacts/fields:
    servers:
      - url: https://api.sendrcs.eu/api
        description: Contacts API
    get:
      tags:
        - Custom Fields
      summary: List custom field definitions
      description: |
        List all custom merge field definitions for your account.
        These define the available custom fields — use the `tag` value as `{{tag}}` in message templates.
      operationId: listCustomFields
      responses:
        '200':
          description: List of custom field definitions
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/CustomFieldDefinition'
              example:
                data:
                  - id: "665ghi789jkl012mno345pqr"
                    name: "Customer Status"
                    tag: "customer_status"
                    description: "Customer tier level"
                    category: "Business"
                    createdAt: "2025-01-10T08:00:00Z"
                  - id: "665ghi789jkl012mno345pqs"
                    name: "Loyalty Points"
                    tag: "loyalty_points"
                    description: null
                    category: "Custom"
                    createdAt: "2025-02-20T14:00:00Z"
        '401':
          description: Unauthorized
    post:
      tags:
        - Custom Fields
      summary: Create custom field definition
      description: |
        Create a new custom merge field definition. The `tag` must be unique per account
        and can only contain letters, numbers, and underscores (e.g., `customer_status`).

        Once created, you can set values per contact and use `{{tag}}` in message templates.
      operationId: createCustomField
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateCustomFieldRequest'
            example:
              name: "Customer Status"
              tag: "customer_status"
              description: "Customer tier level"
              category: "Business"
      responses:
        '201':
          description: Custom field created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/CustomFieldDefinition'
        '400':
          description: Validation error or tag already exists
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Unauthorized

  /v1/contacts/fields/{fieldId}:
    servers:
      - url: https://api.sendrcs.eu/api
        description: Contacts API
    get:
      tags:
        - Custom Fields
      summary: Get custom field definition
      description: Get a single custom merge field definition by ID.
      operationId: getCustomField
      parameters:
        - name: fieldId
          in: path
          required: true
          schema:
            type: string
          description: Custom field definition ID
      responses:
        '200':
          description: Custom field definition
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/CustomFieldDefinition'
        '404':
          description: Custom field not found
        '401':
          description: Unauthorized
    put:
      tags:
        - Custom Fields
      summary: Update custom field definition
      description: |
        Update a custom merge field definition. All fields are optional — only provided fields are updated.
        If updating the `tag`, it must still be unique per account.
      operationId: updateCustomField
      parameters:
        - name: fieldId
          in: path
          required: true
          schema:
            type: string
          description: Custom field definition ID
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateCustomFieldRequest'
            example:
              name: "VIP Status"
              description: "Updated description"
      responses:
        '200':
          description: Updated custom field definition
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/CustomFieldDefinition'
        '400':
          description: Validation error or tag already exists
        '404':
          description: Custom field not found
        '401':
          description: Unauthorized
    delete:
      tags:
        - Custom Fields
      summary: Delete custom field definition
      description: |
        Delete a custom merge field definition. If the field is in use by contacts,
        the request will fail with a `400` status unless `?force=true` is passed.

        When `force=true`, all per-contact values for this field are also deleted.
      operationId: deleteCustomField
      parameters:
        - name: fieldId
          in: path
          required: true
          schema:
            type: string
          description: Custom field definition ID
        - name: force
          in: query
          schema:
            type: string
            enum: ["true", "false"]
          description: Force delete even if field is in use by contacts
      responses:
        '200':
          description: Custom field deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                  deletedContactData:
                    type: integer
                    description: Number of per-contact values that were deleted
              example:
                message: "Custom field deleted"
                deletedContactData: 42
        '400':
          description: Field is in use — pass `?force=true` to delete anyway
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                  usageCount:
                    type: integer
              example:
                message: "This field is used by 42 contacts. Use ?force=true to delete anyway."
                usageCount: 42
        '404':
          description: Custom field not found
        '401':
          description: Unauthorized

  /v1/contacts/{id}:
    servers:
      - url: https://api.sendrcs.eu/api
        description: Contacts API
    get:
      tags:
        - Contacts
      summary: Get contact
      description: |
        Get a single contact by ID or by phone number.
        Pass `?include=customFields` to embed custom field values.

        The `{id}` path parameter accepts either a contact ID or a phone number.
        The phone number must include the country code and is accepted with or
        without a prefix: `4512345678`, `+4512345678` or `004512345678`.

        ```
        GET /api/v1/contacts/665abc123def456ghi789jkl
        GET /api/v1/contacts/4512345678
        GET /api/v1/contacts/+4512345678
        ```
      operationId: getContact
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Contact ID, or phone number with country code (`45...`, `+45...` or `0045...`)
        - name: include
          in: query
          schema:
            type: string
            enum: [customFields]
          description: "Pass `customFields` to embed custom field values"
      responses:
        '200':
          description: Contact details
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Contact'
        '404':
          description: Contact not found
        '401':
          description: Unauthorized
    put:
      tags:
        - Contacts
      summary: Update contact
      description: |
        Update a contact. All fields are optional — only provided fields are updated.

        **Group changes:** When `groups` is provided, the contact's group memberships are
        updated using bidirectional relationship management. Contacts are automatically
        synced to/from linked campaigns and message flows.

        **Disable/enable:** Set `disabled: true` to soft-delete or `disabled: false` to re-enable.
      operationId: updateContact
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Contact ID
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateContactRequest'
            example:
              firstName: "Peter"
              lastName: "Thomsen"
              email: "peter.new@example.com"
              groups: ["60f1b2c3d4e5f6a7b8c9d0e1", "60f1b2c3d4e5f6a7b8c9d0e2"]
              customFields:
                - fieldId: "665ghi789jkl012mno345pqr"
                  value: "Gold"
      responses:
        '200':
          description: Updated contact
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Contact'
        '400':
          description: Validation error
        '404':
          description: Contact not found
        '409':
          description: Phone number already exists on another contact
        '401':
          description: Unauthorized
    delete:
      tags:
        - Contacts
      summary: Delete contact
      description: |
        Permanently delete a contact and all associated data:
        - Scheduled messages are cancelled
        - Flow enrollments are removed
        - Event participations are removed
        - Group memberships are removed (counts updated)
        - Custom field values are deleted
      operationId: deleteContact
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Contact ID
      responses:
        '200':
          description: Contact deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                  id:
                    type: string
              example:
                message: "Contact deleted"
                id: "665abc123def456ghi789jkl"
        '404':
          description: Contact not found
        '401':
          description: Unauthorized

  /v1/contacts/{id}/fields:
    servers:
      - url: https://api.sendrcs.eu/api
        description: Contacts API
    get:
      tags:
        - Custom Fields
      summary: Get contact's custom field values
      description: |
        Get all custom field values for a specific contact.
        Returns the field definition metadata (tag, name) alongside each value.

        **Required permissions:** `contacts:read` and `custom-fields:read`
      operationId: getContactCustomFields
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Contact ID
      responses:
        '200':
          description: Custom field values for the contact
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/ContactCustomFieldValue'
              example:
                data:
                  - fieldId: "665ghi789jkl012mno345pqr"
                    tag: "customer_status"
                    name: "Customer Status"
                    value: "Premium"
                    updatedAt: "2025-03-10T14:30:00Z"
        '404':
          description: Contact not found
        '401':
          description: Unauthorized
    put:
      tags:
        - Custom Fields
      summary: Bulk set custom field values for a contact
      description: |
        Set multiple custom field values for a contact in a single request.
        Fields can be referenced by `fieldId` or `tag`. Existing values are overwritten.

        **Required permissions:** `contacts:write` and `custom-fields:write`
      operationId: bulkSetContactCustomFields
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Contact ID
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [fields]
              properties:
                fields:
                  type: array
                  items:
                    type: object
                    required: [value]
                    properties:
                      fieldId:
                        type: string
                        description: Custom field definition ID (use this or `tag`)
                      tag:
                        type: string
                        description: Custom field tag (use this or `fieldId`)
                      value:
                        type: string
            example:
              fields:
                - tag: "customer_status"
                  value: "Gold"
                - fieldId: "665ghi789jkl012mno345pqs"
                  value: "1250"
      responses:
        '200':
          description: Results for each field
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        fieldId:
                          type: string
                        tag:
                          type: string
                          nullable: true
                        value:
                          type: string
                        ok:
                          type: boolean
                        error:
                          type: string
        '404':
          description: Contact not found
        '401':
          description: Unauthorized

  /v1/contacts/{id}/fields/{fieldId}:
    servers:
      - url: https://api.sendrcs.eu/api
        description: Contacts API
    put:
      tags:
        - Custom Fields
      summary: Set a custom field value for a contact
      description: |
        Set or update a single custom field value for a contact.
        Creates the value if it doesn't exist, updates it if it does (upsert).

        **Required permissions:** `contacts:write` and `custom-fields:write`
      operationId: setContactCustomField
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Contact ID
        - name: fieldId
          in: path
          required: true
          schema:
            type: string
          description: Custom field definition ID
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [value]
              properties:
                value:
                  type: string
            example:
              value: "Premium"
      responses:
        '200':
          description: Custom field value set
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/ContactCustomFieldValue'
        '404':
          description: Contact or custom field not found
        '401':
          description: Unauthorized
    delete:
      tags:
        - Custom Fields
      summary: Delete a custom field value for a contact
      description: |
        Remove a custom field value from a contact. The field definition is not deleted —
        only the per-contact value is removed.

        **Required permissions:** `contacts:write` and `custom-fields:write`
      operationId: deleteContactCustomField
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Contact ID
        - name: fieldId
          in: path
          required: true
          schema:
            type: string
          description: Custom field definition ID
      responses:
        '200':
          description: Custom field value deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
              example:
                message: "Custom field value deleted"
        '404':
          description: Custom field value not found
        '401':
          description: Unauthorized

  # Groups API  (base URL: https://api.sendrcs.eu/api/v1/groups)
  # ───────────────────────────────────────────────────────────────

  /v1/groups:
    servers:
      - url: https://api.sendrcs.eu/api
        description: Groups API
    get:
      tags:
        - Groups
      summary: List groups
      description: |
        List all contact groups for the authenticated user.
        Returns a lightweight summary sorted by newest first, including contact counts.
      operationId: listGroups
      responses:
        '200':
          description: List of groups
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/Group'
              example:
                data:
                  - id: "60f1b2c3d4e5f6a7b8c9d0e1"
                    name: "VIP Customers"
                    description: "High-value customers"
                    contactCount: 150
                    createdAt: "2025-01-15T10:00:00Z"
                    updatedAt: "2025-01-20T14:30:00Z"
                  - id: "60f1b2c3d4e5f6a7b8c9d0e2"
                    name: "Newsletter Subscribers"
                    description: null
                    contactCount: 1200
                    createdAt: "2025-01-10T08:00:00Z"
                    updatedAt: "2025-01-10T08:00:00Z"
        '401':
          description: Unauthorized
    post:
      tags:
        - Groups
      summary: Create group
      description: |
        Create a new contact group. Groups are used to organize contacts and can be linked to campaigns and message flows.
      operationId: createGroup
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateGroupRequest'
            example:
              name: "VIP Customers"
              description: "High-value customers"
      responses:
        '201':
          description: Group created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Group'
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Unauthorized

  /v1/groups/{id}:
    servers:
      - url: https://api.sendrcs.eu/api
        description: Groups API
    get:
      tags:
        - Groups
      summary: Get group
      description: |
        Get a single group by ID. Optionally includes a paginated list of contacts in the group.

        **Include contacts:** Pass `?includeContacts=true` to get contacts in the group.
        Use `limit` and `offset` for pagination when including contacts.

        **Example requests:**
        ```
        GET /api/v1/groups/60f1b2c3d4e5f6a7b8c9d0e1
        GET /api/v1/groups/60f1b2c3d4e5f6a7b8c9d0e1?includeContacts=true&limit=50&offset=0
        ```
      operationId: getGroup
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Group ID (MongoDB ObjectId)
        - name: includeContacts
          in: query
          schema:
            type: string
            enum: ["true", "false"]
          description: Include paginated contact list in response
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 200
            default: 50
          description: Max contacts to return when includeContacts=true
        - name: offset
          in: query
          schema:
            type: integer
            minimum: 0
            default: 0
          description: Number of contacts to skip for pagination
      responses:
        '200':
          description: Group details
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    allOf:
                      - $ref: '#/components/schemas/Group'
                      - type: object
                        properties:
                          contacts:
                            type: array
                            description: Contact list (only when includeContacts=true)
                            items:
                              $ref: '#/components/schemas/GroupContact'
                          contactsReturned:
                            type: integer
                            description: Number of contacts in this response
                          contactsOffset:
                            type: integer
                            description: Current offset
                          hasMore:
                            type: boolean
                            description: Whether more contacts are available
              example:
                data:
                  id: "60f1b2c3d4e5f6a7b8c9d0e1"
                  name: "VIP Customers"
                  description: "High-value customers"
                  contactCount: 150
                  createdAt: "2025-01-15T10:00:00Z"
                  updatedAt: "2025-01-20T14:30:00Z"
                  contacts:
                    - id: "665abc123def456ghi789jkl"
                      firstName: "Peter"
                      lastName: "Thomsen"
                      phone: "4512345678"
                      email: "peter@example.com"
                      disabled: false
                  contactsReturned: 1
                  contactsOffset: 0
                  hasMore: true
        '404':
          description: Group not found
        '401':
          description: Unauthorized
    put:
      tags:
        - Groups
      summary: Update group
      description: |
        Update a group's name and/or description. Does not modify group membership — use the contacts endpoints for that.
      operationId: updateGroup
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Group ID (MongoDB ObjectId)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateGroupRequest'
            example:
              name: "Premium Customers"
              description: "Updated description"
      responses:
        '200':
          description: Group updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/Group'
        '400':
          description: Validation error or no fields to update
        '404':
          description: Group not found
        '401':
          description: Unauthorized
    delete:
      tags:
        - Groups
      summary: Delete group
      description: |
        Delete a contact group. This does NOT delete the contacts in the group — it only removes the group itself and clears the group reference from all contacts that belonged to it.
      operationId: deleteGroup
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Group ID (MongoDB ObjectId)
      responses:
        '200':
          description: Group deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                  id:
                    type: string
                  name:
                    type: string
              example:
                message: "Group deleted"
                id: "60f1b2c3d4e5f6a7b8c9d0e1"
                name: "VIP Customers"
        '404':
          description: Group not found
        '401':
          description: Unauthorized

  /v1/groups/{id}/contacts:
    servers:
      - url: https://api.sendrcs.eu/api
        description: Groups API
    post:
      tags:
        - Groups
      summary: Add contacts to group
      description: |
        Add contacts to a group by contact IDs. Uses bidirectional sync to maintain consistency between Contact.groups and Group.contacts.

        If the group is linked to campaigns or message flows, contacts are automatically synced to those entities as well.

        Duplicate contacts (already in the group) are silently ignored.
      operationId: addContactsToGroup
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Group ID (MongoDB ObjectId)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/GroupContactsRequest'
            example:
              contactIds:
                - "665abc123def456ghi789jkl"
                - "665abc123def456ghi789jkm"
      responses:
        '200':
          description: Contacts added
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/GroupContactsResponse'
              example:
                message: "2 contacts added to group"
                contactsProcessed: 2
                newRelationships: 2
                linkedEntitiesSync:
                  campaignsUpdated: 1
                  flowsUpdated: 0
        '400':
          description: Validation error
        '404':
          description: Group not found
        '401':
          description: Unauthorized
    delete:
      tags:
        - Groups
      summary: Remove contacts from group
      description: |
        Remove contacts from a group by contact IDs. Uses bidirectional sync to maintain consistency.

        If the group is linked to campaigns or message flows, contacts are automatically removed from those entities as well.

        Contacts not in the group are silently ignored. The contacts themselves are NOT deleted.
      operationId: removeContactsFromGroup
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Group ID (MongoDB ObjectId)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/GroupContactsRequest'
            example:
              contactIds:
                - "665abc123def456ghi789jkl"
      responses:
        '200':
          description: Contacts removed
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                  contactsProcessed:
                    type: integer
                  removedRelationships:
                    type: integer
                  linkedEntitiesSync:
                    type: object
                    nullable: true
                    properties:
                      campaignsUpdated:
                        type: integer
                      flowsUpdated:
                        type: integer
              example:
                message: "1 contacts removed from group"
                contactsProcessed: 1
                removedRelationships: 1
                linkedEntitiesSync:
                  campaignsUpdated: 0
                  flowsUpdated: 0
        '400':
          description: Validation error
        '404':
          description: Group not found
        '401':
          description: Unauthorized

  # Templates API  (base URL: https://api.sendrcs.eu/api/v1/templates)
  # ───────────────────────────────────────────────────────────────

  /v1/templates:
    servers:
      - url: https://api.sendrcs.eu/api
        description: Templates API
    get:
      tags:
        - Templates
      summary: List templates
      description: |
        List message templates with page-based pagination and filtering.
        Supports filtering by type (SMS/RCS), RCS message subtype, template group, context, and name search.

        For RCS templates, each result includes a `parsedContent` field with the structured message content.

        **Example requests:**
        ```
        GET /api/v1/templates?type=RCS&limit=20
        GET /api/v1/templates?search=welcome&context=event
        GET /api/v1/templates?type=SMS&sort=createdAt&order=desc
        ```
      operationId: listTemplates
      parameters:
        - name: type
          in: query
          schema:
            type: string
            enum: [SMS, RCS]
          description: Filter by template type
        - name: rcsMessageType
          in: query
          schema:
            type: string
            enum: [textBasic, text, richCard, carousel, image, video, audio, file, media]
          description: Filter by RCS message subtype (only applicable when type=RCS)
        - name: templateGroupId
          in: query
          schema:
            type: string
          description: Filter by template group (folder). Pass a template group ID or `none` for ungrouped templates. Template groups are NOT contact groups.
        - name: context
          in: query
          schema:
            type: string
            enum: [event, flow, general]
          description: Filter by context
        - name: search
          in: query
          schema:
            type: string
            maxLength: 200
          description: Search by template name (case-insensitive)
          example: "welcome"
        - name: page
          in: query
          schema:
            type: integer
            minimum: 1
            default: 1
          description: Page number
        - name: limit
          in: query
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 20
          description: Results per page (1-100)
        - name: sort
          in: query
          schema:
            type: string
            enum: [name, createdAt, updatedAt]
            default: name
          description: Sort field
        - name: order
          in: query
          schema:
            type: string
            enum: [asc, desc]
            default: asc
          description: Sort order
      responses:
        '200':
          description: Paginated list of templates
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TemplateListResponse'
              example:
                data:
                  - id: "665ghi789jkl012mno345pqr"
                    name: "Welcome Message"
                    content: "Welcome {{firstName}}! Thanks for signing up."
                    type: "SMS"
                    rcsMessageType: null
                    context: "general"
                    templateGroupId: null
                    isDefault: false
                    createdAt: "2025-01-15T10:00:00Z"
                    updatedAt: "2025-01-15T10:00:00Z"
                pagination:
                  total: 25
                  page: 1
                  limit: 20
                  pages: 2
                  hasNext: true
                  hasPrev: false
        '400':
          description: Validation error
        '401':
          description: Unauthorized
    post:
      tags:
        - Templates
      summary: Create template
      description: |
        Create a new message template.

        **For SMS templates:** Pass `content` as a plain text string.

        **For RCS templates:** Pass `content` as an object (same format as rcs_send content field).
        You must also provide `rcsMessageType`. The content is stored internally as a JSON string
        and returned with a `parsedContent` field containing the structured data.

        **Setting as default:** Pass `isDefault: true` to make this the default template for its type+context.
        Any existing default for the same type+context will be unset.
      operationId: createTemplate
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateTemplateRequest'
            examples:
              sms:
                summary: SMS template
                value:
                  name: "Welcome SMS"
                  type: "SMS"
                  content: "Welcome {{firstName}}! Thanks for signing up."
                  context: "general"
              rcs:
                summary: RCS rich card template
                value:
                  name: "Product Card"
                  type: "RCS"
                  rcsMessageType: "richCard"
                  content:
                    title: "New Product Available"
                    description: "Check out our latest product!"
                    media:
                      url: "https://example.com/image.jpg"
                      height: "TALL"
                    cardActions:
                      - type: "url"
                        label: "View Product"
                        url: "https://example.com/product"
                  templateGroupId: "665ghi789jkl012mno345abc"
      responses:
        '201':
          description: Template created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/TemplateObject'
        '400':
          description: Validation error (e.g. missing rcsMessageType for RCS, wrong content type)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '401':
          description: Unauthorized

  /v1/templates/groups:
    servers:
      - url: https://api.sendrcs.eu/api
        description: Template Groups API
    get:
      tags:
        - Template Groups
      summary: List template groups
      description: |
        List all template groups (folders for message templates), sorted by name,
        each with its `templateCount`. Also returns `ungroupedTemplateCount` —
        the number of templates not in any group.

        Template groups are NOT contact groups.
      operationId: listTemplateGroups
      responses:
        '200':
          description: List of template groups
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/TemplateGroupListResponse'
              example:
                data:
                  - id: "665abc123def456ghi789jkl"
                    name: "Campaigns DK"
                    templateCount: 5
                    createdAt: "2025-01-15T10:00:00Z"
                    updatedAt: "2025-01-15T10:00:00Z"
                ungroupedTemplateCount: 12
        '401':
          description: Unauthorized
    post:
      tags:
        - Template Groups
      summary: Create template group
      description: |
        Create a new template group (folder for message templates).
        Names must be unique per account — a duplicate name returns `400` with `error: duplicate_name`.
      operationId: createTemplateGroup
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateTemplateGroupRequest'
            example:
              name: "Campaigns DK"
      responses:
        '201':
          description: Template group created
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/TemplateGroupObject'
        '400':
          description: Validation error or duplicate name
        '401':
          description: Unauthorized

  /v1/templates/groups/assign:
    servers:
      - url: https://api.sendrcs.eu/api
        description: Template Groups API
    post:
      tags:
        - Template Groups
      summary: Assign templates to a template group
      description: |
        Move one or more templates into a template group, or remove them from any group
        by passing `templateGroupId: null`. Each template belongs to at most one template group.

        Template IDs that don't exist or belong to another account are silently skipped;
        `modifiedCount` reports how many templates were actually updated.
      operationId: assignTemplatesToTemplateGroup
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/AssignTemplatesToTemplateGroupRequest'
            examples:
              move:
                summary: Move templates into a group
                value:
                  templateIds: ["665ghi789jkl012mno345pqr"]
                  templateGroupId: "665abc123def456ghi789jkl"
              ungroup:
                summary: Remove templates from their group
                value:
                  templateIds: ["665ghi789jkl012mno345pqr"]
                  templateGroupId: null
      responses:
        '200':
          description: Templates moved
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                  modifiedCount:
                    type: integer
                    description: Number of templates that were updated
        '400':
          description: Validation error or unknown template group
        '401':
          description: Unauthorized

  /v1/templates/groups/{id}:
    servers:
      - url: https://api.sendrcs.eu/api
        description: Template Groups API
    put:
      tags:
        - Template Groups
      summary: Rename template group
      operationId: updateTemplateGroup
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Template group ID (MongoDB ObjectId)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/CreateTemplateGroupRequest'
            example:
              name: "Campaigns DK 2025"
      responses:
        '200':
          description: Template group renamed
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/TemplateGroupObject'
        '400':
          description: Validation error or duplicate name
        '404':
          description: Template group not found
    delete:
      tags:
        - Template Groups
      summary: Delete template group
      description: |
        Delete a template group. **Templates inside the group are NOT deleted** —
        they become ungrouped. `ungroupedTemplateCount` in the response reports
        how many templates were affected.
      operationId: deleteTemplateGroup
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Template group ID (MongoDB ObjectId)
      responses:
        '200':
          description: Template group deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                  id:
                    type: string
                  name:
                    type: string
                  ungroupedTemplateCount:
                    type: integer
                    description: Number of templates that became ungrouped
        '404':
          description: Template group not found

  /v1/templates/{id}:
    servers:
      - url: https://api.sendrcs.eu/api
        description: Templates API
    get:
      tags:
        - Templates
      summary: Get template
      description: |
        Get a single template by ID. For RCS templates, includes `parsedContent` with the structured message content.
      operationId: getTemplate
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Template ID (MongoDB ObjectId)
      responses:
        '200':
          description: Template details
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/TemplateObject'
        '404':
          description: Template not found
        '401':
          description: Unauthorized
    put:
      tags:
        - Templates
      summary: Update template
      description: |
        Update an existing template. All fields are optional — only provided fields are updated.

        **RCS content handling:** If updating content for an RCS template, pass it as an object.
        If changing `rcsMessageType` without providing new content, the existing content is re-wrapped
        with the new message type.
      operationId: updateTemplate
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Template ID (MongoDB ObjectId)
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UpdateTemplateRequest'
            example:
              name: "Updated Welcome Message"
              content: "Hi {{firstName}}, welcome aboard!"
      responses:
        '200':
          description: Template updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  data:
                    $ref: '#/components/schemas/TemplateObject'
        '400':
          description: Validation error
        '404':
          description: Template not found
        '401':
          description: Unauthorized
    delete:
      tags:
        - Templates
      summary: Delete template
      description: |
        Permanently delete a message template.
      operationId: deleteTemplate
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: string
          description: Template ID (MongoDB ObjectId)
      responses:
        '200':
          description: Template deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  message:
                    type: string
                  id:
                    type: string
                  name:
                    type: string
                  type:
                    type: string
              example:
                message: "Template deleted"
                id: "665ghi789jkl012mno345pqr"
                name: "Welcome Message"
                type: "SMS"
        '404':
          description: Template not found
        '401':
          description: Unauthorized

webhooks:
  buttonClick:
    post:
      tags:
        - Webhooks
      summary: Button click callback
      description: |
        Sent to your webhook URL when a user clicks a button in an RCS message.
        Configure the webhook URL per button in your message's `suggestions` or `cardActions`.

        **Correlating with sent messages:**
        The `message.id` field in the payload matches the `messageRecordId` from
        the `/send` response.

        **Signature verification (optional):**
        Every request includes an `X-RCS-Signature: t=<unix-seconds>,v1=<hex-hmac>`
        header. To verify, compute `HMAC_SHA256(secret, t + "." + rawBody)` and
        compare to the `v1=` portion. Reject if `|now - t| > 300` seconds. See the
        **Webhooks** section overview for a full Node.js example. Verification is
        optional — you can safely ignore the header if you don't need it.
      operationId: buttonClickWebhook
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/WebhookPayload'
            example:
              event: "rcs_button_clicked"
              timestamp: "2024-01-15T10:30:00Z"
              interaction:
                id: "507f1f77bcf86cd799439014"
                type: "button_click"
                buttonText: "Book Appointment"
                postbackData: "book_appointment_service_123"
                clickedAt: "2024-01-15T10:30:00Z"
              message:
                id: "507f1f77bcf86cd799439011"
                campaignName: "Summer Sale 2024"
                sentAt: "2024-01-15T09:00:00Z"
                rcsData:
                  messageType: "richCard"
                  brandId: "your-rcs-brand-id"
              contact:
                id: "507f1f77bcf86cd799439012"
                phone: "+4512345678"
                name: "John Doe"
              user:
                id: "507f1f77bcf86cd799439013"
                businessName: "Your Business Name"
              metadata:
                conversationId: "507f1f77bcf86cd799439015"
                platform: "RCS"
      responses:
        '200':
          description: Webhook received successfully

  deliveryStatus:
    post:
      tags:
        - Webhooks
      summary: Delivery status callback
      description: |
        Sent to your webhook URL when a message delivery status changes
        (delivered, read, failed). Configure status webhooks in
        [Settings > Webhooks](https://app.sendrcs.eu/settings/webhooks).

        **When `failed` fires:** for failures reported by the recipient's network
        (undeliverable, rejected, expired) and also for messages the platform could
        not send at all: country not supported or blocked, insufficient credits,
        disabled contact, or a rejection at submission. When an RCS message falls
        back to SMS you receive a `failed` `rcs_status` event for the RCS leg
        (error "... - SMS fallback used") followed by `sms_status` events for the
        SMS. The `error` text is the same as the `error` field in `GET /api/messages/log`.

        **Correlating with sent messages:**
        The `messageId` in the payload matches the `messageRecordId` field
        from the `/send` response. For `/send-batch`, messages are created
        asynchronously - use the batch job status endpoint to track individual messages.

        **Signature verification (optional):**
        Every request includes an `X-RCS-Signature: t=<unix-seconds>,v1=<hex-hmac>`
        header. To verify, compute `HMAC_SHA256(secret, t + "." + rawBody)` and
        compare to the `v1=` portion. Reject if `|now - t| > 300` seconds. See the
        **Webhooks** section overview for a full Node.js example. Verification is
        optional — you can safely ignore the header if you don't need it.
      operationId: statusWebhook
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/StatusWebhookPayload'
            example:
              event: "rcs_status"
              messageId: "507f1f77bcf86cd799439011"
              status: "delivered"
              phoneNumber: "+4512345678"
              contactId: "507f1f77bcf86cd799439012"
              campaignName: "Summer Sale 2024"
              timestamp: "2024-01-15T10:30:00Z"
              error: null
              test: false
      responses:
        '200':
          description: Webhook received successfully

  rcsIncoming:
    post:
      tags:
        - Webhooks
      summary: Incoming RCS message callback
      description: |
        Sent to your webhook URL when a contact sends an RCS message to one of
        your RCS sender names — texts, images, videos, audio, files, contact
        cards, shared locations, and button/suggestion taps. Configure one
        webhook per sender name in
        [Settings > Webhooks](https://app.sendrcs.eu/settings/webhooks) under
        the **Incoming Webhooks** tab.

        **Message types:**
        `rcs.messageType` tells you what arrived: `text`, `image`, `video`,
        `audio`, `file`, `vcard`, `location`, or `button`. For media messages
        the `rcs.media` block is present; for shared locations `rcs.location`
        carries the coordinates.

        **Button taps:**
        Taps on suggested replies/buttons are delivered too, with
        `rcs.isButtonReply: true` plus `rcs.buttonText` and `rcs.postbackData`.
        If you also use per-button click webhooks you may receive both events
        for the same tap — filter on `rcs.isButtonReply` if you only want
        free-form messages here.

        **Media access:**
        `rcs.media.url` is a relative SendRCS API path that requires an
        authenticated session to download. Incoming media is retained for
        48 hours after receipt — see `rcs.media.expiresAt`.

        **Signature verification (optional):**
        Every request includes an `X-RCS-Signature: t=<unix-seconds>,v1=<hex-hmac>`
        header. To verify, compute `HMAC_SHA256(secret, t + "." + rawBody)` and
        compare to the `v1=` portion. Reject if `|now - t| > 300` seconds. See the
        **Webhooks** section overview for a full Node.js example. Verification is
        optional — you can safely ignore the header if you don't need it.
      operationId: rcsIncomingWebhook
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RcsIncomingWebhookPayload'
            example:
              event: "rcs_incoming"
              timestamp: "2024-01-15T10:30:00Z"
              rcs:
                id: "507f1f77bcf86cd799439011"
                from: "+4512345678"
                to: "your-agent-id"
                message: "Hi, do you have this in stock?"
                messageType: "text"
                isButtonReply: false
                receivedAt: "2024-01-15T10:30:00Z"
              contact:
                id: "507f1f77bcf86cd799439012"
                name: "John Doe"
                phone: "+4512345678"
                email: "john@example.com"
              sendername:
                id: "507f1f77bcf86cd799439016"
                name: "Your Brand"
                agentId: "your-agent-id"
              user:
                id: "507f1f77bcf86cd799439013"
                email: "owner@business.com"
              metadata:
                messageType: "RCS"
                direction: "INBOUND"
                platform: "RCS"
                provider: "SendRCS"
                conversationId: "507f1f77bcf86cd799439015"
      responses:
        '200':
          description: Webhook received successfully

  smsIncoming:
    post:
      tags:
        - Webhooks
      summary: Incoming SMS message callback
      description: |
        Sent to your webhook URL when someone sends an SMS to one of your
        virtual numbers. Configure one webhook per virtual number in
        [Settings > Webhooks](https://app.sendrcs.eu/settings/webhooks) under
        the **Incoming Webhooks** tab. Virtual numbers can be subscribed in
        [Settings > Numbers](https://app.sendrcs.eu/settings/numbers).

        **Contacts:**
        If the sender is not yet in your contact list, a contact is created
        automatically and referenced in the `contact` block.

        **Signature verification (optional):**
        Every request includes an `X-RCS-Signature: t=<unix-seconds>,v1=<hex-hmac>`
        header. To verify, compute `HMAC_SHA256(secret, t + "." + rawBody)` and
        compare to the `v1=` portion. Reject if `|now - t| > 300` seconds. See the
        **Webhooks** section overview for a full Node.js example. Verification is
        optional — you can safely ignore the header if you don't need it.
      operationId: smsIncomingWebhook
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SmsIncomingWebhookPayload'
            example:
              event: "sms_incoming"
              timestamp: "2024-01-15T10:30:00Z"
              sms:
                id: "507f1f77bcf86cd799439011"
                from: "+4512345678"
                to: "+4587654321"
                message: "Hi, I would like to book an appointment"
                encoding: "GSM-7"
                segments: 1
                receivedAt: "2024-01-15T10:30:00Z"
              contact:
                id: "507f1f77bcf86cd799439012"
                name: "John Doe"
                phone: "+4512345678"
                email: "john@example.com"
              virtualNumber:
                id: "507f1f77bcf86cd799439017"
                phoneNumber: "+4587654321"
                subscriptionId: "507f1f77bcf86cd799439018"
              user:
                id: "507f1f77bcf86cd799439013"
                email: "owner@business.com"
              metadata:
                messageType: "SMS"
                direction: "INBOUND"
                platform: "SMS"
      responses:
        '200':
          description: Webhook received successfully

  formSubmission:
    post:
      tags:
        - Webhooks
      summary: Form submission callback
      description: |
        Sent to your webhook URL every time a visitor submits one of your forms —
        **before** the double opt-in is confirmed. Configure one webhook per form in
        [Settings > Webhooks](https://app.sendrcs.eu/settings/webhooks) under the
        **Form Submission Webhooks** tab. The same webhook also receives the
        `form_optin_confirmed` event when the visitor later confirms.

        **Opt-in status:**
        `optIn.status` is always `PENDING` at submit time — the contact is not
        activated (and, for existing contacts, not updated) until the visitor
        confirms via the opt-in link. `contact.optInStatus` reports the contact's
        current stored status. `optIn.confirmationSent` is `false` when the form's
        "submit once" throttle suppressed a repeat opt-in message.

        **Submitted data:**
        The `data` block contains exactly what the visitor submitted, including
        custom fields with their labels. For existing contacts these values are
        NOT yet written to the contact — that happens on confirmation.

        **Signature verification (optional):**
        Every request includes an `X-RCS-Signature: t=<unix-seconds>,v1=<hex-hmac>`
        header. To verify, compute `HMAC_SHA256(secret, t + "." + rawBody)` and
        compare to the `v1=` portion. Reject if `|now - t| > 300` seconds. See the
        **Webhooks** section overview for a full Node.js example. Verification is
        optional — you can safely ignore the header if you don't need it.
      operationId: formSubmissionWebhook
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/FormSubmissionWebhookPayload'
            example:
              event: "form_submission"
              timestamp: "2024-01-15T10:30:00Z"
              form:
                id: "507f1f77bcf86cd799439021"
                name: "Newsletter signup"
                title: "Join our newsletter"
                slug: "a1b2c3d4e5f6a7b8c9d0e1f2"
              submission:
                id: "507f1f77bcf86cd799439022"
                action: "created"
                submittedAt: "2024-01-15T10:30:00Z"
                ip: "203.0.113.10"
              optIn:
                status: "PENDING"
                confirmationSent: true
                confirmedAt: null
              contact:
                id: "507f1f77bcf86cd799439012"
                phone: "+4512345678"
                optInStatus: "PENDING"
              data:
                phone: "+4512345678"
                firstName: "John"
                lastName: "Doe"
                email: "john@example.com"
                customFields:
                  - id: "507f1f77bcf86cd799439033"
                    label: "Company"
                    value: "Acme Inc"
              user:
                id: "507f1f77bcf86cd799439013"
      responses:
        '200':
          description: Webhook received successfully

  formOptInConfirmed:
    post:
      tags:
        - Webhooks
      summary: Form opt-in confirmed callback
      description: |
        Sent to the same form webhook when the visitor taps the confirmation link
        in the double opt-in message — the final opt-in. Fires exactly once per
        confirmation; repeat taps on the link send nothing.

        **Opt-in status:**
        `optIn.status` is always `CONFIRMED` for this event. At this point the
        contact has been activated, and (when `optIn.dataApplied` is `true`) the
        submitted data and group memberships have been written to the contact.

        **Signature verification (optional):**
        Every request includes an `X-RCS-Signature: t=<unix-seconds>,v1=<hex-hmac>`
        header. To verify, compute `HMAC_SHA256(secret, t + "." + rawBody)` and
        compare to the `v1=` portion. Reject if `|now - t| > 300` seconds. See the
        **Webhooks** section overview for a full Node.js example. Verification is
        optional — you can safely ignore the header if you don't need it.
      operationId: formOptInConfirmedWebhook
      security: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/FormOptInConfirmedWebhookPayload'
            example:
              event: "form_optin_confirmed"
              timestamp: "2024-01-15T10:35:00Z"
              form:
                id: "507f1f77bcf86cd799439021"
                name: "Newsletter signup"
                title: "Join our newsletter"
                slug: "a1b2c3d4e5f6a7b8c9d0e1f2"
              optIn:
                status: "CONFIRMED"
                confirmedAt: "2024-01-15T10:35:00Z"
                submittedAt: "2024-01-15T10:30:00Z"
                dataApplied: true
              contact:
                id: "507f1f77bcf86cd799439012"
                phone: "+4512345678"
                firstName: "John"
                lastName: "Doe"
                email: "john@example.com"
                optInStatus: "CONFIRMED"
              data:
                phone: "+4512345678"
                firstName: "John"
                lastName: "Doe"
                email: "john@example.com"
                customFields:
                  - id: "507f1f77bcf86cd799439033"
                    label: "Company"
                    value: "Acme Inc"
              user:
                id: "507f1f77bcf86cd799439013"
      responses:
        '200':
          description: Webhook received successfully

components:
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-Key
      description: API key for programmatic access. Obtain your API key by logging into your SendRCS account and navigating to Settings > Api Keys.


  schemas:
    SendMessageRequest:
      type: object
      required:
        - phoneNumber
        - messageType
        - content
        - sendernameId
      properties:
        phoneNumber:
          type: string
          description: Recipient phone number (E.164 format)
          example: "+4512345678"
        messageType:
          type: string
          enum: [text, textBasic, richCard, carousel, media, file, image, video, audio]
          description: Type of RCS message
        content:
          $ref: '#/components/schemas/MessageContent'
        sendernameId:
          type: string
          description: MongoDB ObjectId of approved RCS sender
          example: "60f1b2c3d4e5f6a7b8c9d0e1"
        contactId:
          type: string
          description: Optional existing contact ID
        skipContactCreation:
          type: boolean
          default: false
          description: Skip creating a contact record (not allowed for conversation-based senders)
        skipDisabled:
          type: boolean
          default: true
          description: Skip sending to disabled contacts
        campaignName:
          type: string
          maxLength: 100
          description: Campaign name for tracking
        scheduleAt:
          type: string
          description: ISO 8601 date-time for scheduling
          example: "2024-02-15T14:00:00"
        timeZone:
          type: string
          description: IANA timezone for scheduling
          example: "Europe/Copenhagen"
        smsFallback:
          $ref: '#/components/schemas/SmsFallback'

    SendBatchRequest:
      type: object
      required:
        - phoneNumbers
        - messageType
        - content
        - sendernameId
      properties:
        phoneNumbers:
          type: array
          items:
            type: string
          maxItems: 10000
          description: Array of recipient phone numbers
        messageType:
          type: string
          enum: [text, textBasic, richCard, carousel, media, file, image, video, audio]
        content:
          $ref: '#/components/schemas/MessageContent'
        sendernameId:
          type: string
        skipContactCreation:
          type: boolean
          default: false
        skipDisabled:
          type: boolean
          default: true
        campaignName:
          type: string
          maxLength: 100
        scheduleAt:
          type: string
        timeZone:
          type: string
        smsFallback:
          $ref: '#/components/schemas/SmsFallback'
        batchSize:
          type: integer
          minimum: 1
          maximum: 1000
          default: 100
          description: Number of messages per batch
        delayMs:
          type: integer
          minimum: 0
          maximum: 10000
          default: 50
          description: Delay between batches in milliseconds

    SendSmsRequest:
      type: object
      required:
        - phoneNumber
        - message
        - sendernameId
      properties:
        phoneNumber:
          type: string
          description: Recipient phone number (E.164 format)
          example: "+4512345678"
        message:
          type: string
          maxLength: 1530
          description: |
            SMS message text (max 1530 characters / 10 segments).
            Supports the full merge field set: contact fields (`{{contact_first_name}}`,
            `{{contact_last_name}}`, `{{contact_phone}}`, `{{contact_email}}`), company fields
            (`{{company_name}}`, `{{company_phone}}`, `{{company_website}}`, ...), `{{today}}`,
            custom field tags (`{{your_tag}}`), and `{{unsubscribe_link}}` - all replaced before
            segments are calculated, so billing reflects the resolved length. Contact and custom
            fields resolve from the recipient's contact record (auto-created unless
            `skipContactCreation: true`); unknown or unresolvable placeholders are replaced with an
            empty string. `{{unsubscribe_link}}` is replaced with a unique per-recipient opt-out URL
            and requires a contact - it is left as-is when `skipContactCreation: true` is used for a
            number that is not already a contact.
          example: "Hello! How can we help you today?"
        sendernameId:
          type: string
          description: ID or name of an approved SMS sender
          example: "60f1b2c3d4e5f6a7b8c9d0e2"
        campaignName:
          type: string
          maxLength: 100
          description: Campaign name for tracking
        skipContactCreation:
          type: boolean
          default: false
          description: Skip creating a contact record (message still logged with phone number)
        skipDisabled:
          type: boolean
          default: true
          description: Skip sending to disabled contacts
        scheduleAt:
          type: string
          description: ISO 8601 date-time for scheduling
          example: "2026-06-15T09:00:00"
        timeZone:
          type: string
          description: IANA timezone for scheduling
          example: "Europe/Copenhagen"

    SendSmsResponse:
      type: object
      properties:
        success:
          type: boolean
        messageId:
          type: string
          description: Queue job ID (immediate) or message record ID (scheduled)
        scheduled:
          type: boolean
          description: Whether the message was scheduled for later
        queued:
          type: boolean
          description: Whether the message was queued for immediate sending
        jobId:
          type: string
          description: Scheduled job ID (only when scheduled)
        smsSegments:
          type: integer
          description: Number of SMS segments the message will use
          example: 1
        smsEncoding:
          type: string
          enum: [GSM-7, Unicode]
          description: Character encoding used
          example: "GSM-7"
        creditsEstimated:
          type: integer
          description: Estimated credits (equals smsSegments, only for immediate sends)
          example: 1
        scheduleTime:
          type: string
          format: date-time
          description: Scheduled send time in UTC (only when scheduled)
        localScheduleTime:
          type: string
          description: Scheduled send time in local timezone (only when scheduled)
        effectiveTimezone:
          type: string
          description: Timezone used for scheduling (only when scheduled)
        timezoneSource:
          type: string
          enum: [request, user, default]
          description: Where the timezone was resolved from (only when scheduled)

    MessageContent:
      type: object
      description: |
        Message content varies by messageType:
        - **text**: `text` string + optional `suggestions` (max 11 chips)
        - **textBasic**: `text` string only (no suggestions, max 160 characters)
        - **richCard**: At least `title` or `description` required, optional `media` (omitted if no fileUrl), `cardActions` (max 4), optional `suggestions` (max 11)
        - **carousel**: `cardContents` array (max 10 cards, each requiring at least `title` or `description`, optional `media`) + optional `suggestions`
        - **media**: `url` + optional `caption` (sent as rich card if caption present), or `media` object + optional `suggestions`
        - **file**: `url` or `media` object with file URL (PDFs, documents, etc.)
        - **image**: `url` or `media` object with image URL + optional `caption`
        - **video**: `url` or `media` object with video URL + optional `caption`
        - **audio**: `url` or `media` object with audio URL

        **Merge fields:** text, card titles/descriptions and button/chip URLs support `{{merge_field}}`
        placeholders (contact/company fields, custom tags, and `{{unsubscribe_link}}`), replaced per
        recipient at send time. See "Merge Fields & Unsubscribe Link" in the API overview.
      properties:
        url:
          type: string
          format: uri
          maxLength: 2000
          description: Direct URL to media file (for media/image/video/audio/file). Alternative to using the media object.
        caption:
          type: string
          maxLength: 2000
          description: Caption text for media messages. When present, the message is sent as a rich card with the caption as description.
        text:
          type: string
          maxLength: 3072
          description: Text content (for text messages)
        title:
          type: string
          maxLength: 200
          description: Card title (for richCard). At least title or description is required.
        description:
          type: string
          maxLength: 2000
          description: Card description (for richCard). At least title or description is required.
        cardOrientation:
          type: string
          enum: [VERTICAL, HORIZONTAL]
          default: VERTICAL
          description: Card layout orientation (for richCard)
        imageAlignment:
          type: string
          enum: [LEFT, RIGHT]
          default: RIGHT
          description: Image alignment for horizontal cards (for richCard)
        media:
          $ref: '#/components/schemas/Media'
          description: Optional media (image/video). Omitted from payload if no fileUrl is set.
        cardActions:
          type: array
          maxItems: 4
          items:
            $ref: '#/components/schemas/CardAction'
          description: Card action buttons (max 4)
        cardContents:
          type: array
          minItems: 2
          maxItems: 10
          items:
            $ref: '#/components/schemas/CardContent'
          description: Carousel cards (min 2, max 10)
        suggestions:
          type: array
          maxItems: 11
          items:
            $ref: '#/components/schemas/Suggestion'
          description: Chip suggestions (max 11)

    Media:
      type: object
      properties:
        height:
          type: string
          enum: [SHORT, MEDIUM, TALL]
          description: Media height
        contentInfo:
          type: object
          properties:
            fileUrl:
              type: string
              format: uri
              maxLength: 2000
              description: URL to media file (HTTPS required, max 2000 chars)
            contentType:
              type: string
              enum: [image/jpeg, image/jpg, image/png, image/gif, video/h263, video/mp4, video/mpeg, video/webm]
              description: MIME type of the media file
        thumbnailUrl:
          type: string
          format: uri
          description: Placeholder image URL shown while media loads
        forceRefresh:
          type: boolean
          default: false
          description: Whether to force refresh the media from source

    CardAction:
      type: object
      description: >-
        Button/chip action. This schema is shared between card buttons (cardActions) and action
        chips (suggestions), but not every type is valid on both surfaces. Card-only types:
        postback (on chips, use a reply with postbackData instead) and addToCalendar (with
        calendarAction). Chip-only types: createCalendarEvent (with createCalendarEventAction)
        and unsubscribe. All other types are valid on both.
      required:
        - text
        - type
      properties:
        text:
          type: string
          maxLength: 25
          description: Button text
        type:
          type: string
          enum: [postback, openUrl, openUrlInWebview, dial, addToCalendar, createCalendarEvent, viewLocation, shareLocation, unsubscribeLink, unsubscribe]
          description: >-
            postback and addToCalendar are valid on card buttons only; createCalendarEvent and
            unsubscribe are valid on chips only. All other types are valid on both.
        postbackData:
          type: string
          maxLength: 2048
          description: Data sent to webhook on click
        webhookUrl:
          type: string
          format: uri
          description: Webhook URL for postback actions
        openUrlAction:
          type: object
          properties:
            url:
              type: string
              format: uri
        openUrlInWebviewAction:
          type: object
          description: Opens URL in messaging app webview (not external browser)
          properties:
            url:
              type: string
              format: uri
              description: URL to open in the webview
            description:
              type: string
              description: Optional accessibility description
            viewMode:
              type: string
              enum: [FULL, TALL, HALF]
              default: FULL
              description: Size of the webview window
        dialAction:
          type: object
          properties:
            phoneNumber:
              type: string
        calendarAction:
          type: object
          description: "Calendar action for card buttons (type: addToCalendar). Card-only - chips use createCalendarEventAction with type createCalendarEvent instead."
          properties:
            startTime:
              type: string
              format: date-time
            endTime:
              type: string
              format: date-time
            title:
              type: string
            description:
              type: string
        createCalendarEventAction:
          type: object
          description: "Calendar action for chips (type: createCalendarEvent). Chip-only - card buttons use calendarAction with type addToCalendar instead."
          properties:
            startTime:
              type: string
              format: date-time
            endTime:
              type: string
              format: date-time
            title:
              type: string
            description:
              type: string
        viewLocationAction:
          type: object
          properties:
            latLong:
              type: object
              properties:
                latitude:
                  type: number
                longitude:
                  type: number
            label:
              type: string
        shareLocationAction:
          type: object
          description: Prompts user to share their current location (no properties required)
        unsubscribeAction:
          type: object
          description: "Unsubscribe action — sends a stop keyword as a reply to trigger the unsubscribe flow. Chip-only (not available as a card action). The stopKeyword is auto-corrected at preview time to match the active trigger configuration for the sendername."
          properties:
            stopKeyword:
              type: string
              default: stop
              description: "The keyword sent as a reply when the recipient taps the chip. Auto-corrected by the server to match the active trigger's configured keyword."

    CardContent:
      type: object
      description: Single card in a carousel (VERTICAL orientation only). At least title or description is required. Media is optional and omitted from payload if no fileUrl is set.
      properties:
        title:
          type: string
          maxLength: 200
          description: Card title. At least title or description is required.
        description:
          type: string
          maxLength: 2000
          description: Card description. At least title or description is required.
        media:
          $ref: '#/components/schemas/Media'
          description: Optional media (image/video). Omitted from payload if no fileUrl is set.
        cardActions:
          type: array
          maxItems: 4
          items:
            $ref: '#/components/schemas/CardAction'

    Suggestion:
      type: object
      description: >-
        Chip suggestion (reply button or action). Action chips use the CardAction shape,
        so an openUrlInWebview chip supports openUrlInWebviewAction.viewMode (FULL, TALL, or HALF)
        exactly like an openUrlInWebview card action - viewMode is not card-only. Exceptions:
        postback and addToCalendar are card-only (use a reply chip with postbackData, and
        createCalendarEvent with createCalendarEventAction, respectively); the unsubscribe
        stop-keyword type is chip-only.
      properties:
        reply:
          type: object
          properties:
            text:
              type: string
              maxLength: 25
            postbackData:
              type: string
              maxLength: 2048
            webhookUrl:
              type: string
              format: uri
        action:
          $ref: '#/components/schemas/CardAction'
          description: >-
            Action chip. Uses the same shape as a card action, so openUrlInWebview chips support
            openUrlInWebviewAction.viewMode (FULL/TALL/HALF) to control the webview height.

    SmsFallback:
      type: object
      description: SMS fallback when RCS is unavailable
      required:
        - message
        - sendernameId
      properties:
        message:
          type: string
          maxLength: 1530
          description: |
            SMS fallback text. Supports the same merge fields as the RCS body (contact, company and
            custom fields, `{{today}}`, `{{unsubscribe_link}}`), substituted per recipient when the
            fallback is sent. `{{unsubscribe_link}}` becomes a unique per-recipient opt-out URL when
            the recipient is a contact.
        sendernameId:
          type: string
          description: SMS sender ID

    SendMessageResponse:
      type: object
      properties:
        success:
          type: boolean
          description: |
            `true` when the message was handed to the network (RCS) or queued for sending
            (SMS fallback). This is an acceptance result, not a delivery result: use status
            webhooks or `GET /api/messages/log` for the final delivery status.
        messageId:
          type: string
          nullable: true
          description: Network message ID for RCS sends. Null when the SMS fallback was used (the SMS is queued).
        messageRecordId:
          type: string
          nullable: true
          description: |
            Internal message record ID. Use this to correlate with status webhook and button click
            webhook events and to look the message up in `GET /api/messages/log?messageId=`.
            When the SMS fallback was used this is the ID of the RCS record the fallback belongs to.
        method:
          type: string
          enum: [RCS, SMS_FALLBACK]
          description: Channel actually used. `SMS_FALLBACK` when the SMS fallback was queued instead of RCS.
        scheduled:
          type: boolean
        queued:
          type: boolean
          description: "`true` when the message (SMS fallback) is queued and will be sent asynchronously."
        creditsUsed:
          type: number
          description: Credits charged for an RCS send. For a queued SMS fallback this equals `estimatedCredits`; the actual charge happens when the SMS is sent.
        estimatedCredits:
          type: number
          description: Credits charged (RCS) or expected to be charged (queued SMS fallback). `0` when nothing will be charged.
        fallbackUsed:
          type: boolean
          description: "`true` when the SMS fallback was sent instead of RCS."
        fallbackWillBeUsed:
          type: boolean
        fallbackReason:
          type: string
          nullable: true
          description: |
            Why the fallback (or no fallback) was used, e.g. `COUNTRY_NOT_SUPPORTED`,
            `COUNTRY_BLOCKED`, `PROVIDER_ERROR`, `NO_FALLBACK_COUNTRY_NOT_SUPPORTED`,
            `NO_FALLBACK_COUNTRY_BLOCKED`, `NO_FALLBACK_PROVIDER_ERROR`, or
            `SMS_COUNTRY_NOT_SUPPORTED` when neither RCS nor SMS is available for the
            destination (then `success` is `false` and nothing is sent or charged).
        hasInteractiveButtons:
          type: boolean
        error:
          type: string
          nullable: true
          description: Error details when `success` is `false`.
        errorType:
          type: string
          nullable: true
          description: Error category when `success` is `false` (e.g. `INVALID_PHONE_NUMBER`, `VALIDATION_ERROR`).
        countrySupported:
          type: boolean
          description: |
            `false` when RCS is not available for the destination country (not in the price
            list, or disabled in your account's country settings). The message is then either
            sent as SMS fallback (`fallbackUsed: true`) or fails (`success: false`).
        countryError:
          type: string
          nullable: true
          description: |
            Explanation when `countrySupported` is `false`. When SMS is not available for the
            destination either, the request fails synchronously (`success: false`,
            `fallbackReason: SMS_COUNTRY_NOT_SUPPORTED`) and this explains both.
        conversation:
          $ref: '#/components/schemas/ConversationInfo'

    BatchFiltering:
      type: object
      description: Contact filtering summary for a batch request
      properties:
        totalOriginal:
          type: integer
        totalEnabled:
          type: integer
        totalDisabled:
          type: integer
        totalInvalid:
          type: integer
        totalTestPhones:
          type: integer
        skipDisabled:
          type: boolean
        disabledContacts:
          type: array
          items:
            type: string
          description: Disabled phone numbers (first 50)
        invalidPhoneNumbers:
          type: array
          items:
            type: string
          description: Invalid phone numbers (first 50)
        message:
          type: string
        testPhoneMessage:
          type: string
          nullable: true

    SendBatchResponse:
      type: object
      description: Returned with HTTP 202 when the batch is queued for immediate background processing
      properties:
        success:
          type: boolean
        scheduled:
          type: boolean
          example: false
        queued:
          type: boolean
          example: true
        totalQueued:
          type: integer
        totalPhoneNumbers:
          type: integer
        estimatedDurationSeconds:
          type: number
        estimatedCompletionTime:
          type: string
          format: date-time
        message:
          type: string
        firstJobId:
          type: string
          description: First queued job ID (batch progress is tracked via /queue/status)
        totalJobs:
          type: integer
        hasInteractiveButtons:
          type: boolean
        warnings:
          type: array
          items:
            type: string
        filtering:
          $ref: '#/components/schemas/BatchFiltering'
        tracking:
          type: object
          properties:
            info:
              type: string
            statusEndpoint:
              type: string
              example: "/api/rcs/queue/status"
            pollingRecommendation:
              type: string

    SendBatchScheduledResponse:
      type: object
      description: Returned with HTTP 200 when scheduleAt is provided. Use jobId with /batch/{jobId}/status and DELETE /batch/{jobId}.
      properties:
        success:
          type: boolean
        scheduled:
          type: boolean
          example: true
        jobId:
          type: string
        totalRecipients:
          type: integer
        scheduleTime:
          type: string
          format: date-time
        localScheduleTime:
          type: string
        effectiveTimezone:
          type: string
        timezoneSource:
          type: string
        smsFallbackEnabled:
          type: boolean
        hasInteractiveButtons:
          type: boolean
        warnings:
          type: array
          items:
            type: string
        filtering:
          $ref: '#/components/schemas/BatchFiltering'

    ConversationInfo:
      type: object
      properties:
        conversationId:
          type: string
        chargedMessage:
          type: boolean
        freeMessage:
          type: boolean
        billingMode:
          type: string
          enum: [per_message, auto]
          description: Billing mode used for this message (per_message senders always charge per message, auto uses conversation windows)
        status:
          type: string
          enum: [NEW, ACTIVE, PER_MESSAGE]
          description: PER_MESSAGE when sender uses per-message billing (no conversation window applies)
        remainingTime:
          type: string
          nullable: true
          description: Remaining conversation window time. Null for per-message billing senders.
        messageCount:
          type: object
          properties:
            total:
              type: integer
            outbound:
              type: integer
            inbound:
              type: integer

    ConversationListItem:
      type: object
      properties:
        conversationId:
          type: string
        phoneNumber:
          type: string
          example: "+4512345678"
        contact:
          type: object
          nullable: true
          properties:
            _id:
              type: string
            name:
              type: string
            phone:
              type: string
            email:
              type: string
        status:
          type: string
          enum: [ACTIVE, EXPIRED]
          description: ACTIVE while the 24-hour window is open, EXPIRED once it has passed
        isActive:
          type: boolean
        messageCount:
          type: object
          properties:
            total:
              type: integer
            outbound:
              type: integer
            inbound:
              type: integer
        creditsCharged:
          type: number
        freeMessagesUsed:
          type: integer
        initiatedBy:
          type: string
          enum: [BUSINESS, USER]
          description: Who opened the current window. USER means the contact messaged first.
        sessionStarted:
          type: string
          format: date-time
        sessionExpires:
          type: string
          format: date-time
        lastMessage:
          type: string
          format: date-time
        remainingTime:
          type: integer
          description: Milliseconds left in the window. 0 when expired.

    ConversationStatus:
      type: object
      properties:
        success:
          type: boolean
        phoneNumber:
          type: string
        hasActiveConversation:
          type: boolean
          description: Whether there is an active 24-hour session window
        conversation:
          type: object
          nullable: true
          description: Conversation details (null when no active conversation)
          properties:
            id:
              type: string
              description: Conversation ID
            senderId:
              type: string
              nullable: true
              description: RCS sender ID used for this conversation
            conversationType:
              type: string
              enum: [MARKETING, AUTHENTICATION, SERVICE, UTILITY, USER_INITIATED]
            pricingModel:
              type: string
              enum: [PER_MESSAGE, CONVERSATION_BASED]
            startedAt:
              type: string
              format: date-time
            expiresAt:
              type: string
              format: date-time
            remainingTime:
              type: string
              description: Human-readable remaining time (e.g. "23h 57m")
            messageCount:
              type: integer
              description: Total messages in this conversation
            creditsCharged:
              type: number
              description: Credits charged for this conversation
            isCharged:
              type: boolean
              description: Whether conversation billing has been applied

    TimezonesResponse:
      type: object
      properties:
        success:
          type: boolean
        user:
          type: object
          properties:
            configuredTimezone:
              type: string
            currentLocalTime:
              type: string
            currentUtcTime:
              type: string
        timezoneOptions:
          type: array
          items:
            type: object
            properties:
              timezone:
                type: string
              localTime:
                type: string
              offset:
                type: string
              isCurrent:
                type: boolean

    ValidateScheduleResponse:
      type: object
      properties:
        success:
          type: boolean
        valid:
          type: boolean
        input:
          type: object
          properties:
            scheduleAt:
              type: string
            providedTimezone:
              type: string
            wasAbsoluteTime:
              type: boolean
        parsed:
          type: object
          properties:
            scheduleTime:
              type: string
            localScheduleTime:
              type: string
            effectiveTimezone:
              type: string
            timezoneSource:
              type: string
            isInFuture:
              type: boolean

    ValidateMessageRequest:
      type: object
      properties:
        messageType:
          type: string
        content:
          $ref: '#/components/schemas/MessageContent'

    ValidationResult:
      type: object
      properties:
        valid:
          type: boolean
          description: Overall result (message and webhook validation combined)
        messageValidation:
          type: object
          properties:
            valid:
              type: boolean
            errors:
              type: array
              items:
                type: string
            warnings:
              type: array
              items:
                type: string
        webhookValidation:
          type: object
          properties:
            valid:
              type: boolean
            errors:
              type: array
              items:
                type: string
        hasInteractiveButtons:
          type: boolean
        suggestionLimits:
          type: object
          properties:
            total:
              type: integer
              example: 11
            textMaxLength:
              type: integer
              example: 25

    # ───────────────────────────────────────────────────────────────
    # Sandbox schemas
    # ───────────────────────────────────────────────────────────────

    SandboxRequest:
      type: object
      description: |
        The body of `POST /send` (or `/send-batch`). Only `messageType` and `content` are used;
        every other field is accepted so you can post the exact body you will send later, and is
        reported back under `sandbox.ignored_fields`.
      required:
        - messageType
        - content
      properties:
        messageType:
          type: string
          enum: [text, textBasic, richCard, carousel, media, file, image, video, audio]
        content:
          $ref: '#/components/schemas/MessageContent'
        smsFallback:
          type: object
          description: Only `message` is used (shown under the phone); `sendernameId` is ignored.
          properties:
            message:
              type: string
              maxLength: 1530
            sendernameId:
              type: string
        phoneNumber:
          type: string
          description: Accepted and ignored
        phoneNumbers:
          type: array
          items:
            type: string
          description: Accepted and ignored
        sendernameId:
          type: string
          description: Accepted and ignored
        contactId:
          type: string
          description: Accepted and ignored
        campaignName:
          type: string
          description: Accepted and ignored
        scheduleAt:
          type: string
          description: Accepted and ignored
        timeZone:
          type: string
          description: Accepted and ignored
        skipDisabled:
          type: boolean
          description: Accepted and ignored
        skipContactCreation:
          type: boolean
          description: Accepted and ignored
        batchSize:
          type: integer
          description: Accepted and ignored
        delayMs:
          type: integer
          description: Accepted and ignored

    SandboxMediaReport:
      type: object
      description: One media slot found in the content and what the preview page renders for it.
      properties:
        path:
          type: string
          example: content.media.contentInfo.fileUrl
        original:
          type: string
          description: The URL you sent (only in POST responses)
        allow_listed:
          type: boolean
          description: True when the URL is one of the sample media files and is rendered as is
        rendered:
          type: string
          nullable: true
          description: The URL shown on the preview page; `null` when a thumbnail was removed
        original_host:
          type: string
          nullable: true
          description: Host of a foreign URL (only in GET /sandbox/preview/{slug})

    SandboxInfo:
      type: object
      properties:
        nothing_sent:
          type: boolean
          example: true
        ignored_fields:
          type: array
          items:
            type: string
          example: [phoneNumber, sendernameId]
        unknown_fields:
          type: array
          items:
            type: string
          description: Top-level fields that are neither sandbox nor send fields (usually typos)
        media:
          type: array
          items:
            $ref: '#/components/schemas/SandboxMediaReport'
        next_step:
          type: string
        samples_url:
          type: string
          format: uri
        docs_url:
          type: string
          format: uri
        signup_url:
          type: string
          format: uri
        note:
          type: string

    SandboxValidateResponse:
      type: object
      properties:
        success:
          type: boolean
          example: true
        valid:
          type: boolean
        errors:
          type: array
          items:
            type: string
        warnings:
          type: array
          items:
            type: string
        messageType:
          type: string
        normalizedContent:
          description: Your content after defaults were applied (for example `media.height` = TALL), with your original URLs
          $ref: '#/components/schemas/MessageContent'
        suggestionLimits:
          type: object
          properties:
            total:
              type: integer
              example: 11
            textMaxLength:
              type: integer
              example: 25
        limits:
          type: object
          description: Every content limit the validator enforces (same as `GET /limits`)
        hasChipSuggestions:
          type: boolean
        hasCardActions:
          type: boolean
        buttonAnalysis:
          type: object
          properties:
            cardActions:
              type: array
              items:
                type: object
            chipSuggestions:
              type: array
              items:
                type: object
            recommendations:
              type: array
              items:
                type: string
        sandbox:
          $ref: '#/components/schemas/SandboxInfo'

    SandboxPreviewResponse:
      allOf:
        - $ref: '#/components/schemas/SandboxValidateResponse'
        - type: object
          properties:
            preview:
              type: object
              properties:
                preview_id:
                  type: string
                  description: Prefixed `sbx_`; cannot be used to send
                  example: sbx_Qm9vdGhpc2lzYXNsdWcxMjM0
                share_url:
                  type: string
                  format: uri
                  example: https://app.sendrcs.eu/sandbox/Qm9vdGhpc2lzYXNsdWcxMjM0
                expires_at:
                  type: string
                  format: date-time
                summary:
                  type: string
                deduplicated:
                  type: boolean
                  description: True when identical input already had a preview (its expiry is kept)
                sendable:
                  type: boolean
                  example: false

    SandboxValidationFailedResponse:
      type: object
      description: Same body as a failed `POST /send`, plus the `sandbox` block.
      properties:
        success:
          type: boolean
          example: false
        message:
          type: string
          example: RCS message validation failed
        errors:
          type: array
          items:
            type: string
        warnings:
          type: array
          items:
            type: string
        buttonAnalysis:
          type: object
        sandbox:
          $ref: '#/components/schemas/SandboxInfo'

    SandboxRateLimited:
      type: object
      properties:
        success:
          type: boolean
          example: false
        code:
          type: string
          example: SANDBOX_RATE_LIMITED
        message:
          type: string
        retry_after_seconds:
          type: integer

    SandboxSamplesResponse:
      type: object
      properties:
        success:
          type: boolean
        base_url:
          type: string
          format: uri
          example: https://sendrcs.eu/images/rcsexamples/
        placeholder_url:
          type: string
          format: uri
          description: What non-allow-listed images are replaced with on the preview page
        samples:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
                example: shoes-1
              url:
                type: string
                format: uri
              contentType:
                type: string
                example: image/jpeg
              kind:
                type: string
                enum: [image, video, file]
              label:
                type: string
        usage:
          type: object
          description: Where to put a sample URL for each message type
        policy:
          type: string

    SandboxSharedPreview:
      type: object
      properties:
        preview_id:
          type: string
        sandbox:
          type: boolean
          example: true
        message_type:
          type: string
        content:
          description: Rendered content (placeholders applied)
          $ref: '#/components/schemas/MessageContent'
        sms_fallback:
          type: object
          nullable: true
          properties:
            message:
              type: string
        summary:
          type: string
        warnings:
          type: array
          items:
            type: string
        media:
          type: array
          items:
            $ref: '#/components/schemas/SandboxMediaReport'
        created_at:
          type: string
          format: date-time
        expires_at:
          type: string
          format: date-time
        expires_in_hours:
          type: integer
        agent:
          type: object
          properties:
            name:
              type: string
              example: SendRCS

    ButtonGuidelines:
      type: object
      properties:
        maxCardActions:
          type: integer
          example: 4
        maxChipSuggestions:
          type: integer
          example: 11
        maxButtonTextLength:
          type: integer
          example: 25
        maxTextLength:
          type: integer
          example: 3072
        maxPostbackDataLength:
          type: integer
          example: 2048
        maxPayloadSizeBytes:
          type: integer
          example: 256000
          description: Maximum JSON payload size (250 KB)
        maxMediaUrlLength:
          type: integer
          example: 2000
        maxMediaFileSizeBytes:
          type: integer
          example: 104857600
          description: Maximum media file size (100 MB)
        supportedActionTypes:
          type: array
          items:
            type: string
          example: ["postback", "openUrl", "openUrlInWebview", "dial", "addToCalendar", "createCalendarEvent", "viewLocation", "shareLocation", "unsubscribeLink", "unsubscribe"]

    BatchJobStatus:
      type: object
      properties:
        jobId:
          type: string
        status:
          type: string
          enum: [pending, processing, completed, failed, cancelled]
        progress:
          type: object
          properties:
            total:
              type: integer
            processed:
              type: integer
            successful:
              type: integer
            failed:
              type: integer
        createdAt:
          type: string
          format: date-time
        completedAt:
          type: string
          format: date-time

    WebhookPayload:
      type: object
      description: Payload sent to your webhook URL when a button is clicked
      properties:
        event:
          type: string
          example: "rcs_button_clicked"
        timestamp:
          type: string
          format: date-time
        interaction:
          type: object
          properties:
            id:
              type: string
              description: Unique interaction ID
            type:
              type: string
              example: "button_click"
            buttonText:
              type: string
            postbackData:
              type: string
            clickedAt:
              type: string
              format: date-time
              description: When the button was clicked
        message:
          type: object
          properties:
            id:
              type: string
              description: Message record ID. Matches `messageRecordId` from the /send response.
            campaignName:
              type: string
              nullable: true
              description: Campaign name from the original message, if set
            sentAt:
              type: string
              format: date-time
              description: When the message was originally sent
            rcsData:
              type: object
              properties:
                messageType:
                  type: string
                brandId:
                  type: string
        contact:
          type: object
          properties:
            id:
              type: string
            phone:
              type: string
            name:
              type: string
        user:
          type: object
          properties:
            id:
              type: string
            businessName:
              type: string
              nullable: true
        metadata:
          type: object
          properties:
            conversationId:
              type: string
              nullable: true
            platform:
              type: string
              example: "RCS"

    StatusWebhookPayload:
      type: object
      description: Payload sent to your status webhook URL on delivery status changes
      properties:
        event:
          type: string
          enum: [rcs_status, sms_status]
          description: Event type (rcs_status for RCS messages, sms_status for SMS Fallback messages)
        messageId:
          type: string
          description: Message record ID. Matches the `messageRecordId` from the /send response.
        status:
          type: string
          enum: [sent, delivered, read, failed]
          description: Current delivery status
        phoneNumber:
          type: string
          description: Recipient phone number
        contactId:
          type: string
          nullable: true
          description: Contact ID if the recipient is a known contact. Null when skipContactCreation was used and no contact exists.
        campaignName:
          type: string
          nullable: true
          description: Campaign name from the original message, if set
        timestamp:
          type: string
          format: date-time
        error:
          type: string
          nullable: true
          description: Error message when status is failed
        test:
          type: boolean
          description: Whether this is a test webhook (true when triggered via test endpoint)

    RcsIncomingWebhookPayload:
      type: object
      description: Payload sent to your incoming RCS webhook URL when a contact messages one of your RCS sender names
      properties:
        event:
          type: string
          example: "rcs_incoming"
        timestamp:
          type: string
          format: date-time
        rcs:
          type: object
          properties:
            id:
              type: string
              description: Message record ID of the inbound message
            from:
              type: string
              description: Sender phone number (the contact)
            to:
              type: string
              description: Your RCS agent ID that received the message
            message:
              type: string
              description: Text content, or a short description for media/location messages
            messageType:
              type: string
              enum: [text, image, video, audio, file, vcard, location, button]
              description: What kind of message arrived
            isButtonReply:
              type: boolean
              description: True when the message is a tap on a suggested reply/button
            buttonText:
              type: string
              nullable: true
              description: Button label, only present when isButtonReply is true
            postbackData:
              type: string
              nullable: true
              description: Button postback data, only present when isButtonReply is true
            media:
              type: object
              nullable: true
              description: Present for image/video/audio/file/vcard messages
              properties:
                type:
                  type: string
                  description: Media kind (matches messageType)
                name:
                  type: string
                  nullable: true
                  description: Original file name, when available
                url:
                  type: string
                  description: Relative SendRCS API path to download the media (requires an authenticated session)
                expiresAt:
                  type: string
                  format: date-time
                  nullable: true
                  description: When the media stops being available (48 hours after receipt)
            location:
              type: object
              nullable: true
              description: Present for shared locations
              properties:
                latitude:
                  type: number
                longitude:
                  type: number
                label:
                  type: string
                  nullable: true
            receivedAt:
              type: string
              format: date-time
        contact:
          type: object
          properties:
            id:
              type: string
            name:
              type: string
            phone:
              type: string
            email:
              type: string
              nullable: true
        sendername:
          type: object
          properties:
            id:
              type: string
              description: Sender name ID
            name:
              type: string
              description: Sender name display name
            agentId:
              type: string
              description: RCS agent ID of the sender name
        user:
          type: object
          properties:
            id:
              type: string
            email:
              type: string
        metadata:
          type: object
          properties:
            messageType:
              type: string
              example: "RCS"
            direction:
              type: string
              example: "INBOUND"
            platform:
              type: string
              example: "RCS"
            provider:
              type: string
              example: "SendRCS"
            conversationId:
              type: string
              nullable: true
              description: RCS conversation this message belongs to

    SmsIncomingWebhookPayload:
      type: object
      description: Payload sent to your incoming SMS webhook URL when someone texts one of your virtual numbers
      properties:
        event:
          type: string
          example: "sms_incoming"
        timestamp:
          type: string
          format: date-time
        sms:
          type: object
          properties:
            id:
              type: string
              description: Message record ID of the inbound message
            from:
              type: string
              description: Sender phone number (the contact)
            to:
              type: string
              description: Your virtual number that received the message
            message:
              type: string
              description: SMS text content
            encoding:
              type: string
              example: "GSM-7"
              description: SMS encoding (GSM-7 or UCS-2)
            segments:
              type: integer
              description: Number of SMS segments
            receivedAt:
              type: string
              format: date-time
        contact:
          type: object
          properties:
            id:
              type: string
            name:
              type: string
            phone:
              type: string
            email:
              type: string
              nullable: true
        virtualNumber:
          type: object
          properties:
            id:
              type: string
              description: Virtual number ID
            phoneNumber:
              type: string
              description: The virtual number in E.164 format
            subscriptionId:
              type: string
              description: Your subscription ID for this virtual number
        user:
          type: object
          properties:
            id:
              type: string
            email:
              type: string
        metadata:
          type: object
          properties:
            messageType:
              type: string
              example: "SMS"
            direction:
              type: string
              example: "INBOUND"
            platform:
              type: string
              example: "SMS"

    FormSubmissionWebhookPayload:
      type: object
      description: Payload sent to your form webhook URL when a visitor submits the form (before opt-in confirmation)
      properties:
        event:
          type: string
          example: "form_submission"
        timestamp:
          type: string
          format: date-time
        form:
          type: object
          properties:
            id:
              type: string
              description: Form ID
            name:
              type: string
              description: Internal form name
            title:
              type: string
              nullable: true
              description: Public form title
            slug:
              type: string
              description: Public form ID used in the form URL
        submission:
          type: object
          properties:
            id:
              type: string
              nullable: true
              description: Submission record ID
            action:
              type: string
              enum: [created, updated]
              description: Whether a new contact was created or the phone number matched an existing contact
            submittedAt:
              type: string
              format: date-time
            ip:
              type: string
              description: Visitor IP address
        optIn:
          type: object
          properties:
            status:
              type: string
              enum: [PENDING]
              description: Always PENDING at submit time — the contact is not activated until the visitor confirms
            confirmationSent:
              type: boolean
              description: False when the form's "submit once" throttle suppressed a repeat opt-in message
            confirmedAt:
              type: string
              format: date-time
              nullable: true
              description: Always null for this event
        contact:
          type: object
          properties:
            id:
              type: string
            phone:
              type: string
            optInStatus:
              type: string
              enum: [PENDING, CONFIRMED]
              description: The contact's current stored opt-in status (CONFIRMED when an already-confirmed contact re-submits)
        data:
          type: object
          description: Exactly what the visitor submitted. For existing contacts these values are not written to the contact until confirmation.
          properties:
            phone:
              type: string
            firstName:
              type: string
            lastName:
              type: string
            email:
              type: string
            customFields:
              type: array
              items:
                type: object
                properties:
                  id:
                    type: string
                    description: Custom field ID
                  label:
                    type: string
                    description: Field label as shown on the form
                  value:
                    type: string
                    description: Submitted value
        user:
          type: object
          properties:
            id:
              type: string
        test:
          type: boolean
          description: Whether this is a test webhook (true when triggered via test endpoint)

    FormOptInConfirmedWebhookPayload:
      type: object
      description: Payload sent to your form webhook URL when the visitor confirms the double opt-in (final opt-in)
      properties:
        event:
          type: string
          example: "form_optin_confirmed"
        timestamp:
          type: string
          format: date-time
        form:
          type: object
          properties:
            id:
              type: string
              description: Form ID
            name:
              type: string
              description: Internal form name
            title:
              type: string
              nullable: true
              description: Public form title
            slug:
              type: string
              description: Public form ID used in the form URL
        optIn:
          type: object
          properties:
            status:
              type: string
              enum: [CONFIRMED]
              description: Always CONFIRMED for this event
            confirmedAt:
              type: string
              format: date-time
              description: When the visitor tapped the confirmation link
            submittedAt:
              type: string
              format: date-time
              description: When the original form submission happened
            dataApplied:
              type: boolean
              description: Whether the submitted data and group memberships were written to the contact ("Update contact" setting, or a new contact)
        contact:
          type: object
          properties:
            id:
              type: string
            phone:
              type: string
            firstName:
              type: string
              nullable: true
            lastName:
              type: string
              nullable: true
            email:
              type: string
              nullable: true
            optInStatus:
              type: string
              enum: [CONFIRMED]
        data:
          type: object
          description: The originally submitted values, including custom fields with their labels
          properties:
            phone:
              type: string
            firstName:
              type: string
            lastName:
              type: string
            email:
              type: string
            customFields:
              type: array
              items:
                type: object
                properties:
                  id:
                    type: string
                    description: Custom field ID
                  label:
                    type: string
                    description: Field label as shown on the form
                  value:
                    type: string
                    description: Submitted value
        user:
          type: object
          properties:
            id:
              type: string

    MessageLogResponse:
      type: object
      properties:
        messages:
          type: array
          items:
            $ref: '#/components/schemas/MessageLogEntry'
        pagination:
          type: object
          properties:
            nextCursor:
              type: string
              nullable: true
              description: Base64 cursor for the next page. Null when there are no more results.
            hasNext:
              type: boolean
              description: Whether more results are available
            limit:
              type: integer
              description: The limit that was applied
            count:
              type: integer
              description: Number of messages in this response

    MessageLogEntry:
      type: object
      properties:
        id:
          type: string
          description: Message record ID
        type:
          type: string
          enum: [SMS, RCS, MMS]
          description: Message type
        status:
          type: string
          enum: [SCHEDULED, SENDING, SENT, DELIVERED, READ, RECEIVED, FAILED, BLOCKED, REJECTED]
          description: Current delivery status
        direction:
          type: string
          enum: [OUTBOUND, INBOUND]
          description: Message direction
        source:
          type: string
          enum: [EVENT_REMINDER, MESSAGE_FLOW, DIRECT_MESSAGE, API_MESSAGE, API_SMS, RCS_INBOX_REPLY, SMS_INBOX_REPLY]
          description: How the message was created (API_SMS for messages sent via /send-sms)
        phoneNumber:
          type: string
          description: Recipient phone number
          example: "+4512345678"
        content:
          type: string
          description: Message text content
        campaignName:
          type: string
          nullable: true
          description: Campaign name if set
        sendernameId:
          type: string
          nullable: true
          description: ID of the sender name used for this message
        smsFallback:
          type: object
          nullable: true
          description: SMS fallback details if fallback was used
        creditsCost:
          type: number
          nullable: true
          description: Credits charged for this message
        createdAt:
          type: string
          format: date-time
          description: When the message record was created
        sentAt:
          type: string
          format: date-time
          nullable: true
          description: When the message was sent
        deliveredAt:
          type: string
          format: date-time
          nullable: true
          description: When the message was delivered
        readAt:
          type: string
          format: date-time
          nullable: true
          description: When the message was read
        scheduledAt:
          type: string
          format: date-time
          nullable: true
          description: Scheduled send time (for scheduled messages)
        failedAt:
          type: string
          format: date-time
          nullable: true
          description: When the message failed
        error:
          type: string
          nullable: true
          description: |
            Failure reason when `status` is FAILED, BLOCKED or REJECTED (for example
            "Country not supported", or a delivery failure reported by the recipient's
            network). Null for messages that did not fail. The same text is sent in
            the `error` field of status webhooks.

    MessageContentResponse:
      type: object
      properties:
        message:
          $ref: '#/components/schemas/MessageContentDetail'

    MessageContentDetail:
      type: object
      description: |
        Full reusable content of a message. The shape of `content` depends on `type`:
        a structured object for RCS, a plain string for SMS/MMS.
      properties:
        id:
          type: string
          description: Message record ID
        type:
          type: string
          enum: [SMS, RCS, MMS]
          description: Message type
        status:
          type: string
          enum: [SCHEDULED, SENDING, SENT, DELIVERED, READ, RECEIVED, FAILED, BLOCKED, REJECTED]
          description: Current delivery status
        direction:
          type: string
          enum: [OUTBOUND, INBOUND]
          description: Message direction
        source:
          type: string
          enum: [EVENT_REMINDER, MESSAGE_FLOW, DIRECT_MESSAGE, API_MESSAGE, API_SMS, RCS_INBOX_REPLY, SMS_INBOX_REPLY]
          description: How the message was created
        phoneNumber:
          type: string
          description: Recipient phone number
          example: "+4512345678"
        messageType:
          type: string
          nullable: true
          enum: [text, textBasic, richCard, carousel, media, file, image, video, audio]
          description: RCS message type. Null for SMS/MMS. Pass this to the RCS preview/send endpoints to recreate the message.
        content:
          nullable: true
          description: |
            Reusable message content. For RCS this is the structured content object
            (matching the RCS send `content` field). For SMS/MMS this is the message text string.
          oneOf:
            - $ref: '#/components/schemas/MessageContent'
            - type: string
              description: SMS message text
        text:
          type: string
          nullable: true
          description: Flat text representation — the SMS body, or the RCS fallback/display text.
        error:
          type: string
          nullable: true
          description: Failure reason when the message failed (same text as in the message log and status webhooks). Null otherwise.
        smsFallback:
          type: object
          nullable: true
          description: SMS fallback configuration set on the original RCS message (null if none). RCS only.
          properties:
            message:
              type: string
              description: SMS fallback message text
            sendernameId:
              type: string
              nullable: true
              description: Sender name used for the fallback SMS
        smsSegments:
          type: integer
          nullable: true
          description: SMS segment count. SMS/MMS only.
        smsEncoding:
          type: string
          nullable: true
          enum: [GSM-7, Unicode]
          description: SMS encoding. SMS/MMS only.
        campaignName:
          type: string
          nullable: true
          description: Campaign name if set
        sendernameId:
          type: string
          nullable: true
          description: ID of the sender name used for this message
        createdAt:
          type: string
          format: date-time
          description: When the message record was created
        sentAt:
          type: string
          format: date-time
          nullable: true
          description: When the message was sent

    Sendername:
      type: object
      properties:
        id:
          type: string
          description: Use this as sendernameId when sending
        name:
          type: string
        type:
          type: string
          enum: [SMS, RCS]
        status:
          type: string
          enum: [DRAFT, PENDING, APPROVED, REJECTED]
        isDefault:
          type: boolean
        isTest:
          type: boolean
        supportsConversationBilling:
          type: boolean
        rejectionReason:
          type: string
          description: Present only when status is REJECTED
        testPhones:
          type: array
          items:
            type: string
          description: Present only for approved test senders

    ErrorResponse:
      type: object
      properties:
        message:
          type: string
        error:
          type: string
        errors:
          type: array
          items:
            type: object
            properties:
              msg:
                type: string
              param:
                type: string
              location:
                type: string

    # ───────────────────────────────────────────────────────────────
    # Contacts API Schemas
    # ───────────────────────────────────────────────────────────────

    Contact:
      type: object
      properties:
        id:
          type: string
          description: Contact ID
        firstName:
          type: string
          description: First name (max 100 characters)
        lastName:
          type: string
          description: Last name (max 100 characters)
        phone:
          type: string
          description: Phone number with country code, stored as digits only (no `+`/`00` prefix). Unique per account.
          example: "4512345678"
        email:
          type: string
          nullable: true
          description: Email address
        disabled:
          type: boolean
          description: Whether the contact is soft-deleted (disabled contacts cannot receive messages)
        groups:
          type: array
          items:
            type: object
            properties:
              id:
                type: string
              name:
                type: string
          description: Groups the contact belongs to
        createdAt:
          type: string
          format: date-time
        customFields:
          type: array
          description: Only present when `?include=customFields` is requested
          items:
            $ref: '#/components/schemas/ContactCustomFieldValue'

    ContactCustomFieldValue:
      type: object
      properties:
        fieldId:
          type: string
          description: Custom field definition ID
        tag:
          type: string
          nullable: true
          description: Custom field tag (for use in templates as `{{tag}}`)
        name:
          type: string
          nullable: true
          description: Custom field display name
        value:
          type: string
          description: The field value for this contact
        updatedAt:
          type: string
          format: date-time
          description: When this value was last updated

    CustomFieldDefinition:
      type: object
      properties:
        id:
          type: string
          description: Custom field definition ID
        name:
          type: string
          description: Display name (1-100 chars)
        tag:
          type: string
          description: "Tag for use in templates as `{{tag}}`. Alphanumeric + underscores only (1-50 chars)."
          example: "customer_status"
        description:
          type: string
          nullable: true
          description: Optional description
        category:
          type: string
          description: Category for organization (default "Custom")
        createdAt:
          type: string
          format: date-time

    CreateContactRequest:
      type: object
      required: [phone]
      properties:
        firstName:
          type: string
          maxLength: 100
        lastName:
          type: string
          maxLength: 100
        phone:
          type: string
          description: "Phone number with country code (6-20 digits, optional + or 00 prefix - stored without the prefix). Must be unique per account."
          pattern: '^(\+|00)?[0-9]{6,20}$'
          example: "4512345678"
        email:
          type: string
          format: email
        groups:
          type: array
          items:
            type: string
          description: Array of group IDs to assign the contact to
        customFields:
          type: array
          description: Custom field values to set on creation
          items:
            type: object
            properties:
              fieldId:
                type: string
                description: Custom field definition ID
              value:
                type: string

    UpdateContactRequest:
      type: object
      properties:
        firstName:
          type: string
          maxLength: 100
        lastName:
          type: string
          maxLength: 100
        phone:
          type: string
          pattern: '^(\+|00)?[0-9]{6,20}$'
        email:
          type: string
          format: email
        groups:
          type: array
          items:
            type: string
          description: Replace the contact's group memberships with this list
        customFields:
          type: array
          items:
            type: object
            properties:
              fieldId:
                type: string
              value:
                type: string
        disabled:
          type: boolean
          description: Set to `true` to soft-delete, `false` to re-enable

    CreateCustomFieldRequest:
      type: object
      required: [name, tag]
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 100
          description: "Display name. Letters, numbers, spaces, underscores, hyphens."
        tag:
          type: string
          minLength: 1
          maxLength: 50
          pattern: '^[a-zA-Z0-9_]+$'
          description: "Unique tag for merge fields. Letters, numbers, underscores only."
          example: "customer_status"
        description:
          type: string
          maxLength: 500
        category:
          type: string
          maxLength: 50
          description: "Category for organization (default: \"Custom\")"

    BulkContactRequest:
      type: object
      required: [contacts]
      properties:
        contacts:
          type: array
          minItems: 1
          maxItems: 1000
          items:
            type: object
            required: [phone]
            properties:
              phone:
                type: string
                pattern: '^(\+|00)?[0-9]{6,20}$'
              firstName:
                type: string
                maxLength: 100
              lastName:
                type: string
                maxLength: 100
              email:
                type: string
                format: email
              customFields:
                type: array
                items:
                  type: object
                  properties:
                    fieldId:
                      type: string
                      description: Custom field definition ID (use this or `tag`)
                    tag:
                      type: string
                      description: Custom field tag (use this or `fieldId`)
                    value:
                      type: string
        upsert:
          type: boolean
          default: false
          description: "When `true`, existing contacts (matched by phone) are updated. When `false`, duplicates are skipped."
        groups:
          type: array
          items:
            type: string
          description: Group IDs to assign all contacts to

    BulkContactResponse:
      type: object
      properties:
        created:
          type: integer
          description: Number of contacts created
        updated:
          type: integer
          description: Number of contacts updated (only in upsert mode)
        failed:
          type: integer
          description: Number of contacts that failed
        errors:
          type: array
          items:
            type: object
            properties:
              phone:
                type: string
              error:
                type: string

    ContactListResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/Contact'
        pagination:
          type: object
          properties:
            nextCursor:
              type: string
              nullable: true
              description: Base64 cursor for the next page. Null when there are no more results.
            hasNext:
              type: boolean
              description: Whether more results are available
            limit:
              type: integer
              description: The limit that was applied
            count:
              type: integer
              description: Number of contacts in this response

    # Groups schemas
    Group:
      type: object
      properties:
        id:
          type: string
          description: Group ID (MongoDB ObjectId)
        name:
          type: string
          description: Group name
        description:
          type: string
          nullable: true
          description: Group description
        contactCount:
          type: integer
          description: Number of contacts in the group
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    GroupContact:
      type: object
      properties:
        id:
          type: string
        firstName:
          type: string
        lastName:
          type: string
        phone:
          type: string
        email:
          type: string
          nullable: true
        disabled:
          type: boolean

    CreateGroupRequest:
      type: object
      required:
        - name
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 200
          description: Group name (required)
        description:
          type: string
          maxLength: 1000
          description: Optional group description

    UpdateGroupRequest:
      type: object
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 200
          description: New group name
        description:
          type: string
          maxLength: 1000
          description: New group description

    GroupContactsRequest:
      type: object
      required:
        - contactIds
      properties:
        contactIds:
          type: array
          minItems: 1
          maxItems: 10000
          items:
            type: string
          description: Array of contact IDs (MongoDB ObjectIds) to add or remove

    GroupContactsResponse:
      type: object
      properties:
        message:
          type: string
        contactsProcessed:
          type: integer
          description: Total number of contact IDs provided
        newRelationships:
          type: integer
          description: Number of new contacts actually added (excludes duplicates)
        linkedEntitiesSync:
          type: object
          nullable: true
          description: Sync results for linked campaigns/flows
          properties:
            campaignsUpdated:
              type: integer
            flowsUpdated:
              type: integer

    # Templates schemas
    TemplateObject:
      type: object
      properties:
        id:
          type: string
          description: Template ID (MongoDB ObjectId)
        name:
          type: string
          description: Template name
        content:
          type: string
          description: Template content (plain text for SMS, JSON string for RCS)
        type:
          type: string
          enum: [SMS, RCS]
          description: Template type
        rcsMessageType:
          type: string
          nullable: true
          enum: [textBasic, text, richCard, carousel, image, video, audio, file, media]
          description: RCS message subtype (null for SMS)
        context:
          type: string
          enum: [event, flow, general]
          description: Template context
        templateGroupId:
          type: string
          nullable: true
          description: ID of the template group (folder) this template belongs to, or null if ungrouped. Template groups are NOT contact groups.
        isDefault:
          type: boolean
          description: Whether this is the default template for its type+context
        parsedContent:
          type: object
          description: Parsed RCS content (only present for RCS templates)
          properties:
            messageType:
              type: string
            content:
              type: object
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    CreateTemplateRequest:
      type: object
      required:
        - name
        - type
        - content
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 200
          description: Template name (required)
        type:
          type: string
          enum: [SMS, RCS]
          description: Template type (required)
        content:
          description: "Template content. For SMS: a plain text string. For RCS: an object (same format as rcs_send content field)."
          oneOf:
            - type: string
            - type: object
        rcsMessageType:
          type: string
          enum: [textBasic, text, richCard, carousel, image, video, audio, file, media]
          description: RCS message subtype (required when type=RCS)
        templateGroupId:
          type: string
          nullable: true
          description: Template group (folder) to place the template in. Omit or pass null for ungrouped. Template groups are NOT contact groups.
        context:
          type: string
          enum: [event, flow, general]
          description: Template context (default "general")
        isDefault:
          type: boolean
          description: Set as default template for this type+context (default false)

    UpdateTemplateRequest:
      type: object
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 200
        type:
          type: string
          enum: [SMS, RCS]
        content:
          description: "New content. For RCS: object. For SMS: text string."
          oneOf:
            - type: string
            - type: object
        rcsMessageType:
          type: string
          enum: [textBasic, text, richCard, carousel, image, video, audio, file, media]
        templateGroupId:
          type: string
          nullable: true
          description: Move the template to a template group (folder), or null to ungroup. Omit to leave unchanged. Template groups are NOT contact groups.
        context:
          type: string
          enum: [event, flow, general]
        isDefault:
          type: boolean

    TemplateListResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/TemplateObject'
        pagination:
          type: object
          properties:
            total:
              type: integer
              description: Total number of templates matching the filter
            page:
              type: integer
              description: Current page number
            limit:
              type: integer
              description: Results per page
            pages:
              type: integer
              description: Total number of pages
            hasNext:
              type: boolean
              description: Whether there is a next page
            hasPrev:
              type: boolean
              description: Whether there is a previous page

    TemplateGroupObject:
      type: object
      description: A template group — a folder for organizing message templates. NOT a contact group.
      properties:
        id:
          type: string
          description: Template group ID (MongoDB ObjectId)
        name:
          type: string
          description: Template group name (unique per account)
        templateCount:
          type: integer
          description: Number of templates in this group
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time

    TemplateGroupListResponse:
      type: object
      properties:
        data:
          type: array
          items:
            $ref: '#/components/schemas/TemplateGroupObject'
        ungroupedTemplateCount:
          type: integer
          description: Number of templates that are not in any template group

    CreateTemplateGroupRequest:
      type: object
      required:
        - name
      properties:
        name:
          type: string
          minLength: 1
          maxLength: 100
          description: Template group name (required, unique per account)

    AssignTemplatesToTemplateGroupRequest:
      type: object
      required:
        - templateIds
        - templateGroupId
      properties:
        templateIds:
          type: array
          minItems: 1
          items:
            type: string
          description: MongoDB ObjectIds of the templates to move
        templateGroupId:
          type: string
          nullable: true
          description: Target template group ID, or null to remove the templates from any group
