# SMS365 — integration guide for AI coding assistants

Hand this whole file to your coding agent (Claude Code, Cursor, Copilot, etc.) and
ask it to integrate SMS365 into your project. It has everything needed: the base
URL, authentication, the rules that must be respected, a ready-to-use client, and
the full endpoint list. You only need to supply an **API key** (see *Setup*).

> This file is generated from the live API description, so every endpoint below is
> one the platform actually serves.

---

## What this is

SMS365 is an Australian SMS gateway with one REST API. You POST a phone number and
a message; it normalises the number, applies the account rules, and sends. It also
does one-time-code verification, scheduled and bulk sends, contacts, campaigns,
appointment reminders, link tracking, and signed delivery webhooks — all on the
same API.

- **Base URL:** `https://app.sms365.com.au`
- **Auth:** every request carries `Authorization: Bearer <API_KEY>`.
- **Format:** JSON in, JSON out. `Content-Type: application/json` on writes.
- **Price:** about $0.10 per SMS segment, drawn from prepaid credit.

---

## Setup (do this first)

1. Sign in at https://app.sms365.com.au and create an API key under **Integrations → API keys**,
   granting it only the permissions this project needs (each endpoint below names
   the permission it requires).
2. Put the key in the environment — never hard-code it, never commit it:

```bash
# .env  (git-ignored)
SMS365_API_KEY=sk_live_your_key_here
SMS365_BASE_URL=https://app.sms365.com.au
```

3. Make sure the workspace is **in credit** — SMS365 is prepaid, and a send is
   refused with `402 insufficient_funds` when the balance cannot cover it. Top up
   under **Billing**.

---

## Rules your integration must respect

These are enforced server-side; handle them rather than fighting them.

- **Australian numbers.** `to` accepts `04…`, `+61…`, `61…` (spaces/brackets fine).
  A bare number with no country code and no leading zero is refused, not guessed.
- **Sender ID.** A message is sent from an *authorised* sender ID. Configure yours in the console under Settings → Sender IDs.
- **Idempotency.** Send an `Idempotency-Key` header on every send. A retry after a
  timeout then returns the first result instead of sending twice.
- **Prepaid balance.** Expect and handle `402 insufficient_funds` — surface "top up
  to keep sending" rather than retrying in a loop.
- **Segments & cost.** Over 160 GSM-7 characters (70 with an emoji) a message is
  split into billed segments. Keep transactional texts short.
- **Opt-out (marketing only).** Put the literal `{{unsubscribe}}` in a marketing
  body; SMS365 rewrites it to a working per-recipient unsubscribe link. Required by
  the Spam Act for marketing. Use {{unsubscribe}} rather than building your own opt-out.
- **Rate limits.** A burst can return `429`; back off and retry.

---

## Quickstart — send a message

```bash
curl -X POST https://app.sms365.com.au/v1/messages \
  -H "Authorization: Bearer $SMS365_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-4711" \
  -d '{"to":"0412345678","body":"Your order is on its way."}'
```

```ts
// A tiny client — drop this in as lib/sms365.ts
const BASE = process.env.SMS365_BASE_URL!;
const KEY = process.env.SMS365_API_KEY!;

export async function sendSms(to: string, body: string, idempotencyKey?: string) {
  const res = await fetch(`${BASE}/v1/messages`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${KEY}`,
      "Content-Type": "application/json",
      ...(idempotencyKey ? { "Idempotency-Key": idempotencyKey } : {}),
    },
    body: JSON.stringify({ to, body }),
  });
  if (!res.ok) {
    const err = await res.json().catch(() => ({}));
    // err.error is a stable machine code, e.g. "insufficient_funds".
    throw new Error(`SMS365 ${res.status}: ${err.error ?? "request_failed"}`);
  }
  return res.json(); // { id, status, segments, cost_cents, ... }
}
```

```python
import os, requests

def send_sms(to: str, body: str, idempotency_key: str | None = None):
    headers = {"Authorization": f"Bearer {os.environ['SMS365_API_KEY']}"}
    if idempotency_key:
        headers["Idempotency-Key"] = idempotency_key
    r = requests.post(
        f"{os.environ['SMS365_BASE_URL']}/v1/messages",
        headers=headers, json={"to": to, "body": body})
    r.raise_for_status()
    return r.json()
```

---

## One-time codes (verify / OTP)

```bash
# 1) send a code
curl -X POST https://app.sms365.com.au/v1/verify/start \
  -H "Authorization: Bearer $SMS365_API_KEY" -H "Content-Type: application/json" \
  -d '{"to":"0412345678"}'

# 2) check what the user typed
curl -X POST https://app.sms365.com.au/v1/verify/check \
  -H "Authorization: Bearer $SMS365_API_KEY" -H "Content-Type: application/json" \
  -d '{"to":"0412345678","code":"123456"}'
```
The code is never returned or stored — only checked. Codes expire, are
attempt-capped, and rate-limited.

---

## Delivery events (webhooks)

Instead of polling, subscribe an endpoint and SMS365 POSTs HMAC-signed events
(`message.sent`, `message.failed`, `link.clicked`, and more). Verify the signature
over the raw request body. Configure this under **Integrations → Event webhooks**.

---

## Error handling

Errors are JSON: `{ "error": "<code>", "message": "<human text>" }`. Branch on
`error`, not the message. The ones to handle explicitly:

| Status | `error` | What it means |
|---|---|---|
| 400 | `invalid_number` | The `to` number could not be parsed as Australian. |
| 402 | `insufficient_funds` | Out of prepaid credit — prompt a top-up. |
| 403 | `forbidden` | The key lacks the permission for this endpoint. |
| 409 | (idempotency) | A duplicate `Idempotency-Key`; the first result is returned. |
| 429 | `rate_limited` | Slow down and retry with backoff. |

---

## Full endpoint reference

All paths are relative to `https://app.sms365.com.au`. Each names the key permission it needs.

### Messages

- `POST /v1/messages` — Send a message _(key permission: `messages:send`)_
- `POST /v1/messages/bulk` — Send to many numbers, or to a contact list _(key permission: `messages:send`)_
- `POST /v1/messages/preview` — Price a message without sending it _(key permission: `messages:read`)_
- `POST /v1/ai/draft` — Draft or improve a message with AI _(key permission: `messages:send`)_
- `POST /v1/ai/variations` — Generate several message options with AI _(key permission: `messages:send`)_
- `GET /v1/messages` — List messages _(key permission: `messages:read`)_
- `GET /v1/messages/:id` — Read one message, its full history, and its tracked-link clicks _(key permission: `messages:read`)_
- `POST /v1/messages/:id/handled` — Mark a received reply as handled (or re-open it) _(key permission: `messages:read`)_
- `GET /v1/conversations` — List conversation threads (per number ↔ contact) _(key permission: `messages:read`)_
- `GET /v1/conversations/thread` — The full back-and-forth between one of your numbers and a contact _(key permission: `messages:read`)_
- `POST /v1/conversations/assign` — Assign a conversation to a teammate (or unassign with null) _(key permission: `messages:read`)_
- `POST /v1/conversations/status` — Open or close a conversation _(key permission: `messages:read`)_
- `GET /v1/conversations/notes` — Internal notes on a conversation (private, not sent) _(key permission: `messages:read`)_
- `POST /v1/conversations/notes` — Add a private internal note to a conversation _(key permission: `messages:read`)_
- `POST /v1/conversations/ai-draft` — Draft a reply to a conversation with AI (suggestion only, never sends) _(key permission: `messages:send`)_
- `GET /v1/canned-replies` — List saved (canned) replies _(key permission: `messages:read`)_
- `POST /v1/canned-replies` — Save a canned reply _(key permission: `messages:send`)_
- `DELETE /v1/canned-replies/:id` — Delete a canned reply _(key permission: `messages:send`)_
- `POST /v1/messages/:id/cancel` — Cancel a message that has not gone yet _(key permission: `messages:send`)_

### Verify

- `POST /v1/verify/start` — Send a one-time code _(key permission: `verify:write`)_
- `POST /v1/verify/check` — Check a code _(key permission: `verify:write`)_
- `GET /v1/verify/:id` — Read a verification’s status _(key permission: `verify:read`)_

### Appointments

- `POST /v1/appointments` — Schedule reminders for a visit _(key permission: `messages:send`)_
- `GET /v1/appointments` — List visits in a date range _(key permission: `messages:read`)_
- `GET /v1/appointments/defaults` — The default reminder offsets and morning hour _(key permission: `messages:read`)_
- `GET /v1/appointments/:id` — Read a visit and its reminders _(key permission: `messages:read`)_
- `POST /v1/appointments/:id/cancel` — Cancel a visit and every reminder still to go _(key permission: `messages:send`)_

### Campaigns

- `GET /v1/batches` — List campaigns and their progress _(key permission: `campaigns:read`)_
- `GET /v1/batches/:id` — Read one campaign _(key permission: `campaigns:read`)_
- `POST /v1/batches/:id/cancel` — Stop a campaign _(key permission: `campaigns:write`)_

### Audience

- `GET /v1/contacts` — List contacts _(key permission: `contacts:read`)_
- `POST /v1/contacts` — Create or update a contact _(key permission: `contacts:write`)_
- `DELETE /v1/contacts/:id` — Delete a contact _(key permission: `contacts:write`)_
- `POST /v1/contacts/import` — Import a spreadsheet _(key permission: `contacts:write`)_
- `GET /v1/groups` — List groups _(key permission: `contacts:read`)_
- `POST /v1/groups` — Create a group _(key permission: `contacts:write`)_
- `DELETE /v1/groups/:id` — Delete a group _(key permission: `contacts:write`)_
- `POST /v1/groups/:id/members` — Add contacts to a group _(key permission: `contacts:write`)_
- `DELETE /v1/groups/:id/members/:contactId` — Remove a contact from a group _(key permission: `contacts:write`)_
- `GET /v1/segments` — List dynamic segments (rule-based audiences) with live counts _(key permission: `contacts:read`)_
- `POST /v1/segments/preview` — Preview how many contacts a filter matches, with a sample _(key permission: `contacts:read`)_
- `POST /v1/segments` — Create a dynamic segment _(key permission: `contacts:write`)_
- `PATCH /v1/segments/:id` — Update a segment _(key permission: `contacts:write`)_
- `DELETE /v1/segments/:id` — Delete a segment _(key permission: `contacts:write`)_
- `POST /v1/segments/:id/save-as-group` — Snapshot a segment’s current matches into a new group _(key permission: `contacts:write`)_
- `GET /v1/suppressions` — List blocked numbers _(key permission: `suppressions:read`)_
- `POST /v1/suppressions` — Block a number _(key permission: `suppressions:write`)_
- `DELETE /v1/suppressions/:e164` — Unblock a number _(key permission: `suppressions:write`)_

### Configuration

- `GET /v1/senders` — List sender IDs _(key permission: `senders:read`)_
- `POST /v1/senders` — Register a sender ID _(key permission: `senders:write`)_
- `DELETE /v1/senders/:id` — Remove a sender ID _(key permission: `senders:write`)_
- `GET /v1/templates` — List templates _(key permission: `templates:read`)_
- `POST /v1/templates` — Create or update a template _(key permission: `templates:write`)_
- `DELETE /v1/templates/:id` — Delete a template _(key permission: `templates:write`)_
- `PATCH /v1/tenant` — Change the workspace limits _(key permission: `settings:write`)_
- `GET /v1/providers` — Sending capabilities available to your workspace _(key permission: `settings:read`)_
- `GET /v1/workflows` — List packaged workflows and which are enabled _(key permission: `settings:read`)_
- `POST /v1/workflows/:key` — Turn a workflow on or off _(key permission: `settings:write`)_
- `GET /v1/numbers` — List your dedicated numbers and their status _(key permission: `senders:read`)_
- `GET /v1/numbers/search` — Search available dedicated numbers to request _(key permission: `senders:write`)_
- `POST /v1/numbers/request` — Request a dedicated number _(key permission: `senders:write`)_
- `POST /v1/numbers/:id/checkout` — Pay for an approved number _(key permission: `senders:write`)_
- `DELETE /v1/numbers/:id` — Release a dedicated number _(key permission: `senders:write`)_
- `GET /v1/sender-registrations` — List your alphanumeric Sender ID registrations and their status _(key permission: `senders:read`)_
- `POST /v1/sender-registrations` — Register an alphanumeric Sender ID (AU) _(key permission: `senders:write`)_
- `GET /v1/keywords` — List keyword auto-responder rules _(key permission: `settings:read`)_
- `POST /v1/keywords` — Create a keyword rule (auto-reply, subscribe or unsubscribe) _(key permission: `settings:write`)_
- `PATCH /v1/keywords/:id` — Update a keyword rule _(key permission: `settings:write`)_
- `DELETE /v1/keywords/:id` — Delete a keyword rule _(key permission: `settings:write`)_
- `GET /v1/opt-in-forms` — List hosted opt-in forms _(key permission: `settings:read`)_
- `POST /v1/opt-in-forms` — Create a hosted opt-in form _(key permission: `settings:write`)_
- `PATCH /v1/opt-in-forms/:id` — Update a hosted opt-in form _(key permission: `settings:write`)_
- `DELETE /v1/opt-in-forms/:id` — Delete a hosted opt-in form _(key permission: `settings:write`)_
- `GET /v1/ai-assistant` — Get the workspace AI assistant settings (off / draft / auto + business context) _(key permission: `settings:read`)_
- `POST /v1/ai-assistant` — Set how the AI assistant handles inbound replies _(key permission: `settings:write`)_
- `GET /v1/consents.csv` — The consent log as an audit-grade CSV (compliance export) _(key permission: `contacts:read`)_
- `GET /v1/knowledge` — List AI knowledge-base documents _(key permission: `settings:read`)_
- `POST /v1/knowledge` — Add a document the AI assistant retrieves from (chunked + embedded) _(key permission: `settings:write`)_
- `DELETE /v1/knowledge/:id` — Delete a knowledge-base document _(key permission: `settings:write`)_
- `GET /v1/consents` — The consent log — who opted in or out, when, and how (compliance record) _(key permission: `contacts:read`)_
- `GET /v1/schedules` — List recurring send schedules _(key permission: `messages:read`)_
- `POST /v1/schedules` — Create a recurring send (daily, weekly or monthly, in your timezone) _(key permission: `messages:send`)_
- `PATCH /v1/schedules/:id` — Update a schedule, or enable/disable it _(key permission: `messages:send`)_
- `DELETE /v1/schedules/:id` — Delete a schedule _(key permission: `messages:send`)_

### Integrations

- `GET /v1/webhooks` — List inbound webhook endpoints _(key permission: `ingress:read`)_
- `POST /v1/webhooks` — Create an inbound webhook endpoint _(key permission: `ingress:write`)_
- `DELETE /v1/webhooks/:id` — Delete a webhook endpoint _(key permission: `ingress:write`)_
- `GET /v1/links` — List trigger URLs _(key permission: `ingress:read`)_
- `POST /v1/links` — Create a trigger URL _(key permission: `ingress:write`)_
- `DELETE /v1/links/:id` — Delete a trigger URL _(key permission: `ingress:write`)_
- `GET /v1/email-ingress` — List email-to-SMS addresses _(key permission: `ingress:read`)_
- `POST /v1/email-ingress` — Create an email-to-SMS address _(key permission: `ingress:write`)_
- `PATCH /v1/email-ingress/:id` — Change an email-to-SMS address _(key permission: `ingress:write`)_
- `DELETE /v1/email-ingress/:id` — Delete an email-to-SMS address _(key permission: `ingress:write`)_
- `GET /v1/event-types` — The events SMS365 can send you _(key permission: `events:read`)_
- `GET /v1/events` — List event subscriptions _(key permission: `events:read`)_
- `POST /v1/events` — Receive delivery events _(key permission: `events:write`)_
- `POST /v1/events/:id/enable` — Re-enable a subscription disabled after repeated failures _(key permission: `events:write`)_
- `POST /v1/events/:id/test` — Send yourself a test event _(key permission: `events:write`)_
- `GET /v1/events/:id/deliveries` — Recent delivery attempts for a subscription _(key permission: `events:read`)_
- `DELETE /v1/events/:id` — Delete a subscription _(key permission: `events:write`)_
- `GET /v1/connections` — List connected external systems (e.g. Xero) _(key permission: `settings:read`)_
- `POST /v1/connections/:provider/sync` — Sync a connection now (send reminders for new overdue invoices) _(key permission: `settings:write`)_
- `POST /v1/connections/:provider/settings` — Set reminder controls for a connection _(key permission: `settings:write`)_
- `GET /v1/connections/reminders` — Reminders awaiting your approval _(key permission: `settings:read`)_
- `POST /v1/connections/reminders/decide` — Approve or skip a queued reminder _(key permission: `settings:write`)_
- `DELETE /v1/connections/:provider` — Disconnect an external system _(key permission: `settings:write`)_

### Account

- `GET /v1/whoami` — Confirm the API key and return the workspace it belongs to
- `GET /v1/usage` — Balance, spend today and the recent ledger _(key permission: `usage:read`)_
- `GET /v1/sub-accounts` — List sub-accounts with what each has sent and spent _(key permission: `subaccounts:read`)_
- `POST /v1/sub-accounts` — Create a sub-account _(key permission: `subaccounts:write`)_
- `PATCH /v1/sub-accounts/:id` — Set a sub-account’s sell rate _(key permission: `subaccounts:write`)_
- `GET /v1/sub-accounts/usage` — Usage per account over a period _(key permission: `usage:read`)_
- `GET /v1/sub-accounts/usage.csv` — The same report as a spreadsheet _(key permission: `usage:read`)_
- `GET /v1/billing` — Balance, plan, and whether billing is available _(key permission: `usage:read`)_
- `POST /v1/billing/checkout` — Start a Stripe Checkout to top up credit or subscribe to a plan _(key permission: `settings:write`)_
- `GET /v1/billing/statement` — A statement of every credit and charge with a running balance _(key permission: `usage:read`)_
- `GET /v1/billing/statement.csv` — The same statement as a spreadsheet _(key permission: `usage:read`)_
- `PATCH /v1/billing/preferences` — Set low-balance alerts and auto-recharge _(key permission: `settings:write`)_
- `GET /v1/insights` — Engagement and deliverability: click-through and failure reasons _(key permission: `usage:read`)_
- `GET /v1/audit` — What has been done in this workspace _(key permission: `settings:read`)_
- `GET /v1/audit.csv` — Export the audit trail as a spreadsheet _(key permission: `settings:read`)_
- `POST /v1/data/forget` — Erase a number: delete the contact, purge message content, suppress it _(key permission: `settings:write`)_
- `GET /v1/data/contacts.csv` — Export your contacts as a spreadsheet (data portability) _(key permission: `contacts:read`)_
- `GET /v1/team` — List the people here _(key permission: `team:read`)_
- `POST /v1/team/invites` — Invite someone _(key permission: `team:write`)_
- `DELETE /v1/team/invites/:id` — Revoke an invitation _(key permission: `team:write`)_
- `PATCH /v1/team/:userId` — Change someone’s role _(key permission: `team:write`)_
- `DELETE /v1/team/:userId` — Remove someone _(key permission: `team:write`)_

### Security

- `GET /v1/api-keys` — List API keys _(key permission: `keys:read`)_
- `POST /v1/api-keys` — Issue an API key _(key permission: `keys:write`)_
- `DELETE /v1/api-keys/:id` — Revoke a key _(key permission: `keys:write`)_
- `GET /v1/scopes` — The permission catalogue
- `GET /v1/openapi.json` — This API, as OpenAPI 3.1 _(no key required)_
- `GET /v1/reference` — The same description in a form the console renders _(no key required)_
- `GET /v1/integration.md` — A Markdown setup guide to feed an AI coding assistant _(no key required)_

---

## Task for the assistant

Integrate SMS365 into this project:

1. Add `SMS365_API_KEY` and `SMS365_BASE_URL` to the environment and to any
   `.env.example`, and confirm `.env` is git-ignored.
2. Create a small typed client (like the snippet above) in the project's
   conventional location, with one function per capability the project needs.
3. Always pass an `Idempotency-Key` on sends, derived from the domain object
   (order id, booking id) so a retry is safe.
4. Handle `402 insufficient_funds` and `429 rate_limited` explicitly.
5. For any marketing message, include `{{unsubscribe}}` in the body.
6. Add a short README section documenting how to get a key and top up credit.

Full machine-readable API: `https://app.sms365.com.au/v1/openapi.json`.

