Skip to main content
Meridian receives your app’s Shopify webhooks over Pub/Sub and forwards the ones you routed to your app. Meridian re-serializes the event, so Shopify’s own HMAC can no longer validate the body. Verify Meridian’s signature instead.

Credentials

On Meridian hosting, your next deploy injects MERIDIAN_WEBHOOK_SECRET and MERIDIAN_API_KEY automatically, along with MERIDIAN_APP_ID and MERIDIAN_API_URL. The signing secret verifies forwards and the host challenge. The API key authenticates SDK calls such as event tracking. For local development or hosting elsewhere, declare these credentials yourself. A manually declared MERIDIAN_WEBHOOK_SECRET or MERIDIAN_API_KEY also takes precedence on Meridian hosting. See Credentials and environment for the managed values, override rules, and staging API configuration.

The receiver route

One route handles both the verification handshake and real events. Try the challenge first, then fall through:
app/routes/webhooks.shopify.tsx
Verify against the raw body, exactly as received. Parsing and re-serializing the JSON changes the bytes the signature was computed over, and verification fails.
The default forward path is /webhooks/shopify. Your dashboard’s webhook configuration decides which topics reach it. See Pub/Sub.

The host verification challenge

An app hosted outside Meridian (Heroku, Fly, your own server) receives forwarded webhooks at an external domain. Declare its https:// origin on the app’s Settings > Domains page, under External domains, then click Verify. Meridian sends a signed POST to your forward path on that origin with the body { type: "meridian/verify", nonce, app_id }, expecting an answer only the holder of your signing secret can compute. A domain that answers is verified: your app runs there and its SDK holds the secret. It does not prove who owns the domain. Then pick the domain in the Forward host select on the Pub/Sub > Webhooks page. The select lists your Meridian deployment when the app is deployed, and every verified external domain. Pending and failed domains are listed but can’t be picked, so the forward host you choose takes effect from the next event. See External domains. meridianChallengeResponse computes the answer. A non-null result is the JSON to respond with, at HTTP 200. null means this is not a challenge, or the signature did not verify. Fall through to the event path.

Replay protection

verifyMeridianWebhook is the one to use. Forwards carry two headers: The timestamp has to be within 300 seconds of now, in either direction, and a signature has to match, so a captured request stops verifying once the window passes. Override it with toleranceSeconds. A request arriving without those headers falls back to the legacy body-only check, which has no replay protection. Pass requireTimestamp: true to refuse the fallback instead. verifyForwardedWebhook(rawBody, signatureHeader, secret) is that legacy check on its own. Prefer verifyMeridianWebhook.

Rotating the signing secret

The signing secret comes from your app in the Meridian dashboard. Meridian hosting injects it as MERIDIAN_WEBHOOK_SECRET; elsewhere, set that variable yourself. When you rotate it, the v2 header carries a signature for both secrets for 24 hours, so a receiver still holding the old value keeps verifying while you roll out the new one. Two details matter during that window:
  • The legacy body-only check carries one signature, so it switches to the new secret immediately. An app still on verifyForwardedWebhook gets no grace period.
  • The challenge answer is accepted from either secret for the same 24 hours.
Redeploy each Meridian-hosted environment promptly to receive the new secret. If you set MERIDIAN_WEBHOOK_SECRET yourself, update its value before redeploying.

What the SDK registers for you

You never declare webhook topics. afterAuth reconciles a shop’s Pub/Sub subscriptions from the set Meridian resolves (the managed billing topics plus whatever you routed), and unions the managed ones in even when the configuration is unreachable, so an outage cannot drop billing events. Four topics are managed by the platform and always registered: Apps on the same Meridian environment publish to that environment’s Pub/Sub topic, so each subscription is named with your app’s ID for routing. The SDK asks the API which topic to target, so you never configure it. registerMeridianWebhooks registers the four managed topics alone and, because it takes no API key, defaults to the production topic (pass delivery to target another environment); registerWebhooks on the app object reconciles the full set, including your own topics, against the served target.

Signature constants

If you build the verification yourself, the header names and the default window are exported: MERIDIAN_SIGNATURE_HEADER, MERIDIAN_SIGNATURE_V2_HEADER, MERIDIAN_TIMESTAMP_HEADER and MERIDIAN_SIGNATURE_TOLERANCE_SECONDS.
Last modified on September 26, 2026