Credit and spending limits
How included and granted credit are used, the balance, monthly spending limits for an organization and each project, what happens at a limit, alerts by email and webhook, and how to use limits safely. Buying credit is coming.
Two things control the money in Boxline. Credit is what you have to spend. A spending limit is the most you allow yourself to spend in a month. Both belong to the organization, so all its projects share them. A project can also have a limit of its own.
You see credit and usage, and set limits, in the console. They are not in the API: an API key cannot read or change them. That is on purpose. See Use limits safely.
What counts as usage
Usage is what you run, at the prices on the pricing page. There are five kinds.
| Kind | What is counted | What is not |
|---|---|---|
| Session time | Every second a session runs, at the session rates. | – |
| Model calls | Agent runs, extract and plain-English steps on Boxline's model keys. | Calls on your own model key: that cost is yours. The run's session time still counts. |
| Proxy data | Data through Boxline's residential and datacenter proxies. | Your own proxy. |
| CAPTCHA solving | Each automatic attempt that reached the solving service, solved or not. | A CAPTCHA that a person solved. |
| Web search | Searches beyond your plan's monthly allowance, at the plan's price per 1,000. | Searches inside the allowance, and answers from the cache. |
Boxline records usage as it happens. A running session’s time is recorded every minute. The rest is recorded within a minute. Each month is the calendar month in UTC.
Every amount is recorded for the organization, the project and the API key that started the work. Work started from the console or by a schedule has no key.
How credit is used
Credit comes in two kinds, and usage takes them in this order:
- Included credit. Each plan includes credit every month. It starts over on the 1st (UTC) and never carries over.
- Granted credit. Our team can grant credit to an organization. A grant expires after 12 months, unless we set another date. Credit that expires first is used first.
| Plan | Included credit each month |
|---|---|
| Free | $5.00 |
| Hobby | $15.00 |
| Startup | $80.00 |
| Scale | Custom |
Your balance is what is left of this month’s included credit, plus the granted credit that has not expired. If you move to a plan with more included credit during the month, the difference is added at once. A plan with less takes effect next month.
Usage beyond your balance is still recorded, and shown as not covered by credit. Nothing is billed later for it.
When credit runs out
Work keeps running. While buying credit is not available, using up your credit does not stop anything. Sessions and runs go on, usage is still recorded, and you get an alert by email and by webhook (see Alerts).
The API has an error for this case, 402 out_of_credit, and a matching session stop reason, out_of_credit. They are for when Boxline refuses work once credit is used up. That is not switched on. Handle them in your code now, so nothing surprises you later (see Handle it in your code).
Spending limits
A spending limit is an amount in US dollars for a calendar month (UTC). Set one for the organization, and if you like one for each project. A limit is a number from 0 to 1,000,000, or none. None is the default.
| Limit | What it covers |
|---|---|
| The organization’s | All the usage of all its projects, deleted ones included for the month they were used. |
| A project’s | The usage of that project. It stops that project and no other. |
Spend is this month’s recorded usage, plus the time of running sessions that is not recorded yet. It starts over on the 1st (UTC). Work is refused when spend reaches a limit, so a limit of 0 refuses all new work.
Who can set and read limits
Owners, admins and billing members set limits. Every member can read them. A developer or a viewer who tries to set one gets 403 role_forbidden. See Organizations, projects and team members.
- Developers and viewers with access to chosen projects see only those projects. They still see the organization’s totals.
- Billing members see the spend of every project. They see money, not project data.
- Limits need a console login. An API key gets
403 console_login_required, for its own organization too. - Every change is in the organization’s audit list as
billing.limit_changed, with the old and the new limit.
What happens at a limit
New work is refused with 402 spend_limit. The message says whether the organization or the project reached its limit, and what the limit is. These are refused:
- new sessions, and resuming a stopped session;
- agent runs, task runs, resumed runs, and each further step of a run that is working;
- extract and plain-English steps;
- fetch, screenshot, PDF and crawl;
- web searches that need a fresh answer. An answer from the cache still works.
Running sessions stop within about a minute. Each one is saved like any stop, with stopReason: "spend_limit", so you can resume it once the limit is raised. An agent run working in a stopped session fails with errorCode: "session_ended", and its error names spend_limit. A run can also fail with spend_limit when it is refused before its next step. Sessions that were already stopped stay stopped. See Why a session stopped.
| Where | What you see |
|---|---|
| A new call | 402 spend_limit |
| A session that was running | Stopped (stopReason: "spend_limit"), within about a minute |
| An agent run in that session | failed, with errorCode session_ended or spend_limit |
| A stopped session | Stays stopped. Resuming it gets 402 spend_limit. |
Raise or remove the limit, and work can start again at once. Sessions that were stopped stay stopped: resume them when you are ready, or start new ones.
The plan’s model spend cap
Your plan also caps the model cost on Boxline’s keys each month (modelSpendCapUsd). It protects the shared model keys, and it stays. It uses the same code, 402 spend_limit, but its message names the model spend, and it only moves with your plan. Your spending limit is yours, covers all usage, and is on top of it. Neither replaces the other. See Plans and features.
Alerts
Boxline warns you before you reach a limit, and when your credit runs low. Every alert goes by email to the organization’s owners and billing members. Admins, developers and viewers do not get these emails.
| When | Webhook | |
|---|---|---|
| Spend reaches 50% or 80% of a limit | Yes | No |
| The organization's spending limit is reached (100%) | Yes | Yes, kind spend_limit, to every project |
| A project's own limit is reached (100%) | Yes | Yes, kind project_spend_limit, to that project |
| Credit runs low: 20% or less of this month's credit is left | Yes | No |
| Credit is used up | Yes | Yes, kind credit, to every project |
- Each alert is sent once a month for each limit. If several steps pass at once, one email names the highest.
- If you raise a limit during the month, the alerts you already got for it are not sent again that month.
- The email subject does not name the organization. Each alert is also in the audit list as
billing.alert, made bysystem.
The webhook
At 100% of a limit, and when credit is used up, Boxline sends the usage.limit_reached webhook to the project’s endpoints that asked for it. The organization’s alerts go to every live project in it. Its kind says which one:
kind | When | limit | used |
|---|---|---|---|
spend_limit | The organization reached its spending limit. | The limit, in USD. | This month's spend, in USD. |
project_spend_limit | A project reached its own spending limit. | The project's limit, in USD. | The project's spend this month, in USD. |
credit | The organization used up its credit. | The credit it used this month, in USD. | Its whole spend this month, in USD. |
period is the UTC month, such as "2026-10", and resetsAt is the 1st of the next month. Each one is sent once per limit and month, however many calls are refused.
{ "id": "evt_…", "type": "usage.limit_reached", "createdAt": "2026-10-18T09:41:07.000Z", "projectId": "…", "data": { "kind": "spend_limit", "limit": 200, "used": 200.01, "period": "2026-10", "resetsAt": "2026-11-01T00:00:00.000Z" }}The usage page
The console’s usage page shows where the money goes: this month’s spend and earlier months, and the spend by kind, by project and by API key. It also shows your balance, what is left of the included credit and of each grant, and every limit with how much is spent against it. Any member can open it. What a member sees depends on the role (see Who can set and read limits).
To read usage from code, GET /v1/project/usage totals one project. See Pricing.
Use limits safely
- Set an organization limit now. There is none by default. Your plan’s caps cover model spend, proxy data, CAPTCHA solves and searches. The spending limit also covers session time.
- Give risky work its own project and its own limit: tests, loops, and agents you are still tuning. Start low, and raise the limit after you have seen the usage.
- Make sure someone reads the alerts. The emails go to owners and billing members only. Use a shared mailbox for one of them. For a fast reaction, send
usage.limit_reachedto your own system and page your team (see the example below). Always verify the signature. - An API key cannot change a limit. A leaked key cannot raise or remove it. It can still spend up to the limit, so revoke a leaked key at once. As a quick stop, set the project’s limit to 0: nothing new starts in it, and its running sessions stop within a minute.
- Keep the roles tight. Only owners, admins and billing members change limits. Check the audit list for
billing.limit_changednow and then. - Do not retry a refused call.
402 spend_limitand402 out_of_creditwill not change by themselves. Raise the limit, wait for the 1st, or add credit. - Do not rely on credit to stop work. Using up your credit alerts you, and does not stop anything. A spending limit does.
Handle it in your code
A call refused at a limit fails before anything starts, so nothing is billed. In the SDKs it is a BoxlineError with status 402. Check code, not the message.
curl -X POST https://api.boxline.dev/v1/sessions \ -H "x-api-key: $BOXLINE_API_KEY" \ -H "content-type: application/json" \ -d '{"browser": true, "timeout": 600}'import { Boxline, BoxlineError } from "@boxline/sdk";const bx = new Boxline(); // reads BOXLINE_API_KEY and BOXLINE_API_URLtry { const session = await bx.sessions.create({ browser: true, timeout: 600 }); console.log("started", session.id); await session.stop();} catch (err) { if (err instanceof BoxlineError && err.status === 402 && (err.code === "spend_limit" || err.code === "out_of_credit")) { // Do not retry. Only a raised limit, the 1st of the month or more credit changes this. console.error("A limit stopped this:", err.message); } else { throw err; }}from boxline import Boxline, BoxlineErrorbx = Boxline() # reads BOXLINE_API_KEY and BOXLINE_API_URLtry: session = bx.sessions.create(browser=True, timeout=600) print("started", session.id) session.stop()except BoxlineError as err: if err.status == 402 and err.code in ("spend_limit", "out_of_credit"): # Do not retry. Only a raised limit, the 1st of the month or more credit changes this. print("A limit stopped this:", err.message) else: raise{ "error": { "code": "spend_limit", "message": "this project reached its monthly spending limit ($50.00); the organization's owners, admins and billing members can raise it in the console, or it starts over on the 1st (UTC)", "requestId": "req_4fJx0aQm2Lk9Pz7N" }}To see why a session stopped, read its stopReason:
curl https://api.boxline.dev/v1/sessions/$SESSION_ID \ -H "x-api-key: $BOXLINE_API_KEY"# "status": "STOPPED", "stopReason": "spend_limit"import { Boxline } from "@boxline/sdk";const bx = new Boxline(); // reads BOXLINE_API_KEY and BOXLINE_API_URLconst sessionId = process.argv[2]!; // the id of the session to look atconst session = await bx.sessions.get(sessionId);const reason: string | null = session.data.stopReason; // a plain string: a new reason never breaks the buildif (reason === "spend_limit" || reason === "out_of_credit") { console.log("Stopped by a spending limit or by credit. Raise the limit, then start a new session.");}import sysfrom boxline import Boxlinebx = Boxline() # reads BOXLINE_API_KEY and BOXLINE_API_URLsession_id = sys.argv[1] # the id of the session to look atsession = bx.sessions.get(session_id)if session.data["stopReason"] in ("spend_limit", "out_of_credit"): print("Stopped by a spending limit or by credit. Raise the limit, then start a new session.")To be told when a limit is reached, subscribe an endpoint to usage.limit_reached, and handle the three kinds above. The signing secret is shown once.
curl -X POST https://api.boxline.dev/v1/webhooks \ -H "x-api-key: $BOXLINE_API_KEY" \ -H "content-type: application/json" \ -d '{"url": "https://example.com/webhooks/boxline", "events": ["usage.limit_reached"]}'# The answer holds "secret": "whsec_…", shown only this once. Put it in your secret manager.import { Boxline } from "@boxline/sdk";const bx = new Boxline(); // reads BOXLINE_API_KEY and BOXLINE_API_URLconst endpoint = await bx.webhooks.create({ url: "https://example.com/webhooks/boxline", events: ["usage.limit_reached"],});// endpoint.secret ("whsec_…") is shown only now: put it in your secret manager, never in code or logs.console.log(endpoint.id);from boxline import Boxlinebx = Boxline() # reads BOXLINE_API_KEY and BOXLINE_API_URLendpoint = bx.webhooks.create("https://example.com/webhooks/boxline", ["usage.limit_reached"])# endpoint["secret"] ("whsec_…") is shown only now: put it in your secret manager, never in code or logs.print(endpoint["id"])import { verifyWebhook, WebhookSignatureError, type WebhookEvent } from "@boxline/sdk";type LimitData = { kind: string; limit: number; used: number; period: string; resetsAt: string | null };const MONEY = new Set(["spend_limit", "project_spend_limit", "credit"]);// body: the raw request body exactly as it arrived; header: its Boxline-Signature. Returns the status to answer.export function receive(body: Buffer, header: string | undefined): number { let event: WebhookEvent<LimitData>; try { event = verifyWebhook<LimitData>(body, header, process.env.BOXLINE_WEBHOOK_SECRET!); } catch (err) { if (err instanceof WebhookSignatureError) return 400; throw err; } if (event.type === "usage.limit_reached" && MONEY.has(event.data.kind)) { const { kind, limit, used, resetsAt } = event.data; console.warn(`limit ${kind} reached (${used} of ${limit} USD), resets ${resetsAt}`); // Page your team here. } return 200; // any other type: ignore it, and still answer 200}import osfrom boxline import WebhookSignatureError, verify_webhookMONEY = {"spend_limit", "project_spend_limit", "credit"}def receive(body: bytes, header) -> int: """body: the raw request body exactly as it arrived; header: its Boxline-Signature. Returns the status to answer.""" try: event = verify_webhook(body, header, os.environ["BOXLINE_WEBHOOK_SECRET"]) except WebhookSignatureError: return 400 if event["type"] == "usage.limit_reached" and event["data"]["kind"] in MONEY: d = event["data"] print(f"limit {d['kind']} reached ({d['used']} of {d['limit']} USD), resets {d['resetsAt']}") # Page your team here. return 200 # any other type: ignore it, and still answer 200More
- Organizations, projects and team members: roles, and how limits are shared by the projects.
- Plans and features: your plan’s caps and features.
- Pricing: what each kind of usage costs.
- Webhooks: signatures, retries and every event type.
- Errors and request ids: every error code.
These docs describe a beta API. Endpoints, fields and SDK methods may change before general availability; breaking changes will be listed in the changelog.