Developer documentationDocsGet API keys

Quickstart

From nothing to a live recurring subscription. Everything below runs in test mode with the keys already in your dashboard — no bank connection required.

1. Create something to sell

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, … }

2. Open a checkout session

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.

3. Listen for the result

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.

4. Watch the recurring charge happen

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.

5. Charge a saved card yourself

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"}'

What to build next

  • Handle subscription.past_due — that is a customer whose card failed, still recoverable.
  • Send customers to the portal so they can update their card without emailing you: create a link with POST /v1/portal/sessions and redirect a signed-in customer to its url.
  • Read the test cards and try the one that saves successfully then fails on renewal — it is the fastest way to see dunning work.