FeesPaymentsPricingDevelopersAbout
Get early access
  • Fees
  • Payments
  • Pricing
  • Developers
  • About
Get early access

Developers

An API that shows its working.

Create a payment in one call, get told exactly what happened, and see what it cost, line by line. REST and JSON, with a TypeScript SDK.

Request test keysRead the quick start
JSONA test payment
{
  "id": "pay_01k7f3m9q2v8x4c6n0b5h1t7wz",
  "object": "payment",
  "amount": 38600,
  "currency": "ZAR",
  "status": "succeeded",
  "payment_method_type": "card",
  "description": "Order #2048",
  "metadata": { "order_id": "2048" },
  "amount_refunded": 0,
  "fee": {
    "currency": "ZAR",
    "total": 795,
    "costs": 359,
    "margin": 436,
    "vat": 57,
    "margin_excl_vat": 379,
    "price_plan": "plan_01k6z8c3r4w5t2y9n7b1d0m6qe",
    "lines": [
      { "code": "interchange", "kind": "cost",
        "label": "Card issuer (interchange)", "amount": 224 },
      { "code": "scheme", "kind": "cost",
        "label": "Card scheme", "amount": 58 },
      { "code": "acquirer", "kind": "cost",
        "label": "Acquiring bank", "amount": 77 },
      { "code": "rano", "kind": "margin",
        "label": "rano", "amount": 436 }
    ]
  },
  "net": 37805,
  "next_action": null,
  "last_error": null,
  "cancellation_reason": null,
  "livemode": false,
  "created": 1791532800
}

On this page

  1. 1Quick start
  2. 2API basics
  3. 3Endpoints
  4. 4Fees on every payment
  5. 5Webhooks
  6. 6Test mode
  7. 7Errors
  8. 8SDKs and tooling

Quick start

Three steps from test keys to a working payment.

  1. Get your test keys

    Request access and we’ll send you a secret key starting sk_test_. Test mode needs no paperwork. Keep the key on your server, never in the browser.

  2. Create a payment

    Amounts are in cents. Send an idempotency key so a retry can never charge twice.

    POST /v1/payments
    curl https://api.rano.co.za/v1/payments \
      -H "Authorization: Bearer $RANO_SECRET_KEY" \
      -H "Idempotency-Key: order-2048" \
      -H "Content-Type: application/json" \
      -d '{
        "amount": 38600,
        "currency": "ZAR",
        "payment_method_type": "card",
        "confirm": true
      }'
  3. Read the result

    The payment comes back with its status and the full fee breakdown, like the object at the top of this page. Listen for webhooks to hear about anything that changes after the call returns.

API basics

REST and JSON, shaped the way most developers already expect. If you’ve used a modern payments API, nothing here will surprise you.

One base URL

Every request goes to https://api.rano.co.za with your secret key in an Authorization: Bearer header. Requests and responses are JSON.

Amounts are integers in cents

R386.00 is 38600, always sent with a currency. No floats, no rounding surprises.

IDs tell you what they are

Payments start pay_, refunds rfnd_, events evt_. They sort by time and can’t be guessed.

Retries are safe

Send an Idempotency-Key header when you create, confirm, cancel or refund. Repeating the request within 24 hours returns the original result instead of charging twice. The SDK adds one for you.

Test and live keys are separate

sk_test_ keys only ever reach the simulator. sk_live_ keys move real money, once your business is verified.

Payments have honest states

requires_payment_method, requires_action, processing, succeeded, failed, canceled. If a bank times out, we say processing and find out, rather than guess.

Endpoints

Lists return the newest first and take limit (1 to 100, default 10) and starting_after to page through.

MethodPathWhat it does
POST/v1/paymentsCreate a payment, and confirm it in the same call
GET/v1/paymentsList payments, newest first
GET/v1/payments/:idRetrieve a payment
POST/v1/payments/:id/confirmConfirm a payment
POST/v1/payments/:id/cancelCancel a payment before it’s paid
POST/v1/refundsRefund a payment
GET/v1/refundsList refunds for a payment
GET/v1/refunds/:idRetrieve a refund
GET/v1/balanceYour available and pending balance
GET/v1/eventsList events, optionally by type
GET/v1/events/:idRetrieve an event
POST/v1/webhook_endpointsAdd a webhook endpoint
GET/v1/webhook_endpointsList webhook endpoints
DELETE/v1/webhook_endpoints/:idRemove a webhook endpoint

Fees on every payment

Every payment carries a fee object: the total, what went to banks and card networks (costs), what we kept (margin), and one line per party that took a share. The breakdown is fixed when the payment is made, so later price changes never rewrite history.

CodeKindAppears onWhat it is
interchangecostCard paymentsThe customer’s card issuer. Set by the card network.
schemecostCard paymentsVisa or Mastercard network fees.
acquirercostCard paymentsThe acquiring bank that processes the card.
bankcostPayShap and EFTClearing and bank costs, flat per payment.
ranomarginEvery paymentOur fee. The only part we keep.

VAT at 15% applies to our margin only and is already in it: fee.vat shows how much, and fee.margin_excl_vat the rest. net is what reaches your bank account: amount minus fee.total.

Webhooks

We POST an event to your endpoint whenever a payment or refund changes. Every event is signed and retried with backoff for about three days. Subscribe to the types you need, or * for all of them.

HTTPDelivery
POST /webhooks/rano HTTP/1.1
Content-Type: application/json
Rano-Signature: t=1791532800,v1=5f0c9e2a7b…

{
  "id": "evt_01k7f3m9t5r2y8d4p6e0a3s9gq",
  "object": "event",
  "type": "payment.succeeded",
  "livemode": false,
  "created": 1791532800,
  "data": {
    "object": { "id": "pay_01k7f3m9q2v8x4c6n0b5h1t7wz", "status": "succeeded", … }
  }
}

Verify the signature

The Rano-Signature header holds a Unix timestamp t and v1, a hex HMAC-SHA256 of <t>.<body> using your endpoint’s secret. The SDK checks it for you, including rejecting timestamps more than five minutes old to block replays.

Verify a webhook
import { Rano } from "@rano/sdk";

const rano = new Rano(process.env.RANO_SECRET_KEY);

// Throws if the signature doesn't match. Pass the body exactly as received.
const event = rano.webhooks.constructEvent(
  rawBody,
  req.headers["rano-signature"],
  process.env.RANO_WEBHOOK_SECRET,
);

Events

payment.created
A payment was created.
payment.requires_action
Waiting on the customer: 3D Secure, approval in their banking app, or an EFT.
payment.succeeded
Money is confirmed. Safe to fulfil the order.
payment.failed
The payment was declined or failed. last_error says why.
payment.canceled
You canceled the payment, or the customer didn’t approve it in time. cancellation_reason says which.
refund.created
A refund was created and is on its way to the bank.
refund.succeeded
The refund has been sent back to the customer.
refund.failed
The bank rejected the refund. failure_code says why.
  • Delivery is at least once. Use the event id to ignore duplicates.
  • Order isn’t guaranteed. Each event carries the object as it was when the event happened. When order matters, retrieve the object for its latest state.
  • Reply with any 2xx quickly, then do slow work in the background.
  • Missed one? Every event can be fetched again with GET /v1/events or GET /v1/events/:id.

Test mode

A full copy of rano running against simulated banks. Pick the outcome with the last two digits of the amount in cents. Declines come back as a payment with status: failed and the reason in last_error.code.

Cents end inExampleOutcomeYou’ll see
0110001Declinedfailed · insufficient_funds
0210002Declinedfailed · do_not_honour
0510005Times out, then succeedsprocessing → succeeded
0610006Times out, then failsprocessing → failed
0710007Times out before the bank sees itprocessing → failed · processing_error
0810008Card needs 3D Securerequires_action
Anything else38600Card succeeds; PayShap and EFT wait for the customersucceeded · requires_action

PayShap and EFT in test mode

PayShap and EFT payments wait for the customer, just like in real life, unless the amount picks one of the outcomes above. In test mode, play the customer with a test helper: approve or decline. Test helpers don’t exist in live mode. Refunds follow the same rule: an amount ending in 01 is rejected, and 05 times out, then succeeds.

Approve or decline
// PayShap and EFT payments wait for the customer in test mode too.
await rano.testHelpers.payments.approve("pay_01k7f3m9q2v8x4c6n0b5h1t7wz");

// Or have the customer say no.
await rano.testHelpers.payments.decline("pay_01k7f3m9q2v8x4c6n0b5h1t7wz");

Errors

Every error has the same shape: a type for broad handling, a code for your logic, a message written for whoever is debugging, and the param at fault, if any.

JSON400 Bad Request
{
  "error": {
    "type": "invalid_request_error",
    "code": "parameter_missing",
    "message": "currency is required.",
    "param": "currency"
  }
}
TypeHTTPMeaning
invalid_request_error400, 404, 409A parameter is missing or wrong, the object doesn’t exist, or it’s in the wrong state. param says which.
authentication_error401The API key is missing, revoked or for the wrong mode.
idempotency_error400, 409, 422A key was reused with a different request, or the first request is still running.
api_error500Something went wrong on our side. Safe to retry with the same key.

Common codes

parameter_missing
A required parameter wasn’t sent.
parameter_invalid
A parameter has the wrong type or format.
parameter_unknown
A parameter we don’t recognise. Check the spelling.
amount_too_small
The fee would be at least the whole payment. Use a larger amount.
payment_method_unavailable
That payment method isn’t available for this currency yet.
payment_unexpected_state
The payment can’t do that from its current status.
resource_missing
No object with that ID on your account.
api_key_invalid
The API key isn’t recognised or has been revoked.
api_key_wrong_mode
A live key sent to test mode, or the other way round.
idempotency_key_reused
The same Idempotency-Key with a different request body.

Declines aren’t errors

A declined payment is a normal result, not an HTTP error. You get 200 and the payment, with status: failed and last_error holding a code for your logic and a sentence you can show your customer.

JSONA declined payment
{
  "id": "pay_01k7f3n2c8w5j9r1x6m4d0a7kp",
  "object": "payment",
  "amount": 10001,
  "status": "failed",
  "last_error": {
    "code": "insufficient_funds",
    "message": "The customer's account has insufficient funds."
  },
  …
}

SDKs and tooling

Both come with your test keys. The SDK is for Node.js on your server, where your secret key stays.

TypeScript SDK
@rano/sdk: typed methods for payments, refunds, events and webhook endpoints, webhook verification, idempotency keys and automatic retries.
OpenAPI spec
One file describing every endpoint, object and error, served at GET /openapi.json.

Request test keys.

Leave your email and we’ll send sandbox keys to early-access developers as we onboard them.

Payments for South African businesses, with the fee on the receipt.

Get early access

Product

How fees workPayment methodsPricingCalculator

Developers

API docsTest modeWebhooksErrors

Company

AboutSecurityContactEarly access

Legal

Terms of usePrivacyCookiesPAIA manualAcceptable useMerchant terms

rano is in early access and is not yet processing live payments. The fees for your business are confirmed in your merchant agreement.

© 2026 rano · Built in South Africa