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:
- Security and Threat Detection
- Compliance and Legal Protection
- Operational Efficiency and Troubleshooting
- Accountability and Fraud Prevention
- Forensic Analysis and Incident Response
Ripple Custody (Wallet) keeps track of audit logs, allowing you to configure an endpoint where to receive a copy. To set up delivery, see Audit logging.
compressedJsonData carries the payload for every record type. Deprecated: Ripple Custody (Wallet) 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 and the deprecation notice.
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.

Once configured, Ripple Custody (Wallet) will buffer and upload logs in batch every 15 seconds.
Every time you save or enable the configuration, Ripple Custody (Wallet) 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. Disabling the configuration skips validation, so it delivers no test record.
The IAM role that grants access to Ripple Custody (Wallet) to upload logs to AWS Firehose must have following policies:
{
"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 Ripple Custody (Wallet) to assume this role, it must have the following IAM trust relationship:
{
"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 Ripple Custody (Wallet) support to get your respective PALISADE_ACCOUNT_ID and PALISADE_IAM_RESOURCE_NAME.
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. | 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. | compressedJsonData only. |
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, COMPILATION_PENDING, COMPILED, SIGNATURE_PENDING, SIGNED, PUBLISH_PENDING, PUBLISHED, CONFIRMATION_PENDING, CONFIRMED, FAILED, REJECTED |
4 (wallet) | KeyProvisioning, KeyProvisioned, KeyProvisionedWallet, KeyProvisioningFailed |
A transaction collects any required approvals while it's in POLICY_CHECK_PENDING. Records written before approvals moved into the policy check can also carry APPROVAL_CHECK_PENDING and APPROVAL_CHECK_PASSED. See Transaction status changes.
Access records reach every environment. Transaction and wallet records are active in Sandbox. Ripple enables them in Production next and announces the date. Ripple Custody (Wallet) doesn't backfill transaction or wallet records for activity before an environment is enabled.
Every record is a JSON object with the following envelope. 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. |
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. |
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. Ripple Custody (Wallet) 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 Ripple Custody (Wallet) rebuilt an oversize access payload with request.body cleared. Omitted otherwise. See 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 Ripple Custody (Wallet) 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.
Read the payload from compressedJsonData for every record type:
- Base64-decode
compressedJsonDatato get the Brotli-compressed bytes. - Brotli-decompress those bytes to get the uncompressed JSON bytes.
- Parse the result as JSON.
- Optionally verify integrity: the decompressed byte length must equal
originalJsonDataSize, and its SHA-256 must equaloriginalJsonDataSha256.
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.
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"));
}An access record during the migration window carries both payload representations. jsonData and compressedJsonData decode to the same bytes.
{
"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:
{
"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. Ripple Custody (Wallet) 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. |
response.statusCode | The HTTP status code. |
response.bodyLength | The response body length in bytes. Ripple Custody (Wallet) doesn't log response bodies. |
durationMs | How long the request took, in milliseconds. |
remoteAddr | The client address the gateway saw. |
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.
{
"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):
{
"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"
}
}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.
{
"id": 21119,
"type": 1,
"jsonData": "eyJ1cmwiOiIvdjIvd2FsbGV0cyIsIm1ldGhvZCI6IkdFVCJ9",
"source": "api-gateway"
}jsonData holds base64-encoded uncompressed JSON bytes. No compression fields are present.
{
"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.
{
"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.
{
"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.
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, Ripple Custody (Wallet) clears
request.bodyin the payload, rebuilds bothjsonDataandcompressedJsonDatafrom the reduced payload, and setslegacyJsonDataTruncatedtotrue.originalJsonDataSizeandoriginalJsonDataSha256then describe the reduced payload, so integrity checks still pass. - Transaction and wallet records carry only the compressed payload, and Ripple Custody (Wallet) never truncates them.
- Because an access record carries the payload twice during the migration window,
request.bodyis what usually exhausts the budget. - If a record still reaches 1,000,000 bytes after truncation, Ripple Custody (Wallet) 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.
- One persisted audit event maps to exactly one Firehose record.
- Delivery is at least once. Deduplicate on
id. - Ripple Custody (Wallet) delivers records approximately every 15 seconds.
- While the configuration is disabled, Ripple Custody (Wallet) delivers nothing. When you re-enable it, Ripple Custody (Wallet) delivers the events it recorded while the configuration was off.
- If delivery keeps failing, Ripple Custody (Wallet) disables the Firehose configuration. Fix the delivery stream or IAM role, and then re-enable the configuration from Settings > Audit logs. See Audit logging.
- Support for AWS Firehose only
- Logs are buffered for 15 seconds before sending to firehose
- Records must stay below 1,000,000 encoded bytes. An oversize access payload loses
request.body