Search the audit logs

Circularo keeps a record of what happens in your organization: who did it, to which entity, from where, and when. This guide reads that history four ways — a period of activity, one kind of action, everything that happened to a single document, and the records left by one request. It takes five calls.

Before you begin

  • An organization-administrator key. A key issued to a regular member can read the audit logs as well, but only ever sees the actions that member performed. Organization-wide history needs an administrator.

Step 1: List the activity of a period

Bound the period you care about and read what happened in it, newest first:

GET /audit-logs?occurredAfter=2026-05-11T00:00:00.000Z&occurredBefore=2026-05-12T00:00:00.000Z&limit=2
Authorization: Bearer YOUR_API_KEY
HTTP/2 200

{
    "pagination": {
        "limit": 2,
        "nextCursor": "4c81f7d3a9b25e60c8471da35f92b6e0f14c8a37d605b9e2f83c1a74d06b5e39",
        "offset": 0
    },
    "results": [
        {
            "activityTarget": "user",
            "activityType": "created",
            "actor": "john.smith@example.com",
            "actorType": "user",
            "id": "Vb4kY7nCwR2mXt5qLd8H",
            "ipAddress": "203.0.113.9",
            "kyc": null,
            "occurredAt": "2026-05-11T10:05:41.000Z",
            "requestId": "d3a92f60c15b8e472a6c94e0b1f785d3",
            "subjectId": "emma.wilson@example.com",
            "userAgent": "Circularo-Integration/1.4"
        },
        {
            "activityTarget": "document",
            "activityType": "signed",
            "actor": "robert.brown@example.com",
            "actorType": "user",
            "id": "Ds5bN1kRfW8vXq3mTy7C",
            "ipAddress": "198.51.100.17",
            "kyc": null,
            "occurredAt": "2026-05-11T09:46:07.000Z",
            "requestId": "c8e01b4d7f2a695ce3b90f174d28a6c5",
            "subjectId": "Hn6vQ2tYbK4mXe8wRzD5",
            "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)"
        }
    ],
    "total": 6
}

Both bounds are optional — without them you get the organization's entire history, which is rarely what you want.

A record answers four questions. activityType with activityTarget say what happened and to what kind of entity, and subjectId names the entity itself — the account above, a document, a group. actor with actorType say who did it. occurredAt says when. ipAddress and userAgent say where from, as far as the request revealed it; the IP address is shown to organization administrators only.

Records appear on their own: every action writes its own, the calls your integration makes included. What you cannot do is touch them afterwards — there is no endpoint that edits or removes a record, which is what makes the log worth reading in the first place.

Note

This is the organization-wide history. The story of a single document, told in transactions and recipients rather than raw activity, is its own audit trail — see Collect the evidence of a signed document.

Step 2: Follow the cursor to the next page

An audit log outgrows one page quickly, so walk it with the cursor:

GET /audit-logs?cursor=4c81f7d3a9b25e60c8471da35f92b6e0f14c8a37d605b9e2f83c1a74d06b5e39
Authorization: Bearer YOUR_API_KEY
HTTP/2 200

{
    "total": 6,
    "pagination": {
        "limit": 2,
        "nextCursor": "9e07b4c1d582a36f0b7e94153ca8d26f70b3e5814da96c02f7b845e13c069adf",
        "offset": 2
    },
    "results": [
        {
            "activityTarget": "notification",
            "activityType": "send",
            "actor": null,
            "actorType": "system",
            "occurredAt": "2026-05-11T09:30:02.000Z",
            ...
        },
        {
            "activityTarget": "document",
            "activityType": "shared",
            "actor": "jane.doe@example.com",
            "actorType": "user",
            "occurredAt": "2026-05-11T09:12:33.412Z",
            ...
        }
    ]
}

The first record here is one Circularo wrote by itself, without anyone asking: a notification it sent out. Such a record is marked system and carries no actor, no requestId and no connection information, so treat all of these as optional when you consume the log.

The cursor travels alone and remembers the filters of the first page, as on every other collection — Page through collections covers the mechanics. Do not pause for long in the middle of a walk: an audit-log cursor expires after a period of inactivity, and a stale one is refused with 400.

Step 3: Narrow the search to one kind of action

Filters keep a search down to what you came for:

GET /audit-logs?activityTarget=document&activityType=signed&occurredAfter=2026-05-11T09:00:00.000Z&occurredBefore=2026-05-12T00:00:00.000Z
Authorization: Bearer YOUR_API_KEY
HTTP/2 200

{
    "total": 1,
    "results": [
        {
            "id": "Ds5bN1kRfW8vXq3mTy7C",
            "activityType": "signed",
            "actor": "robert.brown@example.com",
            "occurredAt": "2026-05-11T09:46:07.000Z",
            "subjectId": "Hn6vQ2tYbK4mXe8wRzD5",
            ...
        }
    ],
    ...
}

Every filter you add has to match, so narrowing the kind of action on top of the period leaves one record here. Repeating one filter widens it instead: two activityType values match records of either kind. You can also filter by actor, and turn the ordering around with order.

Step 4: Follow one document through its life

subjectId collects everything recorded about a single entity:

GET /audit-logs?subjectId=Hn6vQ2tYbK4mXe8wRzD5&order=asc
Authorization: Bearer YOUR_API_KEY
HTTP/2 200

{
    "total": 4,
    "results": [
        {
            "activityType": "created",
            "actor": "jane.doe@example.com",
            "occurredAt": "2026-05-11T08:14:22.000Z",
            ...
        },
        {
            "activityType": "shared",
            "actor": "jane.doe@example.com",
            "occurredAt": "2026-05-11T09:12:33.000Z",
            ...
        },
        {
            "activityType": "shared",
            "actor": "jane.doe@example.com",
            "occurredAt": "2026-05-11T09:12:33.412Z",
            ...
        },
        {
            "activityType": "signed",
            "actor": "robert.brown@example.com",
            "occurredAt": "2026-05-11T09:46:07.000Z",
            ...
        }
    ],
    ...
}

Read oldest first and the history tells itself: the agreement was created, went out to two recipients, and was signed. The same filter works for any entity you hold an identifier for, so keep the identifiers your integration creates — they are what you search the log by later.

Step 5: Collect the records of one request

One call can leave several records, and they share a requestId:

GET /audit-logs?requestId=b2c47f19e05d3a864f21c8e7d90b53af&order=asc
Authorization: Bearer YOUR_API_KEY
HTTP/2 200

{
    "total": 2,
    "results": [
        {
            "id": "Rt2nW8yFkD6bZq1xHm5V",
            "activityType": "shared",
            "actor": "jane.doe@example.com",
            "requestId": "b2c47f19e05d3a864f21c8e7d90b53af",
            ...
        },
        {
            "id": "Lp9cV4hJmQ7tYw2sBn6X",
            "activityType": "shared",
            "actor": "jane.doe@example.com",
            "requestId": "b2c47f19e05d3a864f21c8e7d90b53af",
            ...
        }
    ],
    ...
}

Sending the agreement out was a single call, and each recipient it reached got a record of its own. That identifier is also what the API reports in the requestId of an error envelope and what support asks for, so it ties a call in your own logs to what it did here.

Complete example

Pulling everything that happened since the last run — the shape of a nightly export into your own audit system. Error handling is minimal for brevity — in production, inspect the error envelope of non-2xx responses.

JavaScript
const BASE_URL = "https://sandbox.circularo.com/api/v1/public"; // your instance's base URL
const API_KEY = "YOUR_API_KEY";
const AUTH_HEADER = { "Authorization": `Bearer ${API_KEY}` };

// the watermark your last run stored
let since = "2026-05-11T00:00:00.000Z";
let query = `limit=100&order=asc&occurredAfter=${encodeURIComponent(since)}`;

do {
    const res = await fetch(`${BASE_URL}/audit-logs?${query}`, { headers: AUTH_HEADER });
    if (!res.ok) throw new Error(`Audit log read failed: ${res.status}`); // error handling kept minimal for brevity

    const page = await res.json();
    for (const record of page.results) {
        await archive(record); // your own storage
        since = record.occurredAt;
    }

    // the cursor already carries the filters, so it travels on its own
    query = (page.pagination.nextCursor === null) ? null : `cursor=${page.pagination.nextCursor}`;
} while (query !== null);

console.log(`Archived up to ${since}`);

Next steps