Skip to content

Spend caps, quotas and metering

Galley counts billable units, not requests. One unit is one PNG or JPG, or one page of a PDF. A five-page invoice is five units. Cache hits and failed renders are never counted.

PNG or JPG$0.006 each
PDF$0.015 per page
Cache hitfree
Failed renderfree
Templates, validation, usage and read callsfree
PlanMonthlyIncluded rendersTemplatesAlso
Free$0200/month3
Starter$195,00025webhooks, 30-day retention
Growth$7930,000unlimitedcloud push, 90-day retention, priority queue
Scale$249150,000unlimiteddedicated render pool, SLA

Overage is $4 per 1,000 renders. Annual billing is two months free. Full detail on pricing.

They are checked in this order, before any rendering work starts, and after the cache has been consulted — so a cache hit passes all three without being counted.

A suspended or closed account cannot render at all.

403 Forbidden
{
"error": {
"type": "permission_error",
"message": "Account is suspended. Renders are paused.",
"docs_url": "https://galleyrender.com/docs/errors/permission_error",
"details": { "status": "suspended" }
}
}

2. The trial ceiling — lifetime, not monthly

Section titled “2. The trial ceiling — lifetime, not monthly”

A keyless trial gets 50 billable units for the life of the account. It is deliberately not a monthly allowance: an agent that never signs up cannot roll the counter over by waiting for the 1st.

402 Payment Required
{
"error": {
"type": "quota_exceeded",
"message": "Trial limit reached: 50 renders.",
"docs_url": "https://galleyrender.com/docs/errors/quota_exceeded",
"details": {
"trial": true,
"limit": 50,
"used": 50,
"next_step": "Call the `create_account` tool with an email address to lift this limit, or sign up at https://galleyrender.com/signup. Templates and renders created during the trial are kept.",
"pricing_url": "https://galleyrender.com/pricing"
}
}
}

create_account with an email clears the ceiling on the same account, so the templates and renders made during the trial survive. See Quickstart.

Which one applies depends on the plan, and they never both apply:

PlanEnforced against
freethe monthly free allowance — 200 units by default, raisable per account
everything elsethe monthly spend cap — $50 by default

Free tier reached:

402 Payment Required
{
"error": {
"type": "quota_exceeded",
"message": "Free tier limit reached: 200 renders per month.",
"docs_url": "https://galleyrender.com/docs/errors/quota_exceeded",
"details": {
"plan": "free",
"limit": 200,
"used": 200,
"period": "2026-09",
"resets_on": "2026-10-01",
"upgrade_url": "https://galleyrender.com/pricing"
}
}
}

Spend cap reached:

402 Payment Required
{
"error": {
"type": "spend_cap_exceeded",
"message": "Monthly spend cap of $50.00 reached.",
"docs_url": "https://galleyrender.com/docs/errors/spend_cap_exceeded",
"details": {
"period": "2026-09",
"spend_cap_usd": 50,
"spent_usd": 49.994,
"resets_on": "2026-10-01",
"raise_cap_url": "https://galleyrender.com/docs/billing/spend-cap"
}
}
}

Both reset on the first day of the next month, UTC. The period key is YYYY-MM in UTC, so “this month” means UTC’s month regardless of where you are.

Every account starts with a $50 monthly cap (DEFAULT_MONTHLY_SPEND_CAP_CENTS, in cents). It exists so that a runaway loop costs you dinner rather than a holiday. It is stored per account and can be raised or lowered — the API has no endpoint for it today, so ask through the dashboard or support@galleyrender.com.

The cap is checked against Galley’s own ledger of what this period has cost, not against anything Stripe reports back, because meters only reconcile at invoice time. That is what makes it a hard stop rather than an after-the-fact alert.

What is left
curl -sS https://api.galleyrender.com/v1/usage \
-H "Authorization: Bearer $GALLEY_API_KEY" |
python3 -c '
import json, sys
u = json.load(sys.stdin)
print(f"plan {u[\"plan\"]}")
print(f"period {u[\"period\"]} (resets {u[\"period_resets_on\"]})")
print(f"billable units {u[\"billable_units\"]}")
print(f"cost ${u[\"cost_usd\"]}")
print(f"free remaining {u[\"limits\"][\"free_renders_remaining\"]}")
print(f"spend remaining ${u[\"limits\"][\"spend_remaining_usd\"]}")
if u["trial"]:
print(f"trial remaining {u[\"trial\"][\"renders_remaining\"]} of {u[\"trial\"][\"renders_limit\"]}")
'

Every field of that response is documented at GET /v1/usage. Check it before a large batch: a 50-item batch of three-page PDFs needs 150 units, not 50.

  1. A render succeeds. Its billable units and cost are written onto the render row.
  2. A usage_events row is inserted with status pending and an identifier of render_<render id>. That identifier is the Stripe idempotency key, so the same render can never be billed twice.
  3. A background worker drains pending rows every five seconds, up to 100 at a time, and reports each one as a Stripe billing meter event. A failure is retried up to three times before the row is marked failed.
  4. Accounts with no Stripe customer — free accounts and trials — have their events marked skipped. The usage is still recorded in Galley’s own ledger, which is what GET /v1/usage reports and what the spend cap enforces.

Nothing about this is synchronous with your request. A render’s cost is visible in GET /v1/usage immediately; its arrival at Stripe is a few seconds behind.

Both quota errors are 402, and neither is worth retrying immediately — the condition lasts until you upgrade or until the period turns over.

ErrorRetry?Do this instead
quota_exceeded with details.trialnoCall create_account with an email.
quota_exceeded on the free tierafter details.resets_onUpgrade, or wait for the period.
spend_cap_exceedednoRaise the cap, or wait for details.resets_on.
Stop, do not spin
const res = await fetch("https://api.galleyrender.com/v1/render", { /* … */ });
if (res.status === 402) {
const { error } = await res.json();
// error.type is quota_exceeded or spend_cap_exceeded; error.details says which limit and when it resets.
throw new Error(`${error.type}: ${error.message} (resets ${error.details?.resets_on ?? "on upgrade"})`);
}