@johann0301/whatsapp-rsvp
v0.1.0
Published
Embeddable Lu.ma RSVP integration for WhatsApp agents — installs as a library inside the host application.
Downloads
61
Maintainers
Readme
@johann0301/whatsapp-rsvp
Embeddable Lu.ma RSVP integration for WhatsApp agents.
This package is a library, not a service. It installs into a host application (Stripe SDK style), receives the host's already-open MongoDB connection, resolves credentials via a host callback, and keeps HTTP routes, workers, WhatsApp delivery, and retry queues in host territory.
- Syncs Lu.ma events and guests into the host's MongoDB.
- Confirms/declines RSVPs against the Lu.ma API with local caching.
- Dispatches Lu.ma webhooks through an idempotent, signature-verified pipeline.
- Multi-tenant by design: every operation is scoped by a
tenantId.
Requirements
- Node.js
>= 18.17 - MongoDB driver
mongodb >= 6(peer dependency — the host owns the connection)
Install
npm install @johann0301/whatsapp-rsvp mongodbQuick start (single tenant)
import { MongoClient } from 'mongodb';
import { registerLumaService } from '@johann0301/whatsapp-rsvp';
const client = new MongoClient(process.env.MONGODB_URI);
await client.connect();
const { service, dispatchWebhook, mongoMode } = await registerLumaService({
mongoClient: client,
dbName: 'luma_rsvp',
lumaToken: process.env.LUMA_API_TOKEN,
webhookSecret: process.env.LUMA_WEBHOOK_SECRET,
defaultCountry: 'BR',
});
// Sync an event and its guests from Lu.ma into MongoDB
await service.syncEvent('default', 'evt-123');
// List the events a phone number is invited to
const records = await service.getPersonEvents('default', '+5581999990001');
// Confirm attendance
const result = await service.rsvp('default', 'evt-123', '+5581999990001', 'approved');With a single tenant, pass any stable identifier (for example 'default') as
the tenantId in every call.
Multi-tenant
For hosts that serve multiple Lu.ma calendars, replace the static token with a
tokenResolver callback. The library calls it with the tenantId of each
operation; the host decides where credentials live (database, vault, env):
const { service, dispatchWebhook } = await registerLumaService({
mongoClient: client,
dbName: 'luma_rsvp',
tokenResolver: async (tenantId) => {
const creds = await hostDb.collection('tenants').findOne({ _id: tenantId });
return { token: creds.lumaToken, webhookSecret: creds.webhookSecret };
},
});
await service.syncEvent('acme', 'evt-123'); // resolved against tenant "acme"
await service.syncEvent('globex', 'evt-456'); // resolved against tenant "globex"Every document the library writes carries the tenant_id, and every query
filters by it — tenants never see each other's data.
Tenant id validation
tenantId values are validated at the public API boundary. Invalid values
throw InvalidTenantIdError:
import { InvalidTenantIdError } from '@johann0301/whatsapp-rsvp';
try {
await service.listEvents(''); // empty
} catch (err) {
err instanceof InvalidTenantIdError; // true
}A tenant id is rejected when it is empty, longer than 128 characters, or has leading/trailing whitespace.
Webhooks
registerLumaService returns dispatchWebhook. The host owns the HTTP route
and passes the raw body plus signature headers through:
app.post('/webhooks/luma', express.raw({ type: 'application/json' }), async (req, res) => {
const result = await dispatchWebhook({
tenantId: 'default',
rawBody: req.body, // Buffer — must be the unparsed payload
signature: req.get('x-luma-signature'),
timestamp: req.get('x-luma-timestamp'),
});
res.json(result);
});dispatchWebhook verifies the HMAC signature and replay window, records the
webhook id in an idempotency ledger (duplicates are acknowledged without
reprocessing), and routes the event to the matching handler. The result is a
discriminated union: processed, duplicate, malformed, or unknown_type.
Handled event types: guest.registered, guest.updated, event.created,
event.updated, event.canceled, ticket.registered, calendar.event.added,
and calendar.person.subscribed. Individual handlers can be overridden via the
webhookHandlers option.
Security: configure
webhookSecret(orwebhookSecretFor/tokenResolver) before exposing the webhook route.dispatchWebhookrejects an empty resolved secret before signature verification.
Service API
| Method | Purpose |
| ------------------------------------------------- | ------------------------------------------------------------------------------- |
| service.syncEvent(tenantId, eventId) | Reconcile one event's guests from Lu.ma (upserts, soft-removes missing guests). |
| service.listEvents(tenantId, filters?) | List cached events, optionally by status (active, cancelled, archived). |
| service.getEvent(tenantId, eventId) | Fetch one cached event, or null. |
| service.listGuests(tenantId, eventId, filters?) | List cached guests for an event. |
| service.getPersonEvents(tenantId, phone) | Events a phone number participates in (E.164-normalized). |
| service.rsvp(tenantId, eventId, phone, status) | Confirm (approved) or decline (declined) on Lu.ma + cache. |
Phone numbers are normalized to E.164 via defaultCountry; invalid numbers
throw InvalidPhoneNumberError.
Errors
All errors extend LumaError: InvalidTenantIdError,
InvalidPhoneNumberError, InvalidStatusTransitionError,
WebhookSignatureError, WebhookReplayError, WebhookSecretError,
RateLimitError, ConflictError, and NotFoundError.
MongoDB deployment modes
The library detects the deployment mode at registration time (mongoMode in
the return value). A standalone MongoDB works out of the box; when a
replica set is available, multi-document writes are automatically upgraded to
transactions. No configuration required.
Indexes are created on registration; pass ensureIndexes: false to skip.
CLI acceptance harness
examples/cli ships a demo harness with a stateful mock Lu.ma API server — no real credentials needed:
npm run build
PORT=3002 node examples/cli/mock-luma-server.mjs # terminal 1
node examples/cli/luma-cli.mjs # terminal 2 (see examples/cli/README.md)Development
npm install
npm run typecheck
npm test
npm run buildSee specs/ for the per-feature specifications driving the implementation.
License
MIT
