Skip to main content
The widget notifies your backend of two things: a buy asks for your approval before it executes, and every operation reports its result once it’s settled. Both are POST requests to the webhook URL configured on your account. Signatures go both ways:
  • Ripio → you. Every request is signed with HMAC-SHA256, so you can check it came from Ripio.
  • You → Ripio. Your answer to an approval request is signed with your own Ed25519 key, so an approval can be attributed to you and only you.
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:
Reject any message whose timestamp is more than 5 minutes old. The timestamp is part of the signed body, so it can’t be rewritten without breaking the signature — checking it is what stops a captured request from being replayed later.

Verifying the signature

Every request carries an X-Ripio-Signature-256 header: an HMAC-SHA256 signature of the raw request body, computed with the signing key issued alongside your client_id/client_secret.
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. Python
Node.js
Always compare signatures with a constant-time comparison (hmac.compare_digest, crypto.timingSafeEqual) — a naive == leaks timing information an attacker can use to forge a signature byte by byte.

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.
Don’t assume amount is fiat: when amount_currency is crypto, amount: "0.01" means 0.01 of the crypto being bought, not one cent.

Responding

To approve, respond HTTP 200 with a JSON body that includes the event_id of the request, and sign that body (see Signing your response):
An operation is approved only when all of these hold:
  1. The status is 200.
  2. The body is a JSON object with "approved": true — the boolean. "true", 1 or "yes" are treated as a denial.
  3. The body’s event_id equals the event_id of the request.
  4. X-Partner-Signature-Ed25519 carries a valid signature of the body under the public key you registered with Ripio.
A status other than 200, or a validly signed response whose approved isn’t true, denies the operation, and the user is told it wasn’t approved.
A missing or invalid signature, or an event_id that doesn’t match the request, is not read as a denial: the operation doesn’t execute, and the widget gets error 50007 (“could not be validated right now”). That keeps “you said no” distinguishable from “someone answered who couldn’t prove to be you” — if you see 50007, check your signing key and that you echo the right event_id.

Signing your response

Your approval response 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 approval came from your systems — an HMAC with a shared secret couldn’t prove that, because Ripio holds the same secret. Including the request’s event_id in the signed body is what binds each signature to a single operation: without it, every {"approved": true} would be the same bytes and the same signature, reusable for any other purchase. 1. Generate the key pair
This needs OpenSSL 1.1.1 or later — the LibreSSL build that ships with macOS doesn’t support Ed25519; use Homebrew’s 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. A public key is required before transaction approval can be enabled on 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)
Node.js
3. Test against the reference vector Use this throwaway test key pair — it is not valid for any account — to confirm your implementation signs the way Ripio verifies. Signing the body below with the private key must produce exactly this signature (Ed25519 is deterministic):

transaction_result

Tells you how an operation — buy or sell — 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. Each operation gets at most one result, identified by message.id, with one of two statuses:
  • COMPLETED — the trade executed.
  • 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 a CANCELLED is never followed by a COMPLETED for the same id. If the user tries again, the widget asks for a new quote, which starts a new operation with a new id.
It is delivered at least once: any 2xx response counts as received; anything else, or no response, is retried up to 3 times with exponential backoff (up to 60 seconds between attempts). A retry carries the same event_id — use it to discard duplicates. Your response to this message doesn’t need to be signed. Completed:
Cancelled:
To update your record of the user’s fiat balance, use fiat_amount, not amount. amount is what the user typed: when amount_currency is crypto, it is a quantity of asset, and it never reflects the fee.

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 COMPLETED then.
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.

Configuring your endpoint

Your webhook URL, HMAC signing key and Ed25519 response public key are set on your account during onboarding — reach out to your Ripio contact to configure or change them. Key considerations for your endpoint:
  • 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.
  • 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.
  • Sign your approval. Every response to transaction_approval_request must carry X-Partner-Signature-Ed25519 and echo the request’s event_id. Only one public key is active per account at a time, so coordinate a key rotation with your Ripio contact.
  • Idempotency. Design your handler around event_id: processing the same event twice must not double-count anything on your side.