Static Ads Lab
Guides

Errors and billing

How errors and wallet billing interact, and the patterns that keep production sane.

This guide covers the practical interaction between API errors and wallet billing — what charges, what doesn't, and how to handle the common failure modes without surprise bills.

Billing model

Static Ads Lab charges on success:

OperationCostWhen charged
Generate flat image ad$1.00When the ad transitions to completed
Generate editable image ad$4.00When the ad transitions to completed
Generate design template$0.25When the template transitions to completed

If a job ends in failed, your wallet is not debited. Same when a request errors before queueing (validation, auth, etc.).

What to do per error

ErrorWhat it meansWhat to do
400 VALIDATION_ERRORBody or query failed validationFix and retry. No charge.
401 UNAUTHORIZEDMissing or bad API keyVerify the key. No charge.
402 INSUFFICIENT_BALANCEWallet too lowTop up at https://www.staticadslab.com/settings?tab=billing, then retry.
404 NOT_FOUNDResource doesn't exist or isn't in this orgRe-check IDs. No charge.
422Entity relationship issue (product not in brand, template not ready, etc.)Fix and retry. No charge.
429 RATE_LIMIT_EXCEEDEDToo many requestsHonor Retry-After and back off. No charge.
500 / 503Server-side or dependency failureRetry with backoff. If a job was queued and ended in failed, no charge.

The failed status case

A job can return 202 Accepted (queued, no charge yet), then later transition to status: "failed". The error field describes what happened:

{
  "data": {
    "id": "ia_abc",
    "status": "failed",
    "error": {
      "code": "PIPELINE_ERROR",
      "message": "Image generation failed after 3 attempts"
    }
  }
}

Retry by submitting a fresh POST /v1/image-ads. Wallet was not debited.

Surfacing 402 to operators

If you ship a customer-facing tool, the most operator-hostile failure is 402 INSUFFICIENT_BALANCE mid-batch. Recommended pattern:

  1. Check data.error.code === "INSUFFICIENT_BALANCE" after every POST.
  2. Surface a "Top up wallet" CTA with the URL from data.error.message.
  3. Pause the batch, top up, then resume from the pending IDs.
async function safeGenerate(input) {
  const r = await fetch("https://api.staticadslab.com/v1/image-ads", {
    method: "POST",
    headers,
    body: JSON.stringify(input),
  });
  if (r.status === 402) {
    const body = await r.json();
    throw new InsufficientBalanceError(body.error.message);
  }
  if (!r.ok) {
    const body = await r.json().catch(() => ({}));
    throw new SalApiError(r.status, body?.error?.code, body?.meta?.request_id, body?.error?.message);
  }
  return r.json();
}

Logging request IDs

Every response (success or error) includes meta.request_id. Log it on every error path. Support tickets are dramatically faster to resolve when the request ID is included.

log.error("Static Ads Lab error", {
  status: r.status,
  request_id: body?.meta?.request_id,
  code: body?.error?.code,
  message: body?.error?.message,
});

Idempotency keeps retries safe

Use Idempotency-Key on every POST /v1/image-ads (or any other billable POST). On retry, the cached response replays without a new charge. See Idempotency.