Before you start
Key: API calls need a Loopster API key. See Create and revoke a Loopster API key. Send it as
Authorization: Bearer loopster_sk_…. An MCP client can send the same header, or sign in through OAuth.Address: the base URL is
https://app.loopster.ai/api. Every path starts with/v1.Workspace: your key decides which workspace you work in. No call takes a workspace id.
Ids: table, row and run ids are UUIDs. Column ids start with
col_. A table id in the path that is not a UUID gets a 400, not a 404.Auto-run: the API calls a table's Auto-run setting
autoplay. Read it withGET /v1/tables/{tableId}. Change it withPATCH /v1/tables/{tableId}(MCP:set_table_autoplay). Check it on every table before you insert rows.Important: with Auto-run on, rows you insert are queued and can spend from your wallet. With it off, an insert only writes rows. Switching it on does not run the rows already in the table.
MCP: the MCP tool for each step is in parentheses. Where a tool takes a column, you can give its name or its id.
Which calls can spend from my wallet?
These calls can start paid work, so they can spend from your wallet:
Triggering rows with
POST /v1/tables/{tableId}/trigger, unless it is a dry run (MCP:trigger_rows).Running a function with
POST /v1/functions/{functionId}/runs(MCP:run_function). Every step inside the function bills as it would in a table.Running a capability with
POST /v1/capabilities/{templateId}/runs(MCP:run_capability). It spends from your wallet unless it runs on your own connection.Creating a table from a paid starter with
POST /v1/tables(MCP:create_table). A blank, webhook or search table is free.Inserting rows with
POST /v1/tables/{tableId}/rows, or a row arriving through the table's webhook, when the table has Auto-run on (MCP:insert_rows).Creating a schedule with
POST /v1/tables/{tableId}/schedules(MCP:create_schedule). It spends each time it fires, until it is paused or deleted.Resuming a schedule with
PATCH /v1/tables/{tableId}/schedules/{scheduleId}andpaused: false(MCP:update_schedule). It spends when it fires.Updating a row or cells with
PATCH /v1/tables/{tableId}/rows/{rowId}orPATCH /v1/tables/{tableId}/cells(MCP:update_row,update_cells). The tool descriptions say that on a table with auto-rerun columns, writing a cell also re-queues the columns that read it, and that spends from your wallet.
These do not start paid work by themselves: a dry run (estimate_trigger_cost), creating a blank or webhook table, inserting rows into a table with Auto-run off, and reading a trace. Stop and cancel prevent more spending. They do not give money back.
For prices in the app and where to add funds, see Your wallet and keeping spend under control.
How do I add a column from a template?
List the templates with
GET /v1/tables/{tableId}/columns/templates(list_column_templates). Narrow the list withtype(manual,ai,apiortask),providerortab(import,enrich,tools,functionsorexport).Read the one you want with
GET /v1/tables/{tableId}/columns/templates/{templateId}(get_column_template). It shows the template's settings and defaults. For a task template it also gives the JSON Schema of itstaskConfig.Check the template's
cost. The next section explains it.Create the column with
POST /v1/tables/{tableId}/columnsand atemplateId(create_column). To add up to 20 at once, send{"columns": [ ... ]}(create_columns). They are created in order, and it is all or nothing. If one is refused, none is stored.Set the column up before you run it with
PATCH /v1/tables/{tableId}/columns/{columnId}(update_column). A column left on a template's defaults runs on those defaults and bills for them. An AI column made from a model template comes with a model and no prompt. It does not run until you setaiConfig.prompt.
What does a template's cost tell me?
Each template can carry a cost that says what one row costs from your wallet. Its kind is one of:
free: Loopster charges nothing.byok: the column runs on your own connected account, so nothing leaves the wallet.usage_based: there is no single figure. The steps inside bill as they run.per_row: a dollar amount per row, inusd.
For AI templates and waterfalls the per-row figure is approximate. A waterfall also has maxUsd, the most one row can cost. A manual or api template has no cost key. A missing cost means a price does not apply. It does not mean the price is zero.
How do I see what a run will cost first?
Send the request you plan to make with
?dryRun=trueon the address:POST /v1/tables/{tableId}/trigger?dryRun=true(MCP:estimate_trigger_cost, which takes the same arguments astrigger_rows). Nothing is queued and nothing is charged.Read the size of the run in
rows,columnsandcellsToRun.perColumnbreaks it down by column.Read
maxCostUsd. It is a ceiling, not a forecast. It leaves out any column whose price cannot be known before it runs.unpricedColumnssays how many.Read
wallet.spendableUsdis what the wallet can spend now.rowsCoveredis how many rows it covers.wouldStartsays whether the run would start at all.
Two things to know about the estimate:
Run conditions are not evaluated. Fewer rows than quoted may run, never more.
A cell that already holds a finished answer is left out, because an unforced trigger does not re-run it. To price a forced re-run, send
force: truein the dry run too.
How do I test one row, then run the rest?
Important: without rowIds, a trigger runs every row in the table and spends accordingly. Always send rowIds, and price the run with a dry run first. Through MCP, call estimate_trigger_cost first, or pass specific rowIds.
Pick one row. Its id comes from list rows,
GET /v1/tables/{tableId}/rows.Price it. Send
POST /v1/tables/{tableId}/trigger?dryRun=truewith the body{"rowIds": ["{rowId}"], "columnIds": ["{columnId}"]}.columnIdslimits the run to the columns you name.Run it. Send the same request without
?dryRun=true. The answer comes back at once with arunId, and the work continues in the background.Check the result. See the next sections.
Price the rest, then run it. Send the other row ids as
rowIds, in batches of 10,000 or fewer. An unforced trigger does not re-run a cell that already holds a finished answer, and does not charge for it.force: truere-processes completed cells.
This is the same habit as in the app. See Run columns safely: Auto-run, test one row, then run the rest.
How do I cap what a trigger can cost?
Send maxCostUsd. Loopster estimates the run first, the way a dry run does. If the estimate is over the cap, or the run includes a column with no up-front price and you did not send allowUnpriced: true, nothing is queued and the answer is 422. The cap is a check at the start, not a meter. A run that starts is not stopped part-way, and an AI column's real cost can come in above its estimate.
How do I retry a trigger without running it twice?
Send an Idempotency-Key header (MCP: idempotencyKey) so a retry is safe. Use 1 to 128 characters: letters, digits, _, -, . or :. Use a new key for each new action. Repeat the same request with the same key within 24 hours and you get the first answer back, with "replayed": true. Nothing runs or bills twice.
How do I check how a run is going?
Enrichment is asynchronous and can take minutes, so poll.
GET /v1/runs/{runId}(get_run) reports on one run.statusisrunninguntil every cell of the run has settled, thencompleteorcompleted_with_errors. It isstoppedif a stop reached any of its cells.cellscounts the run's cells: total, inFlight, complete, error, skipped, unsatisfied and notRun.notRunmeans never reached, or reset by a stop.GET /v1/runs/{runId}/items(list_run_items) lists the rows in a run. Each row shows its current state:running,complete,error,incompleteornot_run. A row iscompletewhen all its cells settled without an error. A skipped or unsatisfied cell counts as settled.incompletemeans some cells settled and the rest never started, which a stop leaves behind. A trigger run returns row ids and states, never cell values.GET /v1/tables/{tableId}/status(get_status) reports on the whole table.rowscounts total, idle, queued, generating, complete and error.columnslists every column with its own counts, whether or not it has ever run. It covers all runs on the table, not only yours.GET /v1/runs(list_runs) lists the workspace's runs, newest first. Filter withkind,statusandsince.limitis 1 to 100 (50 by default), andcursorgets the next page.
For the items call, limit is 1 to 100 (100 by default). Pass the previous nextCursor as cursor. A cursor works only for the run it came from.
A few things to know:
A run is never reported finished in its first 30 seconds. A faster run still reads
runninguntil then.The answer for one run is cached for up to 5 seconds. If it is still being worked out,
cellsis null with aRetry-Afterheader, or the last answer comes back withstale: true.The run list shows what is stored, so a finished run nobody has read since can still read
runningthere. Use the single run for live counts.A workspace keeps its 500 most recent runs, for up to 30 days. After that the
runIdanswers 404.
How do I read the results?
Call list rows:
GET /v1/tables/{tableId}/rows(list_rows).pageis 1 by default.pageSizeis 50 by default, up to 500.columnslimits which cells come back.Read each row.
cellsholds the values, keyed by column id.cellStatusestells an empty value apart from a skipped, failed or never-run one.cellErrorsholds the message for a failed cell.Read the row's
status. It is worked out from its cells: still working if any cell is, otherwise error if any cell failed, otherwise complete once anything has run, otherwise idle.To read one row in full, call
GET /v1/tables/{tableId}/rows/{rowId}(get_row). Each cell comes with its value, its status and the error message if it failed.
To find which rows of a run failed, list the run's items, then read those rows. In the app, a failed cell shows its message in the cell, and the labels are Error, Unsatisfied and Skipped. See What a cell's message means.
Why is a waterfall cell empty, and which provider answered?
Call GET /v1/tables/{tableId}/rows/{rowId}/trace (get_waterfall_trace). Pass columnId to trace one waterfall column, or leave it out to trace every traceable column on the row. It only reads. It never re-runs anything.
The answer shows:
What the waterfall settled on:
result,statusandreason, and whether the answer was verified.Every provider it tried, in order:
order,provider,status,outcome,reasonandwon. Theoutcomeis one offound,no_match,not_verified,duplicate,out_of_date,wrong_line_type,not_used,skippedorerror.Two costs:
chargedUsdis what the wallet was billed for the run.attemptedUsdis what the individual steps consumed, plus any check run on a shared-pool address before a step. Attempted can be higher than charged where only the successful lookup is billed.
If successfulProvider is shared_pool, the answer came from Loopster's shared contact pool. No step ran, so attempts is empty. That is free, unless the column verifies its answers and the pooled address had to be checked first. Then the cost is that one check.
How do I stop a run, and is cancel a refund?
You have two brakes. Both prevent further spending. Neither is a refund.
How do I stop everything on a table?
Send POST /v1/tables/{tableId}/stop (MCP: stop_run, which takes a tableId). It stops all ongoing processing for the table. Work that was already charged is not refunded.
The answer reports:
queuedJobsRemoved: queued work removed before it could start. Nothing was charged for it.cellsReleasedandrowsReleased: what the stop released.stillRunning: the cells that work already past the brake may still run, and bill for.cleanupContinues: when it is true, the released counts are lower bounds so far. Nothing new starts either way.
A null count means the worker did not report it. It is not the same as zero. You can call stop twice. With nothing running it reports zero.
How do I cancel one run?
Send POST /v1/runs/{runId}/cancel (MCP: cancel_run). It removes the work of one run that is still queued, across the columns it ran. Work already running finishes and is billed.
The answer gives stoppedCells (queued cells removed before they started, with nothing charged) and alreadyRunningCells (they finish and are billed). A row that another run had already queued is not part of this run, so only that run's cancel stops it. Cancelling a run that already finished, or cancelling twice, changes nothing. The answer says canceled: false with the stored status. Any editor in the run's workspace may cancel any run there.
What happens if my wallet runs short?
Paid work holds its worst-case cost from your wallet before it runs, and is charged what it really cost, never more. The wallet is never overdrawn.
If a run is bigger than the wallet, rows run in order while the wallet covers them. The rest are refused, not run. Their cells show a not-enough-balance error, and nothing is charged for them.
If the wallet cannot cover even the first row, the call is refused with a 402 before anything is queued.
When a call that would start paid work is refused with a 402, nothing was created, queued or charged. The body carries code, requiredUsd (the most the first row or input can cost) and spendableUsd (what the wallet can spend right now, capped by the workspace's spend limit if one is set). The code says which limit refused the call:
insufficient_balance: the wallet cannot cover it. Click the wallet button at the top right, then Manage wallet. Settings opens on Plan & billing, on the Wallet tab, with a Top up button and an Auto top-up card.workspace_limit: the workspace's own spend limit leaves less than the wallet. Open Settings, then Workspaces, click the workspace and change Spend limit with Set spend limit.daily_cogs_limit: the account reached its daily spend safety limit. Topping up does not lift it. It resets at midnight UTC, and Loopster support can raise it.
If the refused call carried an Idempotency-Key, the key is released. The same key runs once you have topped up.
To look before you run, read wallet in a dry run. GET /v1/wallet (get_wallet) shows how much the next run can spend, the workspace's plan and any spend limit on the workspace. GET /v1/usage (get_usage) shows what the workspace spent, by table, by column or by day. Amounts are US dollars. The wallet holds real dollars, not credits. See Top up your wallet, set auto top-up and read your funds history and Workspaces: create one and set a spend limit.
How do I run columns on a schedule?
A schedule runs columns on a table again and again, on its own. Schedules work over both the API and MCP.
Important: a schedule spends from your wallet every time it fires, with nobody watching, until you pause or delete it. Send paused: true to save one without letting it fire.
Create it with
POST /v1/tables/{tableId}/schedules(create_schedule). This needs editor access. Listing schedules withGET /v1/tables/{tableId}/schedules(list_schedules) needs member access.Name the
columns: 1 to 50, by id or by name when no other column shares it. Each must be an AI, API or task column that is set up to run.Set the timing.
cadenceisdaily,weekly,monthlyorinterval.timeis HH:MM on a 24-hour clock (forinterval, the time of the first run).dayOfWeekruns from 0 (Sunday) to 6 (Saturday).dayOfMonthruns from 1 to 28.everyis{count, unit}in hours, days or weeks, at least one hour.Choose which cells each run covers with
cells:empty,error,unsatisfiedorall. The default isall.forcedefaults to true andrerunDependentsto false. Check these before the first firing.Add
timezone, an IANA name such asEurope/Berlin. It defaults to UTC. Loopster converts the time to UTC at the zone's current offset, so a daylight-saving change moves the run by an hour. Times are stored and returned in UTC.
To change a schedule, send PATCH /v1/tables/{tableId}/schedules/{scheduleId} (update_schedule). Only the fields you send change, except the timing. The timing is replaced whole: if you send any of cadence, time, timezone, dayOfWeek, dayOfMonth or every, send cadence and time with it.
paused: truetakes the schedule off the scheduler so it cannot fire.paused: falsestarts it again, and it spends when it fires.DELETE /v1/tables/{tableId}/schedules/{scheduleId}(delete_schedule) makes it never fire again. A run it already started is not stopped.POST /v1/tables/{tableId}/stopdoes that.A table can have 10 active schedules. Resuming a paused one counts against that limit.
A schedule records which key made it. If that key is revoked, or expires before a firing, Loopster pauses the schedule and
pausedReasonreadskey_revokedorkey_expired. For a schedule you paused yourself,pausedReasonis null. Resuming clears it.Each schedule in the list also shows
lastRunAt,nextRunAtand atarget. Aflowtarget runs the table's whole flow, as set up in the app.
Can I see an API schedule in the app?
Yes. A schedule made over the API is stored the way the app's schedule editor stores it, so you can open and change it there. Click Table settings next to the table name. The panel has a Scheduled column reruns section. In the toolbar, Scheduled column runs shows what is scheduled. See Table settings: Auto-run, scheduled reruns and deduplication.
Troubleshooting
Fewer rows started than I sent
What you see: The trigger answered 200, but triggeredCount is lower than the number of rows you sent.
Why it happens: triggeredCount is the rows that were actually enqueued. deduplicated rows already had a run in progress, and nothing extra is charged for them. rejected rows were refused because the queue is full. They did not start and will not start on their own. unavailable shows only when it is above zero. Those rows could not be confirmed, so they may or may not have started. The four numbers add up to what you asked for, and a message says why when some rows did not start.
What to do: Trigger the rejected rows again. For unavailable rows, retrying an unforced row is safe, because a row that did start counts as deduplicated. Retrying a forced run (force: true) can start a row twice.
My trigger answered 503 or 500
What you see: A 503 with code: nothing_started and a Retry-After header, or a 500.
Why it happens: Loopster could not hand the run to its workers, and the request to them was never sent. Nothing was queued and nothing was charged. A 500 with code: nothing_started_internal also started nothing, but it is a fault on Loopster's side, so sending it again now will likely fail the same way. Any other 500 makes no promise: rows may have started.
What to do: After a 503, send the same request again after Retry-After seconds. Use the same Idempotency-Key if you sent one, because this answer is not stored against it. After nothing_started_internal, try later. After any other 500, rows may have started, so check the table's status before you send again.
My trigger answered 400
What you see: A 400, and nothing runs.
Why it happens: A request covers at most 10,000 rows. With no rowIds, a table of more than 10,000 rows is refused. A 400 also comes back when a rowIds entry is not a UUID, when a columnIds entry is not a column of this table, or when none of your row ids belong to this table. If only some belong to another table, those are skipped and listed in notInTable.
What to do: Send rowIds in batches of 10,000 or fewer. Take ids from list rows and list columns. An empty rowIds list runs nothing.
My trigger answered 422
What you see: A 422 with code: cost_cap_exceeded or cost_cap_unpriced_columns.
Why it happens: You sent maxCostUsd, and nothing was queued. cost_cap_exceeded means the estimate is over the cap. cost_cap_unpriced_columns means a column has no up-front price and you did not send allowUnpriced.
What to do: Raise the cap, run fewer rows, or send allowUnpriced: true with the cap. allowUnpriced without maxCostUsd is a 400. This refusal releases an Idempotency-Key, so the same key works once the cap is raised.
I topped up but still get a 402
What you see: A 402 with code: daily_cogs_limit after you added funds, and spendableUsd is 0.
Why it happens: daily_cogs_limit is the account's daily spend safety limit. It is not a low balance. Topping up does not lift it, and asking for less does not help.
What to do: Wait for it to reset at midnight UTC, or ask Loopster support to raise it. See Contact support.
The run still says running, or cells is null
What you see: A run you expect to be done reads running, or cells is null.
Why it happens: A run is never reported finished in its first 30 seconds. The run list shows what is stored, so it can lag. When another request is still working out the answer, cells is null with a Retry-After header.
What to do: Ask again after the Retry-After seconds. Use GET /v1/runs/{runId} for live counts. If the run items call answers 503 with Retry-After, wait and call it again.
The call succeeded but some cells show errors
What you see: The calls answered success and the run finished, but some cells failed.
Why it happens: A call can succeed while single cells fail to enrich. The status call counts how many cells errored, but not why.
What to do: List the run's items, find the rows in error, and read those rows. cellErrors in list rows, or get_row, gives the message. In the app, the failed cell says so, and the footer shows a count such as "1 cell failed".
I stopped a run and was still charged
What you see: The wallet went down after you called stop or cancel.
Why it happens: Stop and cancel prevent further spending. They do not refund. Work that was already charged stays charged, and work already running finishes and is billed.
What to do: Read stillRunning in the stop answer, or alreadyRunningCells in the cancel answer. A null stillRunning means unknown, not zero. Next time, price the run with a dry run and send maxCostUsd.
Good to know
Deleting a table does not stop work that is already running. Stop the run first, or the queued work finishes and bills against the table in Trash.
Through the API, a column you create without naming an account never starts on Loopster's account. The exceptions are a provider whose default your workspace saved as Loopster's account, and an action that runs only on Loopster's account. Name an account with
integrationConnectionIdon the column.If an
Idempotency-Keycannot be used, you get a 409 and nothing runs.idempotency_key_reusedmeans the key was used for a different request.idempotency_key_in_progressmeans the first request is still running, so retry afterRetry-Afterseconds.idempotency_outcome_unknownmeans the first request never finished, so whether it started work is unknown.A stored failure replays as the same failure. Send a new key to try again.
A 429 means a rate limit was reached and nothing was done. Wait the seconds in the
Retry-Afterheader, then send the call again.Six MCP read tools take
response_format:list_tables,get_table,list_columns,list_rows,get_rowandget_status.detailedis the default.concisekeys cells and counts by column name and truncates long values. Usedetailedbefore you change a column.The full list of calls is the OpenAPI file at
https://app.loopster.ai/openapi.json.
Next: Stop Processing · Cancel a Run · Create a Schedule
More about the API and MCP



