Developer API
Last updated 4 August 2026
Book meetings on your own Jitsi servers from Calendly, Zapier or your own code, and receive deployment events. Developer API access is enabled by request.
Base URL and authentication
Every request goes over HTTPS and carries your key in the X-API-Key header. Create keys in Settings, API keys. The full key is shown once at creation; we store only a hash, so a lost key is replaced rather than recovered.
https://oneclick.rexxulabs.com/api/v1
X-API-Key: ocj_live_8f2ac91b4d7e_...A key can be scoped to one server. Do that unless you have a reason not to: it keeps the server id out of your integration's configuration, and a leaked key cannot reach anything else you run.
Scopes and rate limits
Keys carry meetings:read, meetings:write and webhooks:manage in any combination. Creating and cancelling are one scope rather than two, because a caller that books a meeting almost always needs to cancel it, and splitting them only produces keys holding both.
120 requests a minute per key, on a sliding window. Every response carries X-RateLimit-Remaining and X-RateLimit-Reset; going over returns 429 with Retry-After.
Creating a meeting
Jitsi rooms exist as soon as somebody joins them, so this does not provision anything. What it does is pick the room name, hand back a join URL that works, remember the title so the recording is filed under it afterwards, and tell your server's power schedule to be awake in time.
curl -X POST https://oneclick.rexxulabs.com/api/v1/meetings \
-H "X-API-Key: ocj_live_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: calendly-event-9f2a" \
-d '{
"title": "Discovery call with Acme",
"scheduled_at": "2026-09-01T15:00:00Z",
"host_email": "alex@acme.com"
}'201 Created
{
"id": "8a4f...",
"title": "Discovery call with Acme",
"room": "discovery-call-with-acme-4f2a",
"server_id": "1c9d...",
"server_name": "meet",
"scheduled_at": "2026-09-01T15:00:00Z",
"join_url": "https://meet.acme.com/discovery-call-with-acme-4f2a?jwt=...",
"host_join_url": "https://meet.acme.com/discovery-call-with-acme-4f2a?jwt=..."
}Two URLs, not one. Send join_url to attendees. host_join_url carries moderator rights when the server uses tokens. On a server without tokens both are the same, because there is nobody for Jitsi to promote.
Send Idempotency-Key and a retry returns the meeting you already made, with 200 instead of 201. Booking tools retry, and without this an invitee gets two different links for one appointment.
A scheduled meeting wakes the server
If you pause your servers outside working hours, a booking taken for a Saturday would otherwise hand your client a link to a machine that is switched off. It does not. A meeting booked through the API outranks the power schedule: the server starts about ten minutes before the meeting and returns to its schedule afterwards.
Cancelling the meeting undoes it. Nothing about your schedule is rewritten, so there is nothing to put back.
The rest of the meetings API
GET /v1/meetings lists them, GET /v1/meetings/{id} fetches one, PATCH /v1/meetings/{id} changes the title or the time, and DELETE /v1/meetings/{id} cancels.
Rescheduling deliberately keeps the same room, so an invitation already in somebody's calendar keeps working.
Webhooks
Optional. Most integrations only call the API above. Subscribe in Settings, Webhooks or through POST /v1/webhooks if you want events pushed to your own systems.
The events are mostly about your infrastructure rather than your meetings, which is the point. A hosted provider can tell you a call started. Only the platform that deployed your server can tell you it stopped responding at two in the morning.
| server.degraded | A server stopped responding to health checks |
| server.recovered | A server started responding again |
| server.deploy_failed | A deployment did not finish |
| server.deployed | A new server finished deploying and is live |
| recording.ready | A recording or transcript finished uploading |
| server.paused | A server was paused, by schedule or by hand |
| server.resumed | A server was resumed |
| server.resized | A server changed instance size |
| server.settings_applied | Configuration changes went live |
| server.settings_failed | Configuration changes could not be applied |
| server.destroyed | A server and its cloud resources were removed |
| server.destroy_failed | A server could not be fully removed |
Delivery and retries
Each event is POSTed as JSON. If your endpoint returns a non-2xx or times out after 10 seconds, we retry three more times, after 2, 10 and 60 seconds. Delivery is at-least-once, so make your handler idempotent on the X-OneClick-Delivery header.
After 20 consecutive failures a subscription is switched off and the reason is shown in Settings, where the last 50 delivery attempts are listed with their status codes and timings. That log is there so “my webhook is not firing” is answerable from both sides.
POST /your-endpoint HTTP/1.1
Content-Type: application/json
X-OneClick-Event: server.degraded
X-OneClick-Timestamp: 1736592000
X-OneClick-Delivery: 3f9c...
X-OneClick-Signature: sha256=9af3c1...
{
"event": "server.degraded",
"id": "b2e1...",
"occurred_at": "2026-09-01T02:14:11.000Z",
"data": {
"server_id": "1c9d...",
"server_name": "meet",
"title": "meet stopped responding",
"message": "Health checks have failed for 5 minutes.",
"details": [],
"severity": "danger"
}
}Verifying a delivery
The signature is HMAC-SHA256 over the timestamp, a full stop, and the raw request body, using the signing secret shown when you created the subscription. The timestamp is inside the signature rather than merely beside it, so a captured delivery cannot be replayed later.
import crypto from "node:crypto";
function verify(req, secret) {
const ts = req.headers["x-oneclick-timestamp"];
const sig = req.headers["x-oneclick-signature"];
// Reject anything older than five minutes.
if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
const expected =
"sha256=" +
crypto
.createHmac("sha256", secret)
.update(`${ts}.`)
.update(req.rawBody)
.digest("hex");
return crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}Use the raw body, not a re-serialised object. Any JSON library that reorders keys or changes spacing will produce a different signature.
Recipes
Calendly. On invitee.created, POST to /v1/meetings with the event name, start time and invitee email, passing the Calendly event id as your Idempotency-Key. Put the returned join_url in the event location.
Zapier or Make. Use a generic webhook action: POST to /v1/meetings with the X-API-Key header, then map join_url into whatever comes next.
Slack alerts. Subscribe server.degraded and server.deploy_failed to a small relay that posts to Slack. Two events, and you stop finding out from a customer.
Questions
Email admin@rexxulabs.com.