Events and webhooks
There are two useful event layers:
sdk.on(...)listens to events happening in this SDK instance.sdk.webhooksmanages 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#
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#
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):
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#
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.