> ## Documentation Index
> Fetch the complete documentation index at: https://help.the-meridian.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Authentication

The SDK talks to Meridian's developer-facing API at `https://api.the-meridian.ai/api/public`. Two credentials cover every call, and which one a call uses is decided by where the code runs, not by what it does.

| Where your code runs | Credential | Built with |
| - | - | - |
| Your server, on behalf of your app | App API key (`mrd_sk_…`) | `createMeridianApp`, `createMeridianAdminClient` |
| Your server, on behalf of one shop | Shop token (`mrd_shop_…`) | `createMeridianServerClient` |
| The merchant's browser | Shop token (`mrd_shop_…`) | `MeridianProvider` |

Both travel as `Authorization: Bearer <token>`.

## The app API key

Your app's secret key identifies the **app**. It is what mints shop tokens in the first place, and it is the only credential that can write across shops, so it never leaves your server. It authenticates:

* `identify`: the handshake that upserts a shop and returns its shop token
* the affiliate referral stash, and the app lifecycle event that fires your install automations
* [custom field values](/custom-fields-api) written from your backend
* the webhook topic set the SDK reconciles per shop

Every one of those calls also carries your app ID, which Meridian cross-checks against the key. A valid key presented with someone else's app ID is a `401 INVALID_API_KEY`, not a silent write to the wrong app.

## The shop token

A shop token identifies **one install**: this app on this shop. You mint one server-side with `mintShopToken`, and from then on both your server and that shop's browser use it.

It is scoped and short-lived:

* **Scope**: reads and billing writes for that one shop. It cannot reach another shop's data, cannot call the Shopify Admin API, and is not your API key.
* **Lifetime**: about an hour. The provider refreshes it transparently when it expires, and calls `onTokenRefreshed` so you can persist the new value.

Both properties are what make it safe in the browser. Billing calls run client-side with it, so any script on the page can use it while it is valid, bounded to that one shop's billing. Meridian resolves the Shopify Admin token on its own side, so there is no server bridge to build for subscribe, cancel or track.

If you would rather enforce those writes on your own server anyway, the provider takes [write overrides](/sdk-provider#routing-writes-through-your-own-server).

### How a refresh works

An expired token comes back as `401 SHOP_TOKEN_EXPIRED`. The provider rotates it against `POST /auth/refresh` and replays the original call once. The refresh endpoint accepts an expired token, so an idle shop does not need a fresh `identify`.

A refresh **rotates** the token: the previous value stops working immediately, and anything that presents it gets a plain `401` rather than `SHOP_TOKEN_EXPIRED`. That is why the provider hands you the new value through `onTokenRefreshed`, and why the SDK itself never refreshes a token that is still live: a copy you persisted or handed to the browser keeps working for its full lifetime.

### After an uninstall

An uninstall revokes the shop's token. While the shop is uninstalled, that token answers `401 SHOP_TOKEN_REVOKED` ("Shop token is no longer valid for this install.") on every call, refresh included. A token Meridian never issued still answers a plain `401` ("Invalid shop token.").

If the shop installs the app again, the revoked token can still be refreshed until its original hour runs out. Calls made with it answer `SHOP_TOKEN_EXPIRED`, so the provider refreshes it like any expired token, and `POST /auth/refresh` returns the install's current token without rotating it. A copy that the new session already holds keeps working. Past that hour the revoked token is refused for good, and a fresh `identify` replaces it. The SDK handles all of this on its own and shows the merchant nothing.

A **stale Shopify token** is a different `401`, `SHOPIFY_ACCESS_TOKEN_STALE`: the merchant has to relaunch the app to re-authorize. See [Errors](/sdk-errors).

## Transport safety

The SDK logs one warning per base URL that is neither HTTPS nor localhost, because a bearer token would otherwise be sent in plain text. It does not block the request. The warning is there to tell you the configuration is wrong before you ship it.

## Rate limits

Both credentials share the same public-API budget, keyed on the bearer token:

| Calls | Limit |
| - | - |
| Reads, tracking, identify, custom fields, contact capture | 120 per minute |
| Subscribe, cancel, raise cap, discount validation | 30 per minute |

Because the key is the token, one shop's browser cannot exhaust another shop's budget. Exceeding a limit is a `429`; the SDK retries it with backoff on the calls where a retry is safe, and [`createMeridianApp`](/sdk-app) skips further calls locally until the `Retry-After` deadline passes. See [Rate limits](/sdk-errors#rate-limits).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.