{"templateId":"markdown","sharedDataIds":{"sidebar":"sidebar-products/wallet/sidebars.yaml"},"props":{"metadata":{"markdoc":{"tagList":[]},"type":"markdown"},"seo":{"title":"Receive webhook deliveries","description":"User guides, API reference, and support resources.","siteUrl":"https://docs.ripple.com","lang":"en-US","llmstxt":{"hide":false,"sections":[{"title":"Table of contents","includeFiles":["**/*"],"excludeFiles":[]}],"excludeFiles":[]}},"dynamicMarkdocComponents":[],"compilationErrors":[],"ast":{"$$mdtype":"Tag","name":"article","attributes":{},"children":[{"$$mdtype":"Tag","name":"Heading","attributes":{"level":1,"id":"receive-webhook-deliveries","__idx":0},"children":["Receive webhook deliveries"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["This page describes the contract your webhook endpoint must meet, in the order you build it: accept the delivery, verify its signature, acknowledge it, and then process it safely given retries, ordering, and duplicates. For the envelope and payload of each event, see ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/products/wallet/integrations-and-monitoring/webhooks/webhook-event-reference"},"children":["Webhook event reference"]},". To create the webhook that sends to your endpoint, see ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/products/wallet/integrations-and-monitoring/webhooks/manage-webhooks"},"children":["Manage webhooks via the console"]}," or ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/products/wallet/integrations-and-monitoring/webhooks/manage-webhooks-api"},"children":["Manage webhooks via the API"]},"."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"endpoint-requirements","__idx":1},"children":["Endpoint requirements"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Before you create a webhook, develop an HTTPS endpoint on your server to receive webhook events. Your endpoint must:"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Use HTTPS on the standard port, 443."]}," This protects event payloads in transit. Ripple Custody (Wallet) rejects webhook endpoints that don't use HTTPS, and doesn't deliver to other ports or to private, loopback, or link-local IP addresses."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Accept ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["POST"]}," requests with a JSON body."]}," Each delivery is a JSON envelope with a base64-encoded payload and a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Signature"]}," header. See ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/products/wallet/integrations-and-monitoring/webhooks/webhook-event-reference#envelope"},"children":["Envelope"]},"."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Acknowledge each delivery with a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["2xx"]}," response"]},", such as ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["200 OK"]},", within the 30-second timeout. Any ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["2xx"]}," status confirms successful receipt. Ripple Custody (Wallet) treats a slower response as a failed attempt and retries it. See ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"#retries"},"children":["Retries"]},"."]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["We recommend returning a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["2xx"]}," response within 5 seconds. Perform any heavy processing asynchronously after acknowledging receipt to avoid timeouts."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"verify-the-signature","__idx":2},"children":["Verify the signature"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Always verify the webhook signature before processing to ensure the payload is authentic. Never process a webhook payload without verifying the signature first."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Ripple Custody (Wallet) signs each webhook delivery with an ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["ECDSA"]}," private key. Verify the signature on every delivery to confirm it came from Ripple Custody (Wallet) and wasn't tampered with."]},{"$$mdtype":"Tag","name":"ol","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Get the webhook's public key. When you create a webhook, Ripple Custody (Wallet) generates a P-256 ECDSA public key in ASN.1 DER format, base64-encoded, and returns it in the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["publicKey"]}," field. For example:"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"json","header":{"controls":{"copy":{}}},"source":"{\n  \"id\": \"a6a1e8bd-30eb-419f-b6ba-e7ac11e86f84\",\n  \"name\": \"wefewfwe\",\n  \"publicKey\": \"MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEpHiEkdd/ba5dU4sNCgsWrMOE6fLakHywl0OgF5aVfxkiqNh22oybRAREev7jvnwH4jqitHx79KGi6CMwiJjmaw==\",\n  // other fields omitted\n}\n","lang":"json"},"children":[]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["To get the public key later:"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Console:"]}," go to ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Settings"]}," > ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Webhooks"]},", select the webhook, and on the ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["View webhook"]}," page copy the ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Public key"]},". See ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/products/wallet/integrations-and-monitoring/webhooks/manage-webhooks#view-webhooks-via-the-console"},"children":["View webhooks"]},"."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["API:"]}," call ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["GET /v2/webhooks/{id}"]}," and read ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["publicKey"]},". See ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/products/wallet/integrations-and-monitoring/webhooks/manage-webhooks-api#get-a-webhook-via-the-api"},"children":["Get a webhook"]},"."]}]}]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Extract the signature from the ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["Signature"]}," header of the webhook delivery. It's in ASN.1 DER format, base64-encoded. HTTP header names are case-insensitive, so your framework might expose it as ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["signature"]},"."]}]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Verify the signature against the base64-encoded ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["payload"]}," string from the envelope, using the public key with ECDSA and SHA-256. Pass the payload itself to your library's verify function, not a hash of it: ECDSA verify functions hash the payload with SHA-256 internally, as the following examples show."]}]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Only after the signature verifies, base64-decode ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["payload"]}," and parse the resulting JSON."]}]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["If you delete a webhook and create a new one, the new webhook has a new public key, so update your signature verification."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Most programming languages have cryptography libraries that support ECDSA verification."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":3,"id":"code-examples","__idx":3},"children":["Code examples"]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":4,"id":"python","__idx":4},"children":["Python"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"python","header":{"controls":{"copy":{}}},"source":"import base64\nfrom cryptography.hazmat.primitives import hashes\nfrom cryptography.hazmat.primitives.asymmetric import ec\nfrom cryptography.hazmat.primitives.serialization import load_der_public_key\n\ndef verify_webhook(public_key_b64: str, signature_b64: str, payload_b64: str) -> bool:\n    \"\"\"\n    Verify a Ripple Custody (Wallet) webhook signature.\n\n    Args:\n        public_key_b64: Base64-encoded public key from webhook creation\n        signature_b64: Base64-encoded signature from the 'Signature' header\n        payload_b64: The base64-encoded payload string from the webhook body\n\n    Returns:\n        True if signature is valid, False otherwise\n    \"\"\"\n    public_key = load_der_public_key(base64.b64decode(public_key_b64))\n    signature = base64.b64decode(signature_b64)\n\n    try:\n        # ECDSA(SHA256()) hashes the payload with SHA-256 before verifying\n        public_key.verify(signature, payload_b64.encode(), ec.ECDSA(hashes.SHA256()))\n        return True\n    except Exception:\n        return False\n","lang":"python"},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":4,"id":"javascript-nodejs","__idx":5},"children":["JavaScript (Node.js)"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"javascript","header":{"controls":{"copy":{}}},"source":"const crypto = require('crypto');\n\n/**\n * Verify a Ripple Custody (Wallet) webhook signature.\n *\n * @param {string} publicKeyB64 - Base64-encoded public key from webhook creation\n * @param {string} signatureB64 - Base64-encoded signature from the 'Signature' header\n * @param {string} payloadB64 - The base64-encoded payload string from the webhook body\n * @returns {boolean} - True if signature is valid\n */\nfunction verifyWebhook(publicKeyB64, signatureB64, payloadB64) {\n  const publicKey = crypto.createPublicKey({\n    key: Buffer.from(publicKeyB64, 'base64'),\n    format: 'der',\n    type: 'spki'\n  });\n\n  // 'sha256' hashes the payload with SHA-256 before verifying\n  return crypto.verify(\n    'sha256',\n    Buffer.from(payloadB64),\n    publicKey,\n    Buffer.from(signatureB64, 'base64')\n  );\n}\n","lang":"javascript"},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":4,"id":"go","__idx":6},"children":["Go"]},{"$$mdtype":"Tag","name":"CodeBlock","attributes":{"data-language":"go","header":{"controls":{"copy":{}}},"source":"package main\n\nimport (\n\t\"crypto/ecdsa\"\n\t\"crypto/sha256\"\n\t\"crypto/x509\"\n\t\"encoding/base64\"\n)\n\n// VerifyWebhook verifies a Ripple Custody (Wallet) webhook signature.\nfunc VerifyWebhook(publicKeyB64, signatureB64, payloadB64 string) (bool, error) {\n\tpublicKeyDER, err := base64.StdEncoding.DecodeString(publicKeyB64)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tpubKey, err := x509.ParsePKIXPublicKey(publicKeyDER)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\tecdsaPubKey, ok := pubKey.(*ecdsa.PublicKey)\n\tif !ok {\n\t\treturn false, err\n\t}\n\n\tsignature, err := base64.StdEncoding.DecodeString(signatureB64)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\n\thash := sha256.Sum256([]byte(payloadB64))\n\n\treturn ecdsa.VerifyASN1(ecdsaPubKey, hash[:], signature), nil\n}\n","lang":"go"},"children":[]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"retries","__idx":7},"children":["Retries"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Your server must return a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["2xx"]}," response, such as ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["200 OK"]},", to confirm successful receipt of a webhook notification."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Ripple Custody (Wallet) retries a delivery when your server doesn't respond within 30 seconds, the connection fails, or your server returns an HTTP 5xx error. It sends the first attempt immediately, then retries after the following delays (in seconds): 60, 120, 180, 320, 480, 840, 1500, 2820, 5400, 5400."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["The system doesn't retry for client-side errors (HTTP 4xx) except for the following cases:"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["429 Too Many Requests"]},": indicates rate limits have been exceeded."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"code","attributes":{},"children":["408 Request Timeout"]},": indicates the server took too long to respond."]}]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["After 10 failed retries (11 attempts in total), the system marks the notification as failed and stops retrying. A non-retryable response marks the notification as failed immediately."]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Because of retries, your endpoint can receive the same event more than once. See ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"#deduplicate-deliveries"},"children":["Deduplicate deliveries"]},"."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"handle-out-of-order-events","__idx":8},"children":["Handle out-of-order events"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Ripple Custody (Wallet) strives to send notifications in order (per resource), but can't guarantee delivery sequence. A retried delivery can also arrive after a later event for the same resource. Design your endpoint to process events independently without relying on their delivery order. For example, compare the payload's ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["updatedAt"]}," with the latest state you've stored for that resource, and don't overwrite newer state with an older event."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"deduplicate-deliveries","__idx":9},"children":["Deduplicate deliveries"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Your endpoint may receive the same event more than once. The envelope has no event ID, so use the resource ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["id"]}," together with its ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["updatedAt"]}," timestamp, both inside the decoded payload, to detect and safely handle duplicate deliveries. If you've already processed that ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["id"]}," and ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["updatedAt"]}," pair, acknowledge the delivery with a ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["2xx"]}," response and skip processing."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"process-idempotently","__idx":10},"children":["Process idempotently"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Design your webhook handler to be idempotent, so that processing the same event twice has the same result as processing it once. Combined with deduplication, this keeps retries and duplicates from triggering a downstream action, such as crediting a deposit, more than once."]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"monitor-failures","__idx":11},"children":["Monitor failures"]},{"$$mdtype":"Tag","name":"p","attributes":{},"children":["Set up alerting on your endpoint to detect delivery failures or downtime. Ripple Custody (Wallet) also reports each webhook's delivery health:"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["Console:"]}," the ",{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["View webhook"]}," page shows the success and failure counts for the last hour, and the time of the last success and last failure. See ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/products/wallet/integrations-and-monitoring/webhooks/manage-webhooks#view-webhooks-via-the-console"},"children":["View webhooks"]},"."]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"strong","attributes":{},"children":["API:"]}," ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["GET /v2/webhooks/{id}"]}," returns ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["successCount"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["failureCount"]},", ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["lastSuccess"]},", and ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["lastFailure"]},". To find webhooks with recent failures, list webhooks with a filter such as ",{"$$mdtype":"Tag","name":"code","attributes":{},"children":["filter.lastFailure.relativeDays=-1"]},". See ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/products/wallet/integrations-and-monitoring/webhooks/manage-webhooks-api#get-a-webhook-via-the-api"},"children":["Get a webhook"]}," and ",{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/products/wallet/integrations-and-monitoring/webhooks/manage-webhooks-api#list-webhooks-via-the-api"},"children":["List webhooks"]},"."]}]},{"$$mdtype":"Tag","name":"Heading","attributes":{"level":2,"id":"related-topics","__idx":12},"children":["Related topics"]},{"$$mdtype":"Tag","name":"ul","attributes":{},"children":[{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/products/wallet/integrations-and-monitoring/webhooks"},"children":["Webhooks"]}," — What webhooks are and how delivery works"]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/products/wallet/integrations-and-monitoring/webhooks/webhook-event-reference"},"children":["Webhook event reference"]}," — The envelope and the payload for each domain"]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/products/wallet/integrations-and-monitoring/webhooks/manage-webhooks"},"children":["Manage webhooks via the console"]}," — Create, view, and delete webhooks in the console"]},{"$$mdtype":"Tag","name":"li","attributes":{},"children":[{"$$mdtype":"Tag","name":"MarkdownLink","attributes":{"href":"/products/wallet/integrations-and-monitoring/webhooks/manage-webhooks-api"},"children":["Manage webhooks via the API"]}," — Create webhooks and manage subscriptions through the API"]}]}]},"headings":[{"value":"Receive webhook deliveries","id":"receive-webhook-deliveries","depth":1},{"value":"Endpoint requirements","id":"endpoint-requirements","depth":2},{"value":"Verify the signature","id":"verify-the-signature","depth":2},{"value":"Code examples","id":"code-examples","depth":3},{"value":"Python","id":"python","depth":4},{"value":"JavaScript (Node.js)","id":"javascript-nodejs","depth":4},{"value":"Go","id":"go","depth":4},{"value":"Retries","id":"retries","depth":2},{"value":"Handle out-of-order events","id":"handle-out-of-order-events","depth":2},{"value":"Deduplicate deliveries","id":"deduplicate-deliveries","depth":2},{"value":"Process idempotently","id":"process-idempotently","depth":2},{"value":"Monitor failures","id":"monitor-failures","depth":2},{"value":"Related topics","id":"related-topics","depth":2}],"frontmatter":{"title":"Receive webhook deliveries","seo":{"title":"Receive webhook deliveries"}},"lastModified":"2026-10-09T11:09:54.000Z","pagePropGetterError":{"message":"","name":""}},"slug":"/products/wallet/integrations-and-monitoring/webhooks/receive-webhook-deliveries","userData":{"isAuthenticated":false,"teams":["anonymous"]},"isPublic":true}