# Rate limits

> Requests and query time per minute, what happens when you go over, the headers that count them and why webhooks beat polling in a loop.

Page: https://safeonuba.com/en/developers/rate-limits · SafeOnuba API v1 (`api_version: "2026-09-24"`), in preview: the contract may change.

Each client has two limits per minute. Both exist so that an integration that queries too much doesn’t take capacity away from anyone’s alerts, yours included.

## The two limits

| Limit | Default |
| --- | --- |
| Requests per minute | 600 |
| Query time per minute | 30 seconds |

The second is less usual. Each request spends the time it takes to resolve, so **a long list counts for more than a single alert**: asking for 500 alerts from a month uses more of that budget than asking for one by its `id`. If your integration often asks for large lists, paginate with lower `limit` values or, better, stop polling and use webhooks.

Both can be raised by contract.

The token endpoint (`POST /v1/oauth/token`) also has its own limit, per IP. Request a token once an hour and reuse it (see [Renewing it](https://safeonuba.com/en/developers/authentication#renewing-it)).

## Headers

Responses carry how much headroom you have left:

| Header | What it says |
| --- | --- |
| `X-RateLimit-Limit` | Your limit. |
| `X-RateLimit-Remaining` | What you have left in the current window. |

## When you go over

The API responds `429` in `application/problem+json`, with the `Retry-After` header: the seconds you have to wait before trying again.

```javascript
async function request(url, options = {}, attempts = 3) {
  const res = await fetch(url, options);
  if (res.status === 429 && attempts > 0) {
    const wait = Number(res.headers.get('Retry-After') ?? 1);
    await new Promise((r) => setTimeout(r, wait * 1000));
    return request(url, options, attempts - 1);
  }
  return res;
}
```

```python
import time
import requests

def request(method: str, url: str, attempts: int = 3, **kwargs) -> requests.Response:
    res = requests.request(method, url, timeout=10, **kwargs)
    if res.status_code == 429 and attempts > 0:
        time.sleep(float(res.headers.get("Retry-After", "1")))
        return request(method, url, attempts - 1, **kwargs)
    return res
```

```csharp
async Task<HttpResponseMessage> SendAsync(Func<Task<HttpResponseMessage>> send, int attempts = 3)
{
    var res = await send();
    if ((int)res.StatusCode == 429 && attempts > 0)
    {
        var wait = res.Headers.RetryAfter?.Delta ?? TimeSpan.FromSeconds(1);
        await Task.Delay(wait);
        return await SendAsync(send, attempts - 1);
    }
    return res;
}
```

Always respect `Retry-After`. Retrying earlier only lengthens the wait.

## Webhooks instead of polling in a loop

The most common cause of a `429` is asking every few seconds “are there any new alerts?”. It’s what webhooks do best: every alert reaches you in a second or two, without using a single request.

- To find out what’s happening: [webhooks](https://safeonuba.com/en/developers/webhooks).
- To catch up after an outage: [`GET /v1/events`](https://safeonuba.com/en/developers/events#catching-up-on-what-you-missed).
- To load the state at start-up or look for something specific: the lists, paginated.
