# API docs

**The Spam I Am API lets your code or AI agent check whether a number shows a spam label on AT&T, Verizon and T-Mobile.** You start a check, your calling platform calls each carrier's test line from the number being checked, and you read the result: the label, the caller name and a screenshot from a real phone on each carrier.

*Last updated: 2026-10-24*

- **Base address:** `https://ismynumberspam.com/v1`
- **Format:** JSON in, JSON out
- **Keyless first check:** the first check on any number works without a key
- **Keys:** make one on your Balance page after signing up. Send it as `Authorization: Bearer sia_...`
- **Machine-readable spec:** [openapi.json](https://ismynumberspam.com/openapi.json)

The API can't place calls. The test calls must come from the number you're checking, and we never spoof numbers, so your platform (Twilio, Retell, Vapi, Bland, a dialer or a phone) places them.

## Quickstart

### 1. Start a check

```bash
curl -X POST https://ismynumberspam.com/v1/checks \
  -H "Content-Type: application/json" \
  -d '{"number": "2145550100"}'
```

Without a key this uses the number's free check, which covers every carrier that's online. The answer includes a `token`. Keep it: it's the only way to read this check's result.

```json
{
 "id": "9f1c...",
 "number": "+12145550100",
 "status": "waiting_for_calls",
 "token": "ct_...",
 "calls": [
  {"carrier": "AT&T", "dial": "+14695550123", "status": "waiting", "seconds_left": 239},
  {"carrier": "Verizon", "dial": "+1...", "status": "waiting", "seconds_left": 239},
  {"carrier": "T-Mobile", "dial": "+1...", "status": "waiting", "seconds_left": 239}
 ],
 "instructions": "From +12145550100, with caller ID showing, call each 'dial' number..."
}
```

### 2. Call each test line

From the number being checked, with caller ID showing, call each `dial` number, one after another. Our phone reads the screen, then sends the call to voicemail, so you can hang up after a few seconds. You have 2 minutes for one carrier, plus 1 minute for each additional carrier.

### 3. Read the result

Ask for the check every 5 seconds until `status` is `complete`:

```bash
curl "https://ismynumberspam.com/v1/checks/9f1c...?token=ct_..."
```

```json
{
 "id": "9f1c...",
 "status": "complete",
 "flagged": true,
 "flagged_on": ["AT&T"],
 "calls": [
  {"carrier": "AT&T", "status": "complete", "flagged": true, "label": "Spam Risk",
   "caller_name": "Dallas, TX", "checked_at": "2026-10-24T18:02:11.000Z",
   "screenshot_url": "https://ismynumberspam.com/v1/checks/9f1c.../screenshots/....jpg?token=ct_..."},
  {"carrier": "Verizon", "status": "complete", "flagged": false, "label": null,
   "caller_name": null},
  {"carrier": "T-Mobile", "status": "incomplete", "reason": "no_call"}
 ],
 "cost": {"free": true, "held_cents": 0, "charged_cents": 0}
}
```

With a key, send `Authorization: Bearer sia_...` instead of the `token`.

## Endpoints

### POST /v1/checks

Starts a check. Body:

- `number` (required): the US number you'll call from, in any format, such as `"2145550100"`, `"+1 214-555-0100"` or `"(214) 555-0100"`
- `carriers` (optional): a list such as `["AT&T", "Verizon"]`. Leave it out to check every carrier that's online.
- `webhook_url` (optional): an https address to send the result to when the check finishes. See [Webhooks](#webhooks).

Answers `201` with the check. Without a key, the number must still have its free check.

With a key, the number's free check is used if it hasn't been. After that, the price is held from your balance while the check runs. You're charged only for carriers that return a result.

### GET /v1/checks/{id}

The check and each carrier's result. Add `?token=ct_...` for a keyless check. While any carrier is still waiting, the answer has a `Retry-After: 5` header.

Each entry in `calls`:

| Field | Meaning |
|---|---|
| `carrier` | AT&T, Verizon or T-Mobile |
| `dial` | The test line to call for this carrier |
| `status` | `waiting`, `complete` or `incomplete` |
| `seconds_left` | While waiting: time left to place the call |
| `flagged` | When complete: true if the label is a spam warning |
| `label` | When complete: the label shown, such as "Spam Risk", or null |
| `caller_name` | When complete: the caller name shown, or null |
| `reason` | When incomplete: `no_call` (no call arrived in time) or `unreadable_screen` |
| `screenshot_url` | The incoming-call screen, as a JPEG |
| `checked_at` | When the call rang |

At the top level, `flagged` is true if any carrier shows a spam label, false if none do, and null if no carrier returned a result. `flagged_on` lists the carriers that do.

**Incomplete with `no_call`, even though you dialed from the right number with caller ID on?** The carrier may have blocked the call before it rang. Treat that as more urgent than a label, and check your logs for SIP 603+. [What 603+ means](/guides/sip-603-plus-608-network-blocked)

### GET /v1/checks/{id}/screenshots/{result}.jpg

The screenshot. Use `screenshot_url` from the check; it already carries the token for a keyless check.

### GET /v1/balance

Needs a key. Your balance, the amount held for checks in progress, and what's available, in cents. Also this key's spending limit (`key_spend_limit_cents`, null for none) and what it has spent (`key_spent_cents`).

### POST /v1/handoffs and GET /v1/handoffs/{id}

Ask your human to approve spending. See [below](#ask-your-human-to-approve-spending).

### GET /v1/coverage

No key needed. Which carriers are online now, prices in cents by number of carriers, whether the free first check is on, and the dialing window.

## Webhooks

Pass `webhook_url` when you start a check, and we POST the finished check to it: once every carrier has a result or its dialing window has ended.

```json
{"type": "check.completed", "check": { ...the same JSON as GET /v1/checks/{id}... }}
```

- **Secret:** the answer to `POST /v1/checks` has `webhook.secret` (starting `whsec_`). It's shown only there.
- **Signature:** each POST has a `Spamiam-Signature: t=1761321731,v1=5f2b...` header. Compute HMAC-SHA256 of `<t>.<raw body>` with the secret, and compare it with `v1`. Reject old `t` values to stop replays.
- **Answer with any 2xx status.** Otherwise we try again after 30 seconds, 2 minutes, 10 minutes and 1 hour, then stop. `GET /v1/checks/{id}` shows `webhook.status` (`pending`, `delivered` or `failed`) and the last error.
- **Addresses** must be https and reachable on the public internet.

## Ask your human to approve spending

After a number's free check, checks cost money. An agent shouldn't pay or handle card details. Instead it asks its human:

```bash
curl -X POST https://ismynumberspam.com/v1/handoffs \
  -H "Content-Type: application/json" \
  -d '{"reason": "Check our sales line before Monday's campaign", "amount_cents": 500}'
```

```json
{
 "id": "4c7e...",
 "status": "pending",
 "approve_url": "https://ismynumberspam.com/approve/4c7e...",
 "token": "ht_...",
 "expires_at": "2026-10-25T18:00:00.000Z"
}
```

1. **Show `approve_url` to your human.** They sign in or create an account, add funds if needed, set a spending limit and approve (or decline).
2. **Ask for the request every 5 seconds:** `GET /v1/handoffs/{id}?token=ht_...`. Its `status` becomes `approved`, `declined` or `expired` (after 24 hours).
3. **When it's approved, the answer has `api_key`,** shown once. Store it and send it as `Authorization: Bearer sia_...`. It can spend up to the limit your human set.

All fields are optional: `reason` (up to 300 characters, shown to your human), `number` (the number you want to check) and `amount_cents` (the amount you're asking for; suggested as the limit).

**Already have a key that hit its limit?** Send the request with your key. Approving it sets your key's new limit; no new key is made. Read it with the same key.

## Errors

Errors use one format:

```json
{"error": {"code": "too_soon", "message": "(214) 555-0100 was checked recently..."}}
```

| Status | Code | What to do |
|---|---|---|
| 400 | `invalid_number` | Send a 10-digit US number |
| 400 | `invalid_carriers` | Use AT&T, Verizon or T-Mobile |
| 400 | `invalid_json` | Send a JSON object |
| 400 | `test_line` | That's one of our test lines. Send the number you'll call from |
| 401 | `api_key_required` | Send a key |
| 401 | `invalid_api_key` | The key is wrong or revoked. Make a new one |
| 400 | `invalid_webhook_url` | Use a public https address |
| 402 | `insufficient_funds` | Ask your human to add funds ([approval request](#ask-your-human-to-approve-spending)). The error has `price_cents` and `available_cents` |
| 402 | `key_limit` | This key reached its spending limit. Ask your human to approve more. The error has `limit_cents` and `spent_cents` |
| 404 | `not_found` | Wrong id, or the check belongs to another key or token |
| 409 | `free_check_used` | This number had its free check. Use a key |
| 429 | `too_soon` | Each number can be checked once an hour. Wait for `Retry-After` seconds |
| 429 | `too_many_open` | Finish or let your open checks expire first |
| 429 | `daily_limit` | Daily check limit reached |
| 429 | `free_limit` | Too many keyless checks from your network today. Use a key |
| 429 | `handoff_limit` | Too many approval requests from your network today |
| 503 | `no_lines` | No test line is online for those carriers. Try again later |

## Rules

- **Check only numbers you control.** The test calls must come from the number.
- **Ask before spending.** Checks after the first cost money. An agent shouldn't add funds itself. It asks its human with [an approval request](#ask-your-human-to-approve-spending).
- **Once an hour per number.** A check with no calls doesn't count.
- **Don't auto-rotate numbers** when one is flagged. Pause it, tell a human and request a review. [Why numbers get flagged](/guides/why-number-shows-spam-likely)

## Pricing

The first check on any number is free. After that, **$0.25 for one carrier or $0.40 for two or three**, from the same balance you use on the website. Incomplete carriers are free. [Pricing](/pricing)

## MCP

Agents that use Model Context Protocol can connect to `https://ismynumberspam.com/mcp` instead. [MCP server](/docs/mcp)

## FAQ

### Do I need a key?
Not for the first check on a number. After that, sign up, add funds and make a key on the Balance page. An agent can also ask its human for a key with an approval request.

### Can I cap what an agent spends?
Yes. Every key can have a spending limit. Set it when you make the key on the Balance page, or when you approve an agent's request.

### Can the API place the test calls for me?
No. The calls must come from the number you're checking, so your own platform places them. We never spoof numbers.

### How do I get the result?
Ask for the check every 5 seconds until its status is complete, or pass a webhook_url and we'll send it to you. Results usually arrive about 5 seconds after each call rings.

### What does an API check cost?
The same as on the website: $0.25 for one carrier or $0.40 for two or three. Carriers that return no result are free.
