Fail open
A billing outage must never become a product outage.
Revenue sits in the path of every paid action, so treat it like a cache, not a gate. The SDK does all of this for you by default; this page is for calling the API directly. The rule every Corpland app follows:
| Revenue answers | balances.check | balances.track and costs.track |
|---|---|---|
| 2xx | Use the answer | Done |
409 idempotency_replay | Not applicable | Done, it already counted |
| Other 4xx | Use the answer, it is a real no or a bug to fix | Log loudly, do not retry |
| 5xx, timeout, network error | Allow the action and log | Drop with a loud log, or queue and retry later |
Never fail open on a 4xx. A 4xx means Revenue understood the question and said no, or your request is wrong.
const BASE = process.env.REVENUE_API_URL;
const call = async (operation, body, idempotencyKey) => {
try {
const response = await fetch(`${BASE}/v1/${operation}`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.REVENUE_SECRET_KEY}`,
"Content-Type": "application/json",
...(idempotencyKey && { "Idempotency-Key": idempotencyKey }),
},
body: JSON.stringify(body),
signal: AbortSignal.timeout(2500),
});
const data = await response.json().catch(() => ({}));
if (response.ok || data.code === "idempotency_replay") return { ok: true, data };
return { ok: false, status: response.status, data };
} catch (error) {
return { ok: false, status: 0, data: { message: error.message } };
}
};
export const check = async (customerId, featureId, required = 1) => {
const result = await call("balances.check", { customer_id: customerId, feature_id: featureId, required_balance: required });
if (result.ok) return result.data.allowed;
if (result.status >= 400 && result.status < 500) return false;
console.error("[revenue] check failed open", featureId, result.status, result.data.message);
return true;
};
export const track = async (customerId, featureId, value, key) => {
const result = await call("balances.track", { customer_id: customerId, feature_id: featureId, value }, key);
if (!result.ok) console.error("[revenue] track dropped", featureId, result.status, result.data.message);
};
export const trackCost = async (body, key) => {
const result = await call("costs.track", body, key);
if (!result.ok) console.error("[revenue] cost dropped", body.provider, result.status, result.data.message);
};Keep the timeout short. Revenue answers check and track from Redis in a few milliseconds; if it takes more than a couple of seconds something is wrong, and your customer should not wait for it.