Webhook endpoints
A webhook endpoint is a HTTPS URL you register with Ripllo to receive event notifications: discount-code redemptions, referral fulfillments, abandoned-cart recoveries, marketing-campaign send completions, and so on. Ripllo POSTs a signed JSON envelope to your URL; your handler verifies the signature and reacts. This page covers ripllo.webhooks on the Node SDK. For the HTTP surface and the full event catalog, see API → Webhook endpoints; for signature verification, see Webhooks.
Namespace
ripllo.webhooks.listEndpoints()
ripllo.webhooks.createEndpoint(input)
ripllo.webhooks.updateEndpoint(id, patch)
ripllo.webhooks.deleteEndpoint(id)
ripllo.webhooks.listEvents(params?)
Five methods: CRUD on the subscription registry plus a read-only event-history feed (useful for "where's my event?" debugging in the dashboard).
Methods — endpoints
createEndpoint
Signature. ripllo.webhooks.createEndpoint(input): Promise<{ endpoint: WebhookEndpoint; secret: string }>
Registers a new endpoint. The response carries the secret Ripllo will sign deliveries with as a sibling of endpoint, not as a field on it — endpoint.secret is deliberately undefined. This is the only call that returns the secret in plaintext.
const { endpoint, secret } = await ripllo.webhooks.createEndpoint({
url: 'https://yourapp.com/webhooks/ripllo',
events: ['discount_code.redeemed', 'referral.fulfilled'],
description: 'Reward fulfillment listener',
});
console.log(endpoint.id); // cuid, e.g. 'clx8f2k9r0000...'
console.log(secret); // 'whsec_...' — STORE NOW
url must be https:// and must not resolve to a loopback, private, link-local or .local/.internal host — anything else is a 400 VALIDATION.
If you omit events, the endpoint is stored subscribed to the wildcard ['*'], i.e. all events. An explicitly empty array is rejected: send at least one entry, or leave the key out. Most teams scope down to the specific event types they care about.
listEndpoints
Signature. ripllo.webhooks.listEndpoints(): Promise<{ endpoints: WebhookEndpoint[] }>
Returns every endpoint in the workspace, newest first. The secret field is never returned on list — you get a secretPreview (whsec_…cd34) instead, enough to tell two keys apart. If you've lost the secret, rotate the endpoint (delete + recreate) rather than trying to recover.
const { endpoints } = await ripllo.webhooks.listEndpoints();
for (const e of endpoints) {
const scope = e.events.includes('*') ? 'all' : `${e.events.length}`;
console.log(`${e.id} → ${e.url} (${scope} events)`);
}
updateEndpoint
await ripllo.webhooks.updateEndpoint('we_01HX...', {
events: ['discount_code.redeemed', 'referral.fulfilled', 'abandoned_cart.recovered'],
});
PATCH; pass only the fields you want to change. The secret is not rotateable through update — delete and re-create if you need to rotate.
deleteEndpoint
await ripllo.webhooks.deleteEndpoint('we_01HX...');
Hard delete. In-flight deliveries already queued may still arrive; queued retries are dropped.
Methods — events
listEvents
Signature. ripllo.webhooks.listEvents(): Promise<{ events: WebhookEvent[] }>
Returns the workspace's 50 most recent events, newest first. The SDK method accepts type / limit / cursor params, but the server currently ignores all three — there is no filtering and no pagination, and no nextCursor comes back. Filter client-side for now.
const { events } = await ripllo.webhooks.listEvents();
for (const e of events) console.log(e.id, e.createdAt, e.status);
Not yet delivering. Registering endpoints, storing their secrets and reading this feed all work, but no worker dispatches to
WebhookEndpointrows yet — nothing writesWebhookEventrows either, so the feed is empty on every workspace. Register your endpoints now if you like; treat inbound deliveries as not yet available. There is no replay endpoint.
Types
interface WebhookEndpoint {
id: string; // cuid
accountId: string;
url: string; // https only
description: string | null;
events: string[]; // ['*'] → subscribed to all (the default)
active: boolean;
secretPreview: string | null; // on list; 'whsec_…cd34'
createdAt: string;
updatedAt: string;
}
// createEndpoint resolves to { endpoint: WebhookEndpoint; secret: string }
// — the plaintext secret is NOT a field on the endpoint.
interface WebhookEvent {
id: string; // cuid
accountId: string;
endpointId: string;
type: string; // e.g. 'discount_code.redeemed'
payload: Record<string, unknown>;
status: 'pending' | 'sent' | 'failed';
attempts: number;
lastAttemptAt: string | null;
nextRetryAt: string | null;
responseCode: number | null;
createdAt: string;
updatedAt: string;
}
Common patterns
Register on first boot
async function ensureWebhook(url: string) {
const { endpoints } = await ripllo.webhooks.listEndpoints();
const existing = endpoints.find((e) => e.url === url);
if (existing) return existing;
const { endpoint, secret } = await ripllo.webhooks.createEndpoint({
url,
events: ['discount_code.redeemed', 'referral.fulfilled', 'abandoned_cart.recovered'],
});
await secretManager.put('RIPLLO_WEBHOOK_SECRET', secret);
return endpoint;
}
Verify inbound deliveries
import { verifyWebhook } from '@forjio/ripllo-node';
app.post('/webhooks/ripllo', express.raw({ type: 'application/json' }), (req, res) => {
const event = verifyWebhook({
rawBody: req.body,
signature: req.headers['ripllo-signature'] as string,
secret: process.env.RIPLLO_WEBHOOK_SECRET!,
});
// ...dispatch on event.type...
res.status(200).end();
});
verifyWebhook throws a plain Error on tamper / replay / wrong secret — not a RiplloError, so don't branch on e.code. The messages are missing Ripllo-Signature header, malformed signature header, non-numeric timestamp, signature timestamp <n>s out of tolerance (default tolerance 300s), bad signature, and webhook body is not valid JSON. Catch broadly and return a 400. See Webhooks for the full handler shape (raw-body parsing, idempotency, retry semantics).
Rotate the secret
async function rotateWebhookSecret(oldId: string, url: string, events: string[]) {
const { secret } = await ripllo.webhooks.createEndpoint({ url, events });
await secretManager.put('RIPLLO_WEBHOOK_SECRET_NEW', secret);
// After both old and new are accepted by your handler:
await ripllo.webhooks.deleteEndpoint(oldId);
await secretManager.put('RIPLLO_WEBHOOK_SECRET', secret);
}
During rotation, dual-verify in your handler: accept either the old or the new secret. Once the old endpoint is deleted, drop the old secret from your config.
Errors
| Code | Status | Cause |
|---|---|---|
VALIDATION |
400 | URL not HTTPS, URL points at a private/loopback/link-local host, or events is an empty array. |
NOT_FOUND |
404 | Endpoint ID doesn't exist in this workspace. |
NO_ACCOUNT |
403 | The principal carries no accountId. |
INSUFFICIENT_SCOPE |
403 | API key lacks read (for listEndpoints/listEvents) or write (for create/update/delete). |
See Errors for handling.
Next
- Webhooks — the verification helper and handler patterns.
- API → Webhook endpoints — full HTTP reference and event catalog.