End to end: a key, a token, a catalogue, an order, and the codes. Five requests. Everything else in this document is detail on one of them. Build against the sandbox first — https://api-sandbox.regenerate.co.il/v1, keys that start rg_test_, suppliers' test systems and no real money — then switch the base URL to production, https://api.regenerate.co.il/v1, with a key that starts rg_live_. A key works only in the environment that issued it. The examples below use production.
1. Get your key
Your account manager issues it, and the ReGenerate portal shows it once at that moment. It is not recoverable afterwards — we store a hash, not the key — so put it straight into whatever your systems use for secrets.
If it is ever lost or exposed, rotate it from the portal. The outgoing key keeps working for 24 hours so you can roll the new one out across your estate without an outage. If it must stop now — a key in the wrong hands — revoke it instead: it is refused from that moment, and so are the tokens issued with it.
2. Trade the key for a token
The key goes to exactly one endpoint. Every other call carries a token that expires within the hour, which keeps the long-lived secret off the wire.
Every example below sends $TOKEN, so keep the value where the rest of your session can read it.
curl -s -X POST https://api.regenerate.co.il/v1/auth/token \
-H 'content-type: application/json' \
-d '{"apiKey":"rg_live_4XK9ABCD_p7Qm2ZtVx8Ln4Rc9Wd3Yf6Hs"}'
# {"accessToken":"eyJpc3M...","tokenType":"Bearer","expiresIn":3600}
# Keep it for the rest of this walkthrough:
TOKEN=$(curl -s -X POST https://api.regenerate.co.il/v1/auth/token \
-H 'content-type: application/json' \
-d '{"apiKey":"rg_live_4XK9ABCD_p7Qm2ZtVx8Ln4Rc9Wd3Yf6Hs"}' \
| sed -n 's/.*"accessToken":"\([^"]*\)".*/\1/p')
3. Read the catalogue
One entry per SKU. The price is yours: your account’s margin is already in it, so what you see is what the order will cost.
A SKU is either fixed-value or open-value. A fixed one has a face value and you order a quantity of it. An open one has pricingMode of range, and you send requestedAmount inside minAmount…maxAmount, as a multiple of stepAmount. faceValue is in the card's own currency, faceCurrency; price is always in shekels, and faceValueInIls is the face value in shekels at the same rate.
Cache it for minutes rather than hours. Prices move with the exchange rate.
The reference is yours and it is required. Make it unique per order and keep it identical across retries of that order — that is what makes a till safe to retry when a request times out.
Order whatever quantity your account allows. Supplier batch limits are ours to deal with, not yours.
Use a sku from step 3. The one below is an illustration and will answer 404 against your own catalogue.
The order answers immediately, before the supplier has finished. Register a callback and wait to be told, or poll the order every few seconds.
Keep polling while the status is created or processing. Stop on delivered and fetch the codes; stop on failed or refunded too — those are final, and the wallet has already been put back. Treating anything other than processing as final is the mistake to avoid: the first response is created.
Store what you receive. This is a retrieval endpoint, not a vault.
Every amount is an integer of agorot. 1234 is ₪12.34. There are no floating-point amounts anywhere in this API, in either direction, because a rounding error in a price is a rounding error in a payment.
Your key reaches your account and nothing else
There is no parameter that widens it. An order id belonging to another account answers 404, not 403: whether it exists is not something this API will tell you.
Administrative endpoints are not here
They are not hidden behind a role. They are not part of this API at all, so there is nothing to find by guessing paths.
Times are UTC
Every timestamp is ISO 8601 in UTC. Date filters are calendar dates in Israel time, because that is the day a shop means when it says today.
Fields get added
A field may appear in a response that was not there last week. Parse leniently and ignore what you do not know. Nothing will be removed or change type without a new version prefix.
You choose where the API may be called from
In the portal, under your API credentials, you list the addresses your integration calls from — single addresses, or blocks no wider than /24 (IPv4) or /64 (IPv6). Each environment has its own portal, and each portal manages its own environment only: production at https://app.regenerate.co.il, the sandbox at https://sandbox.regenerate.co.il. Production requires a list: until you save one, every production token request is refused with forbidden and a message that says where to save it, and a saved production list can be replaced but not emptied. The sandbox list is optional; without it the sandbox is not restricted. With a list in place, a token is issued only to those addresses and every call is checked against them; a call from anywhere else is refused with forbidden and recorded, so a refused integration has an answer waiting. Each environment also has an Active switch: switched off, every call with that environment's keys is refused with forbidden until it is switched on again. Every change — to a list, to a switch, a key rotation or a revocation — needs a fresh one-time code from the authenticator.
Branch on the code, not the message
Every error carries a stable error.code. The message is for a human reading a log and may be reworded at any time.
Errors
Every failure has the same shape. code is stable and safe to branch on; requestId is worth logging, because quoting it lets us find the exact request.
{
"error": {
"code": "insufficient_balance",
"message": "wallet balance 120000 is below the required 392400",
"requestId": "9f1c2b6e-4a10-4b93-9a0e-2f5a1c3d7e88"
}
}
Code
HTTP
What it means
validation_failed
400
The request body or query did not validate. `details` names the fields.
unauthenticated
401
No token, an expired token, or a key that is not valid.
forbidden
403
The account is blocked, the resource belongs to another account, the environment is switched off in the portal, production has no address list yet, or the call came from an address that is not on your allowlist for this environment. The message says which.
not_found
404
No such order, SKU or callback in your account.
conflict
409
The order is not in a state that allows this. Retrieving codes before delivery is the common case.
insufficient_balance
422
The order costs more than the wallet holds. Nothing was charged and no order was created.
quantity_limit_exceeded
422
Above the per-order quantity agreed for your account.
amount_out_of_range
422
A free-amount order outside the band the SKU allows.
product_unavailable
422
The SKU is not currently sellable.
supplier_unavailable
502
The upstream supplier could not be reached. Safe to retry with the same reference.
supplier_rejected
502
The supplier refused the order. The wallet was refunded automatically.
rate_limited
429
Above the per-key limit. Wait for `Retry-After` seconds.
internal_error
500
Something failed on our side. Safe to retry with the same reference.
What to retry
A 5xx or a network failure is safe to retry with the same reference. That is the whole point of the reference: the retry either creates the order or returns the one already created.
A 429 is safe to retry after Retry-After seconds.
A 4xx other than 429 will fail again. Fix the request.
Never retry with a new reference after a timeout. That is not a retry, it is a second order.
Rate limits
60req / min / key
Sixty requests a minute, counted per key rather than per account. A nightly reconciliation job on its own key therefore cannot spend the allowance your tills need.
The window is fixed and one minute long. Every response carries the state of it, whether or not it was refused, so you can pace yourself instead of discovering the ceiling by hitting it.
The count is one count. It is shared across every server of ours that answers you, so the limit is the same figure whichever machine your request happens to reach, and it does not quietly rise when we add capacity.
If you need a higher limit, ask your account manager. It is a setting on your account, not a code change.
X-RateLimit-Limit
Requests allowed in the current window.
X-RateLimit-Remaining
How many are left in it.
X-RateLimit-Reset
Unix seconds at which the window resets.
Retry-After
On a 429 only: seconds to wait.
Pagination
Listing endpoints take page and pageSize. Pages are one-based and pageSize is capped at 50.
total counts everything matching the filter, not the rows on this page, so the number of pages is ceil(total / pageSize).
Results are newest first. If rows are being written while you page, prefer a closed date range over walking the pages, or you will see a row twice.
Webhooks
A large order finishes when the supplier finishes it, which may be minutes after the response. Polling works and is documented, but a callback means your system reacts to a delivery instead of asking about it.
A callback never contains a code. It says an order reached a state; the codes are fetched over the API afterwards. A misconfigured endpoint therefore cannot leak your inventory.
The URL must be https.
We retry a failed delivery six times with a widening gap, over about an hour. Answer 2xx as soon as you have the event; do your work afterwards.
Your endpoint may be called more than once for the same event. Make handling it idempotent, keyed on orderId and event.
Each request carries X-Regenerate-Signature, X-Regenerate-Timestamp and X-Regenerate-Event. The signature is HMAC-SHA256 over timestamp + "." + rawBody, keyed with the signing secret you received at registration. Compare it in constant time, and refuse a timestamp more than five minutes old — the timestamp is inside the signed material precisely so that a captured call cannot be replayed at you forever.
import { createHmac, timingSafeEqual } from 'node:crypto';
function isOurs(rawBody, headers, secret) {
const timestamp = Number(headers['x-regenerate-timestamp']);
if (!Number.isFinite(timestamp)) return false;
if (Math.abs(Date.now() / 1000 - timestamp) > 300) return false;
const expected = createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`)
.digest('hex');
const a = Buffer.from(expected, 'utf8');
const b = Buffer.from(headers['x-regenerate-signature'] ?? '', 'utf8');
return a.length === b.length && timingSafeEqual(a, b);
}
Endpoint reference
Authentication
Trading a key for a short-lived token.
POST/v1/auth/tokenNo token needed
Exchange an API key for a short-lived token
The only endpoint that accepts the API key itself. Everything else takes the token this returns, so the long-lived secret sitting in your points of sale crosses the wire as seldom as possible.
The token expires within the hour. Fetch a new one when it does, or shortly before; there is no refresh flow, because exchanging the key again is simpler than one.
The key may travel in the body or in an X-API-Key header, whichever suits your client.
Parameters
X-API-Key
header
string
The key, as an alternative to sending it in the body.
{
"error": {
"code": "unauthenticated",
"message": "The access token is not valid.",
"requestId": "9f1c2b6e-4a10-4b93-9a0e-2f5a1c3d7e88"
}
}
Catalog
What you can buy, and what it costs you.
GET/v1/catalog
Products, denominations and your price
One entry per SKU, which is the unit you order. price is what this order will cost your wallet, with your account’s margin already applied.
A SKU is either fixed-value or open-value. A fixed one has a faceValue and you order a quantity of it. An open one has pricing.mode of range and a band, and you send requestedAmount inside that band when you order.
Cache it for minutes rather than hours: prices move with the exchange rate.
{
"error": {
"code": "unauthenticated",
"message": "The access token is not valid.",
"requestId": "9f1c2b6e-4a10-4b93-9a0e-2f5a1c3d7e88"
}
}
GET/v1/catalog/{sku}/availability
Availability before ordering
A cheap pre-check: whether the SKU is sellable, and the largest quantity your account may put in one order.
It is advisory. The binding check happens inside the order, in the same transaction that spends the money, so treat a true here as "worth trying" rather than as a reservation.
{
"error": {
"code": "unauthenticated",
"message": "The access token is not valid.",
"requestId": "9f1c2b6e-4a10-4b93-9a0e-2f5a1c3d7e88"
}
}
404No such SKU.
errorrequired
object
{
"error": {
"code": "not_found",
"message": "No such SKU.",
"requestId": "9f1c2b6e-4a10-4b93-9a0e-2f5a1c3d7e88"
}
}
Orders
Placing orders and collecting codes.
POST/v1/orders
Place an order
reference is your own idempotency key and it is required. Send the same reference again and you get the same order back with status 200 instead of 201 — never a second charge. This is what makes a till safe to retry after a timeout, which is the case that matters: a retry without one is how a retailer buys three hundred cards twice.
Quantity is bounded only by the per-order limit agreed for your account. Our suppliers have batch limits of their own — one of them delivers five cards at a time — and we split large orders internally, so a three-hundred-card order behaves like a three-card one.
The response is immediate and the order comes back created, before the supplier has answered. Wait for the callback, or poll while the status is created or processing, and fetch the codes once it is delivered.
Body
skurequired
string
quantityrequired
integer
referencerequired
string
Your idempotency key. Must be unique per order and stable across retries of that order.
requestedAmount
integer
Required on an open-value SKU, refused on a fixed one. Minor units, inside the SKU band.
200This reference was used before. The original order is returned and nothing was charged again.
orderIdrequired
string (uuid)
referencerequired
string | null
The reference you sent.
skurequired
string
productName
string
quantityrequired
integer
statusrequired
string
Non-terminal: `created`, which is what a new order comes back as, and `processing`. Terminal: `delivered`, `failed` and `refunded`, and the last two mean the wallet has already been put back. Poll while it is non-terminal. createdprocessingdeliveredfailedrefunded
unitPrice
integer (int64)
Price for one card. Minor units: 1234 is ₪12.34.
totalPricerequired
integer (int64)
What was taken from the wallet. Minor units: 1234 is ₪12.34.
requestedAmount
integer | null
currencyrequired
string
ILS
codesAvailable
boolean
Whether `GET /v1/orders/{id}/codes` will return them now.
createdAt
string (date-time)
When the order was accepted. UTC, ISO 8601.
completedAt
string (date-time) | null
201The order was created and your wallet was debited.
orderIdrequired
string (uuid)
referencerequired
string | null
The reference you sent.
skurequired
string
productName
string
quantityrequired
integer
statusrequired
string
Non-terminal: `created`, which is what a new order comes back as, and `processing`. Terminal: `delivered`, `failed` and `refunded`, and the last two mean the wallet has already been put back. Poll while it is non-terminal. createdprocessingdeliveredfailedrefunded
unitPrice
integer (int64)
Price for one card. Minor units: 1234 is ₪12.34.
totalPricerequired
integer (int64)
What was taken from the wallet. Minor units: 1234 is ₪12.34.
requestedAmount
integer | null
currencyrequired
string
ILS
codesAvailable
boolean
Whether `GET /v1/orders/{id}/codes` will return them now.
{
"error": {
"code": "validation_failed",
"message": "The request did not validate.",
"requestId": "9f1c2b6e-4a10-4b93-9a0e-2f5a1c3d7e88"
}
}
401The access token is not valid.
errorrequired
object
{
"error": {
"code": "unauthenticated",
"message": "The access token is not valid.",
"requestId": "9f1c2b6e-4a10-4b93-9a0e-2f5a1c3d7e88"
}
}
404No such SKU.
errorrequired
object
{
"error": {
"code": "not_found",
"message": "No such SKU.",
"requestId": "9f1c2b6e-4a10-4b93-9a0e-2f5a1c3d7e88"
}
}
422The wallet does not hold enough. No order was created.
errorrequired
object
{
"error": {
"code": "insufficient_balance",
"message": "The wallet does not hold enough. No order was created.",
"requestId": "9f1c2b6e-4a10-4b93-9a0e-2f5a1c3d7e88"
}
}
{
"error": {
"code": "unauthenticated",
"message": "The access token is not valid.",
"requestId": "9f1c2b6e-4a10-4b93-9a0e-2f5a1c3d7e88"
}
}
GET/v1/orders/{id}
Order status
id may be our order id or the reference you sent. Both work, deliberately: a till whose request timed out knows only what it sent, and telling it to find our id first would be telling it to do the impossible.
Parameters
idrequired
path
string
Our order id, or your own reference. TILL-4471-20260914-0007
Responses
200The order.
orderIdrequired
string (uuid)
referencerequired
string | null
The reference you sent.
skurequired
string
productName
string
quantityrequired
integer
statusrequired
string
Non-terminal: `created`, which is what a new order comes back as, and `processing`. Terminal: `delivered`, `failed` and `refunded`, and the last two mean the wallet has already been put back. Poll while it is non-terminal. createdprocessingdeliveredfailedrefunded
unitPrice
integer (int64)
Price for one card. Minor units: 1234 is ₪12.34.
totalPricerequired
integer (int64)
What was taken from the wallet. Minor units: 1234 is ₪12.34.
requestedAmount
integer | null
currencyrequired
string
ILS
codesAvailable
boolean
Whether `GET /v1/orders/{id}/codes` will return them now.
createdAt
string (date-time)
When the order was accepted. UTC, ISO 8601.
completedAt
string (date-time) | null
401The access token is not valid.
errorrequired
object
{
"error": {
"code": "unauthenticated",
"message": "The access token is not valid.",
"requestId": "9f1c2b6e-4a10-4b93-9a0e-2f5a1c3d7e88"
}
}
404No such order in your account.
errorrequired
object
{
"error": {
"code": "not_found",
"message": "No such order in your account.",
"requestId": "9f1c2b6e-4a10-4b93-9a0e-2f5a1c3d7e88"
}
}
GET/v1/orders/{id}/codes
Retrieve delivered codes
Available once the order is delivered. Asking earlier returns 409, not an empty list, so a polling loop cannot mistake "not yet" for "none".
Every retrieval is recorded against the key that made it. Store what you receive: this is a retrieval endpoint, not a vault, and treat the response as you would a card number.
{
"error": {
"code": "validation_failed",
"message": "The URL is not https, or no events were given.",
"requestId": "9f1c2b6e-4a10-4b93-9a0e-2f5a1c3d7e88"
}
}
401The access token is not valid.
errorrequired
object
{
"error": {
"code": "unauthenticated",
"message": "The access token is not valid.",
"requestId": "9f1c2b6e-4a10-4b93-9a0e-2f5a1c3d7e88"
}
}
GET/v1/webhooks
List your callbacks
Without the signing secrets, which are not recoverable.