> ## 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.

# Identify and contacts

`identify` is the handshake that tells Meridian a shop exists. It upserts the install, returns the shop token your app needs, and is where you hand over what you know about the merchant.

Most apps never call it directly. [`createMeridianApp`](/sdk-app)'s `mintShopToken` is the same call with the environment already wired in: it fills the developer database tag for you and [caches the handshake](/sdk-app#cached-per-shop-and-payload), so repeats cost nothing. The admin client is the lower-level path, and it is raw: every call reaches Meridian and counts against your [rate budget](/sdk-errors#rate-limits).

```ts theme={null}
import { createMeridianAdminClient } from "@the-meridian/sdk/server"

const admin = createMeridianAdminClient({
  appId: process.env.MERIDIAN_APP_ID!,
  apiKey: process.env.MERIDIAN_API_KEY!,
})

const { shopToken, customer } = await admin.identify({
  shopDomain: "acme.myshopify.com",
})
```

It authenticates with your app's secret API key, so it exists only on the server entry. Its other method is `setCustomFields`. See [Custom field values](/custom-fields-api).

## What identify takes

| Field | Notes |
| - | - |
| `shopDomain` | Required. Must end in `.myshopify.com` |
| `name` | Display name for the shop's contact |
| `email` | The shop-level contact address |
| `contacts` | The named people behind the install. Up to 10 per call |
| `mref` | Affiliate referral code. Maximum 64 characters |
| `mrefClickedAt` | ISO-8601 instant the referral link was clicked |
| `devDatabaseId` | Your managed developer database. Set by `mintShopToken`, never by hand |

It returns `{ shopToken, shopTokenExpiresAt, customer }`. That `customer` is **identity only** and carries no custom field values. Read those from `getCustomer()` or `useMeridian().customer`.

Calling identify again for the same shop returns its current token rather than creating a second install, so a repeat is always safe. It is not free, though: through the raw admin client every call is a request against your 120-per-minute budget. Call it through `mintShopToken`, where a repeat with the same payload is served from memory for the token's lifetime, or [persist the token](/sdk-app#persisting-the-token-yourself) and skip the call entirely while it is live.

Identify also asks Shopify for the shop's contact email, its storefront's primary domain, its country and language, and its billing currency, as best-effort enrichment. All of them need Meridian to reach your app's session store, so on an app [not hosted on Meridian](/sdk-hosting) the email, domain, country and language go unresolved and the currency falls back to `USD`, which prices every plan in dollars for a shop that bills in something else. The handshake itself still succeeds.

On such an app, pass the shop's address in `email` yourself. It's the only way Meridian learns it, and it's where a [Send email](/send-email) step addressed to the store sends, including the welcome email of an [install](/installated) automation.

The primary domain is what your CRM shows for the store (`28collectionz.com` rather than `vyeiyd-4i.myshopify.com`), and it refreshes on every handshake, so a merchant who changes domain is followed. A store with no custom domain keeps showing its `.myshopify.com` one.

The country is the one on the merchant's Shopify shop address, stored as a two-letter ISO 3166-1 code, and the language is the store's primary locale (`en`, `pt-BR`). Both show on the [store's profile](/profile-1) in your CRM and both are available to automations and emails as `store_country` and `store_language`. See [Variables](/automation-variables).

Identify is the **only** place a store's country comes from. A store Meridian knows only from the Partner API, because it installed your app before you shipped the SDK and has not opened it since, has no country at all: the Partner API's shop object carries no address to read one from. The language needs one thing more, the `read_locales` scope on your app, without which it stays empty on a store that otherwise identifies normally.

Neither is ever cleared once known. A lookup that fails leaves the last value in place, so one bad round trip to Shopify can't blank a field your automations branch on.

## Contacts

The only address Shopify's API gives you is the shop's generic inbox, which belongs in `email`. `contacts` is for the staff members who actually use your app.

```ts theme={null}
await meridian.mintShopToken({
  shop: session.shop,
  contacts: [
    { email: "jane@merchant.co", firstName: "Jane", lastName: "Doe", role: "primary" },
    { email: "sam@merchant.co", role: "user" },
  ],
})
```

Each contact takes `email` (required), `firstName`, `lastName`, `phone` and a free-form `role`.

Meridian keys contacts by email within an install, so re-sending the same person updates them rather than duplicating them, and a field you omit keeps its previous value. Contacts are **additive**: `email` stays the shop-level fallback.

Send them whenever you know who the user is: from an online access token's `associated_user` block, or your own onboarding form. They appear in the [CRM](/store-contacts) alongside anyone captured automatically.

## Automatic contact capture

If you do not know who your users are, Meridian finds out. In an embedded app with App Bridge v4 loaded, the SDK reads the current staff member's session token and forwards it to Meridian, which verifies it against your Shopify client secret, exchanges it for an online access token to read the `associated_user` block, then discards that token.

Two things have to be true:

* Something calls capture from the browser: [`MeridianProvider`](/sdk-provider), or `captureCurrentUser` if you do not mount the provider. Both are described below.
* Your Shopify client id and client secret are set on the app in Meridian, in **App Settings** under **Meridian SDK**. Meridian needs them to verify and exchange the token, so they are required for capture whatever your billing or hosting setup. Without both, capture does nothing, and the **Meridian SDK** section shows a notice saying so.

Capture is best-effort: it never blocks a render and never throws into your app. Captured people land in the CRM keyed by the same address as anyone you sent through `contacts`, so the two paths never duplicate each other, and Meridian throttles to one capture per person per shop per day.

Shopify flags collaborators (agencies and freelancers), and Meridian stores them like anyone else, keeping the flag. Staff who reach the store through a Dev Dashboard collaboration are not flagged today: they arrive as plain users, so they show as **User** rather than **Collaborator** and are not excluded from automation sends the way collaborators are.

<Note>
  The SDK never sees your Shopify client secret. It forwards the session token; verification and the exchange happen on Meridian's side. The SDK does not decode the token either.
</Note>

### With the provider

Mounting [`MeridianProvider`](/sdk-provider) is the whole integration. It captures on mount, once per page load, and when it rotates an expired shop token it captures again with the new one, so a capture is never lost to an expired token.

### Without the provider

An app that uses the SDK server-side only (`identify`, `track`, feature checks) never mounts the provider, and nothing captures on its behalf. Call `captureCurrentUser` yourself, once per page load, from the embedded layout:

```tsx theme={null}
import { useEffect } from "react"
import { captureCurrentUser } from "@the-meridian/sdk"

export function AppLayout({ shopToken, children }: { shopToken: string; children: React.ReactNode }) {
  useEffect(() => {
    void captureCurrentUser({ shopToken })
  }, [shopToken])

  return children
}
```

`shopToken` is the token your loader minted with `mintShopToken` or `identify`, the same one you would pass to the provider. The call resolves silently in every failure mode and remembers, for the lifetime of the page, which shop tokens it has already answered for, so calling it from a component that remounts costs nothing.

Unlike the provider, the standalone call does not refresh an expired shop token. A capture refused for that reason is simply retried on the next page load, which is the right outcome when your loader mints the token per request.

### Troubleshooting

Open the Network tab in the embedded app and look for the request to `contacts/capture`.

| What you see | Cause | Fix |
| - | - | - |
| No request at all | Nothing calls capture: the provider is not mounted and `captureCurrentUser` is never called. Or the app is not embedded, or App Bridge v4 is not loaded | Mount the provider or call `captureCurrentUser` once per page load. Check that `window.shopify.idToken` exists in the embedded admin |
| `409` with `CONTACT_CAPTURE_UNAVAILABLE` | No Shopify client id or client secret on the app in Meridian, or capture was switched off for the app | Add both in **App Settings**, under **Meridian SDK** |
| `401` with `SHOPIFY_SESSION_TOKEN_INVALID` | The credentials in Meridian belong to a different Shopify app, the secret is wrong, or the token names another shop than the shop token does | Copy the client id and client secret of the exact app the merchant installed, then save them again |
| `200` with `captured: false` | This person was already captured today, or Shopify returned no usable user (for example no email address) | Nothing to fix. Try another staff account or check again tomorrow |
| `200` with `captured: true` | Capture works | The contact is on the [store's profile](/profile-1) |

Meridian records every `409` and `401` on its side too, so if the table does not settle it, support can tell you which state your app is in.


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