Spend Limits
A spend limit is a per-app monthly cap, in EUR, on transcoding charges. When set, new jobs are rejected once the app’s recorded spend for the current billing period reaches the limit; a warning webhook fires at 80% and a breach webhook at 100%. Unset means unlimited — the default, so existing apps are unaffected.
Setting a limit is not a payment operation. It never charges, authorizes, or reserves funds — it only gates new job admission and fires notification webhooks.
Manage the limit from the app’s Spend Limit settings in the dashboard, or over the API with AppService.UpdateSpendLimit and AppService.GetSpend.
Defaults
| State | Meaning |
|---|---|
monthly_spend_limit_eur unset | Unlimited (default). Zero behavior change — nothing is aggregated or gated. |
monthly_spend_limit_eur = N (> 0) | A cap of €N per billing period. |
What counts toward spend
Spend is the job’s billed total as computed by the pricing engine, summed over the current billing period:
- Terminal jobs (completed, failed, canceled, partial) count their actual billed total. A failed job with no billed cost counts zero.
- In-flight jobs count their recorded probe-time estimate until they reach a terminal state. This includes
awaiting_confirmation(delayed-start) jobs — their estimate counts toward spend until they are confirmed, canceled, or otherwise terminal, so cancel abandoned delayed-start jobs to free budget. - Both totals already fold in the per-job minimum charge and any additive fees — nothing is re-derived from outputs.
- Currency is always EUR.
Jobs are attributed to a period by their creation time, not their completion time — a job created in one period keeps counting against that period even if it finishes in the next. Every job-creating path is covered uniformly, including jobs created from an existing input rather than a fresh upload.
Billing period
The billing period follows the app’s billing anchor day in UTC. It starts at 00:00:00 UTC on the anchor day and ends exactly one calendar month later — the same convention as hosting usage.
- Anchor day 1 (the default) → calendar month, e.g.
2026-07-01…2026-08-01. - Anchor day 15 →
2026-07-15…2026-08-15.
Spend and threshold state from a previous period never carry into the current one.
Enforcement (a guardrail, not a hard cap)
Enforcement is a create-time check against the spend recorded at that moment — billed totals of terminal jobs plus recorded probe-time estimates of in-flight jobs. It is an advisory read (no row locking). Be clear about what it does and does not promise:
- Once recorded spend reaches the limit, the next create is rejected. That is the guarantee.
- Recorded spend can overshoot the limit. A newly admitted job contributes zero recorded cost until the worker probes it and writes an estimate, so a burst of creates just under the limit — or a single large job — can push recorded spend well past it. The overshoot window is the time between a job’s admission and its probe recording an estimate (plus a small gap between the create-time read and the insert commit).
- In-flight jobs are never killed or blocked. The limit gates only new admissions; work already accepted always runs to completion.
- Concurrent creates that are all below the limit are all admitted, for the same reason — each contributes zero recorded cost until probed.
Treat the limit as a guardrail against runaway spend, not an exact ceiling.
Once recorded spend reaches the limit, JobService.Create — and every path that funnels through it — is rejected with Connect code resource_exhausted and the machine-readable error code limit_exceeded:
CodeResourceExhausted
error-code: limit_exceeded
message: app app_k1l2m3n4o5 has reached its monthly spend limit of €10.00
(recorded spend €10.00 this billing period); raise
monthly_spend_limit_eur or wait for the period to reset at
2026-08-01T00:00:00ZSwitch on the limit_exceeded discriminator, not the message text. To restore admission, raise the limit (or clear it) with UpdateSpendLimit, or wait for the period to reset.
JobService.Confirm is not gated. Confirming a delayed-start job is a state transition, not a new admission, so a job created before the limit was reached can still be confirmed after a breach — its estimate already counts toward period spend, so this changes nothing about the recorded total.
Threshold webhooks
Two events fire once per (app, billing period). The crossing is claimed server-side, so exactly one evaluator emits even under concurrency.
| Event type | Fires when |
|---|---|
app.spend_limit_warning | Recorded period spend first reaches 80% of the limit. |
app.spend_limit_exceeded | Recorded period spend first reaches 100% of the limit. A single jump that crosses both thresholds fires both, once each. |
Fire-once is not re-armed by a mid-period limit change: raising or lowering the limit after a threshold event has been claimed for the current period does not re-fire it. The claims reset when the next period starts.
Delivery reaches only endpoints subscribed at crossing time. A threshold event is emitted to the webhook endpoints subscribed to it when the crossing is recorded — so subscribe before setting a limit to receive it. Because the crossing is fire-once per period, subscribing afterward does not backfill a missed notification. The claim and its webhook records are written in one transaction, so a crossing is either fully recorded-and-enqueued or not recorded at all (it retries on the next evaluation).
Both events carry the same small notification object inside the standard webhook envelope’s data field (this is a notification payload, not an API resource snapshot):
{
"app_id": "app_default000",
"period_start": "2026-07-01",
"period_end": "2026-08-01",
"limit_eur": 100.0,
"spent_eur": 85.5,
"threshold_pct": 80,
"currency": "EUR"
}threshold_pct is 80 for the warning and 100 for the breach. period_start / period_end are RFC 3339 full-dates (UTC). Subscribe to these events like any other on the Webhooks page.
Set or clear the limit
Provide monthly_spend_limit_eur (must be > 0 and finite — NaN/Inf are rejected) to set the cap; omit it to clear the cap back to unlimited. A portal user must be an organization Owner or Admin. An API key may set, change, or clear its own app’s limit — a mismatched app_id is rejected — so a leaked key can lift its own app’s cap; rotate compromised keys promptly.
# Set a €500/month cap
curl -X POST https://api.transcodely.com/transcodely.v1.AppService/UpdateSpendLimit
-H "Authorization: Bearer {{API_KEY}}"
-H "X-Organization-ID: {{ORG_ID}}"
-H "Content-Type: application/json"
-d '{"app_id": "app_k1l2m3n4o5", "monthly_spend_limit_eur": 500}'
# Clear the cap (omit the field) — back to unlimited
curl -X POST https://api.transcodely.com/transcodely.v1.AppService/UpdateSpendLimit
-H "Authorization: Bearer {{API_KEY}}"
-H "X-Organization-ID: {{ORG_ID}}"
-H "Content-Type: application/json"
-d '{"app_id": "app_k1l2m3n4o5"}'See Set the spend limit in the API reference for the full request and response shape.
Read current spend
Any organization member can read an app’s current-period spend. warning_triggered and limit_reached report whether the fire-once threshold crossings have been recorded and their notifications enqueued this period — not that any webhook was successfully delivered (a period with no subscribed endpoint still flips these booleans). They follow the claim state, so they stay stable once set.
curl -X POST https://api.transcodely.com/transcodely.v1.AppService/GetSpend
-H "Authorization: Bearer {{API_KEY}}"
-H "X-Organization-ID: {{ORG_ID}}"
-H "Content-Type: application/json"
-d '{"app_id": "app_k1l2m3n4o5"}'{
"period_start": "2026-07-01T00:00:00Z",
"period_end": "2026-08-01T00:00:00Z",
"spent_eur": 85.5,
"monthly_spend_limit_eur": 100,
"currency": "EUR",
"warning_triggered": true,
"limit_reached": false
}monthly_spend_limit_eur is omitted from the response when the app is unlimited.