Best Practices
Follow these recommendations to build reliable, performant integrations with the Repull API.
Monitoring Integration Health via Webhooks
When a customer's PMS or OTA connection breaks — token expired, refresh-rejected upstream, credentials revoked from the provider side — your integration goes silent. Polling for health is expensive and slow: by the time your sweep notices, hours of reservations have already been missed. Webhooks tell you the moment it happens.
Subscribe to two events and you have everything you need to keep an integration healthy without polling:
account.disconnected— fires the instant a connection breaks. Surface this to the affected customer, pause workflows that depend on the connection, and log it to your ops queue. Subsequent API calls for that account will return403.account.connected— fires when the customer reconnects through the Connect flow. Use it to clear the alert, resume paused workflows, and re-run any catch-up sync you owe them.
Why two events, not one
account.connectedis the only signal that's safe to resume on.Minimal handler
A working webhook handler that pauses dependent work on disconnect and resumes on reconnect. Drop this into any Node service.
import express from 'express'
import crypto from 'crypto'
const app = express()
app.post(
'/webhooks/repull',
express.raw({ type: 'application/json' }),
async (req, res) => {
// 1. Verify the signature against the raw body
const signature = req.headers['x-repull-signature'] as string
const expected = crypto
.createHmac('sha256', process.env.REPULL_WEBHOOK_SECRET!)
.update(req.body)
.digest('hex')
if (
!signature ||
!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
) {
return res.status(401).end()
}
const event = JSON.parse(req.body.toString())
const deliveryId = req.headers['x-repull-delivery-id'] as string
// 2. ACK fast — process async to avoid retry storms
res.status(200).end()
// 3. Dedupe on delivery_id to handle retries safely
if (await alreadyProcessed(deliveryId)) return
await markProcessed(deliveryId)
// 4. Branch on event type
switch (event.event) {
case 'account.disconnected': {
const { accountId, reason } = event.data
await db.connections.update(accountId, {
status: 'disconnected',
disconnectedAt: new Date(),
disconnectReason: reason ?? 'unknown',
})
await pauseWorkflowsFor(accountId) // stop scheduled syncs
await notifyCustomer(accountId, reason) // email + in-app banner
await ops.alert('connection_lost', { accountId, reason })
break
}
case 'account.connected': {
const { accountId } = event.data
await db.connections.update(accountId, {
status: 'active',
reconnectedAt: new Date(),
})
await resumeWorkflowsFor(accountId)
await clearCustomerAlert(accountId)
await catchUpSync(accountId) // backfill what was missed
break
}
}
},
)
When account.disconnected fires
- Notify the affected customer first. An in-app banner plus an email is the minimum. They're the only person who can fix the upstream credential.
- Pause dependent workflows. Scheduled syncs, AI auto-replies, pricing pushes — anything that calls Repull on behalf of that account will start failing with
403. Pause it explicitly so the failures don't flood your error tracker. - Log to your ops queue. If a connection stays disconnected past your SLA threshold (24h, 72h — your call), escalate to support outreach. A silent dead connection is worse than a noisy one.
- Don't auto-retry the connection. The upstream credential is gone. Retrying just burns rate-limit budget. Wait for
account.connected.
Idempotency & Webhook Deduping
Every webhook delivery includes an X-Repull-Delivery-Id header. The same event may be delivered more than once (retries, your handler timing out and getting replayed). Dedupe on that ID before doing any side-effecting work — write it to a table or cache with the event ID as a unique key, and short-circuit on conflict.
For outbound API calls, send an Idempotency-Key header on every POST, PUT, and PATCH. UUIDs work, but deterministic keys derived from your business logic (create-res-{guestId}-{checkIn}) are better — they survive retries across process restarts. Keys are cached for 24 hours.
Signature Verification
Verify the X-Repull-Signature header on every webhook delivery. The signature is HMAC-SHA256 over the raw request body (not the parsed JSON — re-serializing changes whitespace and key order, which invalidates the signature). Use a constant-time comparison (crypto.timingSafeEqual in Node, hmac.compare_digest in Python) to prevent timing attacks. See Verify Signatures for full implementations.
Reject unsigned and mismatched deliveries with 401
Backoff & Retry-After
Retry on 429 and 5xx with exponential backoff. When the response carries a Retry-After header, respect it exactly — that's the upstream telling you when capacity will be back. Cap retries at 3 attempts; if the call still fails, log it and move on. Never retry 400, 401, 403, or 404 — those are your bug, not a transient failure, and retrying just adds noise.
Monitor Your Own Webhook Receiver
The Repull dashboard shows you what we delivered — but if your receiver is silently 5xx'ing or timing out, you won't notice until customers complain. Track a few metrics on your side:
- 2xx vs 5xx rate on the webhook endpoint, broken down by event type. A spike in 5xx for one event type is almost always a code bug, not a Repull problem.
- P95 handler latency. Slow handlers cause Repull to mark deliveries as failed and retry. Anything over a couple of seconds is a signal you should ACK first and process async.
- Dedup table size. Unbounded growth means TTLs aren't firing.
Delivery Failover
If your webhook receiver is down, Repull retries with exponential backoff for up to 24 hours, then drops the delivery. The dashboard shows failed deliveries on the endpoint detail page, and you can manually replay any of them. After 10 consecutive failures, the endpoint is auto-disabled — re-enable it from the dashboard once you've fixed the receiver. See Retries & Replay for the full retry schedule.
Pagination
Always paginate list endpoints. Never assume a collection fits in one response.
- Use
cursor-based pagination when available — it is stable across inserts and deletes. - Fall back to
page+limitfor endpoints that support offset pagination. - Keep page sizes reasonable (50-100 items). Larger pages increase latency and memory usage.
- Check the
has_morefield in the response to know when to stop.
Caching
Cache intelligently to reduce API calls and improve performance.
- Property data (names, amenities, photos) changes infrequently — cache for 5-60 minutes.
- Reservation data changes more often — refresh every 1-5 minutes, or rely on webhooks for real-time updates.
- Availability and pricing are time-sensitive — cache for no more than 60 seconds, or use webhooks.
- Use
sync.completewebhook events to invalidate caches when a PMS sync finishes.
Sandbox First
Develop and test against sandbox before touching live data.
- Use
sk_test_API keys for all development and staging environments. - Sandbox provides pre-seeded properties, reservations, and guests — no setup required.
- Use magic IDs to trigger specific error scenarios (see Sandbox Test Scenarios).
- Only switch to
sk_live_keys when you are confident the integration works end-to-end.