# Limits and errors

> Rate limits, request size, SDK error types and codes, HTTP status codes for the SDK API, webhooks and the agent API, and the rules for codes and app slugs.

This page collects every limit and error you can run into: how fast you can call InfluTo, what each SDK throws, what each status code means, and the format rules for codes and app slugs. For the endpoints themselves, see the [HTTP API](https://docs.influ.to/reference/http-api.md); for the methods, the [SDK reference](https://docs.influ.to/reference/sdk.md).

## Rate limits

| What | Limit | Over the limit |
|---|---|---|
| SDK API (`/api/sdk/*`) and referral links | Per IP address, set well above what a real app or campaign produces — including many users behind one mobile-carrier address | `429` |
| MCP tool calls | 240 per minute per connected account | The tool call fails with an error that starts with `429` |
| MCP sign-in and client registration | Per IP address | `429` |

- If you get a `429`, back off and retry; a script calling in a tight loop is the usual cause.
- `identify` is de-duplicated on the server: the same SDK key and user ID within 60 seconds is answered without doing the work again (`"deduplicated": true`). Call it once per launch, never from a render loop.
- The RevenueCat webhook and the agent REST API have no per-request limit; the agent API has [invite limits](#agent-api-and-mcp) instead.

## Request size

- Every request body is limited to **20 MB**; larger requests get `413`.
- Asset uploads in the dashboard (**Assets**) are limited to 20 MB per file.

## What throws and what fails soft

Only `initialize` and `reportPurchase` throw on network and server errors. The rest return a safe value, so a slow or failed request never crashes your app.

| Method | On failure |
|---|---|
| `initialize` | Throws |
| `reportPurchase` | Throws |
| `checkAttribution` | `{ attributed: false, message: "Error checking attribution" }`, not stored, retried next launch. Throws when called before `initialize` succeeded (iOS returns `attributed: false` instead). |
| `identifyUser`, `trackEvent` | Nothing (logged when `debug` is on) |
| `getActiveCampaigns` | `[]` |
| `validateCode` | `{ valid: false, error_code: "NETWORK_ERROR" }` |
| `setReferralCode` | `{ success: false, message: … }` |
| `applyCode` | The validation result with `applied: false` |
| `checkAccess` | `{ hasAccess: false }` |
| Automatic capture | Failed reports are counted in `failed` and retried on a later back-sync. React Native's `enableAutoPurchaseCapture` and `syncExistingPurchases` throw when called before `initialize`. |

## SDK error types

| Platform | Thrown type | What it carries |
|---|---|---|
| React Native | `Error` | Message `API request failed: <status> - <body>`, or `InfluTo SDK not initialized. Call InfluTo.initialize() first.` |
| iOS | `InfluToError` | `.notInitialized`; `.notConfigured(String)` for any `400`; `.retryable(status:)` for any `5xx`, including `503`; `.http(status:body:)` for other statuses; `.decoding(Error)`; `.transport(Error)` |
| Android | `InfluToError` (sealed, an `Exception`) | `NotInitialized`; `Server(status, body)` with `retryable` (status 500 or higher) and `notConfigured` (status 400); `Transport(cause)`; `Decoding(cause)` |
| Flutter | `InfluToException` | `statusCode`, `body`, `message`; `retryable` (500 or higher), `notConfigured` (400). Network failures have no `statusCode`. |
| Web | `InfluToApiError` | `status`, `body`, `message` for any non-2xx answer. Other failures — not initialized, no `apiKey`, `reportPurchase` without `platform` in a browser — throw a plain `Error`. |

`notConfigured` covers every `400` from `/sdk/purchase`: the platform isn't switched to store-direct yet, or the proof is missing or failed verification. A `503` means the exchange rate wasn't available; retry later.

<!-- tabs -->
```ts title="React Native / Expo"
try {
  await InfluTo.reportPurchase({ platform: 'android', purchaseToken });
} catch (e) {
  if (String(e).includes('failed: 503')) scheduleRetry();
}
```
```swift title="iOS (Swift)"
do {
    _ = try await InfluTo.reportPurchase(jws: jws)
} catch InfluToError.retryable(let status) {
    scheduleRetry(after: status)
} catch {
    print("InfluTo:", error)
}
```
```kotlin title="Android (Kotlin)"
try {
    InfluTo.reportPurchase(purchaseToken = purchase.purchaseToken)
} catch (e: InfluToError.Server) {
    if (e.retryable) scheduleRetry()
}
```
```dart title="Flutter (Dart)"
try {
  await InfluTo.instance.reportPurchase(platform: 'android', purchaseToken: token);
} on InfluToException catch (e) {
  if (e.retryable) scheduleRetry();
}
```
```ts title="Web / Capacitor / PWA"
import InfluTo, { InfluToApiError } from '@influto/web';

try {
  await InfluTo.reportPurchase({ platform: 'android', purchaseToken });
} catch (e) {
  if (e instanceof InfluToApiError && e.status === 503) scheduleRetry();
}
```
<!-- /tabs -->

## Code validation errors

`validateCode` and `applyCode` return these in `error_code` (React Native, web) or `errorCode` (iOS, Flutter as a string; Android as the `CodeErrorCode` enum, which adds `UNKNOWN`).

| Code | When | Server text in `error` |
|---|---|---|
| `INVALID_FORMAT` | Fewer than 4 or more than 20 characters after cleanup | `Code must be 4-20 characters` |
| `CODE_NOT_FOUND` | No active creator code in an active campaign, and no free-access code, matches | `Code not found or inactive` |
| `CODE_EXPIRED` | A free-access code that expired or has no seats left | `Code expired or usage limit reached` |
| `NETWORK_ERROR` | Set by the SDK: the request failed, or the SDK isn't initialized | `Network error or invalid response` or `SDK not initialized` |

`/sdk/set-referral-code` can also answer `MISSING_USER_ID` for a free-access code sent without a user ID. The SDKs' `SetCodeResult` doesn't expose the code; you get `success: false` and the message "Could not identify your account. Please try again."

## HTTP status codes for /api/sdk/*

| Status | Meaning | Endpoints |
|---|---|---|
| `200` | Success. "Not attributed", "invalid code" and "duplicate" are all `200` answers — read the body. | All |
| `400` | Empty `app_user_id`; or, on `/sdk/purchase`, the platform isn't store-direct, credentials are missing, the proof is missing or failed verification, or the platform isn't `ios`/`android` | `identify`, `access`, `recent-conversions`, `purchase` |
| `401` | Missing, malformed, unknown or deactivated SDK key | All |
| `404` | Unknown or inactive code | `set-referral-code` |
| `409` | Android one-time purchase not in the PURCHASED state | `purchase` |
| `413` | Body larger than 20 MB | All |
| `422` | A required field or query parameter is missing or has the wrong type | All |
| `429` | Rate limit | All |
| `500` | Unexpected server error | All |
| `503` | Exchange rate unavailable — retry later | `purchase` |

The store-direct `400` is intentional: until you [switch the platform to store-direct](https://docs.influ.to/integrations/store-direct.md), and that switch needs a green credential check, the endpoint rejects reports. So it's safe to ship the SDK first.

## RevenueCat webhook responses

| Status | Meaning |
|---|---|
| `200` | Accepted. If processing failed, the body also has `"error"`; the event isn't retried by RevenueCat or by InfluTo. |
| `400` | The body isn't valid JSON |
| `401` | Missing or wrong Authorization value. The body says how to fix it, and the Setup page shows a warning. |
| `404` | No app with the ID in the URL |

A low wallet balance is not a webhook error: the conversion is still recorded. See [RevenueCat](https://docs.influ.to/integrations/revenuecat.md) and [inbound webhooks](https://docs.influ.to/reference/http-api.md#inbound-webhooks).

## Agent API and MCP

The agent REST API (`/api/agent/*`) and the MCP tools answer with these statuses; an MCP tool error's text starts with the status.

| Status | Meaning |
|---|---|
| `400` | Invalid input, for example a batch of more than 500 invites |
| `401` | Missing, invalid, expired or revoked agent key or token |
| `402` | Not enough unreserved wallet balance: activating a campaign and sending email invites need at least $50; marketplace invites need $20 per open invite |
| `403` | The key or token lacks the scope, or the action is closed to agents (money, payouts, credentials, admin) |
| `404` | Not found — also returned for anything outside the organizations you granted, so nothing leaks |
| `429` | More than 240 MCP tool calls in a minute, or an invite limit: 100 email invites per organization per 24 hours, or one open marketplace invite per $20 of unreserved balance |

Details: [MCP tools, scopes and REST API](https://docs.influ.to/agents/tools.md#limits-and-errors).

## Code format

**Creator referral codes** (the code in `influ.to/{app_slug}/{CODE}`):

- 4–20 characters: `A–Z`, `0–9`, `_` and `-`. Stored uppercase.
- Not one of `ADMIN`, `TEST`, `API`, `SYSTEM`, `NULL`, `ROOT`, `DELETE`, `REMOVE`.
- Can't start with `INFLU-`, which is reserved for creator access codes InfluTo issues itself.

**Free-access codes** you create on **Free Access Codes**: the same characters and length, uppercase, unique across InfluTo, and also never starting with `INFLU-`.

**When a user types a code**, the SDK trims and uppercases it. The server then ignores spaces and any other character outside the alphabet when it looks the code up, and checks creator codes before free-access codes. The SDK stores and sends to RevenueCat the code it was given, so clean the input in your field — see [building your own field](https://docs.influ.to/reference/ui.md#build-your-own-field).

## App slug

The slug is the first part of your referral links, `influ.to/{app_slug}/{CODE}`.

- 3–20 characters: lowercase letters, digits and `-` (the dashboard enforces this as you type).
- Unique across InfluTo, and not one of the names the site itself uses (such as `api`, `dashboard` or a language code). **My Apps → Add App** checks availability as you type.
