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

# API introduction

> Base URL, API keys and scopes, rate limits, errors, pagination, idempotency and the request log for the InstantCampaign public API.

The public API lets your own systems work with an InstantCampaign workspace. You can upsert contacts and manage list membership, record events and goals, send transactional email, subscribe to webhooks, start journeys and connect AI agents over MCP.

The pages in this tab are overviews. The full reference, with every request field, response schema and error code, is available to registered workspaces: open **Settings → API reference** in the app, or fetch the OpenAPI 3.1 document from `GET https://instantcampaign.ai/api/v1/openapi.json` while signed in or with a workspace API key. You can import that document into Postman, Insomnia or an OpenAPI code generator.

<Note>
  Only the endpoints documented here are public. The dashboard calls other `/api/*` routes with a browser session. Those routes are internal, they can change without notice, and they do not accept API keys.
</Note>

## Base URL

```
https://instantcampaign.ai/api
```

| Surface          | Paths                      | Credential                                |
| ---------------- | -------------------------- | ----------------------------------------- |
| REST API         | `/v1/*`                    | Workspace API key                         |
| Website tracking | `/public/events`           | Tracking token (`ict_…`) or API key       |
| Journey trigger  | `/public/journeys/trigger` | API key **and** the journey's trigger key |
| MCP server       | `/mcp`                     | Workspace API key with the `mcp` scope    |

## API keys

To create a key, open **Settings → API Keys**. Creating or deleting keys needs the **API keys: manage** permission: Owners, Administrators, or a custom role that grants it. The full key is shown **once**, when it is created. It starts with `ic_`. We keep only a hash of it, so a lost key has to be replaced.

Send the key as a bearer token:

```bash theme={null}
curl https://instantcampaign.ai/api/v1/lists \
  -H "Authorization: Bearer ic_YOUR_KEY"
```

A key can have an expiry date. After that date every call with it answers `401`. You can delete a key at any time on the same page.

### Scopes

A key can be limited to scopes. **A key with no scopes has full access.** A key with scopes can only call endpoints whose scope it holds. Any other call answers:

```json theme={null}
{ "error": "This API key lacks the \"contacts:write\" scope", "code": "insufficient_scope", "requiredScope": "contacts:write" }
```

Some routes (transactional, public events, MCP) leave out `requiredScope`, so check `code`.

| Scope                 | Grants                                                                                             |
| --------------------- | -------------------------------------------------------------------------------------------------- |
| `contacts:read`       | `GET /v1/contacts`, `GET /v1/contacts/search`, `GET /v1/lists`                                     |
| `contacts:write`      | `POST /v1/contacts`, `POST /v1/lists/{id}/subscribe`, `POST /v1/lists/{id}/unsubscribe`            |
| `events:write`        | `POST /v1/events`, `POST /v1/events/import`, and `POST /public/events` when called with an API key |
| `transactional:write` | `POST /v1/transactional/send`, `POST /v1/transactional/validate`                                   |
| `transactional:read`  | `GET /v1/transactional`, `GET /v1/transactional/{messageId}/status`                                |
| `webhooks:manage`     | `GET /v1/webhooks/events`, `POST /v1/webhooks/subscribe`, `DELETE /v1/webhooks/{id}`               |
| `mcp`                 | `POST /mcp`                                                                                        |

The journey trigger endpoint accepts a key with any scopes.

## Rate limits

Limits are counted **per workspace, per endpoint, per minute**, and each endpoint has its own counter. Going over a limit answers `429` with `{ "error": "…" }`. No `Retry-After` header is sent, so wait and retry with backoff.

| Endpoint                                   | Requests / minute |
| ------------------------------------------ | ----------------- |
| `POST /v1/events`                          | 300               |
| `POST /v1/transactional/send`              | 100               |
| `POST /v1/transactional/validate`          | 100               |
| `GET /v1/transactional/{messageId}/status` | 100               |
| `POST /public/events`                      | 100               |
| `POST /public/journeys/trigger`            | 120               |
| `POST /mcp`                                | 60                |
| Every other `/v1` endpoint                 | 60                |

## Request size limits

Bodies are capped before they are read. A larger body answers `413` with `code: "PAYLOAD_TOO_LARGE"`, and nothing is processed.

| Surface                                      | Limit                                                                              |
| -------------------------------------------- | ---------------------------------------------------------------------------------- |
| `/v1/*`                                      | 16 MB (transactional attachments are also limited to 3 MB each and 10 MB in total) |
| `/public/events`, `/public/journeys/trigger` | 64 KB                                                                              |
| `/mcp`                                       | 1 MB                                                                               |

## Errors

Errors are JSON with an `error` message. Many also include a machine-readable `code`.

```json theme={null}
{ "error": "Unknown status \"archived\". One of ACTIVE, UNSUBSCRIBED, BOUNCED, COMPLAINED, PENDING.", "code": "invalid_status" }
```

A validation failure answers `400`. Some routes also list the fields that failed:

```json theme={null}
{ "error": "Invalid input", "fields": ["to", "attachments.0.filename"] }
```

| Status        | Meaning                                                                                                                                                         |
| ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400`         | Invalid input, or a business rule was broken (for example, `purchase goals require a value`)                                                                    |
| `401`         | Missing, unknown or expired API key (or tracking token / trigger key)                                                                                           |
| `403`         | Missing scope (`insufficient_scope`), workspace suspended (`TEAM_SUSPENDED`), plan contact limit reached (`contact_limit_reached`) or sender domain not allowed |
| `404`         | The list, template, message or subscription does not exist in this workspace                                                                                    |
| `409` / `422` | The request is valid but can't be carried out: a suppressed recipient, missing template variables, a journey that is not active                                 |
| `413`         | Body over the size limit                                                                                                                                        |
| `429`         | Rate limit exceeded                                                                                                                                             |
| `500`         | Server or provider failure                                                                                                                                      |

## Pagination

Two styles are used:

* **Page numbers.** `GET /v1/contacts` takes `page` (from 1) and `limit` (1–100, default 50). It returns `pagination: { page, limit, total, totalPages }`.
* **Cursor.** `GET /v1/transactional` takes `limit` (1–100, default 50) and returns `nextBefore`. To get the next page, pass that value back as `before`. `nextBefore` is `null` once a page comes back less than full.

`GET /v1/lists` returns every list in one response.

## Idempotency

Two write endpoints take an **`externalId`**: a key of your own, unique per workspace. It is best to use the ID of the thing that caused the call, such as an order ID.

* `POST /v1/events`, `/v1/events/import` and `/public/events`: a repeat answers `200` with `status: "duplicate"` and the original event's ID, and records nothing.
* `POST /v1/transactional/send`: a repeat answers `200` with the original message and `deduplicated: true`. Nothing is sent, even when the calls run at the same time.

`POST /v1/webhooks/subscribe` is naturally idempotent: subscribing the same event and URL again returns the existing subscription. No other endpoint deduplicates.

## Request log

API calls are recorded in the request log under **Settings → API log**. That covers every call to `/v1/*`, `/public/events`, `/public/journeys/trigger` and `/mcp`. Viewing the log requires the API-keys management permission. Each row shows the time, method, path, status, duration, and the key that was used. It also shows whatever the call produced, such as a transactional message, event, journey run or contact, along with the recipient and `externalId`.

Request and response bodies are stored for debugging, with a size cap. Fields whose names look like secrets (`password`, `secret`, `token`, `authorization`, `apiKey`, `credential`) are redacted, and attachment contents are left out. Rows are kept for 90 days. Calls that fail authentication are not logged, because there is no workspace to attach them to.

## Webhooks

InstantCampaign can POST workspace events to your endpoints. For signing, retries and the difference between dashboard-registered endpoints and REST-hook subscriptions, see [Webhooks](/api-reference/webhooks).
