Skip to main content
The SDK has two error behaviours. Which one you get depends on the layer you call. The facade a loader calls degrades, so a Meridian problem cannot stop a merchant’s app from loading. The low-level clients hand you the error instead, for you to decide on. For 401 and 403 refusals, the facade logs once per method and error code per process. A 403 FEATURE_NOT_AVAILABLE_FOR_PRIVATE_APP explains the private app restriction without asking you to change MERIDIAN_API_KEY. Other 403 codes and messages are logged as received, unless the code identifies a credential rejection. See Facade diagnostics.

MeridianApiError

Branch on code. The message is prose and can be reworded. One failure is not a MeridianApiError: a dated event whose Date is invalid makes track and trackMany throw a TypeError before any request is sent. The createMeridianApp methods resolve null for it, like any other failure.

The three states worth handling

Four guards are exported from both entries, for the failures that mean something specific:

Shop token expired or revoked

An expired token has aged past its hour. The provider handles this for you: it rotates the token, replays the call, and fires onTokenRefreshed. On the server there is no automatic refresh: call refreshShopToken() and retry, or mint a fresh token. A revoked token belonged to a shop that uninstalled the app. Once the shop reinstalls, a refresh returns the install’s current token, so the provider handles both codes the same way and the merchant sees nothing. While the shop is still uninstalled, or once the revoked token’s original hour is over, the refresh is refused too: mint a fresh token. The createMeridianApp methods do that themselves. See After an uninstall.

The merchant must re-authorize

Your app holds no usable Shopify offline session for this shop, or its refresh keeps failing. No retry can fix it: the merchant has to relaunch the app. The provider calls onShopifyAccessTokenStale once per mount, defaulting to a page reload, which in an embedded app puts the merchant back through authorization. It then rethrows, so your component can react too.

Meridian cannot bill this app at all

This one is yours to fix, not the merchant’s: Meridian could not reach your app’s Shopify session store, which in practice means the app is not hosted on Meridian. err.details.plan_builder_billing_status names the missing precondition: Your Meridian dashboard shows the same reason and disables publishing until it is fixed.

The app’s own Meridian plan ran out

Your app’s Meridian plan meters how many SDK requests and usage events it may make per month (see the comparison). Past the allowance the API answers 402 PLAN_LIMIT_REACHED, and err.details carries meter, limit and used. Importing history from before the app joined Meridian uses neither allowance during the app’s first 30 days. See History from before Meridian. This is yours to fix, not the merchant’s, and it is not retried: the answer will be the same until the allowance resets at the start of next month or you upgrade the app’s plan. Meridian emails your organization’s owners at 80%, 95% and 100% of the allowance, so a 402 here should never be the first you hear of it. Treat it like any other Meridian outage in your app: let the facade degrade, or fall back to your own behaviour. Do not surface it to the merchant.

What gets retried

The SDK retries 429 and 5xx responses up to three attempts, with exponential backoff from 250 ms, capped at 5 seconds, honouring a Retry-After header when one is sent. Network errors are retried the same way. It only does that where a retry is safe: A billing write is never retried on its own. Where accuracy matters for tracking, send an idempotencyKey. See Usage tracking. A dated event always carries one, so it is always retried.

Rate limits

Reads and tracking share 120 requests per minute; billing writes share 30. A batch of up to 50 events counts as one request. Both are keyed on the bearer token, so one shop cannot exhaust another’s budget. A 429 on a retryable call is handled for you; on a billing write it reaches your code, and backing off is the correct response. MeridianApiError.retryAfterSeconds carries the wait the API asked for, in seconds. createMeridianApp goes one step further with a breaker: after a 429, every later call on that app instance is skipped locally, without touching the network, until the Retry-After deadline passes. The breaker logs one console.error naming the deadline when it opens, and nothing for the calls it skips. During that window gates read as ungated and shop tokens as null for every shop, so a rate-limited app degrades deterministically instead of a random share of its calls failing. Nothing ever sleeps or retries on a deadline inside a loader. If you see the breaker’s log line in normal operation, your app is calling Meridian more often than its budget allows. The usual cause is minting a shop token per request on hosting where the in-process cache cannot help; persist the token instead.

Loading and error state in React

Gates read as ungated while loading is true and while error is set, so gating without checking loading flashes the upgrade prompt at someone who has already paid. See fail open or fail closed.

Running unconfigured

An app with no Meridian credentials is a degraded app, not a broken one: configured is false, mintShopToken returns null, gates read as ungated, and the SDK’s page components render a quiet empty state with one console warning naming the likely cause. Mount the provider conditionally and your app renders either way.

What is silent on purpose

Two things swallow every failure and log nothing:
  • Contact capture, which runs beside the provider’s load rather than gating it.
  • captureReferral, so a bad URL or an unreachable API cannot break the page a merchant is on. Whatever was resolved before the failure, notably the cookie, still comes back.
Both report what happened in their return value. Read stashed and cookieSkipped to know whether a capture worked.
Last modified on October 7, 2026