Public API
Aelo’s public API is a call analytics API — conversation intelligence over REST. You send call recordings or chat transcripts in from your own systems — a CRM, a telephony platform, a custom pipeline — and read back records and their analysis (scores, summaries, objections) once processing finishes. It is org-scoped: every request acts on the organization that issued the API key, and every response is limited to that organization’s data.
Available on Starter and above. Free-plan organizations cannot issue or use public API keys.
Getting a key
Section titled “Getting a key”- Go to Settings → API Keys. Only the organization owner (the
clientrole) can see this section — admins, supervisors and agents cannot manage keys. - Choose a name, pick one or both scopes (
records:write,records:read), and optionally set an expiry date. - The plaintext key (
aelo_sk_…) is shown once, at creation. Store it yourself — Aelo only ever keeps its hash and cannot show it to you again. If you lose it, revoke it and issue a new one.
An organization may hold at most 10 active keys at a time; revoke unused ones before issuing more.
Authentication
Section titled “Authentication”Every request carries the key as a bearer token:
Authorization: Bearer aelo_sk_YOUR_KEYAll endpoints live under https://api.aelo.cloud/api/public/v1.
A key only unlocks what its scopes allow:
| Scope | Grants |
|---|---|
records:write | Create records (POST /records, POST /records/upload) |
records:read | List and read records and analyses (the three GET routes) |
A request to an endpoint the key’s scopes don’t cover fails with insufficient_scope,
even if the key itself is valid.
Limits
Section titled “Limits”| Limit | Value |
|---|---|
| Requests per key | 200 / minute |
Multipart upload size (POST /records/upload) | 25 MB |
fileUrl download size (POST /records) | 100 MB, or 25 MB when the source sends no Content-Length |
fileUrl download timeout | 30 seconds |
| Active API keys per organization | 10 |
| Record list page size | 50 by default, capped at 100 (limit above 100 is silently reduced, not rejected) |
The multipart limit is lower than the fileUrl limit because a multipart body has
to be fully buffered in memory before Aelo can read it; a fileUrl download is
streamed straight to storage — as long as the source declares its
Content-Length. A source that declares none cannot be streamed and is held in
memory like a multipart body, under the same 25 MB ceiling. If your audio is larger than 25 MB, upload it to
storage you control and send fileUrl instead of the file itself — see
Uploading calls and chats for both paths in detail.
Endpoints
Section titled “Endpoints”| Method & path | Scope | What it does |
|---|---|---|
POST /records | records:write | Create a record from a fileUrl (calls) or textContent (chats) |
POST /records/upload | records:write | Create a call record from an uploaded audio file (multipart) |
GET /records | records:read | List your organization’s records |
GET /records/{recordId} | records:read | Get one record |
GET /records/{recordId}/analysis | records:read | Get the latest analysis for a record |
The two upload endpoints, their request fields, idempotency behavior and the warnings they can return are covered in Uploading calls and chats. The rest are documented below.
GET /records
Section titled “GET /records”Lists records newest-first. Archived records are excluded — and not marked as
such, since the response has no archived field, so an archived record simply
does not appear.
Query parameters:
| Param | Default | Notes |
|---|---|---|
limit | 50 | Capped at 100 |
offset | 0 | |
createdAfter | — | ISO-8601 datetime. Inclusive (createdAt >= createdAfter) |
createdBefore | — | ISO-8601 datetime. Inclusive (createdAt <= createdBefore) |
{ "data": [ { "id": "rec_...", "projectId": "your-project-id", "type": "call", "status": "completed", "externalId": "crm-call-12345", "createdAt": "2026-07-01T10:00:00.000Z", "occurredAt": "2026-07-01T09:58:12.000Z", "durationSeconds": 184, "agentId": "42", "agentName": "Ivan Petrov", "callDirection": "outbound" } ], "pagination": { "limit": 50, "offset": 0, "total": 1 }}GET /records/{recordId}
Section titled “GET /records/{recordId}”Returns one record in the same shape as the list. 404s as record_not_found if
the id doesn’t exist or belongs to another organization — the two cases are
indistinguishable on purpose.
GET /records/{recordId}/analysis
Section titled “GET /records/{recordId}/analysis”Returns the latest analysis for a record.
{ "recordId": "rec_...", "analysisId": "an_...", "createdAt": "2026-07-01T10:04:30.000Z", "summary": "Customer asked about pricing tiers; agent...", "qualityScore": 78, "metrics": { "greetingScore": 90, "needsScore": 70, "clarityScore": 80, "objectionScore": 65, "closingScore": 75, "politenessScore": 95, "saleProbability": 62 }}qualityScore is the canonical score — the same number your dashboard shows
for this call, not a self-reported figure from the model. Every metric is null
when the analysis produced no value for it (a scoreless call category, or a
rubric item your project has switched off) — never a 0 standing in for
“not scored”. saleProbability only applies to calls; it is null for chats.
The response is deliberately narrower than what Aelo stores internally: provider, model, prompt profile, token counts and cost are not part of the public contract.
Two 404-shaped situations are distinguished by their error code, because your next move differs:
record_not_found— the record id itself doesn’t exist (or isn’t yours). Fix the id.analysis_not_found— the record exists but carries no analysis.details.analysisExpectedsays whether one is still coming:truemeans keep polling,falsemeans the record finished without an analysis and never will have one — stop. Some calls end that way by design: a call too short to grade (details.reason: "short_call") or a record that failed in the pipeline ("record_failed").details.recordStatuscarries the record’s current status either way.
Errors
Section titled “Errors”Every failure returns the same envelope:
{ "error": { "code": "invalid_request", "message": "Human-readable description.", "details": { "field": "..." } }}details is optional and, when present, describes only your own request — never
internal state. An unrecognized field in a JSON body, form or query string
answers with invalid_request and names every offending field, plus a
didYouMean suggestion when one is a close match to a real one (e.g.
projectID → projectId).
| Code | Status | Meaning |
|---|---|---|
invalid_api_key | 401 | Missing/malformed Authorization header, or the key is unknown, revoked or expired (all three look identical, on purpose) |
insufficient_scope | 403 | The key doesn’t carry the scope this endpoint requires |
tier_not_eligible | 403 | The organization is on the free plan |
feature_disabled | 403 | Your organization’s administrator has switched the public API off. The key is still valid and upgrading the plan won’t help — ask them to turn it back on |
insufficient_balance | 402 | The plan’s included minutes for this cycle are spent and the credit balance is empty. Nothing was stored — add credits in Settings → Billing and send the request again |
project_not_found | 404 | projectId doesn’t exist, or belongs to another organization |
project_not_accepting_records | 409 | The project exists but is paused or archived — the caller’s own admin needs to reactivate it before this project can accept records again |
record_not_found | 404 | The record id doesn’t exist, or belongs to another organization |
analysis_not_found | 404 | The record exists but has no analysis — poll on while details.analysisExpected is true, stop when it is false |
not_found | 404 | No such route under /api/public/v1 |
rate_limited | 429 | More than 200 requests/minute on this key |
payload_too_large | 413 | See Limits above |
unsupported_media_type | 415 | The uploaded/downloaded content isn’t audio or video |
invalid_request | 400 | The request body, form or query string failed validation |
fetch_failed | 400 | The file at fileUrl could not be downloaded |
resolution_unavailable | 503 | Aelo couldn’t verify fileUrl’s destination right now — transient, retry (Retry-After header) |
service_unavailable | 503 | The analysis store is temporarily unreachable — transient, retry (Retry-After header) |
internal_error | 500 | Unexpected server-side failure |
A 503 always carries a Retry-After header (in seconds); back off and retry
rather than treating it as a permanent failure.
Is Aelo a call analytics API?
Section titled “Is Aelo a call analytics API?”Yes. Aelo’s public API is a REST call analytics API at
https://api.aelo.cloud/api/public/v1: org-scoped Bearer keys (aelo_sk_…),
records:write / records:read scopes, 200 requests per minute per key.
Available on Starter and above.
How do I upload call recordings for analysis?
Section titled “How do I upload call recordings for analysis?”Two ways: POST /records/upload takes the audio file itself (multipart, up to
25 MB); POST /records takes a fileUrl Aelo downloads from you (up to
100 MB). Chat transcripts go through POST /records as textContent. Repeat
submissions are deduplicated by externalId — send your own identifier and a
retry returns the original record instead of creating a second one. Both
endpoints are covered in Uploading calls and chats.
Does the API work with Bitrix24?
Section titled “Does the API work with Bitrix24?”Bitrix24 usually doesn’t need it: the native integration analyses calls with no API code at all. The public API is for everything else — your own telephony, a data pipeline, or a CRM Aelo doesn’t integrate with yet.
Related
Section titled “Related”- Uploading calls and chats — request fields, size limits, idempotency and warnings for the two upload endpoints
- Pricing — the public API is included on Starter and above