How do I read an API error?
Responses are JSON. A call that worked has success: true. A call that failed has success: false and an error message.
Some refusals also carry a code that your code can branch on, and fields that explain the refusal. A 402 adds requiredUsd and spendableUsd. A role refusal adds requiredRole and role. A 413 with too_many_rows adds maxRows and received.
{ "success": false, "error": "<message>", "code": "<code, when the refusal has one>"}
The API docs show the same format under Error format. A call can also succeed while some cells fail to enrich, so check the cells as well as the status code.
What do the status codes mean?
400: The call was malformed or invalid, so nothing was looked up. A table id in the path that is not a UUID gets a
400, not a404. A search cursor used with a different search gets400withcode: invalid_cursor.401: The call was not accepted because the key is missing or past its expiry. An expired key gets
code: key_expired, with the expiry date in theerrortext.402: A call that would start paid work was refused, and nothing was created, queued or charged. The
codeisinsufficient_balance,workspace_limitordaily_cogs_limit. Trigger Processing, Run a Function, Run a Capability and Create Table with a paid starter can return it.403: The key does not give access to this call. Reasons include that the resource belongs to another workspace, that the table's API is disabled, and that the key's role is below what the call needs. A role refusal carries
requiredRoleandrole.404: Not found. Some calls answer
404for another workspace's item, such as a job, an export, a workbook or a folder. Other cases are asearchIdthat is unknown, expired (it lasts 24 hours) or from another workspace, a list inexclusionsthat no longer exists, a run that is no longer kept, and a table webhook URL that does not point to a live, listening table.405: A table's webhook URL accepts only
POST. Any other method gets405.409: The call conflicts with the current state, or its
Idempotency-Keycannot be used. Thecodesays which:idempotency_key_reused,idempotency_key_in_progressandidempotency_outcome_unknown: see Troubleshooting.cell_running(Update Row) andwaterfall_running(Update Column): a run is still working on what you tried to change, so nothing was saved or changed. Send the request again when the run finishes.capability_table_limit(Run a Capability): you are at the limit of 50 configurations and none was unused for the last hour. Nothing was created or started.connection_in_useandconnection_shared(removing a connection): nothing was removed.connection_not_connected(testing a connection): the connection is not connected.sandbox_required(function sandbox calls): open a sandbox first withPOST /v1/functions/{functionId}/sandbox.
413: Too much in one call.
too_many_rowsmeans an insert carried more than 25,000 rows. A Run a Function or Run a Capability body over 4,000,000 bytes also gets413. Nothing is written or started.422: The call was refused. Codes include
value_too_large,cost_cap_exceededandcost_cap_unpriced_columns. Search answers422when Loopster Data could not answer inside its query time limit, which does not mean the query is invalid. Create Table answers422when its search matched nothing, and no table is created.429: A rate limit was reached and nothing was done. The
codeisrate_limited,search_concurrency,search_busyorprovider_busy.500 and 503: Something on Loopster's side stopped the call. A
503can carrynothing_startedoraccounts_unavailable, and a500can carrynothing_started_internal. Search and Create Table can also answer503.
Which limits apply to one call?
Rows per insert: 25,000. One Insert Rows request (
POST /v1/tables/{tableId}/rows) carries at most 25,000 rows. A larger one is refused with413andcode: too_many_rowsbefore anything is written. The answer givesmaxRows(the cap) andreceived(how many rows you sent). A table's webhook URL and the MCP toolinsert_rowshave the same limit.Bigger loads. Send batches of 10,000 rows or fewer, or import a file. To import a file, get an upload address with
POST /v1/tables/{tableId}/imports/upload-url, upload the file to it, then callPOST /v1/tables/{tableId}/imports. Over MCP, usecreate_upload_linkthenimport_file. A file import costs nothing and starts no enrichment. A file can be up to 50 MB, and a larger file fails the import job.Time for one insert: 300 seconds. An insert runs to completion before it answers. About 25,000 rows fit in that time, and an insert still running at 300 seconds is cut off.
Rows per trigger: 10,000. One Trigger Processing request (
POST /v1/tables/{tableId}/trigger) covers at most 10,000 rows. With norowIds, a table of more than 10,000 rows is refused with400and nothing runs, so sendrowIdsin batches of 10,000 or fewer. An emptyrowIdslist runs nothing. The MCP toolstrigger_rowsandestimate_trigger_costsay the same.Inputs per function or capability run: 1,000. Run a Function and Run a Capability each take up to 1,000 inputs and a body of at most 4,000,000 bytes. A bigger body gets
413and nothing starts, so split the list. An input that cannot run comes back by index inrejected. If none can run, the call is a400and nothing starts.Size of one cell value. The limits are 8 kB for typed text, 100 kB for references and 200 kB for enrichment output. By default a value over its limit is stored empty and listed in
cappedCells, and the rest of the write succeeds. Add?onOversize=rejectto an insert, a row update or a cell update to refuse instead. Nothing in the request is written, and the answer is422withcode: value_too_large.
Which calls can spend from my wallet?
The API spec and the MCP tool descriptions say these calls can start paid work and spend from your wallet:
POST /v1/tables/{tableId}/trigger(Trigger Processing), unless it is a dry run. Over MCP:trigger_rows. WithoutrowIdsit runs every row in the table and spends accordingly.POST /v1/functions/{functionId}/runs(Run a Function). Over MCP:run_function.POST /v1/capabilities/{templateId}/runs(Run a Capability). Over MCP:run_capability. It spends unless it runs on your own connection.POST /v1/tables(Create Table) with a paid starter. Over MCP:create_table. A blank table, a webhook table and asearchtable are free.POST /v1/tables/{tableId}/rows(Insert Rows), and rows sent to a table's webhook URL, when the table hasautoplayon.autoplayis the table's Auto-run setting. In the app, Auto-run is at the top left of the table toolbar. With it off, an insert only writes rows. Over MCP:insert_rows.PATCH /v1/tables/{tableId}/rows/{rowId}andPATCH /v1/tables/{tableId}/cells(Update Row and Update Cells). On a table with auto-rerun columns, writing a cell also re-queues the columns that read it, and that spends from your wallet. Over MCP:update_rowandupdate_cells.POST /v1/tables/{tableId}/schedules(a schedule). A schedule spends from your wallet every time it fires, with nobody present, until it is paused or deleted. Over MCP:create_schedule, andupdate_schedulewhen it setspaused: false.
These spend nothing by themselves: a trigger dry run, estimate_trigger_cost, searching Loopster Data, importing a file and exporting a table.
To price a trigger before you run it, add ?dryRun=true to the call. Nothing is queued and nothing is charged. Use the query string, not dryRun in the body, because an unparseable body is treated as empty, and an empty body means trigger every row. The answer shows maxCostUsd (a ceiling, not a forecast) and a wallet block with spendableUsd, rowsCovered and wouldStart. Over MCP, use estimate_trigger_cost.
To cap a trigger, send maxCostUsd. Loopster checks it once, before anything starts. It is 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.
What happens when my wallet runs short?
Paid work holds its worst-case cost before it runs. You are charged what it really cost, never more.
If the wallet cannot cover even the first row or input, the call is refused with
402before anything is queued.If the wallet covers some rows but not all, rows run in order while the wallet covers them. The rest are refused, not run, and nothing is charged for them. On a trigger, their cells show a not-enough-balance error.
The wallet is never overdrawn.
These three points are also on the Trigger Processing page of the API docs.
To add funds after insufficient_balance:
Click the wallet button at the top right, then Manage wallet.
Settings opens on Plan & billing, on the Wallet tab. It has a Top up button and an Auto top-up card.
To raise a limit after workspace_limit:
Open Settings, then Workspaces.
Click the workspace.
Change its Spend limit with Set spend limit.
All workspaces in an organization share one wallet, and the limit is per workspace. See Top up your wallet, set auto top-up and read your funds history and Workspaces: create one and set a spend limit.
What do I do when I get a 429?
A 429 means a rate limit was reached and nothing was done.
Read the
Retry-Afterheader. It is the number of seconds to wait.Wait that long.
Send the same call again.
The response also carries X-RateLimit-Limit (the size of the window that refused), X-RateLimit-Remaining and X-RateLimit-Scope (which window refused). A provider_busy answer and a table's webhook URL are the exceptions, covered below. The scope is minute or day (the workspace's general budget), credit-spend (calls that can start paid work), exclusion-list (searches that read a table as an exclusion list), or unauthenticated or invalid-credential (counted per calling address, and they appear only on a 429).
The code in the body 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. Wait the seconds inRetry-After.provider_busy: Create Table's starter found a provider on Loopster's account at its limit, or paused after a burst of its errors. The answer hasRetry-Afterbut noX-RateLimit-*headers, and nothing was charged.
You can watch your budget before you hit a 429. Successful calls carry the same three headers, and they describe the workspace's general budget, whichever of its per-minute or per-day windows is closer to running out. A header is left out when its window could not be measured. Create Table, Insert Rows, Update Row, Update Cells, Trigger Processing, Run a Capability and Run a Function also return X-RateLimit-Spend-Limit and X-RateLimit-Spend-Remaining, when the call reserved the paid-work window.
Calls made over MCP count against the same rate limits as REST. A table's webhook URL answers a 429 with Retry-After only, and its successful answers carry no X-RateLimit-* headers.
How do I retry a call without running it twice?
Add an Idempotency-Key header. You choose the key for one action. It is 1 to 128 characters from letters, digits, _, ., : and -. A fresh UUID works. Use a new key for a new action, and the same key when you retry that action. Loopster remembers a key for 24 hours.
If you repeat the same request with the same key within 24 hours, you get the first answer back with its original status and body. The body adds "replayed": true, and the response has an Idempotent-Replayed: true header. Nothing runs again. Over MCP the answer is marked replayed: true.
The API spec lists the header on these eight calls only:
POST /v1/tablesPOST /v1/tables/{tableId}/rowsPATCH /v1/tables/{tableId}/rows/{rowId}PATCH /v1/tables/{tableId}/cellsPOST /v1/tables/{tableId}/triggerPOST /v1/webhooks/{secret}POST /v1/capabilities/{templateId}/runsPOST /v1/functions/{functionId}/runs
Over MCP, these tools take an optional idempotencyKey: create_table, insert_rows, update_row, update_cells, trigger_rows, run_capability and run_function. DELETE /v1/connections/{connectionId} refuses the header with a 400, so do not send one there.
Whether you can reuse a key depends on what happened to the first call:
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.
A
402refusal releases the key, so the same key runs after you top up. A422cost-cap refusal does the same once you raise the cap.Appending to a cell over MCP (
mode: appendinupdate_cells) is not idempotent. Without a key, a retried append adds the text twice.
Troubleshooting
I get a 401
What you see: A 401. If the key has expired, the body has code: key_expired and the expiry date in error.
Why it happens: The key is missing from the call, or it is past its expiry. You choose a key's expiry when you create it, and you cannot change it afterwards.
What to do: Send the key as Authorization: Bearer loopster_sk_…. If the key has expired, open Settings, then Loopster API, and create a new key. Then give the new key to the tool that used the old one. See Create and revoke a Loopster API key.
I get a 403
What you see: A 403. A role refusal has requiredRole (the role the call needs) and role (the role your key holds) in the body.
Why it happens: The key is not allowed to make the call. With requiredRole, the key's role is too low. A key's role is member, editor or admin. For example, publishing a function live needs admin, and share links and file imports need editor. Without requiredRole, the resource may belong to another workspace, or the table's API may be disabled.
What to do: Use a key with the role in requiredRole. You pick a key's role when you create it, under Settings, then Loopster API. A key belongs to one workspace and cannot reach another workspace's data, so check that the id belongs to the workspace that owns the key. The API cannot turn API access on for a table. If a table's API is disabled, ask in Need Help? how to turn it on. See Create and revoke a Loopster API key and Contact support.
I get a 400 for a table id
What you see: A 400 on a path such as /v1/tables/{tableId}, not a 404.
Why it happens: The id in the path is not a UUID. Loopster refuses it before it looks anything up, so a typo can be told apart from a deleted table.
What to do: Check the id. Table, row, view, function and run ids are UUIDs. A row id is a plain UUID, with no prefix such as row_. Column ids start with col_.
My insert is refused with a 400
What you see: A 400 from Insert Rows.
Why it happens: None of a record's keys matches a column, on a table that neither creates columns from keys nor keeps the raw record in a trigger column. Or an element of the array is not an object, and the 400 names the index. Or the only key of your object holds an array of the table's rows, such as {"records": [...]}.
What to do: Send the array itself, with each element an object keyed by column id or column header. Keys that match no column come back in ignoredKeys. To turn new keys into columns, switch on createColumnsFromKeys with PATCH /v1/tables/{tableId}. It is off by default.
I get a 413 with too_many_rows
What you see: A 413 with code: too_many_rows, maxRows and received.
Why it happens: The request carried more than 25,000 rows. Nothing was written.
What to do: Send batches of 10,000 rows or fewer, or import the file. See "Which limits apply to one call?" above.
I get a 402, or I topped up and still get one
What you see: A 402 with code set to insufficient_balance, workspace_limit or daily_cogs_limit, and requiredUsd and spendableUsd in the body. Nothing was created, queued or charged.
Why it happens: Paid work holds its worst-case cost before it runs. requiredUsd is the most the first row or input can cost. spendableUsd is what the wallet can spend right now, capped by the workspace's spend limit if one is set.
What to do: insufficient_balance means the wallet cannot cover the call, so top up or ask for less. workspace_limit means the workspace's own spend limit leaves less than the wallet, so raise the limit or ask for less. daily_cogs_limit means the account reached its daily spend safety limit. Topping up does not lift it, asking for less does not help, and spendableUsd is 0. It resets at midnight UTC, and Loopster support can raise it. See "What happens when my wallet runs short?" above for where to add funds, and Contact support.
My trigger is refused with a 422 and a cost cap code
What you see: A 422 with code: cost_cap_exceeded or code: cost_cap_unpriced_columns. Nothing was queued.
Why it happens: You sent maxCostUsd, and Loopster estimated the run the way a dry run does before anything started. cost_cap_exceeded means the estimate is over your cap. cost_cap_unpriced_columns means some columns have no price known up front and you did not send allowUnpriced.
What to do: Raise maxCostUsd, or run fewer rows. If you accept columns with no up-front price, send allowUnpriced: true together with the cap. If you sent an Idempotency-Key, the same key runs once the cap is raised.
My search returns a 422
What you see: A 422 from POST /v1/search. Over MCP, from search_people or search_companies.
Why it happens: Loopster Data could not answer the search inside its query time limit. It does not mean the query is invalid. Filter combinations that each work alone can still be unanswerable together.
What to do: Retry the same cursor with a smaller limit before you change the filters.
I get a 409 with an idempotency code
What you see: A 409 with code: idempotency_key_reused, idempotency_key_in_progress or idempotency_outcome_unknown.
Why it happens: The key cannot be used, and nothing runs.
What to do: idempotency_key_reused means the key was used for a different request, so use a new key for the new request. idempotency_key_in_progress means the first request is still running, so wait the seconds in Retry-After and send the same request again. idempotency_outcome_unknown means the first request never finished, so whether it started work is unknown. Look at what happened first, for example with GET /v1/tables/{tableId}/status or GET /v1/runs.
I get a 409 with cell_running
What you see: An Update Row call (PATCH /v1/tables/{tableId}/rows/{rowId}) answers 409 with code: cell_running. Or it answers 200 and lists the held cells in rejected with reason: running.
Why it happens: A run is still computing the cells you wrote, and a cell that a run is still computing is not written. After a 409, nothing was saved.
What to do: Wait until the run finishes. After a 409, send the same request again, with the same Idempotency-Key if you sent one. After a 200, send the cells listed in rejected again.
I get a 500 or 503 when I start a run
What you see: A 503 with code: nothing_started from Trigger Processing, Run a Function or Run a Capability. Or a 500.
Why it happens: With nothing_started, Loopster could not hand the run to its workers, and the request to them was never sent. Nothing was queued or charged. A 500 with code: nothing_started_internal also means nothing was started, but it is a fault on Loopster's side. A plain 500 makes no promise: the run may have been created.
What to do: For nothing_started, wait the seconds in Retry-After, then send the same request again, with the same Idempotency-Key if you used one. This answer is not stored against the key. For nothing_started_internal, sending it again right now will likely fail the same way. For a plain 500, rows may have started, so check GET /v1/runs or the table's status first. Then retry. An unforced trigger is safe to send again. A function run or capability run is retried with a new key.
I get a 503 from Create Column, Search or Create Table
What you see: A 503. From Create Column it has code: accounts_unavailable.
Why it happens: With accounts_unavailable, Loopster could not read the workspace's connections and saved default accounts, which decide which account the work would run on. Nothing was created or started. Search answers 503 when it is not configured, is temporarily unavailable, or is switched off for the workspace. Create Table answers 503 when an import source is temporarily unavailable.
What to do: For accounts_unavailable, retry after the seconds in Retry-After, and the same request then runs. For Create Table, the request itself is fine, so send it again later with the same key. The exception is a pause or used-up account met after a google-maps search had already paid for part of its grid. Send that one again with a new key. A switched-off search does not clear on an immediate retry, and nothing you have imported is affected.
My trigger returned 200 but fewer rows started
What you see: A 200, but triggeredCount is lower than the number of rows you asked for.
Why it happens: triggeredCount + deduplicated + rejected + unavailable adds up to the rows you asked for. deduplicated rows already had a run in progress. rejected rows were refused because the queue is full, and they will not start on their own. unavailable rows could not be confirmed, so they may or may not have started.
What to do: Trigger the rejected rows again. Retrying an unforced row is safe. Retrying a forced run (force: true) can start it twice.
My insert succeeded but nothing enriched
What you see: A 201 with insertedCount, but no enrichment runs.
Why it happens: The enrichment field says what happened. started means the run was handed over. none means nothing was meant to run. not_started means Loopster could not hand the run to its workers. unknown means the run was sent but not confirmed, so the rows may be running. The rows are stored in every case.
What to do: On not_started, trigger the ids in rowsToTrigger and do not insert again. On unknown, check the table's status before you trigger, or the rows could run twice. On none, nothing was meant to run. Check the table's Auto-run setting. Also check that the table has a column an Auto-run executes: an AI column with its prompt, or an API or task column that is not manual-trigger-only. A formula column does not count. See Run columns safely: Auto-run, test one row, then run the rest.
My call succeeded but some cells show errors
What you see: A successful status, but some cells fail to enrich. In the app, a failed cell says so in the cell, and a footer shows a count such as "1 cell failed".
Why it happens: The status code describes the call, not each cell. A call can succeed while individual cells fail.
What to do: Read the rows. List Rows gives each row cellStatuses, and cellErrors for failed cells. Over MCP, get_status says how many cells errored but not why, so read a failed row with get_row for the message. The cell labels in the app are Error, Unsatisfied and Skipped. See What a cell's message means.
Good to know
A
200or201does not always mean everything ran. ChecktriggeredCount,enrichmentand your cells.Stopping prevents further spending, but it does not refund work that was already charged.
POST /v1/tables/{tableId}/stopstops a table's processing.POST /v1/runs/{runId}/cancelremoves the queued work of one run. Work already running finishes and is billed.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.
When you poll
GET /v1/runs/{runId},cellscan benullwith aRetry-Afterheader while another request is still working out the answer. Ask again after the wait. A run is never reported finished in its first 30 seconds. A run is kept for 30 days, and a workspace keeps its 500 most recent runs. After that therunIdanswers404.A schedule made with a key is paused if that key is revoked, or expires before a firing. Its
pausedReasonis thenkey_revokedorkey_expired.If another page says there is no fixed cap on rows per insert, that page is not correct. The limit is 25,000 rows per request.
When you contact support, quote the
codeand theerrortext. Do not paste your API key. See Contact support.
More about the API and MCP


