Docs
API log
Every call your keys made, with the request and the response redacted, kept seven days.
When an agent calls the API and something goes wrong, the answer alone rarely says why. The API log keeps one row per authenticated call: the request id, the method, the path, the status, the duration, the key id and the SDK recognised from the User-Agent. Reading it needs audit_read, which a member never holds, so the log is an owner and admin surface.
Read it
curl -s https://api.mueslimail.com/v0/logs?limit=20 \
-H "Authorization: Bearer ***"Filters: key_id, method, status, sdk, after and before (RFC 3339, before exclusive, which is how you page). GET /v0/logs/{log} returns the same row with ip, user_agent and the two redacted bodies. A row of another organization is 404, never 403.
What is redacted, and how you can tell
- A credential field (
authorization,secret,api_key,token,password,cookie) becomes{"redacted": "credential"}. - A mail field (
subject,text,html,raw, attachmentcontent) becomes{"redacted": "mail_content", "bytes": N, "content_type": "...", "sha256": "..."}. - A header outside the allowlist (
accept,content-type,idempotency-key,user-agent,x-mai-api-version,x-request-id) is not kept at all, and its name is listed when it was a credential header. - Anything that looks like a token, a
whsec_signing secret or a bearer value is masked even inside a free text field. redactionsnames every removal, asheader:authorization,mail_content:htmlorcredential:secret: the list is the proof, not the promise.- A response body that is not JSON or markdown is not stored at all;
bytesandsha256still describe it.
SDK and version
The User-Agent of our own SDKs carries the name and the version (aimulti-node/1.2.0, aimulti-go/v1). The log normalises it into sdk and sdk_version, compares it with the newest version this deployment knows and sets sdk_update_available with a sentence a person can act on. Nothing is fetched from outside: MAI_SDK_LATEST (node=1.3.0,go=1.1.0) is what the comparison uses, so shipping a new client does not need a new API release. The same notice arrives as the X-Mai-Sdk-Update header on the call itself, so an integrator sees it without opening the console.
Copy for an assistant
GET /v0/logs/{log}/export answers text/markdown: the shape of the call, the redacted bodies and an explicit list of what the log does not contain. It is a copy a person pastes where they work, not an integration: nothing is sent to a third party and no external service is called.
Retention
Seven days, enforced by the retention job: the row is deleted by age and not by request. The table is in the same purge job and the same weekly restore drill as every other table, so the published window is a fact and not a target.
GET/v0/logs
Redacted API request log of the organization, newest first (requires audit_read)
Parameters
| Name | Type | Notes |
|---|---|---|
limit | integer | in query, min 1, max 200, default 50 |
key_id | string (uuid) | in query |
method | string | in query |
status | integer | in query, min 100, max 599 |
sdk | go | node | python | php | ruby | java | in query |
after | string (date-time) | in query |
before | string (date-time) | in query |
Responses
200OK: object400Error envelope: Error403Error envelope: Error501Error envelope: Error
GET/v0/logs/{log}
One API request with its redacted request and response body (requires audit_read)
Parameters
| Name | Type | Notes |
|---|---|---|
log | string (uuid) | in path, required |
Responses
200OK: RequestLog403Error envelope: Error404Error envelope: Error501Error envelope: Error
GET/v0/logs/{log}/export
The same request as a markdown block a person can paste into an assistant (requires audit_read)
Parameters
| Name | Type | Notes |
|---|---|---|
log | string (uuid) | in path, required |
Responses
200OK: string403Error envelope: Error404Error envelope: Error501Error envelope: Error
RequestLogEntry object
| Name | Type | Notes |
|---|---|---|
id | string (uuid) | |
request_id | string | The X-Request-Id of the call |
key_id | string (uuid) | The key that made the call; absent for a person signed in |
actor | string | user:<id> or api_key:<id> |
actor_type | user | agent | |
method | string | |
path | string | Path and filters, with the value of a credential-shaped query parameter masked |
status | integer | |
duration_ms | integer | |
sdk | string | SDK name normalised from the User-Agent; absent when the caller is not one of our SDKs |
sdk_version | string | |
sdk_update_available | boolean | A newer version of that SDK exists on this deployment |
sdk_latest | string | |
sdk_notice | string | The sentence to show a person: which SDK, which version, which one to move to |
created_at | string (date-time) |
RequestLogBody object
| Name | Type | Notes |
|---|---|---|
headers | object | Request headers only, from an allowlist |
body | object | The JSON as it was, with credentials and mail content replaced by markers |
bytes | integer | Size of the original body |
sha256 | string | Digest of the original body, empty when there was none |
truncated | boolean | The stored body is a summary: the original was larger than the log keeps |