Cross-cutting Concerns
These conventions apply uniformly to both Core API and Intelligence Service.
Errors
All errors use a single envelope:
{
"error": {
"code": "validation_failed",
"message": "Human-readable description.",
"details": [
{
"field": "attributes.severity",
"issue": "must be one of: low, medium, high, critical"
}
],
"request_id": "req_01HQ...",
"documentation_url": "https://developer.bcmlogic.com/errors/validation_failed"
}
}
Error codes are stable snake_case strings. Always log the request_id — support requests must include it.
HTTP Status Codes
| Status | Meaning |
|---|---|
400 | Malformed request |
401 | No or invalid credentials |
403 | Authenticated but not authorised |
404 | Resource not found — also returned when access is denied by tenant scope, to avoid information leakage |
409 | Conflict — e.g. concurrent modification, duplicate idempotency key with a different body |
422 | Validation failure — field-level errors in details |
429 | Rate limited — see Rate Limiting |
500 | Server error |
503 | Service unavailable |
Example — handling a 422
const response = await fetch('https://api.bcmlogic.com/v1/core/erm/risks', {
method: 'POST',
headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ attributes: { severity: 'extreme' } }),
});
if (!response.ok) {
const { error } = await response.json();
console.error(`[${error.request_id}] ${error.code}: ${error.message}`);
error.details?.forEach(d => console.error(` ${d.field}: ${d.issue}`));
}
Pagination
All list endpoints use cursor-based pagination. Do not construct cursors yourself — they are opaque server-generated tokens.
Pass ?cursor=<token>&page_size=50 on list requests. The maximum page size is 200.
{
"data": [ ],
"pagination": {
"next_cursor": "eyJpZCI6...",
"has_more": true,
"page_size": 50
}
}
When has_more is false, you have reached the end of the collection. When next_cursor is absent, do not make a further request.
Offset/limit pagination is not supported — it does not scale and produces inconsistent results under concurrent writes. See ADR-005.
Example — paginate through all risks
async function* fetchAllRisks(token: string) {
let cursor: string | undefined;
do {
const url = new URL('https://api.bcmlogic.com/v1/core/erm/risks');
url.searchParams.set('page_size', '100');
if (cursor) url.searchParams.set('cursor', cursor);
const res = await fetch(url, { headers: { 'Authorization': `Bearer ${token}` } });
const body = await res.json();
yield* body.data;
cursor = body.pagination.has_more ? body.pagination.next_cursor : undefined;
} while (cursor);
}
Filtering and Sorting
- Filtering uses query parameters:
?status=active&severity=high. - Multi-value filters use repeated keys:
?status=active&status=pending. - Sorting uses
?sort=fieldfor ascending,?sort=-fieldfor descending. Multiple sort keys are comma-separated:?sort=-created_at,name. - Not every field is filterable or sortable — available options are documented per endpoint.
Idempotency
All POST and PUT operations on Core API require an Idempotency-Key header. Intelligence Service generation endpoints accept the header optionally — use it to safely retry expensive generation requests.
Keys are scoped to your tenant and the request path, and are cached for 24 hours. Reuse within 24 hours returns the cached response. Reuse with a different request body returns 409 Conflict.
See ADR-007 for the rationale.
Rate Limiting
Rate limits are enforced at the gateway, per tenant, per endpoint family. Current limits are returned on every response:
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 873
X-RateLimit-Reset: 1747300800
Hitting a limit returns 429 Too Many Requests with a Retry-After header in seconds.
Intelligence Service has tighter limits than Core API because generation is compute-intensive. Design clients to batch requests and cache responses.
Webhooks
BCMLogic Next emits webhooks for significant events: document indexed, report generated, vendor status changed, audit event recorded. Configure endpoints in the developer portal.
Payloads are signed with HMAC-SHA256. Always verify the signature before processing the payload.
Delivery is at-least-once — consumers must be idempotent.
Signature verification
import { createHmac, timingSafeEqual } from 'crypto';
function verifyWebhook(payload: Buffer, signature: string, secret: string): boolean {
const expected = createHmac('sha256', secret).update(payload).digest('hex');
return timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
}
Tracing
Every request accepts a traceparent header (W3C Trace Context). If absent, the gateway generates one. The same trace ID propagates through Core API, Intelligence Service, and internal services.
Include traceparent in client-side observability to get end-to-end debugging across the full request path.