# Firehose audit log reference

Audit logs and access logs are critical tools for enterprises, providing visibility into system activities, enhancing security, ensuring compliance, and supporting operational efficiency. They are useful for:

1. Security and Threat Detection
2. Compliance and Legal Protection
3. Operational Efficiency and Troubleshooting
4. Accountability and Fraud Prevention
5. Forensic Analysis and Incident Response


Wallet-as-a-Service keeps track of audit logs, allowing you to configure an endpoint where to receive a copy.

Deprecated: jsonData
`compressedJsonData` carries the payload for every record type. **Deprecated:** Wallet-as-a-Service emits `jsonData` on access records only, during the migration window, and it might be absent. Read the payload from `compressedJsonData` instead — see [Decode the payload](#decode-the-payload) and the [deprecation notice](/products/wallet/changelogs/firehose-audit-payload-compression).

## How it works

Customer configures an IAM role and a firehose endpoint in Organization settings. During the upload, the following environment specific IAM roles assume the customer configured IAM roles in order to perform PutRecordBatch on the configured firehose endpoint.

![Audit logging via firehouse](/assets/audit_log_firehouse1.afb45b355e5cc57fdf6d4db2067a4520ef714f560b88f37e50a9a6e758c4008a.77dbe64b.png)

Once configured, Wallet-as-a-Service will buffer and upload logs in batch every 15 seconds.

Every time you save an enabled configuration, Wallet-as-a-Service validates it by delivering a single test record: `id` is `0`, `type` is `1`, `orgId` is `null`, and `source` is `audit-service-validation`. Filter it out in your pipeline. Saving a disabled configuration skips validation, so it delivers no test record.

## IAM Role

The IAM role that grants access to Wallet-as-a-Service to upload logs to AWS Firehose must have following policies:

```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "AllowFirehosePutRecordBatch",
            "Effect": "Allow",
            "Action": "firehose:PutRecordBatch",
            "Resource": "arn:aws:firehose:REGION:ACCOUNT_ID:deliverystream/STREAM_NAME"
        },
        {
            "Sid": "AllowStsGetCallerIdentity",
            "Effect": "Allow",
            "Action": "sts:GetCallerIdentity",
            "Resource": "*"
        }
    ]
}
```

Additionally, in order to allow Wallet-as-a-Service to assume this role, it must have the following IAM trust relationship:

```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "AllowPalisadeAssumeRole",
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::PALISADE_ACCOUNT_ID:PALISADE_IAM_RESOURCE_NAME"
            },
            "Action": "sts:AssumeRole"
        }
    ]
}
```

Please contact Wallet-as-a-Service support to get your respective `PALISADE_ACCOUNT_ID` and `PALISADE_IAM_RESOURCE_NAME`.

## Record types

The `type` field identifies the kind of event the record carries.

| `type` | Event | Payload fields |
|  --- | --- | --- |
| `1` | **Access**: An API or console request, including method, URL, headers, and response metadata. | `compressedJsonData`, plus the deprecated `jsonData` during the migration window. |
| `3` | **Transaction**: A transaction lifecycle event. The decoded payload wraps the transaction under `tx`, and `source` carries the transaction status. See [Source values](#source-values). | `compressedJsonData` only. |
| `4` | **Wallet**: A wallet lifecycle event. The decoded payload wraps the wallet under `wallet`, and `source` carries the wallet event. See [Source values](#source-values). | `compressedJsonData` only. |


### Source values

Use `source` together with `type` to filter records. Transaction and wallet records use these values:

| `type` | `source` values |
|  --- | --- |
| `3` (transaction) | `REQUESTED`, `POLICY_CHECK_PENDING`, `POLICY_CHECK_PASSED`, `APPROVAL_CHECK_PENDING`, `APPROVAL_CHECK_PASSED`, `COMPILATION_PENDING`, `COMPILED`, `SIGNATURE_PENDING`, `SIGNED`, `PUBLISH_PENDING`, `PUBLISHED`, `CONFIRMATION_PENDING`, `CONFIRMED`, `FAILED`, `REJECTED` |
| `4` (wallet) | `KeyProvisioning`, `KeyProvisioned`, `KeyProvisionedWallet`, `KeyProvisioningFailed` |


Under Policy Engine v2, Wallet-as-a-Service doesn't emit `APPROVAL_CHECK_PENDING` or `APPROVAL_CHECK_PASSED`. See [Policy Engine v2: transaction status changes](/products/wallet/changelogs/policy-engine-v2-transaction-status-changes).

Availability of lifecycle records
Access records reach every environment. Transaction and wallet records are active in Development. Ripple enables them in Sandbox and then Production, and announces each date.

## Record fields

Every record is a JSON object with the following envelope. [Payload formats](#payload-formats) covers the payload fields.

| Field | Type | Always present | Description |
|  --- | --- | --- | --- |
| `id` | number | Yes | The identifier of the persisted audit event. Use it as the deduplication key — see [Delivery guarantees](#delivery-guarantees). |
| `orgId` | string (UUID) | Yes | Your organization's ID. `null` only on the validation record. |
| `type` | number | Yes | The record type: `1` access, `3` transaction, `4` wallet. See [Record types](#record-types). |
| `userId` | string (UUID) | No | The user who initiated the action, when the event has one. Otherwise `null`. On transaction and wallet records, this is the user who created the transaction or wallet, not the user who caused the status change. |
| `deviceId` | string (UUID) | No | The device that initiated the action, when the event has one. Otherwise `null`. |
| `requestId` | string | No | The request ID that correlates this event with other records for the same request. Otherwise `null`. |
| `jsonData` | string (base64) | No | **Deprecated.** Base64-encoded **uncompressed** JSON payload bytes. Wallet-as-a-Service emits it on access records (`type: 1`) only, during the migration window, and omits it from every other record type. Replaced by `compressedJsonData` for all event types. Don't use it in new consumers. |
| `compressedJsonData` | string (base64) | Yes | Base64-encoded **Brotli-compressed** JSON payload bytes. `compressedJsonData` is the canonical payload for every record type. |
| `compressionAlgorithm` | string | Yes | The compression algorithm. Currently always `br` (Brotli). |
| `compressionVersion` | number | Yes | The version of the compression contract. Currently `1`. |
| `compressionQuality` | number | Yes | The Brotli quality level used by the encoder. Currently `6`. Decoding doesn't depend on it. |
| `originalJsonDataSize` | number | Yes | The byte length of the JSON payload that `compressedJsonData` represents, before compression. |
| `originalJsonDataSha256` | string (hex) | Yes | The SHA-256 of those same uncompressed payload bytes, as lowercase hex. |
| `legacyJsonDataTruncated` | boolean | No | Present and `true` only when Wallet-as-a-Service rebuilt an oversize access payload with `request.body` cleared. Omitted otherwise. See [Record size limit and truncation](#record-size-limit-and-truncation). |
| `source` | string | Yes | The component or event that produced the record, such as `api-gateway` for access events or the lifecycle status for transaction and wallet events. |
| `receivedAt` | string (ISO 8601) | Yes | When Wallet-as-a-Service stored the event. |
| `createdAt` | string (ISO 8601) | Yes | When the emitting component created the event. It's typically a few milliseconds before `receivedAt`. |


`orgId` corresponds to your organization on every record except the validation record, where it's `null`. On access records, `userId`, `deviceId`, and `requestId` describe the initiator of the request where that information exists, and they aren't guaranteed to be present. On transaction and wallet records, `userId` identifies the creator of the transaction or wallet.

## Decode the payload

Read the payload from `compressedJsonData` for every record type:

1. Base64-decode `compressedJsonData` to get the Brotli-compressed bytes.
2. Brotli-decompress those bytes to get the uncompressed JSON bytes.
3. Parse the result as JSON.
4. Optionally verify integrity: the decompressed byte length must equal `originalJsonDataSize`, and its SHA-256 must equal `originalJsonDataSha256`.


Brotli is a standard, widely implemented codec. Node.js decompresses it with the built-in `node:zlib` module. Python needs the `brotli` or `brotlicffi` package. Java, Go, and .NET have established libraries.

Node.js
```javascript
import { brotliDecompressSync } from "node:zlib";
import { createHash } from "node:crypto";

function decodeAuditRecord(record) {
  const json = brotliDecompressSync(Buffer.from(record.compressedJsonData, "base64"));

  if (json.length !== record.originalJsonDataSize) {
    throw new Error(`size mismatch for audit record ${record.id}`);
  }
  const sha256 = createHash("sha256").update(json).digest("hex");
  if (sha256 !== record.originalJsonDataSha256) {
    throw new Error(`checksum mismatch for audit record ${record.id}`);
  }

  return JSON.parse(json.toString("utf8"));
}
```

Python
```python
import base64
import brotli  # pip install brotli
import hashlib
import json


def decode_audit_record(record):
    payload = brotli.decompress(base64.b64decode(record["compressedJsonData"]))

    if len(payload) != record["originalJsonDataSize"]:
        raise ValueError(f"size mismatch for audit record {record['id']}")
    if hashlib.sha256(payload).hexdigest() != record["originalJsonDataSha256"]:
        raise ValueError(f"checksum mismatch for audit record {record['id']}")

    return json.loads(payload)
```

## Payload formats

### Access record

An access record during the migration window carries both payload representations. `jsonData` and `compressedJsonData` decode to the same bytes.

```json
{
  "id": 21119,
  "orgId": "dac59f97-5984-4298-bebd-92c94b6184b4",
  "type": 1,
  "userId": "c1e08d96-07a3-4561-846f-dd5a5c274e29",
  "deviceId": null,
  "requestId": "2aa53531-2124-4501-b9bb-2ed8208ff0e8",
  "jsonData": "eyJ1cmwiOiIvdjIvdmF1bHRzLzhmMTRlNDVmLWNlZWEtNDY3YS05YzJiLTFmMGQ0YTFkMmYzMS93YWxsZXRzIiwibWV0aG9kIjoiR0VUIiwiaGVhZGVycyI6eyJBY2NlcHQiOiJhcHBsaWNhdGlvbi9qc29uIiwiVXNlci1BZ2VudCI6InBhbGlzYWRlLXNkay1weXRob24vMS44LjAiLCJYLVJlcXVlc3QtSWQiOiIyYWE1MzUzMS0yMTI0LTQ1MDEtYjliYi0yZWQ4MjA4ZmYwZTgiLCJYLUZvcndhcmRlZC1Gb3IiOiIyMDMuMC4xMTMuMjQiLCJBdXRob3JpemF0aW9uLUhhc2giOiI1MTlkZjRmN2FiZTE4OWQ5Zjc4OTFjMjJkMTIwZDY1NCJ9LCJyZXF1ZXN0Ijp7ImJvZHkiOiIifSwicmVzcG9uc2UiOnsiYm9keUxlbmd0aCI6MTg0Miwic3RhdHVzQ29kZSI6MjAwfSwiZHVyYXRpb25NcyI6MTIsInJlbW90ZUFkZHIiOiIyMDMuMC4xMTMuMjQifQ==",
  "compressedJsonData": "G6EBAMSP6d5pfzN2F6JbzTZdSQ6+aAtp3WwH7L9tMAh2UcBJaWqBBhJId92FkZRuEwVvD4naEl+T7v1ift9igP1g+xHmbcm2S+Sjr5OeYgzaN23Q/cSjpuTEBxJOFdnPZpFcMhRoFRhwdfGoOsjtI2P4xck0xWPBAHCUcguFpxzf9Un7zjoGHMN2lYNEnWWjj99ledhbMp1xUHjVpej6BmiWQ6iruiLNxF772pEe+3GUUZSOXZeSi93OdHl4z6ZHieBvrjLOEFWGPRRO5vIheiK2UV+HvMSAmnpJPrVhjNT10qe262liFmInTe3xL5JX3JEnZ8QBK/oaf4h24j7uF2WJgTrPCrmEMuezg0QM7Ny/gszv6NVDxkB8Rv/uUOKJCF37Bw==",
  "compressionAlgorithm": "br",
  "compressionVersion": 1,
  "compressionQuality": 6,
  "originalJsonDataSize": 418,
  "originalJsonDataSha256": "3b3c9d6900086fb8c1fa65b8a6b23694a2fa074deceba8d8ac46907ac3ede3bb",
  "source": "api-gateway",
  "receivedAt": "2026-09-21T15:59:08.764753Z",
  "createdAt": "2026-09-21T15:59:08.757472Z"
}
```

The decoded payload of an access record has the following structure:

```json
{
  "url": "/v2/vaults/8f14e45f-ceea-467a-9c2b-1f0d4a1d2f31/wallets",
  "method": "GET",
  "headers": {
    "Accept": "application/json",
    "User-Agent": "palisade-sdk-python/1.8.0",
    "X-Request-Id": "2aa53531-2124-4501-b9bb-2ed8208ff0e8",
    "X-Forwarded-For": "203.0.113.24",
    "Authorization-Hash": "519df4f7abe189d9f7891c22d120d654"
  },
  "request": {
    "body": ""
  },
  "response": {
    "bodyLength": 1842,
    "statusCode": 200
  },
  "durationMs": 12,
  "remoteAddr": "203.0.113.24"
}
```

The access payload carries the following fields.

| Field | Description |
|  --- | --- |
| `url` | The request path and query string. |
| `method` | The HTTP method. |
| `headers` | The request headers. Wallet-as-a-Service never logs the `Authorization` header: it replaces it with `Authorization-Hash`, a truncated SHA-256 of the header value, so you can correlate requests that used the same credential without holding the credential. Every other header appears as sent. |
| `request.body` | The request body, base64-encoded, captured up to 10 MB. It's an empty string when the request had no body. Large bodies can push the record past the size limit — see [Record size limit and truncation](#record-size-limit-and-truncation). |
| `response.statusCode` | The HTTP status code. |
| `response.bodyLength` | The response body length in bytes. Wallet-as-a-Service doesn't log response bodies. |
| `durationMs` | How long the request took, in milliseconds. |
| `remoteAddr` | The client address the gateway saw. |


### Transaction and wallet records

Transaction (`type: 3`) and wallet (`type: 4`) records carry only the compressed payload: `compressedJsonData` and the compression metadata, never `jsonData`. Read the payload the same way you read it for an access record.

```json
{
  "id": 21124,
  "orgId": "dac59f97-5984-4298-bebd-92c94b6184b4",
  "type": 3,
  "userId": "c1e08d96-07a3-4561-846f-dd5a5c274e29",
  "deviceId": null,
  "requestId": null,
  "compressedJsonData": "GygBAGRz7175aMI12X0OHMLiOv+UY9vyOHzAwALguigLOKAAqM2mE53KQG64w/GvKHzy5hfevuD4hacJB5QtzQctrDoYddvG2nwhD+m5SwuaCif4PBTJt5sltWeLbiyNA9RO3NGnLRq5udVCfbT315hzky61jWOtBjxoLGmXqY0mbyH870j89IW582zD6q6BVlO54prYV59YeVTtTqk9Pa/wn+s3HDBolZzVsUQTVHPCVN84pzUbHLq4ari86eJoWCoyTdG+8CF5NqM7/P8D",
  "compressionAlgorithm": "br",
  "compressionVersion": 1,
  "compressionQuality": 6,
  "originalJsonDataSize": 297,
  "originalJsonDataSha256": "4eb5ec331a4ac4ac5cd0db79f1d9731104d34ac318dc9a25b3a650c454bfc2ea",
  "source": "SIGNED",
  "receivedAt": "2026-09-21T16:02:41.118204Z",
  "createdAt": "2026-09-21T16:02:41.109883Z"
}
```

The decoded payload wraps the entity that changed — `tx` for transaction events, `wallet` for wallet events (abbreviated here):

```json
{
  "tx": {
    "id": "0f3a6c1e-94c2-4f5f-9a6e-2c3b8f0a71d4",
    "walletId": "4b8a7b5e-3a27-4f1a-9a2b-6d5e1c8f2a90",
    "vaultId": "8f14e45f-ceea-467a-9c2b-1f0d4a1d2f31",
    "organizationId": "dac59f97-5984-4298-bebd-92c94b6184b4",
    "createdBy": "c1e08d96-07a3-4561-846f-dd5a5c274e29",
    "createdAt": "2026-09-21T16:02:38.551Z"
  }
}
```

## What changed

Audit records previously carried the payload as uncompressed `jsonData` on access events, and there were no transaction or wallet records. Records now carry Brotli-compressed payloads for every event type. Four format states matter to a consumer.

| Field | 1. Legacy access (before) | 2. Access during migration (now) | 3. Transaction and wallet (now) | 4. Access after removal (target) |
|  --- | --- | --- | --- | --- |
| `jsonData` | Required — base64-encoded uncompressed JSON bytes | **Deprecated**, temporarily retained, optional — base64-encoded uncompressed JSON bytes | Absent | Absent — removed |
| `compressedJsonData` | Absent | **Added**, required — base64-encoded Brotli-compressed JSON bytes | Required — base64-encoded Brotli-compressed JSON bytes | Required — base64-encoded Brotli-compressed JSON bytes |
| `compressionAlgorithm` | Absent | Added, required (`br`) | Required (`br`) | Required (`br`) |
| `compressionVersion` | Absent | Added, required (`1`) | Required (`1`) | Required (`1`) |
| `compressionQuality` | Absent | Added, required (`6`) | Required (`6`) | Required (`6`) |
| `originalJsonDataSize` | Absent | Added, required | Required | Required |
| `originalJsonDataSha256` | Absent | Added, required | Required | Required |
| `legacyJsonDataTruncated` | Absent | Added, optional — present only when `true` | Absent | Absent — removed with `jsonData` |


The envelope fields `id`, `orgId`, `type`, `userId`, `deviceId`, `requestId`, `source`, `receivedAt`, and `createdAt` keep the same meaning in all four states.

### 1. Legacy access record — `jsonData` only

```json
{
  "id": 21119,
  "type": 1,
  "jsonData": "eyJ1cmwiOiIvdjIvd2FsbGV0cyIsIm1ldGhvZCI6IkdFVCJ9",
  "source": "api-gateway"
}
```

`jsonData` holds base64-encoded uncompressed JSON bytes. No compression fields are present.

### 2. Access record during migration — both representations

```json
{
  "id": 21119,
  "type": 1,
  "jsonData": "eyJ1cmwiOiIvdjIvd2FsbGV0cyIsIm1ldGhvZCI6IkdFVCJ9",
  "compressedJsonData": "ixGAeyJ1cmwiOiIvdjIvd2FsbGV0cyIsIm1ldGhvZCI6IkdFVCJ9Aw==",
  "compressionAlgorithm": "br",
  "compressionVersion": 1,
  "compressionQuality": 6,
  "originalJsonDataSize": 36,
  "originalJsonDataSha256": "7b142d8a802972756e06db628e2e05da2f2d8c027d67d24e343112820aa2e9b2",
  "source": "api-gateway"
}
```

Both payload fields decode to the same JSON. `jsonData` holds base64-encoded uncompressed JSON bytes, and Ripple removes it after the deprecation window. `compressedJsonData` holds base64-encoded Brotli-compressed JSON bytes, and it's the field to read.

### 3. Transaction and wallet record — compressed only

```json
{
  "id": 21124,
  "type": 3,
  "compressedJsonData": "GygBAGRz7175aMI12X0OHMLiOv+UY9vyOHzAwALguigLOKAAqM2mE53KQG64w/GvKHzy5hfevuD4hacJB5QtzQctrDoYddvG2nwhD+m5SwuaCif4PBTJt5sltWeLbiyNA9RO3NGnLRq5udVCfbT315hzky61jWOtBjxoLGmXqY0mbyH870j89IW582zD6q6BVlO54prYV59YeVTtTqk9Pa/wn+s3HDBolZzVsUQTVHPCVN84pzUbHLq4ari86eJoWCoyTdG+8CF5NqM7/P8D",
  "compressionAlgorithm": "br",
  "compressionVersion": 1,
  "compressionQuality": 6,
  "originalJsonDataSize": 297,
  "originalJsonDataSha256": "4eb5ec331a4ac4ac5cd0db79f1d9731104d34ac318dc9a25b3a650c454bfc2ea",
  "source": "SIGNED"
}
```

`jsonData` is absent. Consumers that require an uncompressed payload must decompress `compressedJsonData`.

### 4. Access record after removal — the target contract

```json
{
  "id": 21119,
  "type": 1,
  "compressedJsonData": "ixGAeyJ1cmwiOiIvdjIvd2FsbGV0cyIsIm1ldGhvZCI6IkdFVCJ9Aw==",
  "compressionAlgorithm": "br",
  "compressionVersion": 1,
  "compressionQuality": 6,
  "originalJsonDataSize": 36,
  "originalJsonDataSha256": "7b142d8a802972756e06db628e2e05da2f2d8c027d67d24e343112820aa2e9b2",
  "source": "api-gateway"
}
```

After Ripple removes `jsonData`, access records use the same compressed-only contract as transaction and wallet records. A consumer that already reads `compressedJsonData` needs no further change.

## Record size limit and truncation

A single Firehose record must stay below 1,000,000 encoded bytes, counting the complete JSON record and both payload representations.

- When a complete access record reaches 1,000,000 bytes, Wallet-as-a-Service clears `request.body` in the payload, rebuilds both `jsonData` and `compressedJsonData` from the reduced payload, and sets `legacyJsonDataTruncated` to `true`. `originalJsonDataSize` and `originalJsonDataSha256` then describe the reduced payload, so integrity checks still pass.
- Transaction and wallet records carry only the compressed payload, and Wallet-as-a-Service never truncates them.
- Because an access record carries the payload twice during the migration window, `request.body` is what usually exhausts the budget.
- If a record still reaches 1,000,000 bytes after truncation, Wallet-as-a-Service can't deliver it, and delivery to your stream stops. Re-enabling the configuration doesn't clear this state, so contact support with the time your stream stopped. See [Delivery guarantees](#delivery-guarantees).


## Delivery guarantees

- One persisted audit event maps to exactly one Firehose record.
- Delivery is at least once. Deduplicate on `id`.
- Wallet-as-a-Service delivers records approximately every 15 seconds.
- While the configuration is disabled, Wallet-as-a-Service delivers nothing. When you re-enable it, Wallet-as-a-Service delivers the events it recorded while the configuration was off.
- If delivery keeps failing, Wallet-as-a-Service disables the Firehose configuration. Fix the delivery stream or IAM role, and then re-enable the configuration from **Settings** > **Audit logs**. See [Configure audit logging](/products/wallet/admin-guide/configure-audit-logging).


## Limitations

1. Support for AWS Firehose only
2. Logs are buffered for 15 seconds before sending to firehose
3. Records must stay below 1,000,000 encoded bytes. An oversize access payload loses `request.body`