# Rate limits & quotas

> How many requests you can send per minute on each plan, what happens over the limit, the size limits of each call, and how to pace a large job.

Source: https://docs.emelia.io/docs/rate-limits

## Requests per minute

The Emelia API counts the requests of your account over a 60 second window. All the API keys of an account share the same counter.

| Plan           | Requests per minute |
| -------------- | ------------------- |
| No active plan | 30                  |
| Start          | 100                 |
| Grow           | 300                 |
| Scale          | 1,000               |

The window is fixed, not sliding: it starts with your first request and lasts 60 seconds, then a new window starts with the next request.

What does not count:

* The enrichment routes under `/tools/...` (Email Finder, Phone Finder, Email Verifier, and the polling of their results).
* The lists and contacts routes under `/lists/...`.

The [MCP server](https://docs.emelia.io/docs/ai/mcp.md) relays your key to this API: its tools count like direct calls, under the same rules.

## Over the limit: HTTP 500, not 429

Today, a request over the limit gets an HTTP `500` with this body, and no `Retry-After` header:

```json
{ "error": "Internal server error" }
```

That is the current behaviour, not the usual `429`. Here is how to handle it:

* **Recognise it.** A `500` with exactly `{"error": "Internal server error"}` during a burst of requests is almost always the quota.
* **Wait for the next window.** Pause for 60 seconds, then resume at a slower pace. Retrying at once only fails again.
* **Retry safely.** Retry `GET` requests freely. Before retrying a `POST`, check that the first call did not go through (for example, look for the contact you were adding): the quota is checked before the request runs, but a real server error looks the same.

[`GET /advanced/campaigns`](https://docs.emelia.io/docs/campaigns/list-campaigns.md)

**curl**

```bash
# Retry once after the window when the quota is hit
status=$(curl -s -o response.json -w "%{http_code}" https://api.emelia.io/advanced/campaigns \
  -H "Authorization: $EMELIA_API_KEY")
if [ "$status" = "500" ]; then
  sleep 60
  curl -s https://api.emelia.io/advanced/campaigns -H "Authorization: $EMELIA_API_KEY"
else
  cat response.json
fi
```

**JavaScript**

```js
async function emelia(path, init = {}, attempt = 0) {
  const res = await fetch(`https://api.emelia.io${path}`, {
    ...init,
    headers: { Authorization: process.env.EMELIA_API_KEY, 'Content-Type': 'application/json', ...init.headers },
  });
  // Over the per minute quota the API answers 500 today, not 429.
  if (res.status === 500 && attempt < 2 && (init.method ?? 'GET') === 'GET') {
    await new Promise((resolve) => setTimeout(resolve, 60_000));
    return emelia(path, init, attempt + 1);
  }
  return res;
}

const res = await emelia('/advanced/campaigns');
console.log(await res.json());
```

**Python**

```python
import os
import time
import requests

session = requests.Session()
session.headers["Authorization"] = os.environ["EMELIA_API_KEY"]

def get(path, attempt=0):
    res = session.get(f"https://api.emelia.io{path}")
    # Over the per minute quota the API answers 500 today, not 429.
    if res.status_code == 500 and attempt < 2:
        time.sleep(60)
        return get(path, attempt + 1)
    return res

print(get("/advanced/campaigns").json())
```

## Size limits

| What                                                          | Limit                                               |
| ------------------------------------------------------------- | --------------------------------------------------- |
| Request body, main API                                        | 30 MB                                               |
| Request body, lists and contacts (`/lists/...`)               | 5 MB, then `413` with `"code": "PAYLOAD_TOO_LARGE"` |
| Contacts per batch call ([Add contacts in bulk](https://docs.emelia.io/docs/lists.md)) | 1 to 100, `422` otherwise                           |
| Lists per page (`GET /lists/list`)                            | 25 by default, 100 at most                          |
| Contacts per page (`GET /lists/list/{listId}/rows`)           | 50 by default, 100 at most                          |
| Exact lookup of a contact by email or LinkedIn URL            | 50 results, no pagination                           |
| Campaign attachments                                          | 3 files of 20 MB                                    |
| LinkedIn voice note                                           | 10 MB and 5 minutes                                 |
| Duration of a request                                         | 120 seconds                                         |

## Campaign quotas

These limits belong to each campaign, in its settings ([Update campaign settings](https://docs.emelia.io/docs/campaigns/update-campaign-settings.md)):

| Setting                                                                   | Range                                                       |
| ------------------------------------------------------------------------- | ----------------------------------------------------------- |
| `dailyEmailAdded`, new contacts entering the email sequence per day       | 1 to 500                                                    |
| `dailyEmailLimit`, emails sent per day, follow-ups included               | 1 to 500, and at least `dailyEmailAdded`                    |
| `dailyLinkedinAdded`, new contacts entering the LinkedIn sequence per day | 1 to 80, and 40 at most when the sequence sends invitations |
| `dailyWhatsappAdded`, new contacts entering the WhatsApp sequence per day | 1 to 30                                                     |

A campaign starts with 30,000 recipients at most. Above it, the start fails with the error code `TOO_MANY_RECIPIENTS`.

## Pacing a large job

* **Batch first.** Loading 1,000 contacts is 10 batch calls of 100, not 1,000 single calls.
* **Spread the calls.** On Start, keep a steady rhythm under 100 requests per minute rather than bursts.
* **Never retry an enrichment blindly.** A retried `POST /tools/find/email` starts a second search and is billed twice. Poll the `jobId` you already have. See [Enrichment](https://docs.emelia.io/docs/enrichment.md).
* **On an error in the middle of a batch,** resume from the failed row instead of restarting: the rows already loaded come back as duplicates.
