Replace
YOUR_API_KEY with your actual API key before pasting.## 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.

