Malonlin SDK Docs

Events and webhooks

There are two useful event layers:

  1. sdk.on(...) listens to events happening in this SDK instance.
  2. sdk.webhooks manages signed HTTP event deliveries sent by the backend to your service.

The backend Events module currently emits payment.completed and sends it to matching active store subscriptions. This is an outbound webhook—not a GraphQL subscription or a WebSocket connection.

The SDK checks webhook URLs and supported event types before sending create/update requests. The backend remains the authority and applies its own HTTPS, DNS, and subscription validation.

Create a webhook subscription#

ts
const subscription = await sdk.webhooks.create({
  storeId,
  name: 'Order sync',
  endpointUrl: 'https://shop.example.com/hooks/malonlin',
  eventTypes: ['payment.completed'],
});

// Store this secret immediately in a secret manager. It is returned once.
await secretStore.save(subscription._id, subscription.secret);

Webhook endpoints must use HTTPS with no URL credentials or custom port. The backend also resolves the hostname and rejects private/loopback addresses to reduce SSRF risk.

Update and inspect deliveries#

ts
const subscriptions = await sdk.webhooks.list();
await sdk.webhooks.update({ id: subscription._id, isActive: false });
const recentDeliveries = await sdk.webhooks.deliveries(25);
await sdk.webhooks.delete(subscription._id);

The backend signs each body with HMAC-SHA256 using the subscription secret. It sends x-malonlin-event-id, x-malonlin-timestamp, and x-malonlin-signature: v1=.... Verify the signature and timestamp on your receiver before acting; make delivery handling idempotent by event ID.

For a Node.js receiver, verify the original raw request body (before parsing JSON):

ts
import { createHmac, timingSafeEqual } from 'node:crypto';

function isValidWebhook(rawBody: Buffer, timestamp: string, signature: string, secret: string) {
  const ageSeconds = Math.abs(Date.now() / 1000 - Number(timestamp));
  if (!Number.isFinite(ageSeconds) || ageSeconds > 300) return false;

  const expected = `v1=${createHmac('sha256', secret)
    .update(`${timestamp}.${rawBody.toString('utf8')}`)
    .digest('hex')}`;
  if (!/^v1=[0-9a-f]{64}$/i.test(signature)) return false;
  const providedBytes = Buffer.from(signature, 'utf8');
  const expectedBytes = Buffer.from(expected);
  return providedBytes.length === expectedBytes.length &&
    timingSafeEqual(providedBytes, expectedBytes);
}

Also reject stale timestamps and process each event ID at most once. Keep your webhook secret out of logs.

Failed deliveries are retried by the server with backoff, for up to eight attempts. Use delivery history to diagnose a receiver outage without logging webhook secrets.

Listen inside your app#

ts
const stop = sdk.on('checkout:completed', ({ cartId, idempotencyKey }) => {
  analytics.track('checkout_completed', { cartId, idempotencyKey });
});

// Later:
stop();

SDK event reference#

All local events are emitted by this SDK instance. They are useful for UI updates, analytics, and integrations, but they are not durable or server-pushed.

Event Payload When it fires
cart:synced { id: string, items: CartItem[], ...cartFields } A cart is loaded, or a cart mutation completes successfully
cart:updated { id: string, items: CartItem[], ...cartFields } The SDK successfully adds/updates/removes a cart item or replaces cart contents
checkout:started { cartId: string, idempotencyKey: string } Before the checkout request starts
checkout:completed { cartId: string, idempotencyKey: string } After the backend returns a successful checkout response; inspect the order's isPaid before fulfillment
checkout:failed { cartId: string, idempotencyKey: string, error: unknown } When checkout fails; the original error is preserved
request:started { operation: string } Before each GraphQL request and media upload
request:completed { operation: string, durationMs: number } After a successful API request
request:failed { operation: string, durationMs: number, error: unknown } When an API request fails; the original error is preserved

Subscribe with sdk.on(eventName, listener). It returns an unsubscribe function; sdk.off(eventName, listener) is also available.

Backend webhook event reference#

The current backend emits one webhook event type:

Location Fields and types
Envelope id: string, createdAt: string, type: "payment.completed"
data orderId: string, orderNumber: string, amount: number, currency: string, paidAt: string
Headers X-Malonlin-Event-Id: string, X-Malonlin-Timestamp: Unix-seconds string, X-Malonlin-Signature: "v1=<hex>"

The signed event envelope also carries the event ID, type, and creation timestamp. The backend only delivers it to matching active store subscriptions. This is separate from the local SDK event emitter: a browser or Node SDK listener does not receive server events automatically.

SDK subscription method Backend GraphQL operation
sdk.webhooks.create(input) CREATE_EVENT_SUBSCRIPTION
sdk.webhooks.list() EVENT_SUBSCRIPTIONS
sdk.webhooks.update(input) UPDATE_EVENT_SUBSCRIPTION
sdk.webhooks.delete(id) DELETE_EVENT_SUBSCRIPTION
sdk.webhooks.deliveries(limit) EVENT_DELIVERIES

The SDK and server both validate supported event types. At present, only payment.completed is supported; the operation catalog does not imply that every local cart or checkout event is emitted as a backend webhook.

Web-app React hooks are not event subscriptions#

React hook What it coordinates in the website Reusable SDK equivalent
useCartSync Cart loading and React-state synchronization Cart methods plus cart:synced / cart:updated
useWallet Wallet data and screen helpers sdk.wallet methods
useAuthPageGuard Website-specific route and auth-page behavior sdk.auth API calls; route handling stays with the app
useExternalPaymentMethods Deposit/withdrawal method loading sdk.wallet.externalPaymentMethods()
useStoreMemberAccess Active store membership/permission resolution sdk.members methods; server checks remain mandatory

These hooks are implementation helpers from the React website, not SDK exports. Build framework-specific state hooks in your app around SDK methods and events as needed.

Malonlin SDK documentation