How do I read an error from the API?
The API answers in JSON. A failed call has success set to false and an error message. Some failures also carry a code that you can branch on, plus fields that explain the refusal. A 402, for example, adds requiredUsd and spendableUsd. The docs show the same format under Error format.
Read the error text and the code, not only the status number. One number can have more than one cause. A 403 has several.
The MCP tool descriptions use the same codes. For example, insert_rows refuses too many rows with too_many_rows.
Which calls and tools can spend from my wallet?
The wallet holds real dollars, not credits. These calls can spend from it:
Triggering rows:
POST /v1/tables/{tableId}/triggeror the MCP tooltrigger_rows. A dry run does not spend.Running a function (
run_function) or a capability (run_capability). A capability that runs on your own connection does not spend.Creating a table from a paid starter (
create_table). Blank and webhook tables are free, and so is thesearchtype.Inserting rows (
insert_rows) or receiving rows through a table's webhook, when the table has Auto-run on. The API calls this settingautoplay.Updating a row or cells (
update_row,update_cells) on a table with auto-rerun columns. The columns that read the changed cells are queued again.Creating a schedule (
create_schedule). It spends every time it fires, with nobody watching, until you pause or delete it.update_schedulewithpaused: falsestarts one again.
In the app, test inputs in the function editor are charged to your wallet too.
These do not spend by themselves: a trigger dry run, estimate_trigger_cost, search_people and search_companies, importing a file, exporting a table, and reading a trace with get_waterfall_trace.
Stopping a run keeps queued work from starting. It does not refund what was already charged, and work that is already running still bills.
Troubleshooting
Quick answer
401: the key is missing, or it is past its expiry.403: often the key's role is too low, the table's API is off, or the data belongs to another workspace.404: often a table in Trash, or a webhook URL with a wrong secret.400: the call was malformed. Often an id that is not a UUID.402: a call that would start paid work was refused. The wallet, the workspace's spend limit or the account's daily spend safety limit stopped it.409: often a run that is still computing the cells you tried to write, or anIdempotency-Keythat cannot be used.413: often too many rows in one insert.422: often a value that was too large, a cost cap that was hit, or a search that ran out of time.429: wait theRetry-Afterseconds, then send the call again.503or500when you start a run: read thecodebefore you retry.No error, but something is wrong: an insert that spent money, rows that did not start, a run that seems stuck, cells that show errors, or a tool that is missing.
I get 401
What you see: A 401 with success set to false. If the code is key_expired, go to the next entry.
Why it happens: The call has no Authorization header with your key. Every call needs one, except a table's webhook URL.
What to do:
Send your key as a Bearer token:
Authorization: Bearer <your key>. A Loopster API key starts withloopster_sk_.Test the key with
GET https://app.loopster.ai/api/v1/key. It returns the key's name, role and expiry.If you connect an AI client with a key, the client must send the same header.
Find your keys under Settings, then Loopster API. See Create and revoke a Loopster API key.
I get 401 and the code is key_expired
What you see: A 401 with code set to key_expired. The error text holds the expiry date.
Why it happens: The key is past its expiry. A key stops working at that moment. You choose the expiry when you create the key: Never expires, Expires in 30 days, Expires in 90 days or Expires in 1 year. It cannot be changed afterwards.
What to do:
Create a new key under Settings, then Loopster API, and switch your tool to it.
To see when another key ends, call
GET /v1/keywith it.expiresAtisnullfor a key that never expires. The key's row on the Loopster API page also shows when it expires.Check your schedules. A schedule made with a key is paused if that key is revoked, or expires before the schedule fires. Its
pausedReasonthen readskey_revokedorkey_expired. A schedule spends every time it fires, so resume one only when you want that.
I get 403 and the answer names a role
What you see: A 403 that carries requiredRole and role.
Why it happens: The key's role is below what the call needs. requiredRole is the role the call needs: member, editor or admin. role is the role your key holds. For example, a live publish of a function needs an admin key. Share links, file imports, schedule changes and restoring from Trash need an editor key. The same roles apply over MCP.
What to do:
Create a new key with a role that allows the call. In the app the roles read Member — read only, Editor — read, write and run and Admin — everything an API key can do.
Give the key the least access it needs.
GET /v1/keyshows the role of the key you call it with.
I get 403 on one table, or on another workspace's data
What you see: A 403 on a call that names a table or another resource. If the answer names a role, see the entry above.
Why it happens: Two reasons for a 403 on a table call are that the table's API is switched off (apiEnabled is off) and that the resource belongs to another workspace. A key works only in the workspace that owns it, and no endpoint takes a workspace id.
What to do:
Check that the key belongs to the workspace that owns the table. Open Settings, then Workspaces, and click the workspace. Its page lists its API Keys.
You cannot switch
apiEnabledon through the API. Sendingfalseis refused with400, and sendingtruechanges nothing. If you need API access on for that table, tell us with Need Help? at the top right of any page.A search that names one of your tables in
exclusionsalso answers403when that table's API access is off. Exclude by a list of values instead.
I get 404 on a table
What you see: A 404 on a call that names a table id.
Why it happens: The usual causes are these:
The table was deleted. A delete moves it to Trash. A table in Trash is no longer reachable through the API, and deleting it again answers
404.On calls that move tables, workbooks and folders, an item from another workspace also answers
404.
An id that is not a UUID gets a 400, not a 404.
What to do:
List your tables with
GET /v1/tables(MCPlist_tables) and check the id.Look in Trash with
GET /v1/trash(MCPlist_trash). In the app, click the bin icon at the bottom of the left sidebar. Deleted items are kept for 30 days. A table that was deleted with its workbook is not listed on its own.Restore it with
POST /v1/trash/{itemId}/restore(MCPrestore_from_trash). It needs an editor key. In the app, click Restore on its row. For a table that went down with its workbook, restore the workbook. Restoring runs nothing and bills nothing.
See Restore deleted tables and functions from the Trash and the docs page Restore from Trash.
My webhook URL answers 404
What you see: 404 Webhook not found from a table's webhook URL (POST /v1/webhooks/{secret}).
Why it happens: Three cases give the same answer on purpose: the secret is wrong, the table is in Trash, or webhook listening is switched off for the table. Rotating a table's address also makes the previous one stop working at once.
What to do:
Check the secret. The secret in the URL is the credential. Anyone who holds it can add rows to the table, so keep it private.
GET /v1/tables/{tableId}/webhookshows only the last four characters of the address. The full URL is shown once, in the answer ofPOST /v1/tables/{tableId}/webhook/rotate(MCProtate_table_webhook). Both calls need an editor role. Rotating makes a new address, and the old one stops working at once.Check that the table is not in Trash.
Use
POST. Any other method answers405.If the secret is right and the table is not in Trash, webhook listening may be switched off for the table. Ask us with Need Help?.
I get 400 on a call that names an id
What you see: A 400 on a call that names a table, row, view, function or run.
Why it happens: Table, row, view, function and run ids are UUIDs. An id that is not a UUID is refused with 400 before any lookup. This is on purpose, so a typo is not mistaken for a missing table. An id with a prefix, such as row_, is not a UUID. Column ids are different. They look like col_XXXX. In a trigger or a delete, one id that is not a UUID rejects the whole request.
What to do: Take ids from Loopster's own answers. Use list_tables for table ids, list_rows for row ids and list_columns for column ids. Over MCP, tools that take a column accept its name or its id.
My insert is refused with 400
What you see: POST /v1/tables/{tableId}/rows (MCP insert_rows) answers 400.
Why it happens: The usual causes are these:
A record would be stored empty. None of its keys matches a column, and the table neither creates columns from keys nor keeps the raw record in a trigger column.
An item in your array is not an object. A number, a string,
nullor a nested array is rejected, and the answer names the index.You wrapped the rows in an object whose only key holds them, such as
{"records": [...]}.
What to do:
Send the array itself, or one object. An empty object
{}still inserts a blank row.Key each value by a column id or a column header.
list_columnsshows both.Keys that match no column come back in
ignoredKeys. To add a column for each new key instead, turn oncreateColumnsFromKeyswithPATCH /v1/tables/{tableId}(MCPset_table_create_columns_from_keys). It is off by default. Each new key becomes a manual column, listed incolumnsCreated.
My trigger is refused with 400
What you see: POST /v1/tables/{tableId}/trigger (MCP trigger_rows) answers 400, and nothing runs.
Why it happens: The usual causes are these:
The table has more than 10,000 rows and you sent no
rowIds.A row id is not a UUID. One bad id rejects the request.
None of the row ids belongs to the table. If only some do not, those rows are skipped and listed in
notInTable.A column in
columnIdsis not a column of the table.You sent
allowUnpricedwithoutmaxCostUsd.
What to do: Send rowIds in batches of 10,000 or fewer. Take row ids from list_rows. Without rowIds a trigger runs every row and spends accordingly, so price it first with a dry run (?dryRun=true).
I get 413 and the code is too_many_rows
What you see: A 413 with code set to too_many_rows. The answer gives maxRows and received.
Why it happens: One insert request carries at most 25,000 rows. A larger one is refused before anything is written. MCP insert_rows has the same limit. A very large insert also runs under a 300-second limit, and one that is still running then is cut off.
What to do:
Send batches of 10,000 rows or fewer.
Or import a file. Over REST, ask
POST /v1/tables/{tableId}/imports/upload-urlfor an address, upload the file to it, then callPOST /v1/tables/{tableId}/importswith thefileId. Over MCP, usecreate_upload_linkand thenimport_file. An import starts no enrichment and costs nothing.
A 413 on a function run or a capability run is a different limit. The request body can be at most 4,000,000 bytes. A bigger body is refused with 413, and nothing starts. Split the list into smaller calls. A run also takes at most 1,000 inputs.
I get 402 or my run did not cover every row
What you see: A 402 with a code, requiredUsd and spendableUsd. Or a run where only some rows ran, and the rest show a not-enough-balance error.
Why it happens: Paid work holds its worst-case cost from the wallet before it runs. It is charged what it really cost, never more. If the wallet cannot cover even the first row or input, the call is refused with 402. Nothing is created, queued or charged. If the wallet covers some rows, they run in order. The rest are refused, not run, and not charged. The wallet is never overdrawn. The code says which limit refused the call:
insufficient_balance: the wallet cannot cover it.workspace_limit: this workspace's own spend limit leaves less than the wallet.daily_cogs_limit: the account reached its daily spend safety limit.
What to do: Read the code, then:
insufficient_balance: top up, or ask for less. Click the wallet button at the top right, then Manage wallet. Plan & billing opens on the Wallet tab. Click Top up. If you sent anIdempotency-Key, the refusal released it, so the same key runs once you have topped up.workspace_limit: open Settings, then Workspaces, and click the workspace. Change its Spend limit with Set spend limit. All workspaces in an organization share one wallet. The limit is per workspace.daily_cogs_limit: topping up does not lift it, andspendableUsdis 0. It resets at midnight UTC. Support can raise it. Click Need Help? at the top right of any page.
Next time, price the run first. Add ?dryRun=true to the trigger call, or use estimate_trigger_cost. Its wallet block says how many rows the wallet covers and whether the run would start. See the docs page Trigger Processing and Top up your wallet, set auto top-up and read your funds history.
I get 409
What you see: A 409 with a code.
Why it happens: Each code names its own cause.
What to do: Read the code, then:
cell_runningon Update Row: a run is still computing every cell your update writes, so nothing was saved. Send the same request again once the run finishes. If you sent anIdempotency-Key, send the same key. This refusal does not use it up. If other cells were written, the answer is200and the held cells are listed inrejectedwith the reasonrunning. Send those again once the run finishes.waterfall_runningon Update Column: the waterfall has rows running. Changing it now would make them run and be charged again. Nothing changed. Send the change again when they finish.sandbox_requiredon a function sandbox call: open a sandbox first withPOST /v1/functions/{functionId}/sandbox(MCPopen_function_sandbox).idempotency_key_reused: you used the key for a different request. Use a new key for each new action.idempotency_key_in_progress: the first request is still running. Retry after theRetry-Afterseconds.idempotency_outcome_unknown: the first request never finished, so whether it started work is unknown. Check the run or the table's status before you try again.
In the three idempotency cases, nothing runs.
I get 422
What you see: A 422, usually with a code.
Why it happens: Each case below has its own cause.
What to do: Read the code and the error text, then:
value_too_large: you added?onOversize=rejectand a value was over its column's limit. Nothing in the request was written, and the answer names each value. The limits are 8 kB for typed text, 100 kB for references and 200 kB for enrichment output. WithoutonOversize=reject, an oversized value is stored empty and listed incappedCells. If a cell is empty after you wrote a long value, readcappedCells.cost_cap_exceeded: your trigger carriedmaxCostUsd, and the estimate is higher. Nothing was queued. Raise the cap or run fewer rows. A capability run withmaxCostUsdis also refused with422when the accepted inputs could cost more.cost_cap_unpriced_columns: some columns have no price you can know up front. SendallowUnpriced: truewith the cap if you accept that. The cap is a check at the start, not a meter. A run that starts is not stopped part-way, and AI columns are estimated from observed averages, so their real cost can exceed the estimate.A search answers
422when Loopster Data could not answer it inside its query time limit. That does not mean the query is invalid. Retry the same cursor with a smallerlimitbefore you change the filters.Create Table with a search answers
422when the search matched nothing. No table is created.
I get 429
What you see: A 429 with a code in the body. The headers include Retry-After. A 429 also carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Scope, except provider_busy and a table's webhook URL, which send Retry-After only.
Why it happens: A rate limit was reached and nothing was done. The code says which wait it is:
rate_limited: one of the rate-limit windows.search_concurrency: the workspace already has the maximum number of searches running.search_busy: Loopster Data itself is busy.provider_busy: Create Table's starter found a provider on Loopster's account at its limit, or paused after a burst of its errors. Nothing was charged.
Calls over MCP count against the same limits as REST. Calls that can start paid work also count against their own separate window.
What to do: Wait the number of seconds in Retry-After, then send the call again. Successful calls carry the same X-RateLimit-* headers, so you can see your budget before you hit it. A table's webhook URL sends none. If you keep hitting a limit, tell us with Need Help?.
I get 503 or 500 when I start a run
What you see: A 503 or 500 on a trigger, a function run, a capability run or a column call.
Why it happens: Loopster could not hand the work on, or hit a fault. Whether anything started depends on the case below.
What to do: Read the code, then:
503withcodeset tonothing_started: Loopster could not hand the run to its workers, and the request to them was never sent. Nothing was queued or charged. Send the same request again after theRetry-Afterseconds. Use the sameIdempotency-Keyif you sent one, because this answer is not stored against the key.500withcodeset tonothing_started_internal: nothing was started either, but the fault is on Loopster's side. Sending it again now will likely fail the same way. Try later.Any other
500makes no promise. Rows may have started. Check the run or the table's status before you send it again. Retrying an unforced trigger is safe, because a row that did start counts asdeduplicated. For a function or capability run, retry with a new key.503withcodeset toaccounts_unavailableon Create Column or a capability run: the workspace's connections and saved default accounts could not be read. Nothing was created or started. Retry after theRetry-Afterseconds.A search can answer
503when search is temporarily unavailable or switched off for the workspace. A switched-off search does not clear on an immediate retry. Nothing you have imported is affected.
My insert started spending from my wallet
What you see: Your balance drops after you insert rows. The insert answered 201 with startedEnrichment set to true and enrichment set to started.
Why it happens: The table has autoplay on. This is the table's Auto-run setting. With it on, rows you insert, or receive through the table's webhook, are queued for processing, and that can spend. startedEnrichment is true only when autoplay is on, rows were inserted, and the table has at least one column that an automatic run executes. An AI column with its prompt does. A formula column does not. On POST /v1/tables, autoplay defaults to true. In the app, a new table can start with Auto-run on or off, so check it on every table.
What to do: Stop the work first. Stop the whole table with POST /v1/tables/{tableId}/stop (MCP stop_run). Or cancel one run with POST /v1/runs/{runId}/cancel (MCP cancel_run). A stop reports stillRunning, the number of cells that may still run and bill. A null count means unknown, not zero. A cancel reports stoppedCells and alreadyRunningCells. Work that was already running finishes and is billed.
Then:
Turn
autoplayoff withPATCH /v1/tables/{tableId}(MCPset_table_autoplay). In the app, Auto-run is at the top left of the table toolbar. With it off, an insert only writes rows. Nothing runs or spends until you trigger rows. Switching it on again does not run the rows already in the table.See what you spent with
GET /v1/usage(MCPget_usage). It breaks spend down by table, column or day. In the app, see See where your spend goes on the Usage page.Before the next run, price it with a dry run and test one row. See Run columns safely: Auto-run, test one row, then run the rest.
Fewer rows started than I expected
What you see: An insert answered 201, but enrichment is not_started or unknown. Or a trigger answered 200, but triggeredCount is lower than the number of rows you sent.
Why it happens: For an insert, the rows are stored in every case. What did not happen is the run. enrichment says what happened:
started: the run was handed over and at least one column runs, so money is moving.none: nothing was meant to run.not_started: Loopster could not hand the run to its workers. The answer listsrowsToTrigger. Trigger those ids. Do not insert again.unknown: the run was sent but not confirmed, so the rows may be running. Check the table's status before you trigger, or the rows could run twice.
For a trigger, triggeredCount + deduplicated + rejected + unavailable adds up to the rows you asked for:
deduplicated: the row already had a run in progress. Nothing extra is charged.rejected: the queue was full. These rows did not start and will not start on their own. Trigger them again.unavailable: the start could not be confirmed. They may or may not have started, so check the status first.
A message says why when some rows did not start.
What to do:
Read
enrichment, or compare the trigger counts with what you sent.For an insert, trigger the ids in
rowsToTrigger. Do not insert again. Forrejectedrows, send the trigger again.When the answer is
unknownorunavailable, check the status first.Retrying an unforced row is safe. Retrying a forced run (
force: true) can start it twice.
My run seems stuck
What you see: A run stays running, or cells stay empty or queued for a while.
Why it happens: Enrichment is asynchronous and can take minutes. GET /v1/runs/{runId} (MCP get_run) reports running until every cell of that run has settled. Then it reports complete or completed_with_errors, or stopped if a stop reached any of its cells. A run is never reported finished in its first 30 seconds. The answer can be cached for up to 5 seconds. In the runs list, a finished run that nobody has read since can still read running. Rows that were rejected never start on their own.
What to do:
Read the one run with
GET /v1/runs/{runId}. Itscellscount total, in flight, complete, error, skipped, unsatisfied and not run. Ifcellsisnulland there is aRetry-Afterheader, ask again after the wait.See which rows failed with
GET /v1/runs/{runId}/items(MCPlist_run_items). To read why a row failed, read that row (MCPget_row).See the whole table with
GET /v1/tables/{tableId}/status(MCPget_status). It includes other runs. It tells you how many cells errored, but not why.A workspace keeps its 500 most recent runs, for up to 30 days. After that the
runIdanswers not found.To stop spending, stop the table or cancel the run, as in the entry above.
If cells in the app stay empty, see Why didn't my column run?.
My call succeeded but cells show errors, or a cell is empty
What you see: The call answered 200 or 201, but cells show Error, Unsatisfied or Skipped. Or a cell is empty.
Why it happens: A call can succeed while individual cells still fail to enrich. The status code is about the call, not about every cell.
What to do:
List rows with
GET /v1/tables/{tableId}/rows(MCPlist_rows). It returnscellStatuses, andcellErrorswhere a cell failed, so you can tell an empty value from a skipped, failed or never-run cell.Read one row in full with MCP
get_row. It gives every cell with its value, its status and the error message if it failed.For an empty waterfall cell, use
GET /v1/tables/{tableId}/rows/{rowId}/trace(MCPget_waterfall_trace). It shows each provider that was tried, in order, and what each returned. It only reads. It never re-runs a cell and spends nothing.In the app, open the cell and read its message. See What a cell's message means.
A tool is missing in my AI client
What you see: Your AI client does not list a tool you expected, or an agent says it cannot do something.
Why it happens: The usual causes are these:
The thing is done in the app, not over MCP. There is no tool to create, rotate or revoke API keys, to add connections or to create contexts. Rotating a webhook's signing secret is not an MCP tool either. Connections to HubSpot and Slack are removed in the app.
A function is missing from
list_functions. That tool lists only functions that are published, active and open to agents. A draft, and a function with a sandbox open, are not listed. A new function is a draft. An admin key must callpublish_functionandset_function_agent_accessbefore an agent can run it. An empty list does not mean you lack access.The tool is listed, but a call is refused because your key's role is too low. Building functions, share links, provider defaults and the table webhook tools need an editor key. A live
publish_function,set_function_agent_accessand deleting a published function need an admin key.The key has expired. A key past its expiry is refused with
401andcodeset tokey_expired.
What to do:
Check the server address:
https://app.loopster.ai/api/mcp.Check the sign-in. An MCP client signs in through OAuth, or sends
Authorization: Bearer YOUR_API_KEY. An access token from the OAuth sign-in is not an API key.Check the key's role under Settings, then Loopster API.
If a tool should be there and is not, tell us with
send_feedback. It covers a capability you needed and could not find. See the next section.
A retry ran twice
What you see: Rows appear twice, a run was billed twice, or text was added to a cell twice.
Why it happens: A call that times out can be sent again, and it then runs again. A retried forced trigger (force: true) can start twice. A retried append to a cell (mode set to append) adds the text twice.
What to do: Send an Idempotency-Key header with the call.
Choose a key for one action. It is 1 to 128 characters of letters, digits,
_,-,.and:. A fresh UUID works.Send the same key when you retry that action, and a new key for a new action.
Within 24 hours, a repeat with the same key returns the first answer, with
"replayed": trueand anIdempotent-Replayed: trueheader. Nothing runs or bills again.A stored failure replays as the same failure. Send a new key to try again. A refusal that happened before any work started, such as a rate limit or a validation error, is not kept.
These calls take the header: POST /v1/tables, insert rows, update a row, update cells, trigger, run a capability, run a function and the table's webhook URL. Over MCP, create_table, insert_rows, update_row, update_cells, trigger_rows, run_capability and run_function take idempotencyKey.
How do I tell the Loopster team about a problem?
Use send_feedback over MCP, or POST /v1/feedback (Submit Feedback) over REST. Use it for a refusal you believe is wrong, a capability you needed and could not find, documentation that did not match what happened, or something you are unhappy about.
The message reaches the team as you wrote it, with your workspace id.
It does not open a support ticket, and nothing answers it in the conversation.
The
typeismissing_integration,docs_gap,user_complaint,implementation_failureorgeneral.generalis the default.Do not include secrets or personal data.
To talk to a person, click Need Help? at the top right of any page. Say what you were doing, which table or workspace it was, and what you saw. Quote any message the app showed. Do not paste API keys or passwords into the chat. See Contact support.
Good to know
A key belongs to one workspace. The key decides the workspace, and no endpoint takes a workspace id.
Table, row, view, function and run ids are UUIDs. Column ids start with
col_.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.
The API docs are at docs.loopster.ai. In the app, API Docs is at the bottom of the Settings left menu. The OpenAPI spec is at https://app.loopster.ai/openapi.json.
These answers match the Loopster API spec and the MCP tool descriptions as they were on 21 October 2026.
Next: Create and revoke a Loopster API key · Your wallet and keeping spend under control · Contact support
More about the API and MCP



