Wocha Docs

Common Patterns

Production-ready patterns for protecting routes, org switching, token refresh, rate limits, idempotency, and API versioning

These patterns cover the most common integration tasks beyond the quickstart. They apply across frameworks and complement the SDK reference and API reference.


Protecting API routes

Validate the caller's access token on every protected backend route. Do not trust client-side checks alone — the Protect component and similar UI guards are for rendering only.

Next.js (App Router)

Use the @wocha/nextjs middleware and server helpers:

// middleware.ts
import { greetMiddleware } from "@wocha/nextjs/server";
 
export default wochaMiddleware({
  publicRoutes: ["/", "/login", "/signup"],
  issuer: process.env.WOCHA_ISSUER!,
});
 
export const config = {
  matcher: ["/((?!_next|.*\\..*).*)"],
};
// app/api/projects/route.ts
import { auth } from "@wocha/nextjs/server";
import { NextResponse } from "next/server";
 
export async function GET() {
  const session = await auth();
  if (!session?.userId) {
    return NextResponse.json({ error: "unauthorized" }, { status: 401 });
  }
 
  const projects = await db.projects.findByUser(session.userId);
  return NextResponse.json({ projects });
}

Express

Use the Express SDK JWT middleware:

import express from "express";
import { wochaAuth, requireScopes } from "@wocha/express";
 
const app = express();
 
app.use(
  wochaAuth({
    issuer: process.env.WOCHA_ISSUER!,
    audience: process.env.WOCHA_CLIENT_ID!,
  }),
);
 
app.get("/api/projects", requireScopes("projects:read"), (req, res) => {
  res.json({ projects: [] });
});

FastAPI

Validate JWTs with your issuer JWKS or introspection endpoint:

from fastapi import Depends, FastAPI, HTTPException
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
import jwt
import httpx
 
app = FastAPI()
security = HTTPBearer()
 
async def get_current_user(
    credentials: HTTPAuthorizationCredentials = Depends(security),
) -> dict:
    token = credentials.credentials
    # Fetch JWKS from {WOCHA_ISSUER}/.well-known/jwks.json and verify
    try:
        payload = jwt.decode(
            token,
            key=await fetch_jwks(),
            algorithms=["RS256"],
            audience=os.environ["WOCHA_CLIENT_ID"],
            issuer=os.environ["WOCHA_ISSUER"],
        )
        return payload
    except jwt.PyJWTError as exc:
        raise HTTPException(status_code=401, detail="Invalid token") from exc
 
@app.get("/api/projects")
async def list_projects(user: dict = Depends(get_current_user)):
    return {"projects": []}

Role-based UI rendering

Use the Protect component or permission hooks to show UI only when the signed-in user has the required role or permission. Keep sensitive actions enforced on the server as well.

import { Protect, usePermissions } from "@wocha/react";
 
function AdminPanel() {
  return (
    <Protect permission="org:admin">
      <button type="button">Invite member</button>
    </Protect>
  );
}
 
function BillingLink() {
  const { has } = usePermissions();
  if (!has("billing:read")) return null;
  return <a href="/billing">Billing</a>;
}

SpiceDB-backed permissions from the Management API (permissions.check, permissions.lookup) are the source of truth for backend authorisation.


Organisation switching

When a user belongs to multiple organisations, issue a new token with the selected org_id claim. The Account API and SDK org switcher handle the consent flow.

Client-side (React):

import { OrgSwitcher } from "@wocha/ui";
 
export function Header() {
  return <OrgSwitcher afterSwitch={() => window.location.reload()} />;
}

Server-side (session switch hint):

// POST /api/v1/sessions/switch-org (user bearer token)
const response = await fetch(`${WOCHA_API}/api/v1/sessions/switch-org`, {
  method: "POST",
  headers: {
    Authorization: `Bearer ${accessToken}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    targetOrgId: "acme-corp",
    clientId: process.env.WOCHA_CLIENT_ID,
    redirectUri: "https://app.example.com/callback",
  }),
});
 
const { authorizeUrl } = await response.json();
// Redirect the user to authorizeUrl to receive tokens with the new org_id

Prefer the Customer API (/v1/users, /v1/organisations) for management operations; legacy M2M routes under /api/v1/identities and /api/v1/organisations are deprecated.


Token refresh patterns

Access tokens expire. Use refresh tokens (via the offline_access scope) to obtain new access tokens without re-prompting the user.

Next.js — automatic refresh in server helpers:

import { auth } from "@wocha/nextjs/server";
 
export async function getData() {
  const session = await auth(); // refreshes when needed
  if (!session) throw new Error("Unauthenticated");
  return fetchUpstream(session.accessToken);
}

Manual refresh (any framework):

async function refreshAccessToken(refreshToken: string) {
  const response = await fetch(`${process.env.WOCHA_ISSUER}/oauth2/token`, {
    method: "POST",
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
    body: new URLSearchParams({
      grant_type: "refresh_token",
      refresh_token: refreshToken,
      client_id: process.env.WOCHA_CLIENT_ID!,
      client_secret: process.env.WOCHA_CLIENT_SECRET!,
    }),
  });
 
  if (!response.ok) {
    throw new Error("Refresh failed — send user to login");
  }
 
  return response.json() as Promise<{
    access_token: string;
    refresh_token: string;
    expires_in: number;
  }>;
}

Store refresh tokens securely (HTTP-only cookies or encrypted server-side storage). Never expose them to browser JavaScript unless using a confidential backend-for-frontend pattern.


Handling rate limits

The Customer API returns rate limit headers on every authenticated response:

HeaderMeaning
X-RateLimit-LimitMaximum requests in the current window
X-RateLimit-RemainingRequests remaining
X-RateLimit-ResetUnix timestamp when the window resets

On 429 Too Many Requests, the response includes retry_after (seconds) in the JSON body and a Retry-After header.

import { WochaRateLimitError } from "@wocha/sdk";
 
try {
  await wocha.users.list();
} catch (err) {
  if (err instanceof WochaRateLimitError) {
    const delayMs = (err.retryAfter ?? 60) * 1000;
    await sleep(delayMs);
    return wocha.users.list();
  }
  throw err;
}

Implement exponential backoff for background jobs. Avoid tight retry loops that amplify load during outages.


Idempotency keys

Mutation endpoints on the Customer API support the Idempotency-Key header. Send a unique key (UUID v4 recommended) per logical operation so safe retries do not create duplicate resources.

import { randomUUID } from "crypto";
 
await wocha.users.create(
  { email: "user@example.com" },
  { idempotencyKey: randomUUID() },
);
curl -X POST "https://your-tenant.api.wocha.ai/v1/users" \
  -H "Authorization: Bearer wocha_mgmt_..." \
  -H "Idempotency-Key: 6ba7b810-9dad-11d1-80b4-00c04fd430c8" \
  -H "Content-Type: application/json" \
  -d '{"email":"user@example.com"}'

The API caches the response for 24 hours. Replaying the same key with the same payload returns the original response; replaying with a different payload returns 409 Conflict.

Use idempotency keys for:

  • User and organisation creation
  • Webhook registration
  • API key rotation
  • Any client that retries on network failure

API versioning

Wocha uses date-based API versions (similar to Stripe). Pin behaviour with the Wocha-Version header:

curl "https://your-tenant.api.wocha.ai/v1/users" \
  -H "Authorization: Bearer wocha_mgmt_..." \
  -H "Wocha-Version: 2026-06-01"
MechanismPurpose
URL path /v1/API surface version (Customer API vs Account API)
Wocha-Version headerDate-based behaviour version within that surface

When the header is omitted, the latest version (2026-06-01) applies.

Changelog (2026-06-01)

Breaking changes in the current version:

  • Unified error format: { error, message, request_id, docs_url }
  • Legacy M2M routes deprecated (see migration guide)

Legacy error format

To receive the older { error, code } shape during migration, either:

  • Pin Wocha-Version to a date before 2026-06-01, or
  • Send Accept: application/vnd.wocha.v0+json

Deprecated routes

Legacy M2M routes return Deprecation, Sunset, and Link headers:

Legacy routeReplacement
/api/v1/identities/*/api/v1/customer/users/*
/api/v1/organisations/*/api/v1/customer/organisations/*
/api/v1/products/*/api/v1/customer/products/*
/api/v1/webhooks/*/api/v1/customer/webhooks/*

Sunset date: 12 December 2026. Migrate before then to avoid disruption.

See the error reference for standard error codes and handling patterns.

On this page