> ## Documentation Index
> Fetch the complete documentation index at: https://docs.nautilus.co/llms.txt
> Use this file to discover all available pages before exploring further.

# For AI agents

> Copy-paste instructions for AI agents integrating with the Nautilus API

Copy the block below and paste it into your AI agent (Claude, GPT, Cursor, etc.) to give it everything it needs to use the Nautilus API.

<Tip>Replace `YOUR_API_KEY` with your actual API key before pasting.</Tip>

<CodeGroup>
  ```text Agent instructions theme={null}
  ## Nautilus API

  Base URL: https://app.nautilus.co/api

  ### Authentication

  All requests require a Bearer token in the Authorization header.

  Authorization: Bearer YOUR_API_KEY

  Use the `Bearer ` prefix exactly as shown.

  Two key types exist:
  - **Tenant-scoped keys** — bound to a specific organization. SMS sends via the tenant's configured phone number; email sends from `{slug}@mail.nautilus.co`.
  - **Platform keys** — legacy keys that send as the Nautilus brand (shortcode for SMS, `contact@mail.nautilus.co` for email).

  Most integrations use tenant-scoped keys.

  Analytics and canonical data endpoints require tenant-scoped keys. Platform keys are accepted only by the SMS and email send endpoints.

  Analytics date filters are interpreted in UTC. For members, retention offers, cancellations, and cancellation survey, date-only lower bounds start at 00:00:00.000Z and date-only upper bounds include the full UTC day. Cohort start/end values are truncated to their month.

  ### Canonical read API

  The canonical API is read-only and tenant-scoped. Its full OpenAPI 3.1 contract is available without authentication at GET https://app.nautilus.co/api/v1/openapi.json — fetch it when you need exact parameters and response schemas.

  All canonical routes use GET. Collections:

  /v1/sites, /v1/products, /v1/checkout-links, /v1/purchases, /v1/contacts, /v1/contact-lists, /v1/marketing/signups, /v1/forms, /v1/forms/submissions, /v1/vouchers, /v1/vouchers/redemptions, /v1/voucher-groups, /v1/links, /v1/links/analytics, /v1/automations, /v1/automations/runs, /v1/surveys, /v1/surveys/responses, /v1/cases, /v1/cancellations/records

  Detail routes append the resource ID (e.g. /v1/products/{productId}). Nested collections: /v1/contact-lists/{listId}/contacts, /v1/forms/{formId}/submissions, /v1/voucher-groups/{groupId}/vouchers, /v1/links/{linkId}/analytics, /v1/automations/{automationId}/runs, /v1/surveys/{surveyId}/responses.

  Sonny's POS routes (only for organizations on the Sonny's Shared data source; others get 400/404): /v1/pos/sonnys/transactions, /v1/pos/sonnys/customers, /v1/pos/sonnys/recurring-accounts, /v1/pos/sonnys/recurring-account-details, each with a /{id} detail route.

  **Conventions:**

  - Collection responses use the envelope { organizationId, filters, data, pagination: { limit, hasMore, nextCursor } }.
  - limit defaults to 100, capped at 500 (links capped at 100). Pass pagination.nextCursor back as ?cursor= to page. Cursors are opaque and bound to the endpoint that issued them; reusing one elsewhere returns 400.
  - Timestamps are UTC ISO 8601. Timestamp filters (createdAfter, createdBefore, etc.) require an explicit offset, e.g. 2026-07-01T00:00:00Z.
  - Money fields ending in Cents are integer cents.
  - Voucher/purchase codes are excluded by default; pass includeCodes=true to get them.
  - The API key's organization is the only tenant scope — you cannot pass an organization ID. Detail routes return 404 for both missing and cross-tenant IDs.
  - Cancellation records (/v1/cancellations/records) include signupDiscounts, hadSignupDiscount, firstYearPayments, and firstYearPaymentCoverage.
  - Errors: 400 invalid filter/cursor/limit or unsupported provider, 401 bad key, 404 missing resource, 500 internal, 502 upstream unavailable (link resources). Body is { "error": "..." }.

  ### Send SMS — POST /v1/message

  Send an SMS (or MMS) to one or more phone numbers.

  **Request body (JSON):**

  | Field        | Type                  | Required | Description                                                                                          |
  |--------------|-----------------------|----------|------------------------------------------------------------------------------------------------------|
  | to           | string or string[]    | Yes      | One phone number or an array of phone numbers.                                                       |
  | body         | string                | Yes      | The text content of the message.                                                                     |
  | attachments  | { url: string }[]     | No       | Media URLs to attach (sends as MMS).                                                                 |
  | useShortcode | boolean               | No       | Tenant-scoped keys only. When true, routes via Nautilus shortcode instead of the tenant's configured phone number. Ignored on platform keys. |

  Phone numbers can be E.164 (+15551234567), 10-digit (5551234567), or dashed (555-123-4567). All are normalized to E.164 before sending.

  **Example — single recipient:**

  POST https://app.nautilus.co/api/v1/message
  Content-Type: application/json
  Authorization: Bearer YOUR_API_KEY

  {
    "to": "+15551234567",
    "body": "Your wash is ready!"
  }

  **Example — multiple recipients with attachment:**

  POST https://app.nautilus.co/api/v1/message
  Content-Type: application/json
  Authorization: Bearer YOUR_API_KEY

  {
    "to": ["+15551234567", "+15559876543"],
    "body": "Flash sale: 50% off unlimited plans today!",
    "attachments": [{ "url": "https://example.com/promo.png" }]
  }

  **Responses:**

  - 200: All messages sent. Body: { "success": true, "results": [{ "to": "+1...", "success": true }] }
  - 207: Partial success. Body: { "success": false, "results": [{ "to": "+1...", "success": true }, { "to": "+1...", "success": false, "error": "Invalid phone number" }] }
  - 400: Invalid request body. Body: { "error": "Invalid request format", "details": [...] }
  - 401: Bad or missing token. Body: { "error": "Unauthorized" }
  - 422: Tenant-scoped key with no configured phone number. Either configure one or set useShortcode: true. Body: { "error": "Phone number sending is not configured for this tenant..." }
  - 500: Server error. Body: { "error": "..." }

  When sending to multiple recipients, each is processed independently. A failure for one does not block others.

  ### Send email — POST /v1/email

  Send an email to one or more recipients.

  **Request body (JSON):**

  | Field   | Type               | Required | Description                                                                                   |
  |---------|--------------------|----------|-----------------------------------------------------------------------------------------------|
  | to      | string or string[] | Yes      | One email address or an array of email addresses.                                             |
  | subject | string             | Yes      | The email subject line.                                                                       |
  | html    | string             | Yes      | The email body as HTML.                                                                       |
  | from        | string             | No       | Sender address. Tenant-scoped keys: must be exactly `{slug}@mail.nautilus.co` (display name customizable); defaults to `{Tenant Name} <{slug}@mail.nautilus.co>`. Platform keys: must use `@mail.nautilus.co` domain; defaults to `Nautilus <contact@mail.nautilus.co>`. |
  | replyTo     | string             | No       | Reply-to email address.                                                                       |
  | attachments | object[]           | No       | File attachments. Each needs filename + either path (URL) or content (base64).                |

  Each attachment object: { "filename": "file.pdf", "path": "https://..." } or { "filename": "file.pdf", "content": "<base64>" }

  **Example — single recipient:**

  POST https://app.nautilus.co/api/v1/email
  Content-Type: application/json
  Authorization: Bearer YOUR_API_KEY

  {
    "to": "customer@example.com",
    "subject": "Your monthly wash summary",
    "html": "<h1>Hi!</h1><p>You washed 12 times this month.</p>"
  }

  **Example — custom sender with reply-to:**

  POST https://app.nautilus.co/api/v1/email
  Content-Type: application/json
  Authorization: Bearer YOUR_API_KEY

  {
    "to": ["customer@example.com", "other@example.com"],
    "subject": "Welcome to Squeaky Clean",
    "html": "<p>Thanks for joining!</p>",
    "from": "Squeaky Clean <squeaky@mail.nautilus.co>",
    "replyTo": "support@squeakyclean.com"
  }

  **Responses:**

  - 200: All emails sent. Body: { "success": true, "results": [{ "to": "...", "success": true, "id": "abc123" }] }
  - 207: Partial success. Body: { "success": false, "results": [{ "to": "...", "success": true, "id": "abc123" }, { "to": "...", "success": false, "error": "..." }] }
  - 400: Invalid request body (includes `from` that doesn't match the rules for your key type). Body: { "error": "Invalid request format", "details": [...] }
  - 401: Bad or missing token. Body: { "error": "Unauthorized" }
  - 422: Tenant-scoped key whose tenant has no slug configured. Body: { "error": "Email sending is not configured for this tenant..." }
  - 500: Server error. Body: { "error": "..." }

  When sending to multiple recipients, each is processed independently. A failure for one does not block others.

  ### Export members — GET /v1/members

  Export membership rows for signup-date, churn, package, site, trial, amount, and raw POS tag analysis.

  Requires a tenant-scoped key and an eligible tenant.

  **Query parameters:**

  | Field       | Type                      | Required | Description |
  |-------------|---------------------------|----------|-------------|
  | status      | all, active, or churned   | No       | Defaults to all. Invalid values are treated as all. |
  | signupStart | date string               | No       | Inclusive signup lower bound. |
  | signupEnd   | date string               | No       | Inclusive signup upper bound. |
  | limit       | integer                   | No       | Defaults to 500. Maximum 1000. |
  | offset      | integer                   | No       | Defaults to 0. |

  **Example:**

  GET https://app.nautilus.co/api/v1/members?status=active&signupStart=2026-01-01&signupEnd=2026-01-31&limit=100
  Authorization: Bearer YOUR_API_KEY

  **Response shape:**

  { organizationId, filters, pagination, members: [{ membershipId, customerId, signupDate, status, churnType, cancelDate, tenureDays, planName, billingAmount, billingSiteCode, billingSiteName, creationSiteCode, creationSiteName, isOnTrial, trialAmount, tags }] }

  ### Signup cohorts — GET /v1/cohorts

  Aggregate signup-month cohorts overall, by current plan, or by current billing site.

  Requires a tenant-scoped key and an eligible tenant.

  **Query parameters:**

  | Field     | Type                    | Required | Description |
  |-----------|-------------------------|----------|-------------|
  | start     | date string             | No       | Inclusive lower bound on cohort month. |
  | end       | date string             | No       | Inclusive upper bound on cohort month. |
  | dimension | none, plan, or site     | No       | Defaults to none. Invalid values return 400. |

  **Example:**

  GET https://app.nautilus.co/api/v1/cohorts?dimension=plan&start=2026-01-01&end=2026-06-30
  Authorization: Bearer YOUR_API_KEY

  **Response shape:**

  { organizationId, dateRange, source: "membership_signup", available: true, dimension, signupDateCoverage, dimensionValues, cohorts: [{ cohortMonth, dimensionValue, cohortSize, stillActive, churned, voluntary, involuntary, churnRatePct, churnWithin }] }

  ### Retention offers — GET /v1/retention-offers

  Return retention offer views, accepts, declines, acceptance rates, and accepted-vs-declined LTV where supported.

  Requires a tenant-scoped key and the retention-offer analytics capability.

  **Query parameters:**

  | Field | Type        | Required | Description |
  |-------|-------------|----------|-------------|
  | start | date string | No       | Inclusive lower bound on offer events. |
  | end   | date string | No       | Inclusive upper bound on offer events. |

  **Example:**

  GET https://app.nautilus.co/api/v1/retention-offers?start=2026-01-01&end=2026-06-30
  Authorization: Bearer YOUR_API_KEY

  **Response shape:**

  { organizationId, dateRange, totals: { viewed, accepted, declined, acceptanceRatePct }, offers: [{ offerId, offerName, targetPlanName, targetPlanPrice, viewed, accepted, declined, acceptanceRatePct }], ltv }

  If revenue LTV is unsupported or unavailable, ltv contains acceptedMembers: null, declinedMembers: null, coverage: null, and unavailableReason.

  ### Cancellations — GET /v1/cancellations

  Return POS-derived cancellation analytics: voluntary versus involuntary churn, terminal-status reasons, and tenure-at-cancellation statistics.

  Requires a tenant-scoped key and an eligible tenant.

  **Query parameters:**

  | Field | Type        | Required | Description |
  |-------|-------------|----------|-------------|
  | start | date string | No       | Inclusive lower bound on cancellation date. |
  | end   | date string | No       | Inclusive upper bound on cancellation date. |

  **Example:**

  GET https://app.nautilus.co/api/v1/cancellations?start=2026-01-01&end=2026-06-30
  Authorization: Bearer YOUR_API_KEY

  **Response shape:**

  { organizationId, dateRange, source: "membership_status", available: true, totals, reasons, tenure }

  ### Cancellation survey — GET /v1/cancellation-survey

  Return member-stated cancellation reasons selected in the portal cancellation flow.

  Requires a tenant-scoped key. This is separate from POS-derived churn in /v1/cancellations.

  **Query parameters:**

  | Field | Type        | Required | Description |
  |-------|-------------|----------|-------------|
  | start | date string | No       | Inclusive lower bound on survey event timestamp. |
  | end   | date string | No       | Inclusive upper bound on survey event timestamp. |

  **Example:**

  GET https://app.nautilus.co/api/v1/cancellation-survey?start=2026-01-01&end=2026-06-30
  Authorization: Bearer YOUR_API_KEY

  **Response shape:**

  { organizationId, dateRange, source: "exit_survey", available, totals: { selections }, reasons: [{ reason, rawCodes, count, pct }], other }

  If the analytics source cannot be queried after retries, the endpoint can return 200 with available: false, totals.selections: 0, reasons: [], and other: null.
  ```
</CodeGroup>
