Corpland Revenue

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 answersbalances.checkbalances.track and costs.track
2xxUse the answerDone
409 idempotency_replayNot applicableDone, it already counted
Other 4xxUse the answer, it is a real no or a bug to fixLog loudly, do not retry
5xx, timeout, network errorAllow the action and logDrop 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.

revenue.js
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.