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 return 403.
  • 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

Treating reconnect as "the absence of a disconnect" is a common bug. The customer can disconnect, reconnect to a different account, or fix the upstream credential and reconnect to the same account. 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

Don't process the event, don't enqueue it, don't log the body. Repeated verification failures usually mean a misconfigured secret — fix the config, then redeliver from the dashboard.

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 + limit for endpoints that support offset pagination.
  • Keep page sizes reasonable (50-100 items). Larger pages increase latency and memory usage.
  • Check the has_more field 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.complete webhook 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.
AI