Server-to-server integration for Pro accounts and API partners. Push reservations, pull certificates, react to signed status events.
List/create/update properties and packets; push reservations and send packets; check signed status; pull signed certificates (PDF + JSON audit trail); react to lifecycle events via webhooks.
Not a PMS, channel manager, marketplace OAuth surface, payment collector, platform writeback tool, ID verifier, or smart-lock operator. Keys are server-to-server only — issued by the Pro account owner.
Stable endpoints follow the v1 backward-compat promise. Beta endpoints may change with 30 days' notice (clearly marked).
Partner integrations can inspect the sample certificate and verification flow today. Public metrics and customer stories wait for cohort size, date range, permission, and anonymization.
Five minutes from key creation to first authenticated call.
Sign in to your Pro account → Settings → Public REST API. Click Create key, pick secret (full access) or restricted (scoped), copy the value. The plaintext is shown once.
curl https://api.prearrive.com/v1/properties \ -H "Authorization: Bearer prearrive_live_sk_..."
{
"object": "list",
"data": [
{
"id": "prop_4Xz9c2K8b7Lm9nQpRsTuV",
"object": "property",
"name": "Brickell 2BR",
"address": "123 SW 8th St, Miami, FL 33130",
"subdomain": "brickell-2br",
"created_at": "2026-04-12T18:02:11Z",
"updated_at": "2026-05-08T15:31:00Z"
}
],
"has_more": false,
"url": "/v1/properties"
}
Most partner builds start with the same narrow flow: create the stay, send the guest link, wait for signed status, react to arrival-release events, and retrieve the certificate. The API does not replace your PMS, guidebook, lock system, claims workflow, or booking platform.
Use this when a PMS, co-hosting dashboard, guidebook app, or internal tool already has the guest and stay dates.
POST /v1/reservations
{
"property_id": "prop_4Xz9c2K8b7Lm9nQpRsTuV",
"guest_name": "Maya Chen",
"guest_email": "maya@example.com",
"checkin_date": "2026-06-14",
"checkout_date": "2026-06-17",
"source": "airbnb"
}
POST /v1/reservations/res_K2jH4g5F6d7S8a9G0h1F/send
Polling is fine for internal tools. Webhooks are better when another system should act immediately. The reservation carries selfie_required — a snapshot of the property's photo-at-signing requirement at send time — so you can tell whether the guest faces a photo step before they can sign.
GET /v1/reservations/res_K2jH4g5F6d7S8a9G0h1F # response includes: "selfie_required": true, # "status": "sent", ... # wait for status: signed and signature_id GET /v1/certificates/sig_pQrStUvWxYzAbCdEfGhI GET /v1/certificates/sig_pQrStUvWxYzAbCdEfGhI/download
Set packet.arrival_release, subscribe to arrival-detail lifecycle events, verify the signature, then release partner-owned door codes, WiFi, or guidebook sections when PreArrive emits the release event.
PreArrive-Event: reservation.arrival_details_released
PreArrive-Signature: t=1748764800,v1=...
{
"type": "reservation.arrival_details_released",
"data": {
"arrival_release": {
"state": "released",
"released": ["wifi", "door_code"],
"withheld": []
}
}
}
Test keys use separate data, skip real guest emails, and expose email previews so you can verify copy before live traffic.
Every request must include a bearer token:
Authorization: Bearer prearrive_<env>_<type>_<random>
live or test — strictly separated; a live key against test data returns 401.sk (secret — full team access) or rk (restricted — only the scopes you grant).Keys are team-scoped (one team per key) and owner-issued (only the team owner can create them). Restricted keys carry a list of scopes like properties:read, reservations:send. Calling an endpoint outside your scopes returns 403 permission_error.
Optional per-key IP allowlist (CIDR or plain v4) blocks any request from outside the listed ranges.
Hit Rotate in the dashboard to mint a successor with the same name/scopes/IP. The old key keeps working for 24 hours so you can swap it in production without downtime; calls during the grace return a Deprecation: true header and write an audit event so you can see the old key still in use.
Every non-2xx response uses the same envelope:
{
"error": {
"type": "invalid_request_error",
"code": "missing_required_field",
"message": "Field `guest_email` is required.",
"param": "guest_email",
"doc_url": "https://developers.prearrive.com/errors/missing_required_field",
"request_id": "req_K2jH4g5F6d7..."
}
}
The request_id (a req_… value, also returned in the X-Request-Id response header) — include it in any support ticket. Field-level errors add a param naming the offending field. Common types:
| Type | Status | When |
|---|---|---|
invalid_request_error | 400 | Bad JSON, missing/invalid field, business rule. |
authentication_error | 401 | API key missing, malformed, revoked, wrong environment. |
permission_error | 403 | Key valid but missing the required scope. |
not_found_error | 404 | Resource id doesn't exist or isn't owned by this team. |
conflict_error | 409 | Concurrent edit (etag mismatch), uniqueness violation. |
plan_limit_error | 402 | Plan cap reached. Add an add-on or upgrade. |
rate_limit_error | 429 | Too many requests. Respect Retry-After. |
Pro accounts: 100 req/min reads, 30 req/min writes, sliding window. Every response carries:
X-RateLimit-Limit: 100 X-RateLimit-Remaining: 73 X-RateLimit-Reset: 1748764800
429s include Retry-After in seconds. Burst allowance: 2× the per-minute limit over a 10-second window.
Cursor-based. List endpoints accept limit (1–100, default 25), starting_after, and ending_before — pass the id of an item from the current page to navigate. Cursors are stable across mutations; offset is not.
Register an endpoint in the dashboard (Settings → Public REST API → Webhooks) or via the API. Every event POSTs JSON to your URL with a signed header:
POST /your-receiver HTTP/1.1
Host: hooks.example.com
Content-Type: application/json
PreArrive-Signature: t=1748764800,v1=5257a869e7ec...
PreArrive-Event: reservation.signed
PreArrive-Event-Id: evt_pQrStUvWxYzAbCdEfGhI
PreArrive-Delivery-Id: evt_K2jH4g5F6d7S8a9G0h1F
PreArrive-Attempt: 1
{ "id":"evt_pQrSt...", "type":"reservation.signed", "data":{ ... } }
import crypto from 'node:crypto';
function verify(header, rawBody, secret) {
const parts = Object.fromEntries(
header.split(',').map(p => p.split('=', 2))
);
const t = parseInt(parts.t, 10);
if (Math.abs(Date.now()/1000 - t) > 300) return false; // 5-min replay window
const expected = crypto.createHmac('sha256', secret)
.update(`${t}.${rawBody}`).digest('hex');
// Header may carry two v1 sigs during 24h rotation grace.
const sigs = header.split(',').filter(p => p.startsWith('v1='))
.map(p => p.slice(3));
return sigs.some(s =>
s.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(s, 'hex'),
Buffer.from(expected, 'hex')));
}
import hmac, hashlib, time
def verify(header, raw_body, secret):
parts = dict(p.split('=', 1) for p in header.split(','))
t = int(parts['t'])
if abs(time.time() - t) > 300: return False
expected = hmac.new(secret.encode(), f"{t}.{raw_body}".encode(),
hashlib.sha256).hexdigest()
sigs = [p[3:] for p in header.split(',') if p.startswith('v1=')]
return any(hmac.compare_digest(s, expected) for s in sigs)
To swap a webhook signing secret with zero dropped events, POST /v1/webhook_endpoints/<id>/rotate. The response returns the new signing_secret in plaintext once (store it now — it is never shown again) plus a rotation_grace_until timestamp. For the 24-hour grace window, deliveries are signed with both the new and old secrets — that second v1= in the signature header — so a receiver that still has the old secret keeps validating while you roll out the new one.
POST /v1/webhook_endpoints/whe_K2jH4g5F6d7S8a9G0h1F/rotate
{
"object": "webhook_endpoint",
"id": "whe_K2jH4g5F6d7S8a9G0h1F",
"signing_secret": "whsec_…", // shown once
"signing_secret_warning": "Store this secret now — it will not be shown again.",
"rotation_grace_until": "2026-06-03T18:00:00Z"
}
Swap flow: (1) call rotate and capture the new secret; (2) deploy it to your receiver's config; (3) keep accepting either secret until rotation_grace_until passes (the dual-signing already covers this — the verifier loops over every v1=); (4) after the window, the old secret stops being signed with automatically. No second call needed.
Non-2xx responses are retried at 1m, 5m, 30m, 2h, 6h, 12h, 24h, 24h, 24h after the previous failure. After 9 failures, the endpoint is auto-disabled and the team owner is emailed. Re-enable in the dashboard. Inspect every attempt via GET /v1/webhook_endpoints/<id>/deliveries; POST .../redeliver queues a fresh attempt.
Because non-2xx responses are retried, the same event can hit your receiver more than once. Each event carries a stable PreArrive-Event-Id (the id in the body, identical across every retry) and a per-attempt PreArrive-Delivery-Id + PreArrive-Attempt counter. Dedup on PreArrive-Event-Id: record the id the first time you process an event, and no-op if you've seen it before.
const eventId = req.headers['prearrive-event-id']; // evt_… — stable across retries if (await seen(eventId)) return res.sendStatus(200); // already handled; ack so retries stop await process(JSON.parse(rawBody)); await markSeen(eventId); res.sendStatus(200);
Always return 2xx once you've durably accepted the event — that's what stops the retry curve. Use PreArrive-Delivery-Id / PreArrive-Attempt only for logging which attempt arrived, not for dedup.
Current events include reservation lifecycle events, certificate creation, property and packet changes, and sync-source lifecycle events. Start with the narrowest set your workflow needs.
reservation.created reservation.sent reservation.clicked reservation.signed reservation.cancelled reservation.arrival_details_held reservation.arrival_details_released reservation.arrival_details_viewed reservation.arrival_details_override_released certificate.created property.created property.updated packet.updated sync_source.created sync_source.updated sync_source.run.completed
On each iCal run, a new feed event inserts a synced reservation; a modified event (e.g. a guest reschedule) upserts — the check-in/check-out date and time refresh so downstream timing (auto-send, reminders, late alerts) tracks the new dates. A re-sync never overwrites guest name/email (enriched separately) or the packet lifecycle (status, sent, signed), and it will not touch a reservation once it is signed, redacted, or under legal hold — those rows are frozen as evidence. A booking that simply disappears from the feed is not auto-cancelled on every run; cancellation is gated on a healthy (non-empty) feed.
Self-serve Pro covers API keys, webhooks, test mode, idempotency, and the public reference. Partner distribution, DPA review, co-selling, or a larger rollout may need a separate agreement.
Use-case pages for PMS and co-hosting tools, guidebooks, smart-lock workflows, insurance and claims-prep services, and internal tools.
The contact form route preselects API / partner integration and passes the partner source through to the support inbox.
Full reference for all 34 endpoints with hyperlinkable schemas and a try-it panel — rendered live from the OpenAPI spec.
Open API reference → View openapi.yaml Download Postman collection
Questions? Hit reply on any PreArrive email or contact us.