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, callbackurl, 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. |
| callbackurl | 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 canonical URL without the signature parameter (see Signing): same path and base host as Rovas, query parameters sorted by name, then encoded as application/x-www-form-urlencoded. Legacy integrations may still use the exact query-string order from the browser; Rovas accepts either form. |
| price_eur | integer | optional | Price in whole euros. Example: 2 = €2. Overrides the project default. |
| price_chr | integer | optional | Price in whole Chrons. 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", "callbackurl", "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" },
"callbackurl": { "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(canonical_url_without_signature, api_key) lowercase hex; legacy URL order also accepted" },
"price_eur": { "type": "integer", "minimum": 1, "description": "Price in whole euros (e.g. 2 = €2)" },
"price_chr": { "type": "integer", "minimum": 1, "description": "Price in whole Chrons" },
"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 callbackurl 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 callbackurl — 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 and delayed so you can use the same endpoint and handler as for Step 3 bank-transfer webhooks (see event / delayed below). Your response code determines whether the buyer is sent to your callbackurl or shown an error. Bank transfers do not trigger this webhook — 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. |
| delayed | integer | Always 0 (payment settled at checkout). Bank-transfer server webhooks use 1 while settlement is still pending. |
| token | string | The original token from the payment request. Use it to look up the pending order in your database. |
| signature | string | HMAC-SHA256(token, project_api_key) as lowercase hex. Always verify this before activating access. |
| amount_paid | integer | Amount actually paid by the buyer (whole number). Unit depends on currency. |
| 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 callbackurl. 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", "delayed", "token", "signature", "amount_paid", "currency", "email", "occurred_at"],
"properties": {
"event": { "type": "string", "const": "payment-completed", "description": "Immediate card/Chron payment settled" },
"delayed": { "type": "integer", "const": 0, "description": "0 = not a pending bank transfer" },
"token": { "type": "string", "description": "Original token from the payment request" },
"signature": { "type": "string", "description": "HMAC-SHA256(token, project_api_key) as lowercase hex" },
"amount_paid": { "type": "integer", "minimum": 0, "description": "Amount actually paid (whole number, unit matches currency field)" },
"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",
"delayed": 0,
"token": "your-original-token",
"signature": "your-hmac",
"amount_paid": 12,
"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 event + integer delayed convention as Step 2 (delayed is 1 for these bank flows while settlement is incomplete or for bank lifecycle events; Step 2 immediate payments use delayed: 0).
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, token, signature, and delayed=1. Use this to mark the order as awaiting payment in your system.
callbackurl with delayed=1
On the Rovas "complete" page the buyer can click "Return to site". The browser is sent to your callbackurl 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, delayed=1)
| Parameter | Type | Description |
|---|---|---|
| token | string | Original token from the payment request. |
| signature | string | HMAC-SHA256(token, project_api_key) as lowercase hex. Verify before trusting. |
| amount_paid | integer | Order total in whole euros (e.g. 12). |
| currency | string | Always "EUR" for bank transfers. |
| string | Buyer's email address. | |
| delayed | integer | Always 1. Indicates settlement is pending. |
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(token, project_api_key) as lowercase hex. |
| amount_paid | integer | Order total in whole euros. |
| currency | string | Always "EUR". |
| string | Buyer's email address. | |
| delayed | integer | Always 1. |
| 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. Same enum as docs/integration/RovasWebhookDelayedEvent.schema.json. |
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", "delayed", "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(token, project_api_key) as lowercase hex" },
"amount_paid": { "type": "integer", "minimum": 0, "description": "Order total in whole euros (e.g. 12)" },
"currency": { "type": "string", "const": "EUR" },
"email": { "type": "string", "format": "email" },
"delayed": { "type": "integer", "const": 1 },
"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": 12,
"currency": "EUR",
"email": "buyer@example.com",
"delayed": 1,
"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": 12,
"currency": "EUR",
"email": "buyer@example.com",
"delayed": 1,
"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)
Canonical URL (recommended): Build the message exactly as follows so parameter order in the browser does not matter:
- Start with the Rovas base URL for the environment:
https://rovas.apporhttps://dev.rovas.app(no trailing slash), plus path/rewpro. - Parse all query parameters except
signature. - Sort parameter names in ascending byte order (ASCII / UTF-8 codepoint order for names).
- Serialize as
application/x-www-form-urlencoded:name1=value1&name2=value2&...using the same encoding your stack uses forencodeURIComponent-style query strings (PHP:http_build_queryafterksort). - The string to sign is
https://<host>/rewpro?+ that serialized query string. signature= HMAC-SHA256(message = that string, key = API key), lowercase hex.
Legacy: Rovas still accepts a signature computed over the URL with query parameters in the same order as in the incoming request (after removing signature). New integrations should use the canonical form only.
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.
# Canonical request signature (Python-style) params = { "paytype": "project", "recipient": "35384", ... } # no "signature" key query = urllib.parse.urlencode(sorted(params.items())) # sorted by key url_without_sig = "https://rovas.app/rewpro?" + query signature = hmac.new(api_key.encode(), url_without_sig.encode(), hashlib.sha256).hexdigest() # Verifying the webhook signature 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 &callbackurl=https%3A%2F%2F40anywhere.xyz%2FpurchaseCallback.html &expiration=1750489424 &email=somebody%40anywhere.xyz &lang=en &name=Product+name &description=Product+description &price_eur=8 &price_chr=80 &signature=895991d46af5f989b320f4e04e3959b60723736345cd33483205e0ac307953dc
The sample signature value is illustrative; recompute it with your API key using the canonical signing rules (sorted query parameters). Parameter order in this multiline example is not significant for verification.
Rovas Payment Processor · neofund.sk · Developer Info