From nothing to a live recurring subscription. Everything below runs in test mode with the keys already in your dashboard — no bank connection required.
A product is what you sell; a price says how much and how often. One call does both — pass a name with no product and the product is created alongside the price. Add a second price later (a yearly one, say) with POST /v1/plans and the product’s id.
curl -X POST https://api.ronda.am/v1/plans \
-H "Authorization: Bearer sk_test_…" \
-H "Content-Type: application/json" \
-d '{
"name": "Pro monthly",
"amount": 12900,
"currency": "AMD",
"interval": "month",
"trial_days": 14
}'
# → { "id": "plan_9f3a…", "object": "plan", "product": "prod_6f21…", "amount": 12900, … }This is the only way a card enters the system. Ronda returns a URL; send your customer there. The card is typed on your bank's page, not yours.
curl -X POST https://api.ronda.am/v1/checkout/sessions \
-H "Authorization: Bearer sk_test_…" \
-H "Content-Type: application/json" \
-d '{
"type": "subscription",
"plan": "plan_9f3a…",
"customer_email": "anna@example.am",
"success_url": "https://yourapp.am/welcome"
}'
# → { "id": "cs_1b4c…", "url": "https://ronda.am/checkout/cs_1b4c…", … }Redirect to url. Ronda shows the amount and your business name, then forwards to the bank for the card and 3-D Secure. When the customer comes back, Ronda asks the bank what happened, stores the card token, and creates the subscription.
success_url is shown on Ronda's result page as a Continue button, and opened exactly as you wrote it: Ronda adds nothing to it. Put your own reference in it if the page needs one, for example https://yourapp.am/welcome?order=3391. That page is for the person. Access is granted by the webhook, or by your server asking Ronda's API. A browser arriving there proves nothing.
Do not rely on the browser returning — a customer can close the tab. Add a webhook endpoint and act on the event.
curl -X POST https://api.ronda.am/v1/webhook_endpoints \
-H "Authorization: Bearer sk_test_…" \
-H "Content-Type: application/json" \
-d '{"url": "https://yourapp.am/ronda/webhook"}'
# → { "id": "we_…", "secret": "whsec_…" } ← shown once// Node — verify, then act
import { createHmac, timingSafeEqual } from "node:crypto";
app.post("/ronda/webhook", express.raw({ type: "application/json" }), async (req, res) => {
const header = req.headers["ronda-signature"];
const parts = Object.fromEntries(header.split(",").map(kv => kv.split("=")));
const body = req.body.toString();
// Reject anything older than five minutes: that is the replay guard.
if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return res.status(400).end();
const expected = createHmac("sha256", process.env.RONDA_WEBHOOK_SECRET)
.update(`${parts.t}.${body}`)
.digest("hex");
// timingSafeEqual throws on buffers of different lengths — a missing or
// truncated v1 must be a 400, not a crash.
const a = Buffer.from(expected), b = Buffer.from(parts.v1 ?? "");
if (a.length !== b.length || !timingSafeEqual(a, b)) return res.status(400).end();
const event = JSON.parse(body);
if (await alreadyHandled(event.id)) return res.json({ received: true });
// Access follows the subscription's status, from any subscription.* event.
// (A trial never sends subscription.activated, so don't wait for that one.)
if (event.type.startsWith("subscription.")) {
const sub = event.data.object;
const access = { trialing: true, active: true, past_due: true }[sub.status] ?? false;
try {
await setAccess(sub.customer, access, { cardFailed: sub.status === "past_due" });
await markHandled(event.id);
} catch {
return res.status(500).end(); // not saved: Ronda will send it again
}
}
res.json({ received: true });
});Answer 2xx only once the change is saved. If saving fails, answer 5xx and Ronda sends the event again. past_due means a card failed and Ronda is retrying. The example keeps access and shows an "update your card" notice, but whether a customer keeps access while Ronda retries is the business's decision.
Nothing else is needed. At the end of each period Ronda charges the saved card, merchant-initiated, with no 3-D Secure and nobody present. You will receive payment.succeeded, or subscription.past_due followed by up to three retries.
To see a renewal without waiting a month, open the subscription in the dashboard (test mode) and press Simulate renewal, or call POST /v1/subscriptions/:id/simulate_renewal with a test key. It runs the next charge now — trial end, next period or next retry — with the same emails and webhooks.
If you would rather run your own billing logic, the same engine is available directly.
curl -X POST https://api.ronda.am/v1/payments \
-H "Authorization: Bearer sk_test_…" \
-H "Idempotency-Key: invoice-8891" \
-H "Content-Type: application/json" \
-d '{"amount": 45000, "currency": "AMD", "customer": "cus_…", "description": "September"}'subscription.past_due — that is a customer whose card failed, still recoverable.POST /v1/portal/sessions and redirect a signed-in customer to its url.