Skip to content

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

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.

Deprecated: jsonData

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.

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

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.

IAM Role

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.

Record types

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

typeEventPayload fields
1Access: An API or console request, including method, URL, headers, and response metadata.compressedJsonData, plus the deprecated jsonData during the migration window.
3Transaction: A transaction lifecycle event. The decoded payload wraps the transaction under tx, and source carries the transaction status. See Source values.compressedJsonData only.
4Wallet: A wallet lifecycle event. The decoded payload wraps the wallet under wallet, and source carries the wallet event. See Source values.compressedJsonData only.

Source values

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

typesource 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.

Availability of lifecycle records

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.

Record fields

Every record is a JSON object with the following envelope. Payload formats covers the payload fields.

FieldTypeAlways presentDescription
idnumberYesThe identifier of the persisted audit event. Use it as the deduplication key — see Delivery guarantees.
orgIdstring (UUID)YesYour organization's ID. null only on the validation record.
typenumberYesThe record type: 1 access, 3 transaction, 4 wallet. See Record types.
userIdstring (UUID)NoThe 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.
deviceIdstring (UUID)NoThe device that initiated the action, when the event has one. Otherwise null.
requestIdstringNoThe request ID that correlates this event with other records for the same request. Otherwise null.
jsonDatastring (base64)NoDeprecated. 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.
compressedJsonDatastring (base64)YesBase64-encoded Brotli-compressed JSON payload bytes. compressedJsonData is the canonical payload for every record type.
compressionAlgorithmstringYesThe compression algorithm. Currently always br (Brotli).
compressionVersionnumberYesThe version of the compression contract. Currently 1.
compressionQualitynumberYesThe Brotli quality level used by the encoder. Currently 6. Decoding doesn't depend on it.
originalJsonDataSizenumberYesThe byte length of the JSON payload that compressedJsonData represents, before compression.
originalJsonDataSha256string (hex)YesThe SHA-256 of those same uncompressed payload bytes, as lowercase hex.
legacyJsonDataTruncatedbooleanNoPresent and true only when Ripple Custody (Wallet) rebuilt an oversize access payload with request.body cleared. Omitted otherwise. See Record size limit and truncation.
sourcestringYesThe component or event that produced the record, such as api-gateway for access events or the lifecycle status for transaction and wallet events.
receivedAtstring (ISO 8601)YesWhen Ripple Custody (Wallet) stored the event.
createdAtstring (ISO 8601)YesWhen 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.

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"));
}

Payload formats

Access record

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.

FieldDescription
urlThe request path and query string.
methodThe HTTP method.
headersThe 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.bodyThe 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.statusCodeThe HTTP status code.
response.bodyLengthThe response body length in bytes. Ripple Custody (Wallet) doesn't log response bodies.
durationMsHow long the request took, in milliseconds.
remoteAddrThe 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.

{
  "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"
  }
}

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.

Field1. Legacy access (before)2. Access during migration (now)3. Transaction and wallet (now)4. Access after removal (target)
jsonDataRequired — base64-encoded uncompressed JSON bytesDeprecated, temporarily retained, optional — base64-encoded uncompressed JSON bytesAbsentAbsent — removed
compressedJsonDataAbsentAdded, required — base64-encoded Brotli-compressed JSON bytesRequired — base64-encoded Brotli-compressed JSON bytesRequired — base64-encoded Brotli-compressed JSON bytes
compressionAlgorithmAbsentAdded, required (br)Required (br)Required (br)
compressionVersionAbsentAdded, required (1)Required (1)Required (1)
compressionQualityAbsentAdded, required (6)Required (6)Required (6)
originalJsonDataSizeAbsentAdded, requiredRequiredRequired
originalJsonDataSha256AbsentAdded, requiredRequiredRequired
legacyJsonDataTruncatedAbsentAdded, optional — present only when trueAbsentAbsent — 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

{
  "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

{
  "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

{
  "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

{
  "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, Ripple Custody (Wallet) 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 Ripple Custody (Wallet) 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, 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.

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.

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