Rovas payment processor
Integration guide for accepting Chron and Euro payments via Rovas — covering the client-side payment redirect, server-side webhook handling, and the bank-transfer delayed flow.
Contents
Overview & setup
The Rovas payment processor lets a web, desktop, or mobile application redirect users to Rovas to pay for a subscription or product in Chrons (CHR) or Euros (EUR). Your application manages its own user database and tracks access to premium features.
https:// — payment links, callback_url, webhook endpoints, and API hosts. Do not send secrets or payment parameters over plain HTTP.Environments
| Environment | Host |
|---|---|
| Development | dev.rovas.app |
| Production | rovas.app |
One-time setup steps
Create a Rovas account and record your API key from the Rovas API tab of your profile.
Create a Rovas project for your application and note its project ID.
In the project form, expand Reward sharing and enter your Webhook URL — the server-to-server endpoint Rovas will call after payment.
Optional: Set a default minimum price in Chrons or Euros in the project form. These are used as fallbacks when price_chr / price_eur are omitted from the request URL. Euro prices are dynamically converted to Chrons using the current exchange rate.
Step 1 — Client payment request
When a user clicks "Buy" in your app, redirect their browser to the following URL. Rovas renders the order form and handles payment collection.
Query parameters
| Parameter | Type | Description | |
|---|---|---|---|
| paytype | string | required | Entity type to reward. Use "project" in most cases. |
| recipient | integer | required | ID of the Rovas project (or user) to be rewarded. |
| token | string | required | Unique opaque string per purchase, echoed in webhooks so you can correlate to an order. Use at least 128 bits of cryptographically secure randomness (e.g. 32 hex chars or a 22-char base64url string). Generate with a CSPRNG: os.urandom / secrets.token_hex(32) in Python, random_bytes(32) in PHP, crypto.randomBytes(32) in Node.js, secrets.randBytes in Ruby, or SecureRandom in Java. Avoid deterministic tokens derived only from predictable order fields. For bank-transfer flows the token is stored server-side with a maximum length of 255 characters; stay within that for interoperability. |
| expiration | integer | required | UNIX timestamp (seconds) after which this payment link expires. |
| callback_url | string | required | URL-encoded URL of your post-purchase landing page. Rovas redirects the buyer here after checkout. |
| name | string | required | Product name shown on the Rovas order form. No fixed maximum length; keep the full payment URL reasonably short (e.g. under ~2 000 characters total). |
| description | string | required | Product description shown on the Rovas order form. Same practical length guidance as name. |
| signature | string | required | HMAC-SHA256 of the full absolute payment URL without the signature parameter (see Signing): same scheme, host, port (if non-default), path, and query string as Rovas will verify, preserving the exact query parameter order you use when building the link, then encoded as application/x-www-form-urlencoded. |
| price_eur | integer | optional | Price in euro cents. Example: 200 = €2.00. Overrides the project default. |
| price_chr | integer | optional | Price in chron cents (1 chron = 100 chron cents). Overrides the project default. |
| string | optional | Buyer's email address. Pre-populates the Rovas order form. | |
| lang | string | optional | ISO 639-1 language code (e.g. "en", "sk") for name and description. |
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "RovasPaymentRequest",
"description": "Query parameters for the /rewpro payment redirect URL",
"type": "object",
"required": ["paytype", "recipient", "token", "expiration", "callback_url", "name", "description", "signature"],
"properties": {
"paytype": { "type": "string", "enum": ["project", "user"], "description": "Rewarded entity type" },
"recipient": { "type": "integer", "description": "Rovas project or user ID" },
"token": { "type": "string", "minLength": 16, "maxLength": 255, "description": "Unique per-purchase token; use 128+ bits CSPRNG (see docs); 255 max when stored for bank transfer" },
"expiration": { "type": "integer", "description": "UNIX timestamp — link expiry" },
"callback_url": { "type": "string", "format": "uri", "description": "Post-purchase redirect URL (URL-encoded)" },
"name": { "type": "string", "description": "Product name shown on order form" },
"description": { "type": "string", "description": "Product description shown on order form" },
"signature": { "type": "string", "description": "HMAC-SHA256(key: project API secret, message: full absolute URL without signature, query order preserved), lowercase hex" },
"price_eur": { "type": "integer", "minimum": 1, "description": "Price in euro cents (e.g. 200 = €2.00)" },
"price_chr": { "type": "integer", "minimum": 1, "description": "Price in chron cents" },
"email": { "type": "string", "format": "email", "description": "Buyer email — pre-populates order form" },
"lang": { "type": "string", "pattern": "^[a-z]{2}$", "description": "ISO 639-1 language code" }
},
"additionalProperties": false
}expiration, the browser is redirected to callback_url with ?error=timeout. Treat your expiration timestamp as the authority for clearing pending "unpaid" tokens on your side. If the user cancels checkout, Rovas may send them back to the Rovas payment entry page rather than to your callback_url — do not rely on one behaviour for every cancel path.Step 2 — Webhook (immediate payment)
Upon completion of the payment process (card or Chron), Rovas calls your webhook URL server-to-server via POST with a JSON body (Content-Type: application/json), before redirecting the buyer. The payload includes event (always payment-completed here) so you can use the same endpoint and handler as for Step 3 bank-transfer webhooks — route on event only. Your response code determines whether the buyer is sent to your callback_url or shown an error. Bank transfers do not trigger this webhook at checkout — see the delayed flow instead.
Rovas uses an HTTP client timeout of 5 seconds for this POST (it does not wait indefinitely). Rovas does not queue automatic retries for this immediate webhook; implement activation logic that is idempotent on token so a duplicate delivery would not double-grant access. The synchronous bank-transfer "payment completed" POST (after a transfer settles) uses the same timeout and is only attempted once per order.
Content-Type: application/json
X-Rovas-Event: payment-completed
Webhook parameters
| Parameter | Type | Description |
|---|---|---|
| event | string | Always "payment-completed" for this webhook. Combine with Step 3 event values (order-placed, delayed-confirmed, …) to route every notification in one handler. |
| token | string | The original token from the payment request. Use it to look up the pending order in your database. |
| signature | string | Webhook signing (different from payment URL signing): HMAC-SHA256 with key = project owner’s API secret and message = UTF-8 bytes of the token string, lowercase hex digest. Always verify before activating access. |
| amount_paid | integer | Amount actually paid in minor units of currency (euro cents or chron cents). |
| currency | string | "CHR" or "EUR". |
| string | The buyer's email address. | |
| occurred_at | integer | UNIX timestamp (seconds) when Rovas sent this webhook. Reject or down-rank events outside a short freshness window (e.g. a few minutes) to limit replay abuse in addition to signature verification. |
| expiration | integer | Present when available: the same expiration UNIX timestamp from your payment request (or, for some bank flows, the server-side callback deadline). Compare with occurred_at to ensure the notification is still within the intended validity window without relying on your stored copy alone. |
204 No Content to confirm successful processing within the 5-second timeout. Rovas then redirects the buyer to your callback_url. Any other status code suppresses the redirect and shows the buyer: "Thank you for the payment. Due to a temporary problem, the premium features at @project could not be immediately activated. We will remedy the problem, and notify you as soon as possible."
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "RovasWebhookImmediatePayment",
"description": "JSON body sent by Rovas to the project webhook URL after an immediate (card/Chron) payment",
"type": "object",
"required": ["event", "token", "signature", "amount_paid", "currency", "email", "occurred_at"],
"properties": {
"event": { "type": "string", "const": "payment-completed", "description": "Immediate card/Chron payment settled" },
"token": { "type": "string", "description": "Original token from the payment request" },
"signature": { "type": "string", "description": "HMAC-SHA256(key: project API secret, message: token), lowercase hex; not the same as payment URL signing" },
"amount_paid": { "type": "integer", "minimum": 0, "description": "Amount in minor units (euro cents or chron cents)" },
"currency": { "type": "string", "enum": ["CHR", "EUR"] },
"email": { "type": "string", "format": "email" },
"occurred_at": { "type": "integer", "description": "UNIX seconds when Rovas emitted this webhook" },
"expiration": { "type": "integer", "description": "Optional: payment-link expiration from request (or bank callback deadline)" }
},
"additionalProperties": false
}
POST /your/webhook/url HTTP/1.1
X-Rovas-Event: payment-completed
Content-Type: application/json
{
"event": "payment-completed",
"token": "your-original-token",
"signature": "your-hmac",
"amount_paid": 1200,
"currency": "EUR",
"email": "buyer@example.com",
"occurred_at": 1760000000,
"expiration": 1750489424
}Step 3 — Bank transfer delayed flow
Bank transfers are not settled at checkout. Rovas uses a three-step delayed flow. Server-to-server POST bodies use the same shape as Step 2; distinguish flows by event (order-placed, delayed-confirmed, delayed-rejected, or synchronous payment-completed after settlement).
event=order-placed server → server
Sent immediately when the buyer submits the order. Rovas calls your webhook via POST with a JSON body (Content-Type: application/json) including event=order-placed, token, signature, and related fields. Use this to mark the order as awaiting payment in your system.
callback_url
On the Rovas "complete" page the buyer can click "Return to site". The browser is sent to your callback_url with the parameters below appended as query-string parameters. Show an "awaiting payment" state — access is not yet granted. Verify authenticity via signature before trusting any value.
event=delayed-confirmed or delayed-rejected server → server
Sent after the bank confirms or rejects the transfer. Rovas calls your webhook via POST with a JSON body. If confirmed, activate the user's access. If rejected/expired, notify the user. Rovas retries up to 6 times with backoff on non-2xx responses.
Callback URL parameters (browser redirect, pending bank transfer)
| Parameter | Type | Description |
|---|---|---|
| token | string | Original token from the payment request. |
| signature | string | HMAC-SHA256(key: project API secret, message: token), lowercase hex. Verify before trusting. |
| amount_paid | integer | Order total in euro cents. |
| currency | string | Always "EUR" for bank transfers. |
| string | Buyer's email address. |
Webhook parameters for settlement events
All server-to-server delayed webhooks (order-placed, delayed-confirmed, delayed-rejected) are POST requests with a JSON body (Content-Type: application/json) and the following fields. Payloads may also include nested order, payment, and context objects for compatibility with older handlers.
| Parameter | Type | Description |
|---|---|---|
| event | string | "order-placed", "delayed-confirmed", or "delayed-rejected". |
| delivery_id | string | Idempotency key for this delivery attempt. Use to deduplicate retries. |
| occurred_at | integer | UNIX timestamp (seconds) of when the event occurred. |
| token | string | Original token from the payment request. |
| signature | string | HMAC-SHA256(key: project API secret, message: token), lowercase hex. |
| amount_paid | integer | Order total in euro cents. |
| currency | string | Always "EUR". |
| string | Buyer's email address. | |
| expiration | integer | Optional. When present: original payment-link expiration from the request, or the server-side bank callback deadline — same semantics as the immediate payment-completed webhook. |
| bank_intent_status | string (enum) | Bank-payment intent status in Rovas when the webhook was queued (rovas_bank_payment_intent.status). Known values: pending_settlement (usual for order-placed), paid (delayed-confirmed), expired, failed, rejected (delayed-rejected — only these three rejection outcomes are stored). Also possible: manual_review (amount mismatch), created (legacy / DB default), or an empty string if unset. Prefer handling unknown strings defensively for forward compatibility. |
The request also includes HTTP headers:
| Header | Value |
|---|---|
| X-Rovas-Event | Same as the event query parameter. |
| X-Rovas-Delivery-Id | Same as the delivery_id query parameter. |
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "RovasWebhookDelayedEvent",
"description": "JSON body for bank-transfer lifecycle webhooks (order-placed, delayed-confirmed, delayed-rejected). Keep bank_intent_status enum in sync with rovas_bank_payment_intent.status in rovas_project_reward.",
"type": "object",
"required": ["event", "delivery_id", "occurred_at", "token", "signature", "amount_paid", "currency", "email", "bank_intent_status"],
"properties": {
"event": {
"type": "string",
"enum": ["order-placed", "delayed-confirmed", "delayed-rejected"]
},
"delivery_id": { "type": "string", "description": "Idempotency key — deduplicate retries on this value" },
"occurred_at": { "type": "integer", "description": "UNIX timestamp (seconds) of the event" },
"token": { "type": "string" },
"signature": { "type": "string", "description": "HMAC-SHA256(key: project API secret, message: token), lowercase hex" },
"amount_paid": { "type": "integer", "minimum": 0, "description": "Order total in euro cents" },
"currency": { "type": "string", "const": "EUR" },
"email": { "type": "string", "format": "email" },
"bank_intent_status": {
"type": "string",
"enum": ["", "created", "pending_settlement", "paid", "manual_review", "expired", "failed", "rejected"],
"description": "rovas_bank_payment_intent.status when the delivery was queued. Empty string if status is unset. Rejection webhooks use expired, failed, or rejected only."
},
"expiration": { "type": "integer", "description": "Optional: link expiry or bank callback deadline (UNIX seconds)" }
},
"additionalProperties": true
}
POST /your/webhook/url HTTP/1.1
X-Rovas-Event: delayed-confirmed
X-Rovas-Delivery-Id: cb_0f64d9b0f2f0f1f93dcf2f5cc6d6d3aaf6e2d9a0
Content-Type: application/json
{
"event": "delayed-confirmed",
"delivery_id": "cb_0f64d9b0f2f0f1f93dcf2f5cc6d6d3aaf6e2d9a0",
"occurred_at": 1760000000,
"token": "your-original-token",
"signature": "your-hmac",
"amount_paid": 1200,
"currency": "EUR",
"email": "buyer@example.com",
"bank_intent_status": "paid",
"expiration": 1750489424
}
POST /your/webhook/url HTTP/1.1
X-Rovas-Event: delayed-rejected
X-Rovas-Delivery-Id: cb_a1b2c3d4e5f6789012345678901234567890abcd
Content-Type: application/json
{
"event": "delayed-rejected",
"delivery_id": "cb_a1b2c3d4e5f6789012345678901234567890abcd",
"occurred_at": 1760000000,
"token": "your-original-token",
"signature": "your-hmac",
"amount_paid": 1200,
"currency": "EUR",
"email": "buyer@example.com",
"bank_intent_status": "expired",
"expiration": 1750489424
}delivery_id to avoid granting access twice.Signing & verification
All signatures use HMAC-SHA256 with your Rovas project API key as the shared secret, encoded as a lowercase hex string (digest only, not key-derivation). Use a constant-time comparison when checking webhook signatures.
Outgoing payment link (browser redirect)
Build the message as the full absolute URL Rovas will verify (scheme, host, port if non-default, path /rewpro, ?, query string), with the signature parameter omitted. Query parameters must stay in the same order you use when constructing the link; changing order changes the digest.
Serialize the query as application/x-www-form-urlencoded (same encoding your client library uses when building the redirect URL). Then signature = HMAC-SHA256(key = project API secret, message = that UTF-8 string), lowercase hex.
The host and path in the signed string must match what Rovas uses when verifying (production rovas.app, development dev.rovas.app, path /rewpro).
Incoming webhooks
| Direction | Input to HMAC | Purpose |
|---|---|---|
| Webhook POST body | The token string (UTF-8 bytes) |
Proves the webhook was sent by Rovas |
Optionally enforce freshness: require occurred_at (and, when present, expiration) so that occurred_at ≤ expiration and occurred_at is within a few minutes of your server clock.
# Payment URL signature (Python-style) — preserve parameter order, omit signature pairs = [("paytype", "project"), ("recipient", "35384"), ...] # order matters query = urllib.parse.urlencode(pairs) url_without_sig = "https://rovas.app/rewpro?" + query signature = hmac.new(api_key.encode(), url_without_sig.encode(), hashlib.sha256).hexdigest() # Webhook JSON signature (different from URL signing) expected = hmac_sha256(key=api_key, message=webhook_json["token"]).hex() assert expected == webhook_json["signature"] # Optional replay window assert abs(server_unix_time - webhook_json["occurred_at"]) < 300
occurred_at (and expiration when provided) for replay protection.Full request example
https://rovas.app/rewpro ?paytype=project &recipient=35384 &token=69e895fb340de7dcd0d6a3e33e56a139a3066224dbd490cee952d3a0cc3f142a &callback_url=https%3A%2F%2F40anywhere.xyz%2FpurchaseCallback.html &expiration=1750489424 &email=somebody%40anywhere.xyz &lang=en &name=Product+name &description=Product+description &price_eur=800 &price_chr=8000 &signature=895991d46af5f989b320f4e04e3959b60723736345cd33483205e0ac307953dc
The sample signature value is illustrative; recompute it with your API key using the payment URL signing rules (exact parameter order as in your final URL). price_eur / price_chr here are €8.00 and 80.00 Chrons expressed in cents.
Rovas Payment Processor · neofund.sk · Developer Info