Errors use standard JSON responses ({"message": ..., "errors": {...}} for validation) plus a handful of app-specific status/body conventions noted below.
| HTTP status | Meaning here | Likely cause | Corrective action |
|---|---|---|---|
| 400 Bad Request | Malformed request | Rarely used directly — most bad input surfaces as 422 | Check request shape against the endpoint's documented body |
| 401 Unauthorized | Missing/invalid/expired bearer token; also used for webhook signature failures | Token not sent, expired (>7 days), or revoked; or a billing webhook with a bad/missing HMAC signature | Re-authenticate (login/SSO) and resend with a fresh Authorization: Bearer header. For webhooks: verify PADDLE_WEBHOOK_SECRET/PAYMONGO_WEBHOOK_SECRET match the gateway dashboard exactly |
| 402 Payment Required | Subscription lockout | Tenant's subscription lapsed/absent and the request is a write, not a */billing/* route | Subscribe or resolve billing via POST billing/subscribe; reads still work |
| 403 Forbidden | Permission denial, or a feature-gate denial | Caller's effective permissions don't include the route's required permission:<key>; or feature:<key> not entitled on the plan | Grant the permission (Team & Access) or upgrade plan. Response body includes the specific permission/feature key that was missing |
| 404 Not Found | Resource doesn't exist, OR belongs to another tenant, OR country:<ISO2> gate mismatch | UUID route-model binding is tenant-scoped — a cross-tenant record 404s rather than 403ing, so as not to confirm its existence; country-gated routes (e.g. PH-only WHT endpoints) also 404 outright for tenants outside the allowed country | Verify the UUID belongs to the active tenant (X-Tenant header); verify the tenant's country supports the feature |
| 409 Conflict | No active tenant context | X-Tenant header missing or doesn't match an active membership | Select/send a valid tenant id the user actually belongs to |
| 422 Unprocessable Entity | Validation failure | Inline $request->validate([...]) rules failed | Response errors object lists per-field messages; fix and resubmit |
| 429 Too Many Requests | Rate limit exceeded | Global 300/min per user/IP, or a tighter per-route throttle (e.g. login 20/min, register 10/min), or TOTP's independent 5/min cap | Back off and retry after the window; check Retry-After if present |
| 500 Internal Server Error | Unhandled exception | Application bug or unexpected external-service failure | Check storage/logs / the configured LOG_CHANNEL; correlate via the X-Request-Id/X-Correlation-Id header echoed on the request |
App-specific response bodies worth knowing
- Change-request flow: several settings/config writes (organization profile, branches, and period generation or status) return 202 Accepted with a pending
ChangeRequest, not the applied result — the caller must follow up withPOST change-requests/{id}/confirmand an emailed OTP before the change takes effect. - Generic auth responses: registration, password-forgot, and email-resend endpoints deliberately return the same generic success response whether or not the email/account exists, to avoid an account-enumeration oracle.
- Limit-denial bodies:
402 plan_limit_reached(user seats; includesaddon_available) and402 usage_limit_reached(monthly transaction cap at the approve/post step; includesused,limit,period— the record's transaction month — andaddon_available) — both audit-logged with tenant/user/plan/limit/route.
See API Authentication for the auth-specific flow and Security for the underlying middleware implementations.