Skip to content

Export Workspace Usage and Audit Logs

Three endpoints report on the Runway web app workspaces your API project is linked to: one row per generation with the credits it cost, and audit log entries as a list or one at a time with full detail.

The usual reasons to use them:

  • Charging credits back to the client or cost center that spent them.
  • Feeding audit events into the log and security tooling you already run.
  • Pulling a complete trail of Runway activity for a security review.

For one-off answers, use the web app: Enterprise Analytics and Enterprise Audit Logs both filter on screen and export to CSV. The API is for keeping something in sync without a person in the loop.

/v1/organization/usage is different: it reports what this API project spent on its own calls. Web app credits and API credits are separate pools.

The API project has to be linked to a Runway account: generate a one-time code in the developer portal under Manage → Connections, then paste it into the web app.

Whoever creates that link sets what the API project can read:

  • A link created by a person covers every organization workspace where that person is an admin.
  • A link created while acting as a workspace covers that one workspace.

You cannot widen that scope from the API. With no link, requests fail with 400 and No workspace is linked to this API project.

Every request needs the standard headers:

Terminal window
Authorization: Bearer $RUNWAYML_API_SECRET
X-Runway-Version: 2024-11-06

/v1/organization/webapp/usage returns one row per generation, newest first. Both from and to are required here. from is inclusive, to is exclusive.

Terminal window
curl -G https://api.dev.runwayml.com/v1/organization/webapp/usage \
-H "Authorization: Bearer $RUNWAYML_API_SECRET" \
-H "X-Runway-Version: 2024-11-06" \
--data-urlencode "from=2026-01-01T00:00:00Z" \
--data-urlencode "to=2026-02-01T00:00:00Z"

Each row says who generated what, where, and at what cost:

{
"data": [
{
"timestamp": "2026-01-15T18:04:11.000Z",
"userId": 12345,
"email": "member@acme.com",
"workspaceId": 6789,
"workspaceName": "Acme Video Team",
"tool": "Gen-4 Video",
"credits": 50
}
],
"hasMore": true,
"nextCursor": "MjAyNi0wMS0xNVQxODowNDoxMS4wMDBa"
}

Two fields need care. email is empty once that person is deleted, so key your reports on userId. timestamp is when the generation was charged, which is what a bill reconciles against.

These are raw rows rather than summaries. There is no aggregate endpoint; totals like credits per workspace or active users per month are yours to compute, which is what the chargeback example below does.

Both list endpoints share one envelope: data, hasMore, nextCursor. limit accepts 1 to 100, default 50. For the next page, send nextCursor back as cursor with the same filters. Treat cursors as opaque strings and pass them through verbatim. You are done once hasMore is false and nextCursor is null.

type UsageRow = {
timestamp: string;
userId: number;
email: string;
workspaceId: number;
workspaceName: string;
tool: string;
credits: number;
};
const rows: Array<UsageRow> = [];
let cursor: string | null = null;
while (true) {
const page = (await client.get('/v1/organization/webapp/usage', {
query: {
from: '2026-01-01T00:00:00Z',
to: '2026-02-01T00:00:00Z',
limit: 100,
...(cursor ? { cursor } : {}),
},
})) as { data: Array<UsageRow>; hasMore: boolean; nextCursor: string | null };
rows.push(...page.data);
if (!page.hasMore || page.nextCursor === null) break;
cursor = page.nextCursor;
}

A busy month runs to thousands of rows, so mind the rate limit below.

Two optional filters work on all three endpoints. workspaceIds takes up to 50 comma-separated IDs and defaults to every workspace the linked account administers. An ID outside your scope fails the whole request with 400: a typo cannot quietly narrow a report. organizationId picks the organization: optional with one linked, required with several. Add either to any of the three requests:

Terminal window
--data-urlencode "organizationId=0195a3f2-1e5f-7abc-8def-0123456789ab" \
--data-urlencode "workspaceIds=6789,6790"

Every row carries workspaceId and credits, so a chargeback report is a group-by over the rows you already have:

const perWorkspace = new Map<string, number>();
for (const row of rows) {
const spent = perWorkspace.get(row.workspaceName) ?? 0;
perWorkspace.set(row.workspaceName, spent + row.credits);
}
console.log(Object.fromEntries(perWorkspace));

Which gives you the numbers to invoice against:

{
"Acme Video Team": 12480,
"Acme Stills": 3215,
"Acme Social": 890
}

Group by email for a per-person breakdown, or by tool to see where the spend goes. Enterprise credits come from one shared pool; how you split them is your call.

/v1/organization/webapp/audit_logs returns the same events an admin sees under Organization Settings → Audit Logs, newest first, with the same envelope and filters. from and to are optional here. Omit both to walk the full history backwards.

Two filters are specific to it. actions takes up to 50 audit actions and rejects unsupported values with 400. actorEmails takes up to 50 emails, and an address Runway does not recognize returns an empty page rather than an error.

Supported actions cover logins and SSO provisioning, membership changes, asset and sharing activity, and billing events, including AssetDownloaded and CreditsTransferred. The API reference lists every accepted value.

Terminal window
curl -G https://api.dev.runwayml.com/v1/organization/webapp/audit_logs \
-H "Authorization: Bearer $RUNWAYML_API_SECRET" \
-H "X-Runway-Version: 2024-11-06" \
--data-urlencode "actions=MemberInvited,MemberRemoved" \
--data-urlencode "from=2026-01-01T00:00:00Z" \
--data-urlencode "to=2026-02-01T00:00:00Z"
{
"data": [
{
"eventId": "0195a3f2-1e5f-7abc-8def-0123456789ab",
"timestamp": "2026-01-20T09:30:00.000Z",
"action": "MemberInvited",
"actorUsername": "jane",
"actorEmail": "jane@acme.com",
"actorDeleted": false,
"workspaceId": 6789,
"workspaceName": "Acme Video Team"
}
],
"hasMore": false,
"nextCursor": null
}

actorUsername and actorEmail can both be null, so do not depend on either to identify the actor. actorDeleted means that person has since been deleted.

For everything recorded about one event, fetch it by eventId:

Terminal window
curl https://api.dev.runwayml.com/v1/organization/webapp/audit_logs/0195a3f2-1e5f-7abc-8def-0123456789ab \
-H "Authorization: Bearer $RUNWAYML_API_SECRET" \
-H "X-Runway-Version: 2024-11-06"

You get the list fields plus action-specific metadata and the request details an investigation needs:

{
"metadata": {
"Invited member": "new.member@acme.com",
"Role": "member"
},
"resourceType": "membership",
"resourceId": "123",
"clientIpAddress": "203.0.113.10",
"userAgent": "Mozilla/5.0",
"requestId": "req-abc"
}

metadata uses readable keys such as Invited member and Previous role, and only the keys relevant to that action appear, so read it defensively.

This endpoint takes organizationId too. A missing event and an event outside your scope both return 404, so a 404 is not proof that nothing happened.

For a nightly export, ask for a closed time window rather than saving a cursor between runs. A cursor is only valid with the filters it was issued alongside:

Terminal window
--data-urlencode "from=2026-01-20T00:00:00Z" \
--data-urlencode "to=2026-01-21T00:00:00Z"

Because to is exclusive, consecutive windows meet without double counting the boundary row.

Audit logs allow another approach: start at the newest entry and stop paging once you reach an eventId you already stored.

Each endpoint allows 10 requests every 10 seconds per API project, then answers 429 Too Many Requests. A short sleep between pages keeps a paging loop under it.

StatusWhat it means
400A validation failure in from, to, cursor, actions, workspaceIds, organizationId, or actorEmails. Also: no linked account, a missing organizationId when several are linked, or a workspace out of scope.
401A missing or invalid API key, an organization not on an enterprise plan, a linked account with no organization workspace, or an organizationId not linked to this project.
404The audit log entry does not exist or is not visible to your linked workspaces. Detail endpoint only.
429Rate limit exceeded.

Follow the guidance on handling errors so a scheduled export survives the occasional 429 without losing a day of data.