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.
Before you start
Section titled “Before you start”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:
Authorization: Bearer $RUNWAYML_API_SECRETX-Runway-Version: 2024-11-06Export a month of credit usage
Section titled “Export a month of credit usage”/v1/organization/webapp/usage returns one row per generation, newest first.
Both from and to are required here. from is inclusive, to is exclusive.
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"No SDK has a typed method for these, so use the client’s generic get:
import RunwayML from '@runwayml/sdk';
const client = new RunwayML();
const page = await client.get('/v1/organization/webapp/usage', { query: { from: '2026-01-01T00:00:00Z', 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.
Page through every row
Section titled “Page through every row”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.
Report on specific workspaces
Section titled “Report on specific workspaces”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:
--data-urlencode "organizationId=0195a3f2-1e5f-7abc-8def-0123456789ab" \--data-urlencode "workspaceIds=6789,6790"Roll usage up for chargeback
Section titled “Roll usage up for chargeback”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.
Read the audit log
Section titled “Read the audit log”/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.
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.
Get the full detail for one event
Section titled “Get the full detail for one event”For everything recorded about one event, fetch it by eventId:
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.
Run it on a schedule
Section titled “Run it on a schedule”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:
--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.
Errors and rate limits
Section titled “Errors and rate limits”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.
| Status | What it means |
|---|---|
400 | A 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. |
401 | A 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. |
404 | The audit log entry does not exist or is not visible to your linked workspaces. Detail endpoint only. |
429 | Rate limit exceeded. |
Follow the guidance on handling errors so a scheduled export
survives the occasional 429 without losing a day of data.