# Custom Audit Logs

[Zuplo Audit Logs](./audit-logs.mdx) stores your audit trail in Zuplo and gives
you a portal viewer and query API out of the box. If your organization uses an
external audit logging service instead, you can send events to it directly from
the gateway with a [custom policy](../policies/custom-code-inbound.mdx) that
uses [runtime hooks](../programmable-api/runtime-extensions.mdx). This approach
works on any plan — it's just custom code — and gives you full control over
which events are recorded, the event format, and the destination.

## Example: WorkOS Audit Logs

[WorkOS](https://workos.com/) provides various services that help enable
enterprise features on your service such as SSO and Audit Logs. The following
custom policy logs API calls to the WorkOS Audit Logs API after each response is
sent:

```ts
import { ZuploContext, ZuploRequest, environment } from "@zuplo/runtime";

export async function auditLogPlugin(
  request: ZuploRequest,
  context: ZuploContext,
  policyName: string,
) {
  // Clone the request so the body can be read in the hook
  // note: remove this if you don't need content from the body
  const cloned = request.clone();
  context.addResponseSendingFinalHook(async (response) => {
    const incomingBody = await cloned.json();
    // This is an example event. Extract any additional data needed from the
    // request.
    const body = {
      organization_id: "org_01EHWNCE74X7JSDV0X3SZ3KJNY",
      event: {
        action: "user.signed_in",
        occurred_at: "2022-09-02T16:35:39.317Z",
        version: 1,
        actor: {
          type: "user",
          // Use the user sub for authenticated users
          id: request.user.sub,
          metadata: {
            role: "user",
          },
        },
        targets: [
          {
            type: "user",
            id: "user_98432YHF",
            name: "Jon Smith",
          },
          {
            type: "team",
            id: "team_J8YASKA2",
            metadata: {
              owner: "user_01GBTCQ2",
            },
          },
        ],
        context: {
          location: request.headers.get("True-Client-IP"),
          user_agent: request.headers.get("User-Agent"),
        },
        metadata: {
          extra: incomingBody.extra,
        },
      },
    };
    await fetch("https://api.workos.com/audit_logs/events", {
      method: "POST",
      body: JSON.stringify(body),
      headers: {
        Authorization: `Bearer ${environment.WORKOS_API_KEY}`,
        "Content-Type": "application/json",
        // Optional idempotency key.
        // See: https://workos.com/docs/reference/idempotency
        // "Idempotency-Key": "YOUR_KEY_HERE"
      },
    });
  });

  return request;
}
```

The same pattern works for any provider with an HTTP API — swap the event shape
and endpoint for your provider's, and store credentials in
[environment variables](./environment-variables.mdx).

## Related resources

- [Audit logging overview](./audit-logging.mdx) — why audit logging matters and
  how the options compare
- [Zuplo Audit Logs](./audit-logs.mdx) — the built-in feature with portal
  viewing and a query API
