Audit Logs External API — Authentication & Usage Guide (Beta)

Overview

The Audit Logs External API lets partners and integrations programmatically pull a bot's audit log records from your own systems. You authenticate once with OAuth client credentials, exchange them for a short-lived JWT access token, then use that token to incrementally fetch audit logs using a cursor-based pagination model.

All requests use HTTPS and JSON. The API is read-only and idempotent — designed for incremental polling so you only fetch records that have been created or updated since your last successful call.

Getting Started

Follow these four steps to get up and running:

  1. Request an OAuth app from the Leena AI team. You will receive a client_id, client_secret, username, and password. Leena AI will share these with you.
  2. Generate an access token by calling POST https://<region-code>-acl.leena.ai/api/v1.0/oauth/token with your credentials. The issued token must include the audit-logs:read scope.
  3. Fetch the first page of audit logs by calling GET https://<audit-logs-host>/external/v1/audit-logs?updatedAt=<ISO-timestamp> with your token.
  4. Page through results using the returned nextCursor until hasMore is false, then persist the highest updatedAt seen as your new watermark for the next poll.

Region Codes and Endpoints

The OAuth token endpoint and the Audit Logs API endpoint live on different hosts. Use the row matching your bot's region.

📘

Note

For ap-south-1 (the primary region), the host has no region-code prefix. For all other regions, the host is prefixed with the region code as shown above.

Authentication

All endpoints require a Bearer JWT token in the Authorization header. Tokens are obtained via the platform's OAuth token API.

Step 1 — Register an OAuth App

Request an OAuth app from the Leena AI team. You will receive:

CredentialDescription
client_idUnique identifier for your OAuth app
client_secretSecret key for your OAuth app
usernameSystem username for the API execution
passwordPassword for the system user for the API execution

The OAuth app must be provisioned with the audit-logs:read scope. Confirm this with the Leena AI team when requesting the app.

Step 2 — Generate an Access Token

Exchange your credentials for a JWT access token.

POST https://<region-code>-acl.leena.ai/api/v1.0/oauth/token

Headers

HeaderValue
AuthorizationBasic auth, with client_id as username and client_secret as password
Content-Typeapplication/json

Request Body

{
  "username": "<username-received-from-leena>",
  "password": "<password-received-from-leena>",
  "grant_type": "password"
}

Example Request

curl -X POST "https://<region-code>-acl.leena.ai/api/v1.0/oauth/token" \
  -H "Content-Type: application/json" \
  -H "Authorization: Basic <Base64-encoded-string-of-clientId:clientSecret>" \
  -d '{
    "username": "<username-received-from-leena>",
    "password": "<password-received-from-leena>",
    "grant_type": "password"
  }'

Response

{
  "access_token": "eyJhbGciOiJIUzI1NiIs...",
  "token_type": "Bearer",
  "refresh_token": "<Refresh-token>",
  "expires_in": 3600
}

Step 3 — Use the Token

Include the token in the Authorization header for all API calls:

Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

Endpoint

Get Audit Logs

Returns a page of audit log records for your bot, sorted ascending by (updatedAt, _id). Use cursor-based pagination to drain the result set.

GET https://<audit-logs-host>/external/v1/audit-logs

Replace <audit-logs-host> with the value from the Region Codes and Endpoints table for your bot's region.

Headers

HeaderValue
AuthorizationBearer <access_token>

Query Parameters

NameTypeRequiredDefaultDescription
updatedAtISO-8601 stringYes (when no cursor)Lower bound for the first page. Records returned have updatedAt > value.
cursorstringNoOpaque base64 cursor from a prior response. When supplied, overrides updatedAt and continues from the previous page.
limitintegerNo100Maximum records per page. Must be between 1 and 1000.
📘

Note

If both updatedAt and cursor are supplied, cursor wins.

Pagination

Records are returned sorted ascending by (updatedAt, _id). The cursor encodes the (updatedAt, _id) of the last returned record so subsequent pages skip past ties at identical timestamps without gaps or duplicates.

Flow:

  1. First call: pass updatedAt (e.g. epoch or last successful poll watermark).
  2. Read nextCursor and hasMore from the response.
  3. Next call: pass nextCursor back as cursor (the updatedAt parameter is ignored when cursor is set).
  4. Repeat until hasMore is false (nextCursor will also be null).

Cursor wire format (do not parse — treat as opaque):

base64({ "updatedAt": "<ISO-8601 datetime>", "_id": "<24-char hex ObjectId>" })

Invalid cursors return 400 Bad Request with message "Invalid cursor".

Rate Limiting

Rate limits are enforced per OAuth client (keyed by the JWT id claim).

PropertyValue
Window60 seconds
Limit60 requests / minute (default)
Breach response429"Rate limit exceeded. Please retry after some time."

Two different clients hitting on behalf of the same bot get independent buckets.

Response — 200 OK

FieldTypeDescription
dataExternalAuditLog[]Page of audit log records, sorted ascending by (updatedAt, _id).
nextCursorstring or nullPass back as cursor for the next page. null when no more records.
hasMorebooleantrue if a subsequent call will return at least one record.

Example — First Page Request

curl --location \
  'https://<audit-logs-host>/external/v1/audit-logs?updatedAt=2021-05-11T00%3A00%3A00.000Z&limit=1' \
  --header 'Authorization: Bearer <access_token>'

Example — Response

{
  "data": [
    {
      "_id": "6a0edb13c17f8e6581707a63",
      "auditLogId": "DMS-202302-0000001",
      "botId": "68628c943ec15f64a4e9abd9",
      "componentId": "dms",
      "activity": "upload-file",
      "priority": "critical",
      "status": "fail",
      "message": "Error upload the document",
      "generatedAt": "2023-02-14T06:57:25.329Z",
      "updatedAt": "2023-03-17T03:28:58.742Z",
      "actor": {
        "userId": "62971c9eaa0bc914188f62d6",
        "userType": "dashboard"
      },
      "targetUser": {
        "userId": "101234578",
        "userType": "bot",
        "source": "integrations"
      },
      "resourceId": "63086ae9dd99ed5cb559a325",
      "ip": "1.1.1.1",
      "metadata": []
    }
  ],
  "nextCursor": "eyJ1cGRhdGVkQXQiOiIyMDIzLTAzLTE3VDAzOjI4OjU4Ljc0MloiLCJfaWQiOiI2YTBlZGIxM2MxN2Y4ZTY1ODE3MDdhNjMifQ==",
  "hasMore": true
}

Example — Next Page (Cursor)

curl --location \
  'https://<audit-logs-host>/external/v1/audit-logs?cursor=eyJ1cGRhdGVkQXQiOiIyMDIzLTAzLTE3VDAzOjI4OjU4Ljc0MloiLCJfaWQiOiI2YTBlZGIxM2MxN2Y4ZTY1ODE3MDdhNjMifQ%3D%3D&limit=1' \
  --header 'Authorization: Bearer <access_token>'
📘

Note

The cursor value must be URL-encoded — the trailing == becomes %3D%3D.

ExternalAuditLog Schema

Only the fields below are exposed; internal fields on the underlying record (isDeleted, notifications, expireAt, ingestedAt, flaggerUser, etc.) are stripped before the response leaves the service.

FieldTypeRequiredDescription
_idstring (24-char hex)YesMongo ObjectId of the record.
auditLogIdstringYesHuman-readable ID, e.g. DMS-202302-0000001.
botIdstringYesOwning bot — matches the JWT's botId.
componentIdstringYesComponent that produced the log (dms, case-management, onboarding, …).
activitystringYesActivity slug (upload-file, create-ticket, …).
prioritystringYesOne of critical, high, normal, low.
statusstringYesOne of success, in-progress, fail.
messagestringNoFree-form human-readable message.
generatedAtISO-8601 datetimeYesWhen the audited event occurred.
updatedAtISO-8601 datetimeYesServer-side last-modified — drives pagination.
actorActorYesWho initiated the action. See Actor below.
targetUserActorNoWho the action was performed on (if applicable).
submoduleIdstringNoSubmodule scope inside componentId.
resourceIdstringNoID of the resource acted on.
ipstringNoSource IP of the actor.
useragentstringNoUser-agent string of the actor.
metadataMetadata arrayNoArbitrary label/value key-value pairs. See Metadata below.
redirectionLinkstringNoDeep link to the record in the product UI.

Actor

FieldTypeRequiredDescription
userIdstringYesActor identifier.
userTypestringYesOne of dashboard, bot, none.
sourcestringNoOrigin (e.g. integrations).
emailstringNoActor email.
phonestringNoActor phone.
employeeIdstringNoHRIS employee ID.

Metadata

FieldTypeDescription
labelstringField label.
valuestringStringified value.

Error Responses

StatusWhenBody
400 Bad Requestcursor cannot be decoded{ "statusCode": 400, "message": "Invalid cursor" }
400 Bad RequestupdatedAt missing while no cursor supplied, or fails ISO-8601 validationDTO validation error
400 Bad Requestlimit < 1 or > 1000DTO validation error
401 UnauthorizedMissing or non-Bearer Authorization header{ "statusCode": 401, "message": "Missing or invalid Authorization header" }
401 UnauthorizedToken fails signature verification or is malformed{ "statusCode": 401, "message": "Invalid token" }
401 UnauthorizedToken expired{ "statusCode": 401, "message": "Token expired" }
401 UnauthorizedToken does not include the audit-logs:read scope{ "statusCode": 401, "message": "Insufficient OAuth scope" }
401 UnauthorizedToken has no botId{ "statusCode": 401, "message": "Token is missing botId" }
429 Too Many RequestsPer-client rate limit exceeded{ "statusCode": 429, "message": "Rate limit exceeded. Please retry after some time." }

Recommended Polling Pattern

A typical integration follows this flow:

  1. Store the last successful updatedAt seen (initialize to epoch on the first run).
  2. On each poll, call with updatedAt = <stored value> — or cursor = <last nextCursor> if you are mid-page from the previous poll.
  3. Drain pages until hasMore is false.
  4. Persist the highest updatedAt from data as the new watermark for the next poll.
  5. Respect 429 responses — back off and retry after the rate-limit window resets.

Usage Pattern

1. POST https://<region-code>-acl.leena.ai/api/v1.0/oauth/token
      → get access_token

2. GET  https://<audit-logs-host>/external/v1/audit-logs?updatedAt=<watermark>
      → read data[], nextCursor, hasMore

3. While hasMore is true:
      GET https://<audit-logs-host>/external/v1/audit-logs?cursor=<nextCursor>
      → keep draining pages

4. Persist max(data[].updatedAt) as the new watermark.