Skip to content

API Conventions

This page covers the conventions that apply to every endpoint in the oHallo API. Read this before making your first API call.

All API requests use the following base URL:

https://api.ohallo.eu

All endpoints are prefixed with /api/. For example, the conversations list endpoint is:

https://api.ohallo.eu/api/conversations

All request and response bodies use JSON. Set the Content-Type header on requests that include a body:

Terminal window
curl https://api.ohallo.eu/api/workspaces/a1b2c3d4-e5f6-7890-abcd-ef1234567890/kb-entries \
-H "Authorization: Bearer sf_live_v1_a3Bx9kLmP2qR7wYz4nDfGhJkQpStUvWx" \
-H "Content-Type: application/json" \
-d '{"question": "What are your shipping rates?", "answer": "Standard shipping is free for orders over EUR 100.", "sourceType": "manual", "topics": ["shipping"]}'

Every request must include an Authorization header with a Bearer token:

Authorization: Bearer sf_live_v1_a3Bx9kLmP2qR7wYz4nDfGhJkQpStUvWx

See the Authentication page for details on API key creation, scopes, and security practices.

Most endpoints require a workspaceId parameter to scope the request. This is passed either as a path parameter or a query parameter, depending on the endpoint:

Terminal window
# Path parameter
GET /api/workspaces/{workspaceId}/kb-entries
# Query parameter
GET /api/conversations?workspaceId=a1b2c3d4-e5f6-7890-abcd-ef1234567890

If your API key is scoped to a specific workspace, you still need to provide the workspaceId parameter; the API validates that it matches.

Larger list endpoints (e.g. GET /api/conversations) use cursor-based pagination:

ParameterTypeDefaultDescription
limitinteger50Page size (max 100).
cursorstringnoneOpaque cursor returned as nextCursor on the previous response. Omit on the first call.

Cursor-paginated response format:

{
"data": [
{ "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "status": "open" }
],
"nextCursor": "eyJsYXN0SWQiOiJhMWIyYzNkNCJ9"
}

When the response omits nextCursor, you have reached the end of the result set. Cursors are opaque and may change format without warning; pass them back as-is, do not parse or assume their shape.

Smaller list endpoints (for example /contacts, /accounts, /kb-entries/search, /policy-entries/search, /attention-items) return a bare JSON array. Pass the relevant filter and search params to narrow results rather than paginating. The full-page array shape is documented per endpoint.

Example, fetching the next page after the first 50 conversations:

Terminal window
curl "https://api.ohallo.eu/api/conversations?workspaceId=a1b2c3d4-e5f6-7890-abcd-ef1234567890&limit=50&cursor=eyJsYXN0SWQiOiJhMWIyYzNkNCJ9" \
-H "Authorization: Bearer sf_live_v1_a3Bx9kLmP2qR7wYz4nDfGhJkQpStUvWx"

All timestamps are in ISO 8601 format, in UTC:

{
"createdAt": "2026-03-15T14:30:00.000Z",
"updatedAt": "2026-03-15T16:45:12.000Z"
}

All resource identifiers are UUID v4 strings:

{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"workspaceId": "f0e1d2c3-b4a5-6789-0abc-def123456789"
}

All errors follow a consistent shape:

{
"error": "A human-readable error message"
}

Validation errors (422) include structured details:

{
"error": "Invalid request",
"details": {
"fieldErrors": {
"question": ["Required"],
"topics": ["Expected array, received string"]
},
"formErrors": []
}
}
CodeMeaning
200Success
201Resource created
400Bad request: malformed JSON or missing required fields
401Unauthorized: missing or invalid API key
403Forbidden: valid key but insufficient scopes
404Resource not found
422Unprocessable entity: validation failed on request body
429Rate limit exceeded
500Internal server error

API requests are rate-limited at the gateway per client IP address. The limit is 600 requests per minute in production. When the limit is exceeded, the gateway returns 429 Too Many Requests. Back off and retry after a short delay:

async function fetchWithRetry(url: string, options: RequestInit): Promise<Response> {
const response = await fetch(url, options);
if (response.status === 429) {
// Back off before retrying. The gateway enforces a per-IP budget, so
// spreading requests out keeps you comfortably within the limit.
await new Promise((resolve) => setTimeout(resolve, 2000));
return fetchWithRetry(url, options);
}
return response;
}

Because the limit is keyed on the calling IP address, several integrations sharing one outbound IP draw from the same budget. If you run high-volume batch jobs, pace them rather than bursting.