Skip to main content
API

Authentication and rate limits

# Authentication and rate limits This article covers how to authenticate API calls, what error responses mean, and how to stay inside the rate-limit envelope.…

Authentication and rate limits

This article covers how to authenticate API calls, what error responses mean, and how to stay inside the rate-limit envelope.

Authentication

Every BizRnR API call is authenticated with an API key sent as a Bearer token:

GET /v1/conversations HTTP/1.1
Host: api.bizrnr.com
Authorization: Bearer sk_live_...

Keys come in two flavors:

  • sk_live_... — production secret keys. Used for real traffic.
  • sk_test_... — test mode keys. Hits a sandbox tenant; no real calls placed, no Stripe charges. Useful for local dev + CI.

You can have both at once — production code reads sk_live_* from your environment, tests read sk_test_* from .env.test.

Status codes you'll see

| Code | Meaning | What to do | |---|---|---| | 200 / 201 | Success | Parse the body | | 400 | Bad request — malformed JSON or invalid params | Read the error.message field; fix the request | | 401 | Auth failed — key missing, wrong, or revoked | Check your env var; regenerate if needed | | 403 | Auth ok, but key lacks scope (e.g. read-only key on a write endpoint) | Use a key with the right scope | | 404 | Resource not found, OR your key can see it but it doesn't exist | Verify the ID; remember tenant scoping | | 409 | Conflict — usually a duplicate idempotency key with a different body | See the idempotency section below | | 422 | Validation error — the request was understood but a field is invalid | error.fields lists which fields and why | | 429 | Rate limit hit | Read Retry-After; back off | | 500 / 502 / 503 | Our problem | Retry with exponential backoff (1s, 2s, 4s, 8s, give up) |

Every error response has the same shape:

{
  "error": {
    "type": "validation_error",
    "message": "phone is not a valid E.164 number",
    "fields": { "phone": "must start with + and country code" },
    "request_id": "req_..."
  }
}

Always log request_id — when you ping support, it's the fastest way for us to find your call in our logs.

Rate limits

Limits are per-tenant, sliding-window over 60 seconds:

| Tier | Requests/min | Burst | |---|---|---| | AI Receptionist | Not included by default | Not included by default | | Enterprise/API-enabled | Contracted | Contracted |

The "burst" column is what you get for one minute every five — useful for batch jobs.

When you hit the limit, the response is 429 with these headers:

Retry-After: 12
X-RateLimit-Limit: 600
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1714153200

Wait Retry-After seconds before retrying. The official SDK handles this for you with exponential backoff capped at 30s.

Idempotency

Any POST or PUT accepts an Idempotency-Key header. Use a UUID v4 per logical operation:

curl -X POST https://api.bizrnr.com/v1/contacts \
  -H "Authorization: Bearer sk_live_..." \
  -H "Idempotency-Key: 8a1f3c5e-..." \
  -H "Content-Type: application/json" \
  -d '{"phone":"+15555551234","name":"Acme Corp"}'

Replays of the same key within 24 hours return the original response, so a network blip mid-write doesn't create duplicates. If you replay the key with a *different* body, you get 409 conflict — that's a bug in your code, not a flake.

Idempotency keys cost nothing on the limit — they're a header, not a separate call. Use them on every write, especially anything that creates a contact or sends a message.

Webhook signing

Outgoing webhooks (the ones we call when something happens in your tenant) are signed with HMAC-SHA256 over the raw request body using the secret in Settings → Webhooks → Secret.

We send three headers:

  • BizRnR-Signature: t=<timestamp>,v1=<hex-hmac>
  • BizRnR-Event: <event-type>
  • BizRnR-Delivery: <unique-delivery-id>

Verify before processing:

import crypto from 'node:crypto';

function verifyWebhook(body: string, header: string, secret: string): boolean {
  const [tPart, sigPart] = header.split(',');
  const t = tPart!.split('=')[1]!;
  const sig = sigPart!.split('=')[1]!;
  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${t}.${body}`)
    .digest('hex');
  // Reject anything older than 5 minutes — guards against replay.
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
  return crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}

If verification fails, return 400 and we'll retry with backoff. If your endpoint stays down, we exponentially back off for 24 hours then give up — failed deliveries are visible in Settings → Webhooks → Deliveries for 30 days so you can replay them.

What's next

If you're building a public integration that other BizRnR customers will install, look at the OAuth section of the API docs. OAuth lets you act on a user's behalf without them ever sharing their API key.

Was this helpful?
© 2026 BizRnR. All rights reserved.