Delivering Access
Retrieving Access is the pull model — you ask Mosler for a credential when you need it. Delivery is the push model — Mosler sends the credential to you, or directly to the guest, the moment provisioning finishes. No polling required.
There are two delivery modes; you can use either or both.
Mode 1 — Webhook event caller
Configure a callback URL on your Integration in the Mosler client portal. When the worker finishes handling a booking event, Mosler POSTs an authenticated event to that URL. Your endpoint records the credential and replies 2xx.
Booking event COMPLETED
│
▼
Mosler ──POST {signed access.provisioned}──► https://your-system/mosler/callback
◄────────────── 200 OK ──────────────
One callback per accepted event. Every event we answer with
202 Acceptedreaches exactly one terminal state, and if you have a callback configured you receive exactly one callback describing it — success or failure. Because booking ingestion is asynchronous, the202only confirms we accepted the event; the callback is the only place the outcome is reported.That includes outcomes where nothing was provisioned: a room we can't find, a stay that already ended, a duplicate you resent. Those arrive as
access.failedwith a machine-readable code rather than silence, so you never have to infer an outcome from a callback that never came.
Event payload
| Field | Type | Description |
|---|---|---|
schemaVersion | number | Payload contract version, currently 1. Pin or range-check this — new optional fields may be added under the same major. |
event | string | access.provisioned, access.revoked, or access.failed |
referenceId | string | Your booking reference |
occurredAt | string | ISO 8601 UTC timestamp |
booking | object | { id, status, startDate, endDate }. Omitted on an access.failed raised before a booking exists — use referenceId as the key. |
access[] | array | Same per-device shape as GET …/access — type, the credential for that type (passcode, cardId, or lockData + lockMac for e-keys), device/room/bed, validity. Present only on access.provisioned — see which fields arrive on which event. |
error | object | Only on access.failed: { code, message } describing why. code is machine-readable — see failure codes. Replaces access[]. |
Which fields arrive on which event
Branch on event and read the fields that event carries. Do not require access[] on every callback — a validator that does will reject two of the three event types, and because a 4xx is a definitive rejection we do not retry it, so the event is dead-lettered and you never receive it.
| Field | access.provisioned | access.revoked | access.failed |
|---|---|---|---|
booking | ✅ always | ✅ always | ⚠️ omitted when the failure occurred before a booking existed |
access[] | ✅ always | ❌ never — the credentials are gone by definition | ❌ never |
error | ❌ never | ❌ never | ✅ always |
{
"schemaVersion": 1,
"event": "access.provisioned",
"referenceId": "RES-20250718-001",
"occurredAt": "2025-08-01T08:30:02.114Z",
"booking": {
"id": "6576fe78632cfff91c62a3c2",
"status": "active",
"startDate": "2025-08-01T08:30:00.000Z",
"endDate": "2025-08-05T05:30:00.000Z"
},
"access": [
{
"deviceName": "Room 101 Door",
"roomNumber": "101",
"bedNumber": null,
"type": "passcode",
"passcode": "482910",
"validFrom": "2025-08-01T08:30:00.000Z",
"validUntil": "2025-08-05T05:30:00.000Z"
},
{
"deviceName": "Room 101 Door",
"roomNumber": "101",
"bedNumber": null,
"type": "ekey",
"lockData": "Rz7oNSAH1No3YXRiG0j8FKB3xBjhuILrkTjWkOMw...",
"lockMac": "AA:BB:CC:DD:EE:FF",
"validFrom": "2025-08-01T08:30:00.000Z",
"validUntil": "2025-08-05T05:30:00.000Z"
}
]
}
E-key credentials arrive in full. For
type: "ekey"the callback carrieslockDataandlockMacinline — everything the Mobile Key SDK needs. You do not have to call back to the API to complete an e-key.
An access.failed event carries an error object instead of access[]:
{
"schemaVersion": 1,
"event": "access.failed",
"referenceId": "RES-20250718-001",
"occurredAt": "2025-08-01T08:30:02.114Z",
"booking": {
"id": "6576fe78632cfff91c62a3c2",
"status": "active",
"startDate": "2025-08-01T08:30:00.000Z",
"endDate": "2025-08-05T05:30:00.000Z"
},
"error": {
"code": "NO_DEVICE_BOUND",
"message": "No lock is mapped to room 101 at this site."
}
}
For a failure raised before a booking exists — an unmapped room, or a cancel for a reference we've never seen — there is no booking to describe, so the booking block is omitted and the event is keyed by your own referenceId alone:
{
"schemaVersion": 1,
"event": "access.failed",
"referenceId": "RES-20250718-001",
"occurredAt": "2025-08-01T08:30:02.114Z",
"error": {
"code": "LOCATION_NOT_FOUND",
"message": "No room or bed matching RoomName: 101 in site 6530f9dce0c1bd73ded0d1be exists in Mosler. Check the room mapping for this site."
}
}
Failure codes
error.code is machine-readable and safe to branch on. Codes are append-only — new ones may appear under schemaVersion: 1, so treat an unrecognised code as a generic failure rather than rejecting the payload.
| Code | Meaning | Who fixes it |
|---|---|---|
LOCATION_NOT_FOUND | The room or bed in your payload doesn't exist in Mosler for that site | You — re-sync via the Location API and resend |
BOOKING_ALREADY_ENDED | The stay had already ended when the event arrived, so no credential was issued | You — check the check_in / check_out dates you sent |
BOOKING_NOT_FOUND | A cancel or update arrived for a reference_id Mosler has never seen | You — usually a reference that drifted between systems |
NO_ACTIVE_CREDENTIALS | The booking exists but holds no live credential right now | Either — often follows a cancellation or an expired stay |
NO_DEVICE_BOUND | The location resolved, but no lock is mapped to it | Mosler — contact us; nothing to change on your side |
GRANT_FAILED | One or more locks rejected the credential | Mosler — usually a device or gateway problem |
GRANT_ERROR | Provisioning failed before completing | Mosler |
The "who fixes it" column is the useful split: LOCATION_NOT_FOUND, BOOKING_ALREADY_ENDED and BOOKING_NOT_FOUND are almost always something in your payload and are worth surfacing to whoever operates your booking flow. The rest are ours.
Verifying the signature
By default, each call carries an X-Mosler-Signature header: an HMAC-SHA256 of the raw request body, keyed with the callback secret stored on your Integration. Recompute it and compare before trusting the payload.
import crypto from 'crypto';
function isValid(rawBody, signatureHeader, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody) // the exact bytes received
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signatureHeader)
);
}
Outbound auth options
HMAC signing is the default. On your Integration you can instead choose Bearer token, Basic auth, a custom API-key header, or none — Mosler attaches the configured credential to every callback (including the test.ping). Pick the scheme your endpoint expects when you set up the Integration in the client portal.
Delivery guarantees
- Retries: non-
2xxresponses (and timeouts) are retried with exponential backoff. After the final attempt the event is dead-lettered and surfaced for manual replay. - Idempotency: retries reuse the same payload. De-duplicate on (
referenceId,event,occurredAt) — your endpoint may receive a delivery more than once. - Ordering: not guaranteed under retries. Treat
access.revokedas authoritative over an earlieraccess.provisionedfor the same booking by comparingoccurredAt.
Verifying your endpoint before go-live
Before you take a real booking, confirm your callback URL and signature check work end-to-end. Fire a signed test.ping at your registered URL:
POST /api/v4/callbacks/test
curl -X POST https://api.mosler.in/api/v4/callbacks/test \
-H "apikey: YOUR_API_KEY"
Mosler delivers a test.ping event to your registered callback URL — authenticated with the same scheme as real events — and returns the HTTP result it observed from your endpoint:
{
"success": true,
"delivered": true,
"url": "https://your-system/mosler/callback",
"responseStatus": 200,
"authType": "hmac"
}
The test.ping body matches the standard envelope so your verifier exercises the real code path:
{
"schemaVersion": 1,
"event": "test.ping",
"occurredAt": "2025-08-01T08:30:02.114Z",
"message": "Mosler callback verification ping."
}
No booking is created. Use this to validate your endpoint and signature verification without touching a real reservation.
Mode 2 — Direct guest messaging
Let Mosler deliver the credential straight to the guest — no callback handling, no app on your side. When provisioning completes, Mosler sends the passcode (or a mobile-key link) to the guest over WhatsApp and/or email using Mosler templates.
To enable it, include guest contact details on the booking and the property's messaging preference is configured in the Mosler Admin portal:
{
reference_id: "RES-20250718-001",
action: "create",
room_number: "101",
guest: {
name: "Jane Smith",
email: "jane.smith@example.com", // → email delivery
phone: "+919991234567" // → WhatsApp/SMS delivery
},
access_type: "PASSCODE"
}
The guest receives a Mosler-branded message with their PIN (or a link to open their mobile key) and the stay window. Revocation on checkout/cancel can likewise notify the guest that access has ended.
Choosing a mode
| Webhook event caller | Direct guest messaging | |
|---|---|---|
| Who receives the credential | Your system | The guest |
| You build | A callback endpoint | Nothing — just send guest contact |
| Best when | You own the guest experience/app | You want Mosler to handle delivery |
You can combine them: receive the callback for your records and have Mosler message the guest.
Setup
Mode 1 is self-service: create an Integration in the Mosler client portal and set its callback URL, outbound auth scheme, and scope — then verify it end-to-end with POST /api/v4/callbacks/test. Mode 2 (direct guest messaging) channel preferences are configured per company/site in the Mosler Admin portal.