Skip to main content

API limits, errors and retries

This article shows what a Loopster API error looks like, what the main status codes and error codes mean, which limits apply to a call, and how to retry a call without running it twice.

Written by Umer Ishaq

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 Error format section of the Loopster API docs. It shows a JSON error body with success set to false and an error message, and a sentence saying some refusals also carry a code such as insufficient_balance, workspace_limit or daily_cogs_limit on a 402.

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 a 404. A search cursor used with a different search gets 400 with code: 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 the error text.

  • 402: A call that would start paid work was refused, and nothing was created, queued or charged. The code is insufficient_balance, workspace_limit or daily_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 requiredRole and role.

  • 404: Not found. Some calls answer 404 for another workspace's item, such as a job, an export, a workbook or a folder. Other cases are a searchId that is unknown, expired (it lasts 24 hours) or from another workspace, a list in exclusions that 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 gets 405.

  • 409: The call conflicts with the current state, or its Idempotency-Key cannot be used. The code says which:

    • idempotency_key_reused, idempotency_key_in_progress and idempotency_outcome_unknown: see Troubleshooting.

    • cell_running (Update Row) and waterfall_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_use and connection_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 with POST /v1/functions/{functionId}/sandbox.

  • 413: Too much in one call. too_many_rows means an insert carried more than 25,000 rows. A Run a Function or Run a Capability body over 4,000,000 bytes also gets 413. Nothing is written or started.

  • 422: The call was refused. Codes include value_too_large, cost_cap_exceeded and cost_cap_unpriced_columns. Search answers 422 when Loopster Data could not answer inside its query time limit, which does not mean the query is invalid. Create Table answers 422 when its search matched nothing, and no table is created.

  • 429: A rate limit was reached and nothing was done. The code is rate_limited, search_concurrency, search_busy or provider_busy.

  • 500 and 503: Something on Loopster's side stopped the call. A 503 can carry nothing_started or accounts_unavailable, and a 500 can carry nothing_started_internal. Search and Create Table can also answer 503.

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 with 413 and code: too_many_rows before anything is written. The answer gives maxRows (the cap) and received (how many rows you sent). A table's webhook URL and the MCP tool insert_rows have 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 call POST /v1/tables/{tableId}/imports. Over MCP, use create_upload_link then import_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 no rowIds, a table of more than 10,000 rows is refused with 400 and nothing runs, so send rowIds in batches of 10,000 or fewer. An empty rowIds list runs nothing. The MCP tools trigger_rows and estimate_trigger_cost say 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 413 and nothing starts, so split the list. An input that cannot run comes back by index in rejected. If none can run, the call is a 400 and 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=reject to an insert, a row update or a cell update to refuse instead. Nothing in the request is written, and the answer is 422 with code: 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. Without rowIds it 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 a search table are free.

  • POST /v1/tables/{tableId}/rows (Insert Rows), and rows sent to a table's webhook URL, when the table has autoplay on. autoplay is 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} and PATCH /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_row and update_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, and update_schedule when it sets paused: 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 402 before 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.

Three bullets from the Trigger Processing page of the Loopster API docs about what happens when the wallet runs short: a run bigger than the wallet runs what fits, a run that cannot cover its first row is refused up front with a 402, and a dry run checks first.

These three points are also on the Trigger Processing page of the API docs.

To add funds after insufficient_balance:

  1. Click the wallet button at the top right, then Manage wallet.

  2. 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:

  1. Open Settings, then Workspaces.

  2. Click the workspace.

  3. 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.

  1. Read the Retry-After header. It is the number of seconds to wait.

  2. Wait that long.

  3. 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 in Retry-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 has Retry-After but no X-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/tables

  • POST /v1/tables/{tableId}/rows

  • PATCH /v1/tables/{tableId}/rows/{rowId}

  • PATCH /v1/tables/{tableId}/cells

  • POST /v1/tables/{tableId}/trigger

  • POST /v1/webhooks/{secret}

  • POST /v1/capabilities/{templateId}/runs

  • POST /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 402 refusal releases the key, so the same key runs after you top up. A 422 cost-cap refusal does the same once you raise the cap.

  • Appending to a cell over MCP (mode: append in update_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 200 or 201 does not always mean everything ran. Check triggeredCount, enrichment and your cells.

  • Stopping prevents further spending, but it does not refund work that was already charged. POST /v1/tables/{tableId}/stop stops a table's processing. POST /v1/runs/{runId}/cancel removes 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}, cells can be null with a Retry-After header 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 the runId answers 404.

  • A schedule made with a key is paused if that key is revoked, or expires before a firing. Its pausedReason is then key_revoked or key_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 code and the error text. Do not paste your API key. See Contact support.

More about the API and MCP

Did this answer your question?