> Documentation index: https://roxyapi.com/llms.txt. Building an integration? Read https://roxyapi.com/AGENTS.md first.

# API key authentication

Every RoxyAPI request requires an API key. No OAuth, no tokens, no sessions.

## Key types

Two key classes follow the Stripe convention. Pick the one that matches where the call is made from.

| Prefix | Class | Use it from | Browser safe |
|---|---|---|---|
| `sk_live_...`, `sk_test_...` | Secret | Your server, MCP, scripts, CLIs | No |
| `pk_live_...`, `pk_test_...` | Publishable | Browser, widgets, no-code platforms, hosted embeds | Yes, with origin allowlist |

`pk_` keys can be safely placed in client-side JavaScript, HTML widgets, and no-code platform configs. Bind each `pk_` key to one or more allowed origins (your website domains) and a leak from one of those origins becomes an empty exploit: the attacker hits your monthly quota and gets `403 origin_not_allowed` from anywhere else. A native mobile app is not a browser: it sends no `Origin` header, so an allowlisted `pk_` key is refused there and an unrestricted one can be lifted from the bundle and spent. Route mobile traffic through your own server with a secret key.

`pk_` keys are NOT accepted on the MCP endpoints (`/mcp/*`). MCP is server-side traffic with no Origin header to enforce, so a leaked publishable key there would be free tool access. Use a secret key for MCP.

## Getting your API key

Already a customer? [Get your API key](/account?tab=keys) on the **API Keys** tab of your account, where you can also create another.

New here:

1. Pick a plan on [pricing](/pricing) and complete checkout
2. Your key is created the moment payment clears. The welcome email carries a one-time link that shows it in full, once, and the link works for 30 minutes from the moment the key was created.

No sign-up form. No approval queue. Instant activation.

## Where your key is shown, and for how long

A secret key (`sk_`) is shown in full exactly twice: behind the one-time link in your welcome email, and on the **API Keys** tab of your [account](/account?tab=keys) for 30 minutes after it was created. After that the tab shows only its fingerprint, the prefix and the last four characters, such as `sk_live_...4ddd`. That fingerprint is not the key and will not authenticate a request. A secret key is never stored in a readable form, so nobody, including RoxyAPI, can show it again.

Missed the window or lost the key? Create a new one on the API Keys tab. It is shown once, right there, and your old key keeps working until you revoke it. A publishable key (`pk_`) stays readable on that tab for as long as it exists, because it ships inside your web pages anyway and is protected by its origin allowlist rather than by secrecy.

## Using your API key

**Headers** are metadata you send along with your request. Think of the API key header like showing your ID at a door, where the server checks it before letting your request through.

Include the `X-API-Key` header in every request:


### curl

```bash
curl https://roxyapi.com/api/v2/astrology/horoscope/aries/daily \
  -H "X-API-Key: your_api_key_here"
```

### JavaScript

```javascript
const response = await fetch('https://roxyapi.com/api/v2/astrology/horoscope/aries/daily', {
  headers: {
    'X-API-Key': 'your_api_key_here',
    'Content-Type': 'application/json'
  }
});
const data = await response.json();
```

**Tip: New to `fetch()`? The [Quickstart](/docs/quickstart) has an annotated example explaining every line.**

## Using a publishable key in the browser

A publishable key can be sent over `Authorization: Bearer ...` from any front-end. This avoids the extra preflight a custom header would force.

```html
<script>
  fetch('https://roxyapi.com/api/v2/tarot/draw', {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer pk_live_your_publishable_key',
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ count: 1 })
  })
    .then((r) => r.json())
    .then(console.log);
</script>
```

When the request runs from a browser, the browser sends an `Origin` header automatically. The server compares its host against the allowlist on your key (case-insensitive, protocol and port ignored, no wildcards), so you list plain domains like `yourdomain.com` and both `http` and `https` work. Mismatch returns `403 origin_not_allowed`.

If you set no origins on a publishable key, every response includes the header `X-Roxy-Warning: publishable_key_has_no_origin_restrictions`. Add at least one origin before shipping to production.

## Error responses

A key or origin problem answers `401` or `403` with one of these codes:

| Status | Code | When |
|--------|------|------|
| 401 | [`api_key_required`](https://roxyapi.com/docs/errors#api_key_required) | No API key was sent. |
| 401 | [`invalid_api_key`](https://roxyapi.com/docs/errors#invalid_api_key) | The key sent is malformed or cannot be verified. |
| 401 | [`api_key_revoked`](https://roxyapi.com/docs/errors#api_key_revoked) | The key was valid once and has since been revoked from the account page. |
| 401 | [`subscription_not_found`](https://roxyapi.com/docs/errors#subscription_not_found) | The key verifies, but the subscription behind it no longer exists. |
| 401 | [`subscription_inactive`](https://roxyapi.com/docs/errors#subscription_inactive) | The subscription behind the key is cancelled, expired or suspended, or a trial is past its end date. |
| 401 | [`publishable_key_not_allowed_on_mcp`](https://roxyapi.com/docs/errors#publishable_key_not_allowed_on_mcp) | A publishable key was used on a Remote MCP server, which takes a secret key. |
| 401 | [`invalid_client_ip`](https://roxyapi.com/docs/errors#invalid_client_ip) | A keyless request could not be attributed to a caller. |
| 401 | [`unauthorized`](https://roxyapi.com/docs/errors#unauthorized) | Authentication failed with no more specific reason to give. |
| 403 | [`forbidden`](https://roxyapi.com/docs/errors#forbidden) | The key is valid, but this request is not permitted. |
| 403 | [`origin_required`](https://roxyapi.com/docs/errors#origin_required) | A publishable key with an origin allowlist was used with no Origin header, as from a server or a native app. |
| 403 | [`origin_not_allowed`](https://roxyapi.com/docs/errors#origin_not_allowed) | The calling site is not on the origin allowlist of this publishable key. |

All errors return `{ "error": "message", "code": "machine_readable_code", "doc_url": "url" }`. The `error` field is a plain-English description. The `code` field is stable and safe to switch on in your code (e.g., `validation_error`, `api_key_required`, `rate_limit_exceeded`). The `doc_url` field links straight to the entry for that code. Every code, with the fix for each, is on the [error codes](/docs/errors) page.

## Rate limits

Rate limit info is included in every response header:

- `X-RateLimit-Limit`: your monthly request allowance
- `X-RateLimit-Remaining`: requests left this month
- `X-RateLimit-Used`: requests consumed this month
- `X-RateLimit-Reset`: Unix timestamp (seconds, UTC) of the next quota reset

A `429` also carries `Retry-After` with the seconds until that reset, so an HTTP client that retries on `429` by default stops instead of repeating the call.

Every plan also has a per-minute limit that absorbs bursts. Going over it returns `429` with code `rate_limit_per_minute` and a `Retry-After` of at most 60 seconds, and does not count against your monthly allowance. The `X-RateLimit-*` headers always describe the monthly allowance.

Plans range from 50,000 to 3,000,000 requests/month, with custom enterprise volume above that. All endpoints count the same: one request, regardless of complexity.

**Quotas reset on the 1st of every calendar month at 12:00 AM UTC.** The window is the calendar month, not your billing period, so the reset never moves with your renewal date: a plan bought on the 20th still refills on the 1st, and an annual plan refills every month rather than once a year. Unused requests do not roll over. Over the limit, every call returns `429` with code `rate_limit_exceeded` until the rollover.

## Security best practices

**Never expose a SECRET key in client-side code.** Anyone who views your page source can steal it, and a secret key carries no origin restriction to limit the damage. This is what NOT to do:

```html
<!-- DANGER: Anyone can see this key by viewing page source -->
<script>
  fetch('https://roxyapi.com/api/v2/tarot/daily', {
    headers: { 'X-API-Key': 'sk_live_abc123...' }
  });
</script>
```

If you need to call the API from a browser, that is what a publishable `pk_` key is for. See [Using a publishable key in the browser](#using-a-publishable-key-in-the-browser) above.

Instead, call RoxyAPI from your backend server and return the results to your frontend. The [Templates](/docs/templates) show this pattern in practice.

Other best practices:

- **Use environment variables** to store your key (`ROXY_API_KEY`), not hardcoded strings in your code.
- **Rotate your key** if it is ever exposed. Contact [support](/contact) for a new key.

**Warning: The quickstart example puts the key in browser code for learning purposes. That is fine for local testing, but never deploy it that way.**

## Next steps

- [SDK Setup](/docs/sdk): typed API calls in TypeScript and Python
- [MCP Setup](/docs/mcp): connect AI agents via Model Context Protocol
