# Security



Each request to LocationIQ's APIs or Map tiles needs to be authenticated with an `access token`.

### Access Tokens [#access-tokens]

For user-facing applications such as Javascript websites or mobile apps, we recommend that you generate new `access tokens` on your User Dashboard. Create a separate `access token` for each application, label them accordingly - e.g. "my website" - and reissue these tokens frequently to prevent misuse. You can use `access tokens` in both public (websites, apps) and private environments (server backends).

### Access Token Types [#token-types]

Every access token has a type that determines which endpoints it can call.

* **API token** (prefix `pk.`) — Calls product APIs such as Geocoding, Routing and Maps. You can add HTTP referrer and IP address restrictions, and the full token can be revealed again from the dashboard.
* **Account token** (prefix `sk.`) — Manages account resources and cannot call data APIs. The secret is shown only once, when the token is created.

Create an API token for each public website or app. Use account tokens only in private, server-side environments for management tasks.

### Token Scopes [#token-scopes]

A scope is a permission granted to an access token, written as `group:action` (for example `geocoding:forward`). When you create or update a token on the User Dashboard, select the scopes it should have. A token can only call endpoints that match its type and selected scopes.

* API tokens use scopes for the product APIs. Older API tokens created before scopes were introduced are unscoped and keep access to all data APIs.
* Account tokens use scopes for account resources, such as `balance:read`. They cannot call data APIs.

The User Dashboard lists all scopes available for each token type.

<Callout type="info">
  The Balance API requires an account token with the `balance:read` scope from 19th October 2026 (UTC). See [Balance API](/docs/balance-api) for details.
</Callout>

### Per-Access-Token Rate Limits [#rate-limits]

Per-access-token rate limits cap how fast and how much a single API token is used, so one app, website or visitor cannot exhaust your plan's shared quota. They apply to API tokens (`pk.`) on eligible paid plans, and are configured per token from the User Dashboard. A limit applies to the whole token, across every API called with it — for example Geocoding, Routing and Maps.

<Callout type="info">
  Maps calls do not enforce per-token rate limits yet. Account-level limits still apply, and per-token limits will cover Maps soon.
</Callout>

You can set two limits, each for the whole token or for every source IP:

| Limit            | Whole token                             | Per source IP                               |
| ---------------- | --------------------------------------- | ------------------------------------------- |
| **Request rate** | Total requests per second for the token | Requests per second from a single source IP |
| **Daily limit**  | Total requests per day for the token    | Requests per day from a single source IP    |

**Request rate** defaults to your account's per-second limit, so the token can burst as fast as your account allows. Set a custom value to cap it lower. The per-IP request rate defaults to **Off**, which lets a single IP use the token's whole burst; set a custom value to spread that burst across many visitors instead.

**Daily limit** defaults to **No limit**, so the token adds no daily cap of its own and your plan quota applies unchanged. When you set one, it counts requests and resets at 00:00 UTC. It is an additional protection on top of your plan quota and does not change it — whether your account quota resets daily, monthly or not at all, your account quota rules still apply.

<Callout type="info">
  Custom values cannot exceed the account limit for the same period, and the effective limit is always capped by your account. If you lower your plan or an account limit changes, a saved custom value that is now higher is ignored and the lower account limit applies. The dashboard warns you when this happens.
</Callout>

Per-IP limits are shared by everyone behind the same public IP. Clients behind a NAT, VPN or corporate proxy share one limit, and IPv6 addresses in the same `/64` block share one limit.

#### When to Use Which Limit [#rate-limit-choices]

* **Per source IP for public apps and websites.** When requests come from many visitors, cap each IP so no single visitor or scraper can use the token's whole allowance.
* **Per-second limits for bursts.** Set these per source IP so a single visitor cannot burst through the token, smoothing traffic and keeping you under your account's per-second limit.
* **Daily limits for sustained usage.** They cap total volume over time and help contain abuse that a per-second limit alone would let through. Use both together: per-second to control bursts, daily to control volume.
* **Whole-token limits to divide a plan.** If several websites or apps share one account, give each its own token and set a whole-token limit to split the plan quota between them.
* **Scope limits to specific APIs.** A limit covers every API the token can call. To limit only some APIs, create a separate token per API type and use [scopes](#token-scopes) to grant each token only the APIs it needs; its limits then apply only to those APIs.

<Callout type="warn">
  Limits are a safeguard, not a strict guarantee. They are applied close to where requests are served, so very bursty or widely distributed traffic can briefly exceed a whole-token limit. Use them to protect your quota and contain abuse, not as a hard global cap.
</Callout>

To set these limits, open **Access Tokens** on the User Dashboard, select the token and click **Update**, then use the **Rate limits** section. The token view shows the effective settings, and the dashboard's **Rate limits** report breaks down account, token and per-IP rejections by endpoint and date. Exceeding any limit returns HTTP `429`; see [Errors](/docs/errors) for the exact messages.

### Security Best Practices [#security-best-practices]

Secure your access tokens to avoid abuse of your public tokens. We recommend:

* **Use scopes for least privilege.** Grant each token only the permissions it needs. See [Token Scopes](#token-scopes).
* **Set rate limits for shared or public tokens.** Cap each token's request rate and daily usage so a single app or visitor cannot exhaust your plan's quota. See [Per-Access-Token Rate Limits](#rate-limits).
* **Use a separate token for each app or website, and rotate tokens regularly.** Rotate immediately if you notice usage spikes you do not recognize, or when the token is used in a public app.
* **Store tokens safely.** Treat account tokens like passwords: they manage account resources and their secret is shown only once. Keep tokens in a password manager or secrets vault, and never commit them to source control.
* **Add request restrictions where they apply.** Use [HTTP referrer restrictions](#http-referrer-restrictions) for browser and app use, and [IP address restrictions](#ip-address-restrictions) for server-side use.

#### IP Address restrictions [#ip-address-restrictions]

You can define a list of IPv4 addresses or subnets authorized to call our APIs or Maps with an access token. If not specified or empty, it will default to any IP address. Specify one IPv4 address or a subnet using CIDR notation (e.g. 192.168.1.0/24). For multiple IP addresses or subnets, specify one per line, upto a maximum of 5 per access token.

A few examples:

* `192.168.1.1` will restrict access to to requests from this IP
* `172.16.1.0/24` will restrict access to IPs in the range `172.16.1.0` to `172.16.1.255`

Note: It may take up to 10 minutes for settings to take effect.

<Callout type="info">
  Do not use IP restrictions for browser-based use-cases such as showing Maps or an Autocomplete widget. This is because these requests originate from your end user's IP address and not your server's IP. Your user's IP will not pass the IP restriction pattern. Use IP restrictions for API calls that originate from your server with a fixed IP / range.
</Callout>

#### HTTP Referrer restrictions [#http-referrer-restrictions]

You can define a list of HTTP referrers (commonly mis-spelled as referer!) authorized to call our APIs or Maps with an access token. If not specified or empty, it will default to any referrer. Referrers can be targeted by matching a prefix or a suffix using the \* character. For multiple HTTP referrers, specify one pattern per line, upto a maximum of 20 per access token.

<Callout type="warn">
  The referrer is an HTTP header that is sent by browsers and like all HTTP headers, it can be spoofed. Treat referrer restrictions as a deterrent, not a security boundary, and pair them with scoped access tokens and frequent access token rotation.
</Callout>

You can test patterns on the User Dashboard while viewing an access token:

<ImageZoom src="/assets/c9fe38a64386-ec535c5-referrer-validation-10c50722.png" />

By default, modern browsers use the `strict-origin-when-cross-origin` referrer policy. For a cross-origin request, such as an API or tile call, they send only the origin (scheme, host and port) and drop the path and query string. No referer is sent at all when a request goes from HTTPS to HTTP. Match on the host, not the path.

Patterns use shell-style wildcards, where `*` matches any characters, including dots and slashes. Anchor each pattern to the scheme and host, and end the host with `/*` so it matches the origin form browsers send plus any same-origin path. Matching ignores case, so `https://Example.com/*` and `https://example.com/*` behave identically.

| Pattern                                                                        | These referrers match                                        | These referrers do not match                                                                                                                                                                      |
| ------------------------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `https://example.com/*`<br />one exact host over HTTPS                         | `https://example.com/`<br />`https://example.com/maps/route` | `http://example.com/` (different scheme)<br />`https://www.example.com/` (a sub-domain)<br />`https://notexample.com/` (a different host)<br />`https://example.com.evil.com/` (a different host) |
| `https://*.example.com/*`<br />any sub-domain over HTTPS                       | `https://www.example.com/`<br />`https://maps.example.com/`  | `https://example.com/` (add the exact-host pattern too)<br />`https://example.com.evil.com/` (the sub-domain must directly follow `https://`)                                                     |
| `http://example.com/*`<br />the same host when your pages are served over HTTP | `http://example.com/`                                        | `https://example.com/` (different scheme)                                                                                                                                                         |

For most sites, add both `https://example.com/*` and `https://*.example.com/*`, so the domain and all of its sub-domains are allowed.

Add the port if your site uses a non-default one, for example `https://example.com:8443/*`.

<Callout type="warn">
  Do not begin a hostname with `*`. Because `*` matches any characters, including dots, such patterns also match unrelated hosts:

  | Unsafe pattern  | Also matches                                                   |
  | --------------- | -------------------------------------------------------------- |
  | `*example.com*` | `https://notexample.com/`<br />`https://example.com.evil.com/` |
  | `example.com*`  | `https://example.com.evil.com/`                                |
</Callout>

##### Internationalized domains [#internationalized-domains]

If your domain contains non-English characters, use its punycode form — the `xn--` spelling. Browsers convert the host to punycode before sending the `Referer` header, and patterns are matched against the header exactly as it arrives, with no conversion between the two spellings. A readable pattern on its own therefore never matches real browser traffic.

| Your domain      | Pattern to add                                                                       |
| ---------------- | ------------------------------------------------------------------------------------ |
| `münchen.de`     | `https://xn--mnchen-3ya.de/*`<br />`https://*.xn--mnchen-3ya.de/*`                   |
| `каменастрой.рф` | `https://xn--80aaorbnijrjl.xn--p1ai/*`<br />`https://*.xn--80aaorbnijrjl.xn--p1ai/*` |

The Dashboard shows you the punycode form when you type an internationalized domain, so you can copy it from there. If some of your traffic comes from non-browser clients that send the readable spelling instead, add a line for each form.
