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 Webhook event reference. To create the webhook that sends to your endpoint, see Manage webhooks via the console or Manage webhooks via the API.
Before you create a webhook, develop an HTTPS endpoint on your server to receive webhook events. Your endpoint must:
- 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.
- Accept
POSTrequests with a JSON body. Each delivery is a JSON envelope with a base64-encoded payload and aSignatureheader. See Envelope. - Acknowledge each delivery with a
2xxresponse, such as200 OK, within the 30-second timeout. Any2xxstatus confirms successful receipt. Ripple Custody (Wallet) treats a slower response as a failed attempt and retries it. See Retries.
We recommend returning a 2xx response within 5 seconds. Perform any heavy processing asynchronously after acknowledging receipt to avoid timeouts.
Always verify the webhook signature before processing to ensure the payload is authentic. Never process a webhook payload without verifying the signature first.
Ripple Custody (Wallet) signs each webhook delivery with an ECDSA private key. Verify the signature on every delivery to confirm it came from Ripple Custody (Wallet) and wasn't tampered with.
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
publicKeyfield. For example:{ "id": "a6a1e8bd-30eb-419f-b6ba-e7ac11e86f84", "name": "wefewfwe", "publicKey": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEpHiEkdd/ba5dU4sNCgsWrMOE6fLakHywl0OgF5aVfxkiqNh22oybRAREev7jvnwH4jqitHx79KGi6CMwiJjmaw==", // other fields omitted }To get the public key later:
- Console: go to Settings > Webhooks, select the webhook, and on the View webhook page copy the Public key. See View webhooks.
- API: call
GET /v2/webhooks/{id}and readpublicKey. See Get a webhook.
Extract the signature from the
Signatureheader 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 assignature.Verify the signature against the base64-encoded
payloadstring 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.Only after the signature verifies, base64-decode
payloadand parse the resulting JSON.
If you delete a webhook and create a new one, the new webhook has a new public key, so update your signature verification.
Most programming languages have cryptography libraries that support ECDSA verification.
import base64
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.serialization import load_der_public_key
def verify_webhook(public_key_b64: str, signature_b64: str, payload_b64: str) -> bool:
"""
Verify a Ripple Custody (Wallet) webhook signature.
Args:
public_key_b64: Base64-encoded public key from webhook creation
signature_b64: Base64-encoded signature from the 'Signature' header
payload_b64: The base64-encoded payload string from the webhook body
Returns:
True if signature is valid, False otherwise
"""
public_key = load_der_public_key(base64.b64decode(public_key_b64))
signature = base64.b64decode(signature_b64)
try:
# ECDSA(SHA256()) hashes the payload with SHA-256 before verifying
public_key.verify(signature, payload_b64.encode(), ec.ECDSA(hashes.SHA256()))
return True
except Exception:
return Falseconst crypto = require('crypto');
/**
* Verify a Ripple Custody (Wallet) webhook signature.
*
* @param {string} publicKeyB64 - Base64-encoded public key from webhook creation
* @param {string} signatureB64 - Base64-encoded signature from the 'Signature' header
* @param {string} payloadB64 - The base64-encoded payload string from the webhook body
* @returns {boolean} - True if signature is valid
*/
function verifyWebhook(publicKeyB64, signatureB64, payloadB64) {
const publicKey = crypto.createPublicKey({
key: Buffer.from(publicKeyB64, 'base64'),
format: 'der',
type: 'spki'
});
// 'sha256' hashes the payload with SHA-256 before verifying
return crypto.verify(
'sha256',
Buffer.from(payloadB64),
publicKey,
Buffer.from(signatureB64, 'base64')
);
}package main
import (
"crypto/ecdsa"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
)
// VerifyWebhook verifies a Ripple Custody (Wallet) webhook signature.
func VerifyWebhook(publicKeyB64, signatureB64, payloadB64 string) (bool, error) {
publicKeyDER, err := base64.StdEncoding.DecodeString(publicKeyB64)
if err != nil {
return false, err
}
pubKey, err := x509.ParsePKIXPublicKey(publicKeyDER)
if err != nil {
return false, err
}
ecdsaPubKey, ok := pubKey.(*ecdsa.PublicKey)
if !ok {
return false, err
}
signature, err := base64.StdEncoding.DecodeString(signatureB64)
if err != nil {
return false, err
}
hash := sha256.Sum256([]byte(payloadB64))
return ecdsa.VerifyASN1(ecdsaPubKey, hash[:], signature), nil
}Your server must return a 2xx response, such as 200 OK, to confirm successful receipt of a webhook notification.
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.
The system doesn't retry for client-side errors (HTTP 4xx) except for the following cases:
429 Too Many Requests: indicates rate limits have been exceeded.408 Request Timeout: indicates the server took too long to respond.
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.
Because of retries, your endpoint can receive the same event more than once. See Deduplicate deliveries.
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 updatedAt with the latest state you've stored for that resource, and don't overwrite newer state with an older event.
Your endpoint may receive the same event more than once. The envelope has no event ID, so use the resource id together with its updatedAt timestamp, both inside the decoded payload, to detect and safely handle duplicate deliveries. If you've already processed that id and updatedAt pair, acknowledge the delivery with a 2xx response and skip processing.
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.
Set up alerting on your endpoint to detect delivery failures or downtime. Ripple Custody (Wallet) also reports each webhook's delivery health:
- Console: the View webhook page shows the success and failure counts for the last hour, and the time of the last success and last failure. See View webhooks.
- API:
GET /v2/webhooks/{id}returnssuccessCount,failureCount,lastSuccess, andlastFailure. To find webhooks with recent failures, list webhooks with a filter such asfilter.lastFailure.relativeDays=-1. See Get a webhook and List webhooks.
- Webhooks — What webhooks are and how delivery works
- Webhook event reference — The envelope and the payload for each domain
- Manage webhooks via the console — Create, view, and delete webhooks in the console
- Manage webhooks via the API — Create webhooks and manage subscriptions through the API