Developers · Reference

Webb9 API reference.

Authentication, scopes, every v1 endpoint (generated straight from the live OpenAPI spec), idempotency, error codes, and webhook signature verification — everything you need to build against the Webb9 API.

Authentication

Two credential types, one request shape.

API keys

Create a scoped key (wb9_sk_live_… / wb9_sk_test_…) from your Webb9 dashboard's Integrations page, then send it on every request:

Authorization: Bearer wb9_sk_live_...

The key value is shown exactly once, at creation. Rotate or revoke it any time from the same dashboard page — revocation takes effect on the credential's very next request.

OAuth 2.0 (Authorization Code + PKCE)

For apps installed by other Webb9 businesses. Redirect the business owner to your registered app's consent screen, then exchange the resulting code:

POST https://api.webb9.com/oauth/token
Content-Type: application/json

{
  "grant_type": "authorization_code",
  "client_id": "...",
  "client_secret": "...",
  "code": "...",
  "redirect_uri": "https://yourapp.example.com/callback",
  "code_verifier": "..."
}

PKCE with S256 is required — plain is rejected. Access tokens last 30 minutes; refresh tokens rotate on every use, and reusing an already-rotated refresh token revokes the entire token family (forcing re-authorization) as a compromise-detection signal. Register your app at /developers/apply.

Scopes

Request only what you need.

Every credential — API key or OAuth install — carries an explicit list of scopes. There is no wildcard scope on the public API.

customers.readRead customer records
customers.writeCreate and update customer records
leads.readRead leads
leads.writeCreate and update leads
jobs.readRead jobs (work orders)
jobs.writeCreate and update jobs (work orders)
quotes.readRead quotes
quotes.writeCreate quotes and accept them
invoices.readRead invoices
invoices.writeCreate and update invoices
payments.writeRecord external payments against invoices
files.writeUpload and attach files to records
webhooks.manageManage webhook endpoints and view deliveries
Endpoint reference

Every v1 route, generated from the live OpenAPI spec.

This list is generated at request time from the same spec your tools can import — it can never drift out of sync with what's actually deployed.

Download OpenAPI spec (JSON)

The live spec couldn't be loaded right now — try /developers/openapi.json directly, or reload this page shortly.

Idempotency

Every resource-creating request needs an Idempotency-Key.

Send a unique Idempotency-Key header (a UUID is fine) on every POST that creates a resource, plus the quote-accept, file-attach, and webhook-replay actions. Retrying with the same key and the same request body replays the original response byte-for-byte instead of repeating the action — safe for network retries and double-clicks alike. Retrying with the same key and a different body gets a 409 idempotency_conflict. Keys are retained for 24 hours. Plain field-edit PATCH requests don't need one — they're naturally idempotent.

Errors

One envelope, stable machine-readable codes.

{
  "error": {
    "type": "invalid_request_error",
    "code": "invalid_request",
    "message": "email must be a valid email address",
    "requestId": "3f2a1c9e-..."
  }
}
400 invalid_requestMalformed body, bad query params, or a validation failure.
401 unauthorizedMissing, invalid, expired, or revoked credential.
403 insufficient_scopeThe credential is valid but lacks the scope this route needs.
404 not_foundThe resource doesn't exist, or belongs to a different business.
409 conflictAn Idempotency-Key was reused with a different request body, or is still in flight.
429 rate_limitedPer-credential, per-business, or per-IP rate limit exceeded.

Every response — success or error — carries an X-Request-Id header; include it when contacting support about a specific request.

Webhooks

Verify the signature before trusting a delivery.

Every delivery carries x-webb9-signature: sha256=<hex> — an HMAC-SHA256 of {timestamp}.{rawBody} using your endpoint's signing secret (shown once, at creation) — plus x-webb9-timestamp (Unix seconds) and x-webb9-event. Reject any timestamp more than 5 minutes old or in the future, in addition to checking the signature, so a captured request can't be replayed later.

Node.js

const crypto = require('crypto');

function verify(secret, timestamp, rawBody, signatureHeader) {
  const age = Math.abs(Date.now() / 1000 - Number(timestamp));
  if (age > 300) return false; // 5-minute tolerance

  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${timestamp}.${rawBody}`)
    .digest('hex');
  const provided = signatureHeader.replace(/^sha256=/, '');

  const a = Buffer.from(provided, 'utf8');
  const b = Buffer.from(expected, 'utf8');
  return a.length === b.length && crypto.timingSafeEqual(a, b);
}

Python

import hmac, hashlib, time

def verify(secret, timestamp, raw_body, signature_header):
    age = abs(time.time() - float(timestamp))
    if age > 300:  # 5-minute tolerance
        return False

    expected = hmac.new(
        secret.encode(), f"{timestamp}.{raw_body}".encode(), hashlib.sha256
    ).hexdigest()
    provided = signature_header.removeprefix("sha256=")

    return hmac.compare_digest(provided, expected)

rawBody must be the exact bytes received — verify before any JSON parsing/re-serialization touches it. A failed delivery retries with backoff (30s → 2m → 10m → 1h → 6h) and dead-letters after 5 attempts; an endpoint with no successful delivery for 3+ days is auto-disabled and its owners are notified.

Tooling

Import straight into Postman or Bruno.

The OpenAPI spec at /developers/openapi.json is the source of truth — both Postman (File → Import → Link) and Bruno (Import → OpenAPI) can generate a full collection directly from that URL, so it can never go stale the way a hand-maintained collection file would.