Skip to main content
A custom field is your own attribute on every store in your CRM: a customer score, an onboarding stage, an account owner. You define the field once in the Meridian dashboard, then set its value per store, either by hand on the store page or by pushing it from your own backend. This page covers both directions:
  • Writing, with POST /api/public/custom-fields. Server-side only: it takes your secret API key.
  • Reading, with GET /api/public/me. Available anywhere your app holds a shop token, including the browser.
Define the fields first. The write endpoint sets values against definitions that already exist, and a handle Meridian does not recognize fails the whole request.

Authentication

The endpoint is authenticated by your app’s secret API key, sent as a bearer token:
Create the key in the Meridian dashboard. It is shown once and never returned again. The key is a server-side credential: it can write your app’s CRM data, so it must never reach a browser, an embedded app bundle, or a Shopify theme. This is the same credential the SDK uses for identify, and it is unrelated to the shop token your embedded app holds. A shop token cannot write custom fields.

Request

The values keys are field handles, not names. A handle is set when the field is created and never changes afterwards, so renaming a field in the dashboard does not break your integration. Only the handles you send are touched. Every other field on that store keeps its value. The 20-key cap matches the 20 custom fields an app can define, so a single request can always cover every field you have.

Response

data.values is the store’s full set of values after the write, not just the handles you sent, so you can confirm what landed without a second request. Fields the store has no value for are omitted. meta.written counts the values set, meta.cleared counts the values removed.

How each type coerces a value

Extra precision on a number is rounded rather than rejected, because a score computed as 3.14159265 should not fail on its seventh decimal. Magnitude is different: a number too large for the column cannot be stored without changing it, so it comes back as a 422. A date is a calendar day, not an instant. Send the day you mean, already resolved in whatever timezone your business uses. Meridian stores and returns exactly that string. A json field cannot be filtered in segments or reports. A structure has no single comparable value.

Clearing a value

Send null, or an empty string, to remove a value:
Clearing deletes the stored row rather than blanking it, so “no value” has exactly one representation. A cleared field reads back as absent from data.values and shows as empty on the store page. One consequence is worth stating plainly: an empty string is never stored as a text value. A blank text field and an unset one are the same thing. If you need to distinguish “known to be empty” from “unknown”, model it with a select field carrying an explicit option.

Stores Meridian has not seen yet

Your CRM store list is rebuilt from Shopify Partner events, which Meridian mirrors hourly. A store that installed your app minutes ago is not in that list yet. The endpoint accepts writes for any valid myshopify domain anyway, including one it has never seen. This is deliberate: an install hook that pushes an acquisition source or a signup cohort would otherwise be rejected exactly when it has the most useful thing to say. The value waits, and the store page shows it as soon as the mirror catches up.

Errors

Every error uses the standard envelope with a string code. 401, missing or invalid key
401, valid key for a different app
422, a value that does not fit its field
Errors are reported per handle, under details.fields, keyed values.<handle>. Every bad value in the payload is reported at once, so a batch import learns about all of its problems in one round trip. Nothing is partially applied. One rejected value fails the whole request and no value in it is written. Retry with the payload corrected. An unknown handle is reported under details.fields against values rather than against a handle, because there is no field to attribute it to:
429 means you hit the public API rate limit. Back off and retry.

Using the SDK instead

If your backend already uses @the-meridian/sdk, its server entry wraps this endpoint:
createMeridianAdminClient is exported from @the-meridian/sdk/server only. It takes your secret API key, so it must never be imported from the React entry or from any code that ships to the browser. The SDK also de-duplicates for you, which the raw endpoint does not: writing the same values for the same store twice within 30 seconds sends one request, and identical concurrent writes are coalesced into one. Only exact duplicates are skipped; different value maps for the same store are always sent, in order. A skipped write still resolves, with the value map its twin got back, so that map can be up to 30 seconds stale if a field was edited from the dashboard in between. Read current values from /me when that matters. De-duplication is per client instance, so build the client once at module scope rather than per request, the same rule as createMeridianApp.

Reading values back

Your app reads a store’s values from GET /api/public/me, the same call the SDK provider already makes on mount. They arrive under customer.customFields:
Reads are authenticated by the shop token, not by your API key. That is the whole point: the shop token is already in your embedded app, so a value your backend pushed last night is readable from the browser on the next render, with no second credential and no endpoint of your own. The map holds only the fields this store has a value for. A field you never set, or one you cleared, is absent rather than null, the same rule as the write response. A store with no values at all reads {}, and so does an app with no custom fields defined. Values keep their type: a number field reads as a number, a boolean as a boolean, a json field as the parsed structure, and date as its YYYY-MM-DD string.

From React

useMeridian() exposes the customer, so no extra call is needed:
Values refresh with the rest of the provider state when you call refresh().

From your server

The server client reads the same payload:
Two things worth knowing:
  • Custom fields are readable by the merchant’s browser. Anything you push is visible to anyone who can open your embedded app on that store, so do not store a secret, an internal cost, or a note you would not want the merchant to read.
  • identify does not return them. The handshake answers “who is this store”, not “what does its CRM record say”, so its customer object has no customFields. Read them from /me.

What a write triggers

Setting a value fires the custom_field.changed automation trigger, once per field whose stored value actually changed. Writing the same value again changes nothing and fires nothing, so a nightly job that recomputes every score does not flood your automations. Values are also filterable in segments for every type except json, which means a pushed score can drive an audience, an email, or an automation without any further wiring.
Last modified on August 27, 2026