MemberConsole
DevelopersMemberConsole integration reference · API version 2026-09-01

SDK and webhook integration guide.

This is the production implementation contract for connecting an external application to MemberConsole. It is deliberately explicit so an engineer or an AI coding model can build the integration without guessing how signatures, retries, event deduplication, reconciliation, or canonical order state work.

Receive Verify Persist once Reconcile Apply side effects

The integration contract

MemberConsole is the canonical commerce authority for orders, payments, refunds, subscriptions, and the event history generated from those records. Your application may keep derived operational state, but it should never infer that a browser redirect, payment-processor callback, frontend state, or local database row is more authoritative than MemberConsole.

Canonical state firstMemberConsole persists commerce state before downstream delivery.
Webhooks are notificationsThey tell your system that canonical state changed.
Reconciliation closes gapsThe Merchant API lets you recover after downtime or ordering uncertainty.
Trust is cryptographicEvery webhook must be verified before it can cause a business effect.
Recommended mental model

Webhook delivery is a fast signal. The Merchant API is the canonical read path. Your local database is a durable projection optimized for your own product.

Quick start: build the receiver in this order

  1. 1
    Create a server endpoint

    Expose an HTTPS POST endpoint such as /api/memberconsole/webhook. Do not put verification in React or another browser bundle.

  2. 2
    Store secrets server-side

    Use a merchant API key for canonical reads and a webhook signing secret for request verification.

  3. 3
    Read the raw body exactly once

    Signature verification must use the exact bytes/text MemberConsole sent, before JSON is parsed and re-serialized.

  4. 4
    Verify timestamp, signature, and event ID

    Reject stale or forged requests before touching business state.

  5. 5
    Claim the event ID atomically

    Persist event.id with a unique constraint before provisioning, fulfillment, entitlement, email, analytics, or other side effects.

  6. 6
    Acknowledge quickly

    Return any 2xx after durable receipt. Queue expensive work in your own system.

  7. 7
    Reconcile when state matters

    Before an irreversible action, retrieve the current order or other resource from the Merchant API when delivery order is uncertain.

Server environment
MEMBERCONSOLE_API_KEY=mc_live_…
MEMBERCONSOLE_WEBHOOK_SECRET=whsec_…

# Never expose either value in browser code, public env variables, logs, or client bundles.
Credential shape

The server SDK expects a live merchant API key beginning with mc_live_. Webhook signing secrets begin with whsec_. Environment-variable names are your choice; the examples use MEMBERCONSOLE_API_KEY and MEMBERCONSOLE_WEBHOOK_SECRET.

Understand the event envelope

Every delivery carries a signed JSON event. Consumers should be tolerant of additive fields and should branch primarily on event.type, then inspect event.data.object.

Example event
{
  "id": "evt_01JMCEXAMPLE",
  "type": "order.confirmed",
  "api_version": "2026-09-01",
  "created_at": "2026-09-11T03:20:00.000Z",
  "sequence": 1842,
  "resource_version": 7,
  "livemode": true,
  "merchant": {
    "id": "mer_example",
    "slug": "example-store"
  },
  "data": {
    "object": {
      "order_number": "EX-K92L7PQ8",
      "status": "confirmed",
      "payment_status": "paid",
      "fulfillment_status": "unfulfilled"
    }
  },
  "links": {
    "resource": "/v1/merchant/orders/EX-K92L7PQ8"
  }
}
FieldMeaning
idPublic immutable event ID. Use this as the webhook idempotency key.
typeEvent name such as order.confirmed or refund.completed.
api_versionContract version used to serialize the event. Current v1 documentation is 2026-09-01.
created_atWhen MemberConsole created the immutable event.
sequenceOptional event-stream sequence metadata. Do not replace idempotency with sequence assumptions.
resource_versionOptional canonical resource version useful for conflict-aware operational updates.
livemodeWhether the event belongs to live commerce traffic when present.
merchantSigned merchant context containing the MemberConsole merchant ID and slug.
recipientOptional receiving/partner context for explicitly shared workflows.
data.objectImmutable resource snapshot associated with the event.
links.resourceOptional canonical resource path that may be used as a reconciliation hint.

Verify signatures before parsing business data

MemberConsole signs the timestamp plus the exact raw request body using HMAC SHA-256. The receiver should enforce a five-minute replay window by default, compare signatures in constant time, and verify the signed body event ID matches the MemberConsole-Event-Id header.

Signature contract
MemberConsole-Event-Id: evt_…
MemberConsole-Timestamp: 1788990000
MemberConsole-Signature: v1=…
MemberConsole-Version: 2026-09-01

signed_payload = timestamp + "." + raw_request_body
expected = HMAC_SHA256(webhook_secret, signed_payload)
  • Read MemberConsole-Timestamp as a 10-digit Unix timestamp.
  • Reject requests more than 300 seconds away from server time unless you intentionally configure another supported tolerance.
  • Split MemberConsole-Signature on commas and evaluate every v1= signature. Multiple signatures can appear during secret rotation.
  • Compute the expected HMAC over timestamp + "." + rawBody.
  • Use constant-time comparison for the expected and received digest.
  • Only parse JSON after signature validation, then ensure MemberConsole-Event-Id === event.id.
Preferred: @memberconsole/server
import { MemberConsole } from "@memberconsole/server";

const memberconsole = new MemberConsole({
  apiKey: process.env.MEMBERCONSOLE_API_KEY!,
});

export async function POST(request: Request) {
  const rawBody = await request.text();
  const event = await memberconsole.webhooks.verify(
    rawBody,
    request.headers,
    process.env.MEMBERCONSOLE_WEBHOOK_SECRET!,
    { toleranceSeconds: 300 },
  );

  // Insert event.id into durable storage with a UNIQUE/PRIMARY KEY constraint.
  const inserted = await persistWebhookEvent(event);
  if (!inserted) return new Response(null, { status: 204 });

  if (event.type === "order.confirmed") {
    const orderNumber = String(event.data.object.order_number ?? "");
    if (!orderNumber) throw new Error("Missing order_number");

    // Current canonical state wins over assumptions based on delivery order.
    const order = await memberconsole.orders.retrieve(orderNumber);
    await enqueueProvisioningOrFulfillment(order, event.id);
  }

  return new Response(null, { status: 204 });
}
Manual Node.js verifier
import { createHmac, timingSafeEqual } from "node:crypto";

function safeHexEqual(a: string, b: string) {
  const aa = Buffer.from(a, "utf8");
  const bb = Buffer.from(b, "utf8");
  return aa.length === bb.length && timingSafeEqual(aa, bb);
}

export function verifyMemberConsoleWebhook(
  rawBody: string,
  headers: Headers,
  secret: string,
) {
  const timestamp = (headers.get("MemberConsole-Timestamp") ?? "").trim();
  const eventId = (headers.get("MemberConsole-Event-Id") ?? "").trim();
  const signatureHeader = headers.get("MemberConsole-Signature") ?? "";

  if (!/^\d{10}$/.test(timestamp)) throw new Error("invalid_timestamp");
  const age = Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp));
  if (age > 300) throw new Error("stale_timestamp");

  const expected = createHmac("sha256", secret)
    .update(`${timestamp}.${rawBody}`)
    .digest("hex");

  const signatures = signatureHeader
    .split(",")
    .map((value) => value.trim())
    .filter((value) => value.startsWith("v1="))
    .map((value) => value.slice(3));

  if (!signatures.some((value) => safeHexEqual(value, expected))) throw new Error("invalid_signature");

  const event = JSON.parse(rawBody);
  if (!eventId || event.id !== eventId) throw new Error("event_id_mismatch");
  return event;
}
Do not verify reconstructed JSON

JSON.stringify(await request.json()) can change whitespace or key serialization and invalidate the cryptographic contract. Verify the exact raw body MemberConsole transmitted.

The SDK verifier exposes structured error codes that are useful for logs and tests:

missing_bodyinvalid_secretinvalid_timestampstale_timestampmissing_signatureinvalid_signatureinvalid_jsoninvalid_payloadinvalid_event_idinvalid_event_typeinvalid_api_versioninvalid_created_atinvalid_merchantinvalid_dataevent_id_mismatch

Deduplicate before side effects

Webhook delivery is at-least-once. Duplicate delivery is normal and must not create a second license, shipment, account, robot reservation, subscription entitlement, refund action, or customer notification. The safe pattern is to claim the event ID transactionally before any effect leaves your database boundary.

PostgreSQL / Supabase idempotency table
create table if not exists memberconsole_webhook_events (
  event_id text primary key,
  event_type text not null,
  api_version text not null,
  resource_version bigint null,
  payload jsonb not null,
  received_at timestamptz not null default now(),
  processed_at timestamptz null,
  processing_attempts integer not null default 0,
  last_error text null
);

-- Claim delivery before side effects.
insert into memberconsole_webhook_events (event_id, event_type, api_version, resource_version, payload)
values ($1, $2, $3, $4, $5)
on conflict (event_id) do nothing;
Good duplicate behavior

Unique insert conflicts because the event already exists → return 2xx → perform no repeated business effect.

Good failure behavior

Event is durably stored but downstream work fails → keep the row → record the error → retry your own worker without pretending the event was never received.

Use event.id for webhook deduplication. Use a separate stable Idempotency-Key for Merchant API mutations so retrying the same logical outbound write does not create duplicate work.

Current event catalog

These are the event types exposed by the current server SDK contract. Subscribe only to the categories your application needs, but make unknown-event handling forward-compatible by logging and acknowledging event types you do not yet act on.

Webhook

webhook.test

Checkout

checkout.createdcheckout.completedcheckout.expiredcheckout.cancelledcheckout.payment_failed

Payments

payment.createdpayment.pendingpayment.authorizedpayment.succeededpayment.failedpayment.cancelledpayment.refundedpayment.partially_refunded

Orders

order.createdorder.confirmedorder.updatedorder.cancelledorder.fulfilledorder.shippedorder.deliveredorder.sharedorder.share_revoked

Refunds

refund.requestedrefund.processingrefund.completedrefund.failed

Shipments

shipment.createdshipment.updatedshipment.delivered

Subscriptions

subscription.createdsubscription.updatedsubscription.cancelled

Choose business effects from canonical state

Event snapshots are useful context, but the safest action depends on what your application is doing. A common implementation map looks like this:

Event familyRecommended application behavior
webhook.testVerify delivery and observability only. Never trigger production fulfillment.
checkout.*Update checkout/session UX projections. Do not treat checkout completion alone as the canonical paid-order entitlement.
payment.*Synchronize payment status and reporting. When product access depends on the order, reconcile the order before granting or revoking entitlement.
order.confirmedPrimary trigger for order-driven provisioning or fulfillment. Retrieve the canonical order when the action is irreversible or ordering could matter.
order.updatedRefresh the local order projection and compare resource version/state before operational actions.
order.cancelledStop future fulfillment or access according to product policy after confirming current canonical state.
refund.*Track the refund lifecycle. Treat refund.completed or canonical refund state as completion, not merely a refund request.
shipment.*Synchronize delivery progress, tracking, and customer-facing shipment status.
subscription.*Update subscription entitlements from the current canonical subscription state.
Canonical financial boundaries

External applications should not overwrite MemberConsole payment totals, currency, processor status, refund completion, or other canonical financial authority. Operational write-back is intentionally scoped.

Reconcile after downtime or ordering uncertainty

Webhooks are not your only recovery path. Persist the opaque event cursor returned by the Merchant API and use GET /v1/merchant/events to catch up after an outage. When processing an event that controls an irreversible action, retrieve the affected resource directly when current state matters.

SDK event catch-up loop
import { MemberConsole } from "@memberconsole/server";

const memberconsole = new MemberConsole({ apiKey: process.env.MEMBERCONSOLE_API_KEY! });

let cursor = await loadMemberConsoleCursor();

for (;;) {
  const page = await memberconsole.events.list({ limit: 100, cursor });
  for (const event of page.data) {
    if (await claimEventId(event.id, event)) await processEvent(event);
  }

  if (!page.next_cursor) break;
  cursor = page.next_cursor; // opaque: store and send back unchanged
  await saveMemberConsoleCursor(cursor);
}
Cursor rules
  • Store next_cursor exactly as returned.
  • Never decode, increment, construct, or infer a cursor.
  • Advance your saved cursor only after the page has been durably processed.
  • Use orders.changes() when you need a deterministic incremental order projection in addition to event history.
  • Use sync.ack() when your integration records an explicit processing checkpoint.

Use the server SDK for the supported surfaces

The repository includes @memberconsole/server, a server-only SDK. The current package contract is version 0.2.0 and targets Node.js 20 or newer. If your integration environment has access to this package, prefer it for signature verification and Merchant API calls. Otherwise implement the documented HTTP and HMAC contracts exactly.

Create the client
import { MemberConsole } from "@memberconsole/server";

const memberconsole = new MemberConsole({
  apiKey: process.env.MEMBERCONSOLE_API_KEY!,
  // Default base URL: https://api.memberconsole.com/v1/merchant
});
SurfaceSDK methodsTypical scope
Ordersorders.list(), orders.retrieve(), orders.changes()orders:read / sync:read
Paymentspayments.list()payments:read
Refundsrefunds.list(), refunds.create()refunds:read / refunds:write
Subscriptionssubscriptions.list()subscriptions:read
Eventsevents.list(), events.retrieve(), events.replay()events:read / webhooks:manage
Webhookswebhooks.verify(), webhooks.list(), webhooks.test()local verification / webhooks:manage
Fulfillmentfulfillment.update(), shipments.create()orders:write / shipments:write
External refsexternalReferences.set()sync:write / orders:write
Syncsync.ack(), sync.checkpoints()sync:write / sync:read
PartnerspartnerConnections.*, orderShares.*, partnerOrders.*partners:read / partners:write

Mutation helpers automatically generate an idempotency key when one is not supplied, but production integrations should pass a stable key when they may retry the exact same logical mutation. For conflict-sensitive operational updates, use the current resource version / If-Match control and reconcile on a 409 resource_version_conflict.

Framework recipes

The invariant is the same in every runtime: raw body → signature verification → unique event claim → quick 2xx → queued work.

Next.js App Router
import { MemberConsole } from "@memberconsole/server";

const memberconsole = new MemberConsole({ apiKey: process.env.MEMBERCONSOLE_API_KEY! });

export async function POST(request: Request) {
  const rawBody = await request.text(); // read once, before JSON parsing
  const event = await memberconsole.webhooks.verify(
    rawBody,
    request.headers,
    process.env.MEMBERCONSOLE_WEBHOOK_SECRET!,
  );

  if (await claimEventId(event.id, event)) {
    await queueMemberConsoleEvent(event);
  }

  return new Response(null, { status: 204 });
}
Express
import express from "express";
import { verifyMemberConsoleWebhook } from "@memberconsole/server";

const app = express();

// Important: raw body middleware must run before express.json() for this route.
app.post("/webhooks/memberconsole", express.raw({ type: "application/json" }), async (req, res) => {
  const rawBody = req.body.toString("utf8");
  const event = await verifyMemberConsoleWebhook(
    rawBody,
    req.headers,
    process.env.MEMBERCONSOLE_WEBHOOK_SECRET!,
  );

  const inserted = await persistWebhookEventOnce(event);
  if (inserted) await enqueueMemberConsoleWork(event);
  res.sendStatus(204);
});

app.use(express.json());
Supabase Edge Function
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
import { verifyMemberConsoleWebhook } from "./memberconsole-webhook.ts";

Deno.serve(async (request) => {
  const rawBody = await request.text();
  const event = await verifyMemberConsoleWebhook(
    rawBody,
    request.headers,
    Deno.env.get("MEMBERCONSOLE_WEBHOOK_SECRET")!,
  );

  const supabase = createClient(
    Deno.env.get("SUPABASE_URL")!,
    Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!,
  );

  const { data, error } = await supabase.from("memberconsole_webhook_events")
    .insert({ event_id: event.id, event_type: event.type, payload: event })
    .select("event_id")
    .maybeSingle();

  if (error?.code !== "23505" && error) throw error;
  if (data) await enqueueMemberConsoleWork(event);
  return new Response(null, { status: 204 });
});
Cloudflare Worker
export default {
  async fetch(request: Request, env: Env) {
    const rawBody = await request.text();
    const event = await verifyMemberConsoleWebhook(
      rawBody,
      request.headers,
      env.MEMBERCONSOLE_WEBHOOK_SECRET,
    );

    // Durable Object, D1, or another transactional store is preferred for exact deduplication.
    const firstReceipt = await claimEventId(event.id, event, env);
    if (firstReceipt) await env.EVENT_QUEUE.send(event);
    return new Response(null, { status: 204 });
  },
};
Runtime note

The cryptographic contract does not depend on a specific framework. If the SDK package is unavailable in an edge runtime, use Web Crypto or an equivalent HMAC-SHA256 implementation and preserve the raw request body exactly.

Retries, acknowledgement, and event ordering

MemberConsole uses durable event/outbox delivery and treats webhook delivery as at-least-once. Temporary failures and retryable HTTP responses can cause the same event to be delivered again. Your receiver should therefore be fast, durable, and duplicate-safe.

2xx = acknowledged

Return a 2xx after durable receipt. The response body is not canonical order state.

Duplicates are expected

Never assume one HTTP request equals one business action.

Current state wins

When events appear out of order, fetch the current resource before an irreversible action.

Do not build fulfillment correctness around arrival order alone. The event may include sequence and resource_version metadata, but the canonical API is the recovery mechanism when the consumer needs certainty about the latest resource state.

Rotate signing secrets without downtime

During a signing-secret rotation, MemberConsole-Signature may carry multiple comma-separated v1= values. The correct verifier computes the expected digest with the secret it currently trusts and accepts the request if any v1 candidate matches.

  1. 1
    Start rotation

    Generate the next signing secret through the merchant integration controls.

  2. 2
    Update the receiver

    Store the new secret in server-side configuration. Never commit it to source control.

  3. 3
    Send a test event

    Confirm the receiver accepts the rotated signature path and records the event exactly once.

  4. 4
    End overlap

    Retire the previous secret after the configured overlap window.

Production-readiness tests

A receiver is not complete until it proves the failure modes. These tests should exist before normal production traffic depends on the integration.

Valid signature is accepted using the exact raw body.
One-byte/body-content modification causes verification failure.
Timestamp older than the replay tolerance is rejected.
Missing or malformed v1 signature is rejected.
MemberConsole-Event-Id mismatch is rejected.
The same event delivered twice creates only one business effect.
A receiver 5xx/timeout followed by redelivery still produces exactly one effect.
An unknown event type is logged and safely acknowledged without breaking the endpoint.
Multiple v1 signatures are accepted when any one is valid during rotation.
Out-of-order resource events cause canonical reconciliation before irreversible work.
Downtime recovery consumes /events with the exact opaque cursor returned by MemberConsole.
API and webhook secrets are absent from client bundles, public environment variables, and logs.
Definition of done

If the endpoint can be replayed, duplicated, delayed, temporarily offline, and rotated without causing duplicate or incorrect business effects, the receiver has the right shape.

Copy this into an AI coding model

This compact brief is intentionally written as an implementation directive. Give it to a coding model together with the target repository and database access. The model should inspect existing schemas and reuse equivalent tables/fields instead of creating duplicates.

AI implementation brief
Implement MemberConsole as a server-to-server integration. Treat MemberConsole as the canonical commerce source of truth.

Requirements:
1. Create an HTTPS POST webhook endpoint controlled by this application.
2. Read the exact raw request body before JSON parsing.
3. Verify MemberConsole-Timestamp is a 10-digit Unix timestamp and reject requests outside a 300-second replay window.
4. Compute HMAC-SHA256(secret, timestamp + "." + rawBody).
5. Parse MemberConsole-Signature as a comma-separated list and accept the request if any v1= value matches in constant time.
6. Verify MemberConsole-Event-Id exactly equals the signed JSON body event.id.
7. Persist event.id with a UNIQUE or PRIMARY KEY constraint before any provisioning, fulfillment, entitlement, email, or other side effect.
8. Return a 2xx response quickly after durable receipt. Move slow work to a queue/background worker owned by this application.
9. Handle duplicate deliveries as successful no-ops.
10. When ordering matters or an irreversible action depends on current state, retrieve the canonical resource from the MemberConsole Merchant API before acting.
11. Keep MEMBERCONSOLE_API_KEY and MEMBERCONSOLE_WEBHOOK_SECRET server-side only.
12. Never use browser redirects, query strings, localStorage, frontend callbacks, or a payment processor callback as the source of truth for a MemberConsole order.
13. Persist opaque next_cursor values exactly as returned. Never decode or synthesize cursors.
14. For write operations, use stable Idempotency-Key values and If-Match/resource-version controls where applicable.
15. Add tests for valid signature, altered body, stale timestamp, duplicate event, invalid event ID, secret rotation, receiver retry, and reconciliation after downtime.

Prefer @memberconsole/server when it is available to the project. Otherwise implement the verification contract above exactly.
Also give the model these references

Developer overview, webhook reliability guide, the target repository, and the exact MemberConsole merchant/store identifier it is integrating. The model should not invent event fields when a canonical field already exists.

Verify first. Persist once. Reconcile when it matters.

That three-part rule is the core of a reliable MemberConsole integration. It keeps external applications synchronized without letting webhook delivery behavior become your financial source of truth.