# SMS

Texts to US and Canadian phones, the numbers they come from, the phones you test with, and a contact's SMS consent.

## Conventions

- Phones are E.164 strings: `+14155550123`.
- Money is a decimal string in dollars: `"0.015"`.
- The text is `text`, never `body`.
- `POST /sms` and `POST /sms/batch` take an `Idempotency-Key`.

A key needs `sending_access` to send and estimate, and `full_access` for everything else. It also needs SMS access, which an admin gives it on the API keys page: every key starts at `none`. `send` sends and estimates, `read` reads, and `full` does both and manages texts, numbers and test phones. An OAuth token needs `sms:send`, `sms:read` or `sms:manage`; consent is `contacts:write`.

SMS access covers the endpoints below. Texts sent by a broadcast or an automation follow those endpoints' own rules, which need `full_access`.

## Endpoints

### `POST /sms`

One text to a US or Canadian phone, now or later.

#### Headers

| Field | Type | Description |
| --- | --- | --- |
| `Idempotency-Key` | string | Makes the send exactly-once for 24 hours. Derive it from what you are sending, such as an order ID. |

#### Body

| Field | Type | Description |
| --- | --- | --- |
| `to` (required) | string | A US or Canadian phone in E.164 form, `+14155550123`. |
| `text` (required) | string | The text. Required unless `media_urls` has an image. Over 10 messages is `422 validation_error`. |
| `from` | string | One of your active numbers, or its ID. Absent: your default number for the destination's country. |
| `category` | string | `transactional` (the default) or `marketing`. A marketing text needs SMS consent and keeps to quiet hours. |

#### Body, less common

| Field | Type | Description |
| --- | --- | --- |
| `media_urls` | string[] | Up to 10 images for a picture message, US numbers only. Billed as 3 messages. |
| `scheduled_at` | string | An ISO 8601 time up to 30 days ahead. |
| `tags` | object | String values you want back on the text and its webhooks. |

Send a text:

```sh
curl -X POST "https://api.rasket.com/sms" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-1042-shipped" \
  -d '{
  "to": "+14155550123",
  "text": "Your order #1042 has shipped.",
  "category": "transactional",
  "tags": {
    "order": "1042"
  }
}'
```

```ts
const response = await fetch("https://api.rasket.com/sms", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
    "Content-Type": "application/json",
    "Idempotency-Key": "order-1042-shipped",
  },
  body: JSON.stringify({
    to: "+14155550123",
    text: "Your order #1042 has shipped.",
    category: "transactional",
    tags: {
      order: "1042"
    }
  }),
});

const { id } = await response.json();
```

```python
import os

import requests

response = requests.post(
    "https://api.rasket.com/sms",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
        "Idempotency-Key": "order-1042-shipped",
    },
    json={
    "to": "+14155550123",
    "text": "Your order #1042 has shipped.",
    "category": "transactional",
    "tags": {
      "order": "1042"
    }
  },
)

id = response.json()["id"]
```

#### Response `200`

```json
{
  "object": "sms",
  "id": "01928c3e-5b7a-7c1e-9a2f-3d4e5f60718a",
  "status": "queued",
  "from": "+18335550199",
  "to": "+14155550123",
  "text": "Acme: Your order #1042 has shipped.",
  "category": "transactional",
  "encoding": "gsm7",
  "segments": 1,
  "price_each": "0.015",
  "cost": "0.015",
  "currency": "usd",
  "included": true,
  "scheduled_at": null,
  "created_at": "2026-10-19T20:41:00.000Z",
  "tags": {
    "order": "1042"
  }
}
```

- `segments` is how many messages the text is billed as: up to 160 characters each, or 70 with an emoji or another special character.
- A destination outside the US and Canada is `422 unsupported_destination`.
- The month's texts used up is `429 sms_quota_exceeded`. It is never worth retrying.
- Until a number is approved, texts go only to your verified test phones, from our shared test number, with `[Test]` in front.

### `POST /sms/batch`

Up to 100 texts in one request, checked all or nothing.

#### Headers

| Field | Type | Description |
| --- | --- | --- |
| `Idempotency-Key` | string | Makes the send exactly-once for 24 hours. Derive it from what you are sending, such as an order ID. |

Send a batch of texts:

```sh
curl -X POST "https://api.rasket.com/sms/batch" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0" \
  -H "Content-Type: application/json" \
  -d '[
  {
    "to": "+14155550123",
    "text": "Your order #1042 has shipped.",
    "category": "transactional",
    "tags": {
      "order": "1042"
    }
  }
]'
```

```ts
const response = await fetch("https://api.rasket.com/sms/batch", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
    "Content-Type": "application/json",
  },
  body: JSON.stringify([
    {
      to: "+14155550123",
      text: "Your order #1042 has shipped.",
      category: "transactional",
      tags: {
        order: "1042"
      }
    }
  ]),
});

const data = await response.json();
```

```python
import os

import requests

response = requests.post(
    "https://api.rasket.com/sms/batch",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
    json=[
    {
      "to": "+14155550123",
      "text": "Your order #1042 has shipped.",
      "category": "transactional",
      "tags": {
        "order": "1042"
      }
    }
  ],
)

print(response.json())
```

#### Response `200`

```json
{
  "data": [
    {
      "id": "01928c3e-5b7a-7c1e-9a2f-3d4e5f60718a"
    }
  ]
}
```

- One `Idempotency-Key` covers the whole batch.

### `POST /sms/estimate`

Count and price a text without sending it.

#### Body

| Field | Type | Description |
| --- | --- | --- |
| `text` (required) | string | The text to count. |
| `to` | string | One phone, so the opt-out line is counted as a send to it would be. Estimating for a `segment_id` or `topic_id` is not available yet. |

#### Body, less common

| Field | Type | Description |
| --- | --- | --- |
| `media_urls` | string[] | Images, as on a send. |

Estimate a text:

```sh
curl -X POST "https://api.rasket.com/sms/estimate" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0" \
  -H "Content-Type: application/json" \
  -d '{
  "text": "Your code is 481 922",
  "to": "+14155550123"
}'
```

```ts
const response = await fetch("https://api.rasket.com/sms/estimate", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    text: "Your code is 481 922",
    to: "+14155550123"
  }),
});

const data = await response.json();
```

```python
import os

import requests

response = requests.post(
    "https://api.rasket.com/sms/estimate",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
    json={
    "text": "Your code is 481 922",
    "to": "+14155550123"
  },
)

print(response.json())
```

#### Response `200`

```json
{
  "object": "sms_estimate",
  "encoding": "gsm7",
  "characters": 20,
  "segments": 1,
  "recipients": 1,
  "price_each": "0.015",
  "included_remaining": 180,
  "overage_messages": 0,
  "cost": "0.000",
  "currency": "usd",
  "warnings": []
}
```

- `warnings` can say `special_characters`, `multi_part`, `merge_field_length` or `sender_not_active`.

### `GET /sms`

Texts you sent, newest first.

#### Query parameters

| Field | Type | Description |
| --- | --- | --- |
| `limit` | integer | How many items to return, 1–100. Defaults to 20. |
| `after` | string | Return the page that follows this item ID. Mutually exclusive with `before`. |
| `before` | string | Return the page that precedes this item ID. Mutually exclusive with `after`. |
| `status` | string | Only texts in this state. |
| `category` | string | `transactional` or `marketing`. |

#### Query parameters, less common

| Field | Type | Description |
| --- | --- | --- |
| `to` | string | One destination phone. |
| `broadcast_id` | string | One campaign's texts. |

List texts:

```sh
curl -X GET "https://api.rasket.com/sms" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/sms", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const data = await response.json();
```

```python
import os

import requests

response = requests.get(
    "https://api.rasket.com/sms",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

print(response.json())
```

#### Response `200`

```json
{
  "object": "list",
  "has_more": false,
  "data": [
    {
      "object": "sms",
      "id": "01928c3e-5b7a-7c1e-9a2f-3d4e5f60718a",
      "status": "queued",
      "from": "+18335550199",
      "to": "+14155550123",
      "text": "Acme: Your order #1042 has shipped.",
      "category": "transactional",
      "encoding": "gsm7",
      "segments": 1,
      "price_each": "0.015",
      "cost": "0.015",
      "currency": "usd",
      "included": true,
      "scheduled_at": null,
      "created_at": "2026-10-19T20:41:00.000Z",
      "tags": {
        "order": "1042"
      }
    }
  ]
}
```

### `GET /sms/{sms_id}`

One text, with what it cost.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `sms_id` (required) | string | The text's ID. |

Retrieve a text:

```sh
curl -X GET "https://api.rasket.com/sms/{sms_id}" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/sms/{sms_id}", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const { id } = await response.json();
```

```python
import os

import requests

response = requests.get(
    "https://api.rasket.com/sms/{sms_id}",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

id = response.json()["id"]
```

#### Response `200`

```json
{
  "object": "sms",
  "id": "01928c3e-5b7a-7c1e-9a2f-3d4e5f60718a",
  "status": "queued",
  "from": "+18335550199",
  "to": "+14155550123",
  "text": "Acme: Your order #1042 has shipped.",
  "category": "transactional",
  "encoding": "gsm7",
  "segments": 1,
  "price_each": "0.015",
  "cost": "0.015",
  "currency": "usd",
  "included": true,
  "scheduled_at": null,
  "created_at": "2026-10-19T20:41:00.000Z",
  "tags": {
    "order": "1042"
  }
}
```

### `PATCH /sms/{sms_id}`

Move a scheduled text to a new time.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `sms_id` (required) | string | The text's ID. |

#### Body

| Field | Type | Description |
| --- | --- | --- |
| `scheduled_at` (required) | string | The new time. |

Reschedule a text:

```sh
curl -X PATCH "https://api.rasket.com/sms/{sms_id}" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0" \
  -H "Content-Type: application/json" \
  -d '{
  "scheduled_at": "2026-10-20T15:00:00.000Z"
}'
```

```ts
const response = await fetch("https://api.rasket.com/sms/{sms_id}", {
  method: "PATCH",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    scheduled_at: "2026-10-20T15:00:00.000Z"
  }),
});

const { id } = await response.json();
```

```python
import os

import requests

response = requests.patch(
    "https://api.rasket.com/sms/{sms_id}",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
    json={
    "scheduled_at": "2026-10-20T15:00:00.000Z"
  },
)

id = response.json()["id"]
```

#### Response `200`

```json
{
  "object": "sms",
  "id": "01928c3e-5b7a-7c1e-9a2f-3d4e5f60718a",
  "status": "scheduled",
  "from": "+18335550199",
  "to": "+14155550123",
  "text": "Acme: Your order #1042 has shipped.",
  "category": "transactional",
  "encoding": "gsm7",
  "segments": 1,
  "price_each": "0.015",
  "cost": "0.015",
  "currency": "usd",
  "included": true,
  "scheduled_at": null,
  "created_at": "2026-10-19T20:41:00.000Z",
  "tags": {
    "order": "1042"
  }
}
```

- A marketing text is checked against quiet hours again at the new time.

### `GET /sms/{sms_id}/events`

What happened to a text, oldest first.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `sms_id` (required) | string | The text's ID. |

List a text's events:

```sh
curl -X GET "https://api.rasket.com/sms/{sms_id}/events" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/sms/{sms_id}/events", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const data = await response.json();
```

```python
import os

import requests

response = requests.get(
    "https://api.rasket.com/sms/{sms_id}/events",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

print(response.json())
```

#### Response `200`

```json
{
  "object": "list",
  "has_more": false,
  "data": [
    {
      "object": "sms_event",
      "type": "sms.delivered",
      "occurred_at": "2026-10-19T20:41:04.000Z",
      "data": {}
    }
  ]
}
```

### `POST /sms/{sms_id}/cancel`

Stop a scheduled or queued text before it is handed over.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `sms_id` (required) | string | The text's ID. |

Cancel a text:

```sh
curl -X POST "https://api.rasket.com/sms/{sms_id}/cancel" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/sms/{sms_id}/cancel", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const { id } = await response.json();
```

```python
import os

import requests

response = requests.post(
    "https://api.rasket.com/sms/{sms_id}/cancel",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

id = response.json()["id"]
```

#### Response `200`

```json
{
  "object": "sms",
  "id": "01928c3e-5b7a-7c1e-9a2f-3d4e5f60718a",
  "status": "canceled"
}
```

- A text already on its way is `409 resource_locked`. A canceled text is never charged.

### `GET /sms/received`

Texts people sent to your numbers, keyword replies included.

#### Query parameters

| Field | Type | Description |
| --- | --- | --- |
| `limit` | integer | How many items to return, 1–100. Defaults to 20. |
| `after` | string | Return the page that follows this item ID. Mutually exclusive with `before`. |
| `before` | string | Return the page that precedes this item ID. Mutually exclusive with `after`. |

List received texts:

```sh
curl -X GET "https://api.rasket.com/sms/received" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/sms/received", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const data = await response.json();
```

```python
import os

import requests

response = requests.get(
    "https://api.rasket.com/sms/received",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

print(response.json())
```

#### Response `200`

```json
{
  "object": "list",
  "has_more": false,
  "data": [
    {
      "object": "received_sms",
      "id": "01928c3f-0c1d-7e2f-8a3b-4c5d6e7f8091",
      "from": "+14155550123",
      "to": "+18335550199",
      "text": "Is my order on its way?",
      "media": [],
      "keyword": null,
      "action": "forwarded",
      "contact_id": "e169aa45-1ecf-4183-9955-b1499d5701d3",
      "received_at": "2026-10-19T20:45:00.000Z"
    }
  ]
}
```

### `GET /sms/received/{sms_id}`

One received text; images come as signed links.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `sms_id` (required) | string | The text's ID. |

Retrieve a received text:

```sh
curl -X GET "https://api.rasket.com/sms/received/{sms_id}" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/sms/received/{sms_id}", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const { id } = await response.json();
```

```python
import os

import requests

response = requests.get(
    "https://api.rasket.com/sms/received/{sms_id}",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

id = response.json()["id"]
```

#### Response `200`

```json
{
  "object": "received_sms",
  "id": "01928c3f-0c1d-7e2f-8a3b-4c5d6e7f8091",
  "from": "+14155550123",
  "to": "+18335550199",
  "text": "Is my order on its way?",
  "media": [],
  "keyword": null,
  "action": "forwarded",
  "contact_id": "e169aa45-1ecf-4183-9955-b1499d5701d3",
  "received_at": "2026-10-19T20:45:00.000Z"
}
```

### `GET /sms/received/{sms_id}/media/{index}/download`

Follow a received text's signed media link and get the bytes.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `sms_id` (required) | string | The text's ID. |
| `index` (required) | integer | The image's position in the text's `media`, from 0. |

#### Query parameters

| Field | Type | Description |
| --- | --- | --- |
| `expires` (required) | integer | Part of the signature. Copy the whole `url`; do not build this yourself. |
| `token` (required) | string | The link's signature, valid for fifteen minutes and for this image only. |

Download a received image:

```sh
curl -X GET "https://api.rasket.com/sms/received/{sms_id}/media/{index}/download?expires=1789200000&token=1f0c9d3b8a72e5461c0d" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/sms/received/{sms_id}/media/{index}/download?expires=1789200000&token=1f0c9d3b8a72e5461c0d", {
  method: "GET",
  headers: {
    "User-Agent": "acme-billing/1.0",
  },
});

console.log(response.status);
```

```python
import os

import requests

response = requests.get(
    "https://api.rasket.com/sms/received/{sms_id}/media/{index}/download?expires=1789200000&token=1f0c9d3b8a72e5461c0d",
    headers={
        "User-Agent": "acme-billing/1.0",
    },
)

print(response.status_code)
```

- This route takes no `Authorization` header: the link carries its own signed authorization so a browser can follow it.
- Every refusal is a `404` with the same message — a bad signature, an expired link, an image that has been deleted, an ID from another team.
- Webhook payloads never carry these links, because they would expire during retries: read the text with `GET /sms/received/{sms_id}` for fresh ones.

### `GET /sms/senders`

The numbers you text from, with their registration.

#### Query parameters

| Field | Type | Description |
| --- | --- | --- |
| `limit` | integer | How many items to return, 1–100. Defaults to 20. |
| `after` | string | Return the page that follows this item ID. Mutually exclusive with `before`. |
| `before` | string | Return the page that precedes this item ID. Mutually exclusive with `after`. |

List numbers:

```sh
curl -X GET "https://api.rasket.com/sms/senders" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/sms/senders", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const data = await response.json();
```

```python
import os

import requests

response = requests.get(
    "https://api.rasket.com/sms/senders",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

print(response.json())
```

#### Response `200`

```json
{
  "object": "list",
  "has_more": false,
  "data": [
    {
      "object": "sms_sender",
      "id": "0192a0b1-2c3d-7e4f-8a5b-6c7d8e9f0a1b",
      "phone": "+18335550199",
      "name": "Support line",
      "type": "toll_free",
      "country": "US",
      "status": "in_review",
      "use": "both",
      "included": true,
      "monthly_price": "0.00",
      "is_default": true,
      "default_for": ["US", "CA"],
      "reaches": ["US", "CA"],
      "registration": {
        "status": "in_review",
        "rejection_reason": null,
        "fields_to_fix": [],
        "resubmissions_left": 3,
        "canceled_at": null
      },
      "created_at": "2026-10-19T20:41:00.000Z"
    }
  ]
}
```

### `POST /sms/senders`

Buy a number and send its registration in one call.

#### Body

| Field | Type | Description |
| --- | --- | --- |
| `type` (required) | string | `toll_free` (US and Canada) or `local` (US). Short codes are on Enterprise. |
| `country` (required) | string | `US` or `CA`. |
| `phone` | string | A number from List available numbers. Absent: we pick one. |
| `business` (required) | object | `legal_name` (as on your tax record), `registration_number` (EIN in the US, BN in Canada), `entity_type`, `vertical` and a `contact` with `first_name`, `last_name`, `email` and `phone`. `website` and `address` are filled in from your verified domain and Billing when you leave them out. |
| `messages` (required) | object | `opt_in_url`, the page where people agree to your texts. Everything else — `brand_name`, up to five `sample_messages`, `opt_in_description`, `monthly_volume`, `help_email` — is drafted when you leave it out; Draft a registration shows the drafts. |

#### Body, less common

| Field | Type | Description |
| --- | --- | --- |
| `name` | string | What you call it, like Support line: up to 60 characters. |

Get a number:

```sh
curl -X POST "https://api.rasket.com/sms/senders" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0" \
  -H "Content-Type: application/json" \
  -d '{
  "type": "toll_free",
  "country": "US",
  "use": "both",
  "name": "Support line",
  "business": {
    "legal_name": "Acme Corporation",
    "website": "https://example.com",
    "address": {
      "line1": "1 Main St",
      "city": "Portland",
      "region": "OR",
      "postal_code": "97201",
      "country": "US"
    },
    "registration_number": "12-3456789",
    "entity_type": "private_profit",
    "vertical": "retail",
    "contact": {
      "first_name": "Ada",
      "last_name": "Lovelace",
      "email": "ada@example.com",
      "phone": "+15035550123"
    }
  },
  "messages": {
    "sample_messages": ["Acme: your order #1042 has shipped. Reply STOP to opt out."],
    "opt_in_description": "Customers tick a box at checkout to get order updates by text.",
    "opt_in_url": "https://example.com/checkout",
    "monthly_volume": 2000
  }
}'
```

```ts
const response = await fetch("https://api.rasket.com/sms/senders", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    type: "toll_free",
    country: "US",
    use: "both",
    name: "Support line",
    business: {
      legal_name: "Acme Corporation",
      website: "https://example.com",
      address: {
        line1: "1 Main St",
        city: "Portland",
        region: "OR",
        postal_code: "97201",
        country: "US"
      },
      registration_number: "12-3456789",
      entity_type: "private_profit",
      vertical: "retail",
      contact: {
        first_name: "Ada",
        last_name: "Lovelace",
        email: "ada@example.com",
        phone: "+15035550123"
      }
    },
    messages: {
      sample_messages: ["Acme: your order #1042 has shipped. Reply STOP to opt out."],
      opt_in_description: "Customers tick a box at checkout to get order updates by text.",
      opt_in_url: "https://example.com/checkout",
      monthly_volume: 2000
    }
  }),
});

const { id } = await response.json();
```

```python
import os

import requests

response = requests.post(
    "https://api.rasket.com/sms/senders",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
    json={
    "type": "toll_free",
    "country": "US",
    "use": "both",
    "name": "Support line",
    "business": {
      "legal_name": "Acme Corporation",
      "website": "https://example.com",
      "address": {
        "line1": "1 Main St",
        "city": "Portland",
        "region": "OR",
        "postal_code": "97201",
        "country": "US"
      },
      "registration_number": "12-3456789",
      "entity_type": "private_profit",
      "vertical": "retail",
      "contact": {
        "first_name": "Ada",
        "last_name": "Lovelace",
        "email": "ada@example.com",
        "phone": "+15035550123"
      }
    },
    "messages": {
      "sample_messages": ["Acme: your order #1042 has shipped. Reply STOP to opt out."],
      "opt_in_description": "Customers tick a box at checkout to get order updates by text.",
      "opt_in_url": "https://example.com/checkout",
      "monthly_volume": 2000
    }
  },
)

id = response.json()["id"]
```

#### Response `201`

```json
{
  "object": "sms_sender",
  "id": "0192a0b1-2c3d-7e4f-8a5b-6c7d8e9f0a1b",
  "phone": "+18335550199",
  "name": "Support line",
  "type": "toll_free",
  "country": "US",
  "status": "in_review",
  "use": "both",
  "included": true,
  "monthly_price": "0.00",
  "is_default": true,
  "default_for": ["US", "CA"],
  "reaches": ["US", "CA"],
  "registration": {
    "status": "in_review",
    "rejection_reason": null,
    "fields_to_fix": [],
    "resubmissions_left": 3,
    "canceled_at": null
  },
  "created_at": "2026-10-19T20:41:00.000Z"
}
```

- Paid plans only. Approval usually takes 1 to 5 business days; until then the number can only send test texts.
- Send an `Idempotency-Key` so a retry returns the number it bought rather than buying a second.

### `GET /sms/senders/draft`

What Get a number fills in for you, and what only you can give.

#### Query parameters

| Field | Type | Description |
| --- | --- | --- |
| `use` (required) | string | `transactional`, `marketing` or `both`. |

Draft a registration:

```sh
curl -X GET "https://api.rasket.com/sms/senders/draft?use=both" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/sms/senders/draft?use=both", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const data = await response.json();
```

```python
import os

import requests

response = requests.get(
    "https://api.rasket.com/sms/senders/draft?use=both",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

print(response.json())
```

#### Response `200`

```json
{
  "object": "sms_sender_draft",
  "business": {
    "website": "https://example.com"
  },
  "messages": {
    "brand_name": "Acme",
    "sample_messages": ["Acme: Your order #1042 has shipped. Track it at https://example.com/orders/1042"],
    "monthly_volume": 1000
  },
  "missing": ["business.legal_name", "business.registration_number", "messages.opt_in_url"]
}
```

### `GET /sms/senders/available`

Numbers you can pick before you buy one.

#### Query parameters

| Field | Type | Description |
| --- | --- | --- |
| `country` | string | `US` or `CA`. |
| `type` | string | `toll_free` or `local`. |
| `limit` | integer | 1 to 20, default 5. |

#### Query parameters, less common

| Field | Type | Description |
| --- | --- | --- |
| `area_code` | string | Three digits. |
| `contains` | string | Digits the number contains. |

List available numbers:

```sh
curl -X GET "https://api.rasket.com/sms/senders/available?country=US&type=toll_free" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/sms/senders/available?country=US&type=toll_free", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const data = await response.json();
```

```python
import os

import requests

response = requests.get(
    "https://api.rasket.com/sms/senders/available?country=US&type=toll_free",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

print(response.json())
```

#### Response `200`

```json
{
  "object": "list",
  "data": [
    {
      "phone": "+18335550199",
      "type": "toll_free",
      "country": "US",
      "region": null,
      "monthly_price": "2.00"
    }
  ]
}
```

### `GET /sms/senders/{sender_id}`

One number and where its registration is.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `sender_id` (required) | string | The number's ID. |

Retrieve a number:

```sh
curl -X GET "https://api.rasket.com/sms/senders/{sender_id}" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/sms/senders/{sender_id}", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const { id } = await response.json();
```

```python
import os

import requests

response = requests.get(
    "https://api.rasket.com/sms/senders/{sender_id}",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

id = response.json()["id"]
```

#### Response `200`

```json
{
  "object": "sms_sender",
  "id": "0192a0b1-2c3d-7e4f-8a5b-6c7d8e9f0a1b",
  "phone": "+18335550199",
  "name": "Support line",
  "type": "toll_free",
  "country": "US",
  "status": "in_review",
  "use": "both",
  "included": true,
  "monthly_price": "0.00",
  "is_default": true,
  "default_for": ["US", "CA"],
  "reaches": ["US", "CA"],
  "registration": {
    "status": "in_review",
    "rejection_reason": null,
    "fields_to_fix": [],
    "resubmissions_left": 3,
    "canceled_at": null
  },
  "created_at": "2026-10-19T20:41:00.000Z"
}
```

### `PATCH /sms/senders/{sender_id}`

Give a number a name, or send texts from it when no `from` is given.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `sender_id` (required) | string | The number's ID. |

#### Body

| Field | Type | Description |
| --- | --- | --- |
| `name` | string | Up to 60 characters. `null` removes the name. |
| `is_default` | boolean | `true` makes it the default for every country it reaches. |

Rename a number or make it the default:

```sh
curl -X PATCH "https://api.rasket.com/sms/senders/{sender_id}" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0" \
  -H "Content-Type: application/json" \
  -d '{
  "name": "Support line",
  "is_default": true
}'
```

```ts
const response = await fetch("https://api.rasket.com/sms/senders/{sender_id}", {
  method: "PATCH",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    name: "Support line",
    is_default: true
  }),
});

const { id } = await response.json();
```

```python
import os

import requests

response = requests.patch(
    "https://api.rasket.com/sms/senders/{sender_id}",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
    json={
    "name": "Support line",
    "is_default": True
  },
)

id = response.json()["id"]
```

#### Response `200`

```json
{
  "object": "sms_sender",
  "id": "0192a0b1-2c3d-7e4f-8a5b-6c7d8e9f0a1b",
  "phone": "+18335550199",
  "name": "Support line",
  "type": "toll_free",
  "country": "US",
  "status": "active",
  "use": "both",
  "included": true,
  "monthly_price": "0.00",
  "is_default": true,
  "default_for": ["US", "CA"],
  "reaches": ["US", "CA"],
  "registration": {
    "status": "in_review",
    "rejection_reason": null,
    "fields_to_fix": [],
    "resubmissions_left": 3,
    "canceled_at": null
  },
  "created_at": "2026-10-19T20:41:00.000Z"
}
```

- Only an active number can be made the default. Automations and campaigns send from the default and do not fall back past one that cannot send yet.
- A toll-free number becomes the default for the US and Canada, a local number for the US alone, and the previous default for those countries stops being one. `default_for` shows which countries a number is the default for.
- There is always a way back: make the other number the default again. `is_default: false` is refused.

### `POST /sms/senders/{sender_id}/refresh`

Read the registration again, or send a rejected or canceled one back with corrections.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `sender_id` (required) | string | The number's ID. |

#### Body

| Field | Type | Description |
| --- | --- | --- |
| `business` | object | `legal_name` (as on your tax record), `registration_number` (EIN in the US, BN in Canada), `entity_type`, `vertical` and a `contact` with `first_name`, `last_name`, `email` and `phone`. `website` and `address` are filled in from your verified domain and Billing when you leave them out. |
| `messages` | object | `opt_in_url`, the page where people agree to your texts. Everything else — `brand_name`, up to five `sample_messages`, `opt_in_description`, `monthly_volume`, `help_email` — is drafted when you leave it out; Draft a registration shows the drafts. |

Check or fix a registration:

```sh
curl -X POST "https://api.rasket.com/sms/senders/{sender_id}/refresh" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0" \
  -H "Content-Type: application/json" \
  -d '{
  "messages": {
    "sample_messages": ["Acme: your order #1042 has shipped. Reply STOP to opt out."],
    "opt_in_description": "Customers tick a box at checkout to get order updates by text.",
    "opt_in_url": "https://example.com/checkout",
    "monthly_volume": 2000
  }
}'
```

```ts
const response = await fetch("https://api.rasket.com/sms/senders/{sender_id}/refresh", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    messages: {
      sample_messages: ["Acme: your order #1042 has shipped. Reply STOP to opt out."],
      opt_in_description: "Customers tick a box at checkout to get order updates by text.",
      opt_in_url: "https://example.com/checkout",
      monthly_volume: 2000
    }
  }),
});

const { id } = await response.json();
```

```python
import os

import requests

response = requests.post(
    "https://api.rasket.com/sms/senders/{sender_id}/refresh",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
    json={
    "messages": {
      "sample_messages": ["Acme: your order #1042 has shipped. Reply STOP to opt out."],
      "opt_in_description": "Customers tick a box at checkout to get order updates by text.",
      "opt_in_url": "https://example.com/checkout",
      "monthly_volume": 2000
    }
  },
)

id = response.json()["id"]
```

#### Response `200`

```json
{
  "object": "sms_sender",
  "id": "0192a0b1-2c3d-7e4f-8a5b-6c7d8e9f0a1b",
  "phone": "+18335550199",
  "name": "Support line",
  "type": "toll_free",
  "country": "US",
  "status": "in_review",
  "use": "both",
  "included": true,
  "monthly_price": "0.00",
  "is_default": true,
  "default_for": ["US", "CA"],
  "reaches": ["US", "CA"],
  "registration": {
    "status": "in_review",
    "rejection_reason": null,
    "fields_to_fix": [],
    "resubmissions_left": 3,
    "canceled_at": null
  },
  "created_at": "2026-10-19T20:41:00.000Z"
}
```

- At most once a minute. Three corrections after a rejection are included; after that, contact support.

### `POST /sms/senders/{sender_id}/cancel-registration`

Withdraw a number's registration and keep the number, to send it again later.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `sender_id` (required) | string | The number's ID. |

Cancel a registration:

```sh
curl -X POST "https://api.rasket.com/sms/senders/{sender_id}/cancel-registration" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/sms/senders/{sender_id}/cancel-registration", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const { id } = await response.json();
```

```python
import os

import requests

response = requests.post(
    "https://api.rasket.com/sms/senders/{sender_id}/cancel-registration",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

id = response.json()["id"]
```

#### Response `200`

```json
{
  "object": "sms_sender",
  "id": "0192a0b1-2c3d-7e4f-8a5b-6c7d8e9f0a1b",
  "phone": "+18335550199",
  "name": "Support line",
  "type": "toll_free",
  "country": "US",
  "status": "testing",
  "use": "both",
  "included": true,
  "monthly_price": "0.00",
  "is_default": true,
  "default_for": ["US", "CA"],
  "reaches": ["US", "CA"],
  "registration": {
    "status": "draft",
    "rejection_reason": null,
    "fields_to_fix": [],
    "resubmissions_left": 2,
    "canceled_at": "2026-10-19T20:41:00.000Z"
  },
  "created_at": "2026-10-19T20:41:00.000Z"
}
```

- For a number that is being set up or was rejected. A registration the carriers are reviewing, or one they approved, can't be withdrawn: the answer is 422 with the reason.
- You keep the number and its monthly price. A local number's registration is shared by all your local numbers, and all of them go back to testing.
- Send it again with Check or fix a registration and a business or messages block. If it had reached the carriers, that uses one of the 3 included resubmissions.

### `DELETE /sms/senders/{sender_id}`

Give a number up. Its scheduled texts are canceled.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `sender_id` (required) | string | The number's ID. |

Release a number:

```sh
curl -X DELETE "https://api.rasket.com/sms/senders/{sender_id}" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/sms/senders/{sender_id}", {
  method: "DELETE",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const { id } = await response.json();
```

```python
import os

import requests

response = requests.delete(
    "https://api.rasket.com/sms/senders/{sender_id}",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

id = response.json()["id"]
```

#### Response `200`

```json
{
  "object": "sms_sender",
  "id": "0192a0b1-2c3d-7e4f-8a5b-6c7d8e9f0a1b",
  "phone": "+18335550199",
  "name": "Support line",
  "type": "toll_free",
  "country": "US",
  "status": "released",
  "use": "both",
  "included": true,
  "monthly_price": "0.00",
  "is_default": true,
  "default_for": ["US", "CA"],
  "reaches": ["US", "CA"],
  "registration": {
    "status": "in_review",
    "rejection_reason": null,
    "fields_to_fix": [],
    "resubmissions_left": 3,
    "canceled_at": null
  },
  "created_at": "2026-10-19T20:41:00.000Z"
}
```

### `GET /sms/verified-phones`

The phones test texts may go to.

#### Query parameters

| Field | Type | Description |
| --- | --- | --- |
| `limit` | integer | How many items to return, 1–100. Defaults to 20. |
| `after` | string | Return the page that follows this item ID. Mutually exclusive with `before`. |
| `before` | string | Return the page that precedes this item ID. Mutually exclusive with `after`. |

List test phones:

```sh
curl -X GET "https://api.rasket.com/sms/verified-phones" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/sms/verified-phones", {
  method: "GET",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const data = await response.json();
```

```python
import os

import requests

response = requests.get(
    "https://api.rasket.com/sms/verified-phones",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

print(response.json())
```

#### Response `200`

```json
{
  "object": "list",
  "has_more": false,
  "data": [
    {
      "object": "sms_verified_phone",
      "id": "0192a0b2-3d4e-7f50-9b6c-7d8e9f0a1b2c",
      "phone": "+14155550123",
      "status": "verified",
      "verified_at": null,
      "created_at": "2026-10-19T20:41:00.000Z"
    }
  ]
}
```

### `POST /sms/verified-phones`

Text a code to one of your own phones.

#### Body

| Field | Type | Description |
| --- | --- | --- |
| `phone` (required) | string | The phone, E.164. |

Add a test phone:

```sh
curl -X POST "https://api.rasket.com/sms/verified-phones" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0" \
  -H "Content-Type: application/json" \
  -d '{
  "phone": "+14155550123"
}'
```

```ts
const response = await fetch("https://api.rasket.com/sms/verified-phones", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    phone: "+14155550123"
  }),
});

const { id } = await response.json();
```

```python
import os

import requests

response = requests.post(
    "https://api.rasket.com/sms/verified-phones",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
    json={
    "phone": "+14155550123"
  },
)

id = response.json()["id"]
```

#### Response `201`

```json
{
  "object": "sms_verified_phone",
  "id": "0192a0b2-3d4e-7f50-9b6c-7d8e9f0a1b2c",
  "phone": "+14155550123",
  "status": "pending",
  "verified_at": null,
  "created_at": "2026-10-19T20:41:00.000Z"
}
```

- At most 10 test phones, and 5 new ones a day.

### `POST /sms/verified-phones/{verified_phone_id}/verify`

Enter the code the phone received.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `verified_phone_id` (required) | string | The test phone's ID. |

#### Body

| Field | Type | Description |
| --- | --- | --- |
| `code` (required) | string | The 6 digits. |

Confirm a test phone:

```sh
curl -X POST "https://api.rasket.com/sms/verified-phones/{verified_phone_id}/verify" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0" \
  -H "Content-Type: application/json" \
  -d '{
  "code": "481922"
}'
```

```ts
const response = await fetch("https://api.rasket.com/sms/verified-phones/{verified_phone_id}/verify", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    code: "481922"
  }),
});

const { id } = await response.json();
```

```python
import os

import requests

response = requests.post(
    "https://api.rasket.com/sms/verified-phones/{verified_phone_id}/verify",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
    json={
    "code": "481922"
  },
)

id = response.json()["id"]
```

#### Response `200`

```json
{
  "object": "sms_verified_phone",
  "id": "0192a0b2-3d4e-7f50-9b6c-7d8e9f0a1b2c",
  "phone": "+14155550123",
  "status": "verified",
  "verified_at": "2026-10-19T20:41:00.000Z",
  "created_at": "2026-10-19T20:41:00.000Z"
}
```

- A code lasts 10 minutes and allows 3 tries.

### `DELETE /sms/verified-phones/{verified_phone_id}`

Stop sending test texts to a phone.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `verified_phone_id` (required) | string | The test phone's ID. |

Remove a test phone:

```sh
curl -X DELETE "https://api.rasket.com/sms/verified-phones/{verified_phone_id}" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0"
```

```ts
const response = await fetch("https://api.rasket.com/sms/verified-phones/{verified_phone_id}", {
  method: "DELETE",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
  },
});

const { id } = await response.json();
```

```python
import os

import requests

response = requests.delete(
    "https://api.rasket.com/sms/verified-phones/{verified_phone_id}",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
)

id = response.json()["id"]
```

#### Response `200`

```json
{
  "object": "sms_verified_phone",
  "id": "0192a0b2-3d4e-7f50-9b6c-7d8e9f0a1b2c",
  "deleted": true
}
```

### `PATCH /contacts/{contact_id}/sms-consent`

Record that a person agreed to texts, with proof, or that they stopped.

#### Path parameters

| Field | Type | Description |
| --- | --- | --- |
| `contact_id` (required) | string | The contact's ID. |

#### Body

| Field | Type | Description |
| --- | --- | --- |
| `status` (required) | string | `subscribed` or `unsubscribed`. |
| `proof` | object | Required for `subscribed`: `method` (`form`, `keyword`, `import`, `api` or `dashboard`), `occurred_at`, `disclosure_text` (the exact words the person saw), and the `page_url`, `ip` and `user_agent` where you have them. `ip` and `user_agent` are the person's, not your server's. |
| `double_opt_in` | boolean | Text a confirmation first; the contact is `pending` until they reply Y. |

Set a contact's SMS consent:

```sh
curl -X PATCH "https://api.rasket.com/contacts/e169aa45-1ecf-4183-9955-b1499d5701d3/sms-consent" \
  -H "Authorization: Bearer $RASKET_API_KEY" \
  -H "User-Agent: acme-billing/1.0" \
  -H "Content-Type: application/json" \
  -d '{
  "status": "subscribed",
  "proof": {
    "method": "form",
    "occurred_at": "2026-10-19T18:02:11.000Z",
    "page_url": "https://example.com/join",
    "disclosure_text": "By checking this box you agree to receive marketing texts from Acme."
  }
}'
```

```ts
const response = await fetch("https://api.rasket.com/contacts/e169aa45-1ecf-4183-9955-b1499d5701d3/sms-consent", {
  method: "PATCH",
  headers: {
    Authorization: `Bearer ${process.env.RASKET_API_KEY}`,
    "User-Agent": "acme-billing/1.0",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    status: "subscribed",
    proof: {
      method: "form",
      occurred_at: "2026-10-19T18:02:11.000Z",
      page_url: "https://example.com/join",
      disclosure_text: "By checking this box you agree to receive marketing texts from Acme."
    }
  }),
});

const { id } = await response.json();
```

```python
import os

import requests

response = requests.patch(
    "https://api.rasket.com/contacts/e169aa45-1ecf-4183-9955-b1499d5701d3/sms-consent",
    headers={
        "Authorization": f"Bearer {os.environ['RASKET_API_KEY']}",
        "User-Agent": "acme-billing/1.0",
    },
    json={
    "status": "subscribed",
    "proof": {
      "method": "form",
      "occurred_at": "2026-10-19T18:02:11.000Z",
      "page_url": "https://example.com/join",
      "disclosure_text": "By checking this box you agree to receive marketing texts from Acme."
    }
  },
)

id = response.json()["id"]
```

#### Response `200`

```json
{
  "object": "contact",
  "id": "e169aa45-1ecf-4183-9955-b1499d5701d3",
  "email": "steve.wozniak@gmail.com",
  "first_name": "Steve",
  "last_name": "Wozniak",
  "created_at": "2026-10-01T09:00:00.000Z",
  "unsubscribed": false,
  "subscription_status": "subscribed",
  "phone": "+14155550123",
  "sms_status": "subscribed",
  "properties": {}
}
```

- A topic subscription is not SMS consent. Only this, a keyword reply or a form's phone step sets it.
- Someone who texted STOP stays opted out until they text START: `subscribed` answers 403.
- The contact needs a phone first (`phone` on create or update). The same status again changes nothing.
