POST requests to the webhook URL configured on your account.
Signatures go both ways, with the same scheme — Ed25519 over the raw body, sent as ed25519=<hex> — and a key pair on each side:
Each side signs with a key only it holds, so a valid signature proves who produced the message — something a shared secret can’t do, since either side could have computed it. No secret is ever shared, in either direction.
This is a separate contract from the Crypto as a Service webhooks used by the core API — the widget’s events use their own envelope and their own signatures, documented below.
Envelope
Every message shares the same envelope:Verifying the signature
Every request carries anX-Ripio-Signature-Ed25519 header: an Ed25519 signature of the raw request body, made with Ripio’s private key.
PUBLIC KEY). There’s one key per environment: the sandbox key doesn’t verify production requests, and vice versa.
The signature covers the exact bytes of the body as received (compact JSON, no whitespace). Verify it over the raw body before parsing it — re-serializing the parsed JSON won’t necessarily give you back the same bytes. Reject the request if the header is missing, doesn’t start with ed25519=, or doesn’t verify.
Python (cryptography)
Signing your response
Your answer to both messages — the decision on atransaction_approval_request and the acknowledgement of a transaction_result — is signed with an Ed25519 key pair that you generate and hold. You keep the private key; you hand the public key to Ripio during onboarding. Since only you hold the private key, a valid signature proves the answer came from your systems.
Each response body includes the request’s event_id. That’s what binds each signature to a single delivery: without it, every {"approved": true} would be the same bytes and the same signature, reusable for any other purchase.
1. Generate the key pair
openssl or generate the pair with your language’s crypto library.
Send partner_response_public.pem (a PEM PUBLIC KEY) to your Ripio contact. Keep the private key in your secrets manager — never share it. Your public key is required before Ripio can send webhooks to your account.
2. Sign the exact bytes you send
Serialize the response body once, sign those bytes, and send those same bytes. If your framework re-serializes the object after you sign it (different key order, added whitespace), the signature won’t match.
Python (cryptography)
transaction_approval_request
Sent before a buy executes, only if your account has transaction approval enabled. Ripio makes a single attempt with a 5-second timeout and does not follow redirects. If your endpoint doesn’t respond in time, or responds with anything other than a valid, signed approval, the operation does not execute — this request is fail-closed.
Responding
To approve, respondHTTP 200 with a JSON body that includes the event_id of the request, and sign that body (see Signing your response):
- The status is
200. - The body is a JSON object with
"approved": true— the boolean."true",1or"yes"are treated as a denial. - The body’s
event_idequals theevent_idof the request. X-Partner-Signature-Ed25519carries a valid signature of the body under the public key you registered with Ripio.
200, or a validly signed response whose approved isn’t true, denies the operation, and the user is told it wasn’t approved.
transaction_result
Tells you how an operation ended, so you can update your own record of the user’s balance and release anything you reserved when approving it. It is sent to every account with a webhook URL, whether or not transaction approval is enabled, for:
- Buys and sells executed from the widget — described in this section.
- Withdrawals the user sends from the widget, and deposits credited to your users’ deposit addresses — same envelope and statuses, with an on-chain payload. See Deposits and withdrawals.
message.id, with one of two statuses:
COMPLETED— the trade executed, or the transfer was confirmed.CANCELLED— the operation did not execute and never will. It is final: once an operation is cancelled, Ripio refuses any further attempt on it, so aCANCELLEDis never followed by aCOMPLETEDfor the sameid. If the user tries again, the widget asks for a new quote, which starts a new operation with a newid.
event_id — use it to discard duplicates, and acknowledge the duplicate too.
Completed:
Acknowledging
To confirm you received a result, respond with any2xx status and a JSON body that includes the event_id of the request, signed like your approval responses (see Signing your response):
2xx, the body carries "received": true and the request’s event_id, and X-Partner-Signature-Ed25519 verifies under your public key. A bare 2xx — unsigned, wrongly signed, or for another event_id — is not an acknowledgement: Ripio retries it like a missing response, and records the delivery as failed once the retries run out. That’s what lets either side prove later that a result was or wasn’t received.
Cancel reasons
An operation is cancelled only when it is certain that it did not execute: either Ripio rejected it, or the core was never called because your approval couldn’t be used.
Treat any other value as a cancellation too: new reasons may be added.
When you don’t receive a result
- You denied the approval. No result is sent — you already know the outcome, and you reserved nothing.
- The outcome is uncertain. If the execution fails in a way that doesn’t say whether the trade happened (a timeout or an internal error), no result is sent for now. The user can retry the same operation from the widget, and if the trade did go through, you receive its
COMPLETEDthen. - A deposit or withdrawal isn’t final yet, or never got created. See When it’s sent.
The same
id can receive more than one transaction_approval_request: when the user retries after an uncertain failure, the retry asks for your approval again, with a new event_id. Answer each one on its own merits — only the transaction_result closes the operation.For a buy, persisting
id is generally enough to reference the operation later; operation_number is what the user sees, so it’s the easiest reference if they reach out to you about it.Deposits and withdrawals
Deposits and withdrawals report their result through the sametransaction_result: same envelope, same COMPLETED / CANCELLED statuses, same delivery and the same acknowledgement. What changes is the message — there’s no quote and no fiat side, but the on-chain details of the transfer, and the status Ripio’s core reports for it.
Deposit and withdrawal results are enabled per account. Ask your Ripio contact to turn them on.
When it’s sent
A result is sent once the transfer is final — never while it’s still in progress:- Deposit — when it’s credited: the network confirmed it and Ripio completed its checks. Sent for every deposit to any of your users’ deposit addresses, whether or not they had the widget open when the funds arrived. A deposit that was detected but isn’t confirmed yet doesn’t produce a result.
- Withdrawal — when it’s confirmed on-chain (
COMPLETED) or fails after being submitted (CANCELLED), only for withdrawals sent from the widget. The “Transfer started” screen the user sees is not the result: at that point the withdrawal is still pending, and it can take some time for the network to confirm it.
amount is a quantity of asset, never fiat: a deposit or withdrawal doesn’t convert anything. Use it, together with asset, to update your record of the user’s crypto balance.On-chain cancel reasons
A cancelled deposit or withdrawal carries acancel_reason built from the core’s status: core_ followed by core_status in lowercase.
Treat any other
core_ value as a cancellation too.
If the user retries a withdrawal that was cancelled after it was submitted, the widget doesn’t send it again — it shows the same transfer with its final status. To send the funds again, the user starts a new transfer, which gets a new
id.Configuring your endpoint
Webhooks are set up during onboarding, as an exchange of public keys in both directions — reach out to your Ripio contact to configure or change anything:- You give Ripio your webhook URL and your Ed25519 public key (see Signing your response).
- Ripio gives you its Ed25519 public key for the environment, to verify
X-Ripio-Signature-Ed25519(see Verifying the signature).
- HTTPS. Your endpoint URL must use HTTPS and resolve to a public address. Redirects (
3xx) are not followed — point the URL at the final endpoint. - Deposits and withdrawals. Their results are enabled per account — ask your Ripio contact to turn them on (see Deposits and withdrawals).
- Respond quickly. For
transaction_approval_request, respond within 5 seconds — the operation is fail-closed, so a slow response means the operation doesn’t go through. - Verify every request. Check
X-Ripio-Signature-Ed25519over the raw body and thetimestampbefore acting on a message. - Sign every response. Your answer to both
transaction_approval_requestandtransaction_resultmust carryX-Partner-Signature-Ed25519and echo the request’sevent_id. - Key rotation. Only one key is active per side at a time, so a rotation — of your key or Ripio’s — is coordinated with your Ripio contact.
- Idempotency. Design your handler around
event_id: processing the same event twice must not double-count anything on your side.