Start in 5 minutes
1. Create a token in Settings › Security › API tokens. Give it a name and choose its scopes (for example rows:read and rows:write). The token is shown only once: copy it.
2. Check that it works.
export RANGEE_TOKEN="pat…"
curl https://api.rangee.io/v1/me \
-H "Authorization: Bearer $RANGEE_TOKEN"The response gives your identity, the token's name, its scopes, its perimeter and the month's quota.
3. Find your tables. The response of GET /v1/bases/app… lists the base's tables, with their IDs.
curl https://api.rangee.io/v1/bases \
-H "Authorization: Bearer $RANGEE_TOKEN"
curl https://api.rangee.io/v1/bases/app… \
-H "Authorization: Bearer $RANGEE_TOKEN"4. Read your rows, keyed by field name to start with.
curl "https://api.rangee.io/v1/tables/tbl…/rows?limit=10&fieldKey=name" \
-H "Authorization: Bearer $RANGEE_TOKEN"5. Create a row.
curl https://api.rangee.io/v1/tables/tbl…/rows \
-H "Authorization: Bearer $RANGEE_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 6f1c2e7a-creation-atelier-martin" \
-d '{
"rows": [
{ "values": { "Nom": "Atelier Martin", "Statut": "Signé", "Montant": 1250 } }
]
}'In JavaScript:
const res = await fetch("https://api.rangee.io/v1/tables/tbl…/rows", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.RANGEE_TOKEN}`,
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID(),
},
body: JSON.stringify({
rows: [{ values: { Nom: "Atelier Martin", Statut: "Signé", Montant: 1250 } }],
}),
});
const body = await res.json();
if (!res.ok) throw new Error(`${body.error.code} : ${body.error.message}`);In Python:
import os, uuid, requests
res = requests.post(
"https://api.rangee.io/v1/tables/tbl…/rows",
headers={
"Authorization": f"Bearer {os.environ['RANGEE_TOKEN']}",
"Idempotency-Key": str(uuid.uuid4()),
},
json={"rows": [{"values": {"Nom": "Atelier Martin", "Statut": "Signé", "Montant": 1250}}]},
)
res.raise_for_status()fld…) to names in values: renaming a field in the app will break nothing.Access and tokens
Every call carries a personal token in the header:
Authorization: Bearer pat….…A token has a name, scopes, a perimeter and, if you wish, an expiry date.
It inherits its creator's role in each base. The token of a read-only person never writes, even with the rows:write scope.
It is cut off immediately if you revoke it, or if its creator is removed from the workspace or deactivated.
Scopes
| Scope | Gives access to |
|---|---|
rows:read | Read and search rows, their history and comments; download attachments |
rows:write | Create, update, delete and upsert rows; write comments; upload attachments |
schema:read | Read bases, tables, fields and views |
schema:write | Create and update tables, fields and views; move them to the trash |
automations:read | List automations, their variables and their runs |
automations:run | Trigger an automation and follow the run |
webhooks:manage | Create, list, update and delete webhooks |
Perimeter
A token covers either all the workspace bases its creator can access, or a chosen list of bases. A token never crosses two workspaces.
A resource outside the token's perimeter answers 404, exactly like a resource that does not exist.
Identity
GET /v1/meReturns the person, the token's name, its scopes, its perimeter and the month's call quota.
Vocabulary and IDs
| Term | Meaning | ID |
|---|---|---|
| Workspace | The organisation's space, which holds the bases | wsp… |
| Base | A set of linked tables | app… |
| Table | A list of rows with the same schema | tbl… |
| Field | A typed column of a table | fld… |
| View | A saved presentation of a table (filters, sorts, visible fields) | viw… |
| Row | An item of a table; its values are in values | row… |
| Value | A row's content for a field | — |
| Automation | A trigger and a series of steps | aut… |
| Run | One pass of an automation | run… |
| Variable | A piece of data that an API-triggered automation asks for at call time | name of your choice |
| Webhook | An address told about a base's changes | whk… |
| Event | A change sent to a webhook | evt… |
| Attachment | An uploaded file, placed in a value | att… |
| Comment | A message on a row | com… |
| Option | A choice of a select field | sel… |
| Member | A person in the workspace | usr… |
| Token | A person's access key to the API | pat… |
IDs are opaque. Do not build them and do not parse them: only the full ID has a meaning.
Conventions
- Format. JSON in UTF-8. Dates and times in ISO 8601 UTC (
2026-11-03T09:30:00.000Z), dates without time asYYYY-MM-DD. - Resource or list. A single resource is returned as is. A list has the shape
{ "data": [...], "nextCursor": "…" | null }. - Pagination. By opaque cursor:
limit(1 to 1,000, 100 by default) andcursor. A cursor stays valid for 24 hours. WhennextCursorisnull, you have read everything. - Batches. A write carries 1 to 100 rows. A batch is atomic: everything goes through, or nothing.
- Referring to things. A table, a field or a view is referred to by its ID. Field names are only accepted as keys of
values, when writing. - Read keys. Values are keyed by field ID. Add
fieldKey=nameto any row read to receive them keyed by name. - Replay safely. Any
POST,PATCH,PUTorDELETErequest accepts anIdempotency-Keyheader (1 to 255 characters). The same call replayed within 24 hours returns the same response without writing anything again. The same key with a different body answers409 IDEMPOTENCY_CONFLICT. - Support. Every response carries a
Request-Idheader. Quote it to support: it is enough to find the call.
Paginate
async function* allRows(tableId) {
let cursor = null;
do {
const url = new URL(`https://api.rangee.io/v1/tables/${tableId}/rows`);
url.searchParams.set("limit", "1000");
if (cursor) url.searchParams.set("cursor", cursor);
const res = await fetch(url, { headers: { Authorization: `Bearer ${process.env.RANGEE_TOKEN}` } });
const page = await res.json();
yield* page.data;
cursor = page.nextCursor;
} while (cursor);
}Rows
{
"id": "row01JC9…",
"createdAt": "2026-10-12T08:14:03.000Z",
"updatedAt": "2026-11-02T17:40:11.000Z",
"values": {
"fld01Nom…": "Atelier Martin",
"fld01Statut…": { "id": "sel…", "name": "Signé" },
"fld01Montant…": 1250
}
}Empty values. An empty value does not appear in values. To empty a value, write null.
| Operation | Request |
|---|---|
| Read a row | GET /v1/tables/{tableId}/rows/{rowId} |
| List | GET /v1/tables/{tableId}/rows?viewId&fields&limit&cursor&fieldKey |
| Search | POST /v1/tables/{tableId}/rows/query (details) |
| Create | POST /v1/tables/{tableId}/rows with { "rows": [{ "values": {…} }] } |
| Update | PATCH /v1/tables/{tableId}/rows with { "rows": [{ "id": "row…", "values": {…} }] }, or PATCH /v1/tables/{tableId}/rows/{rowId} with { "values": {…} } |
| Replace | PUT /v1/tables/{tableId}/rows/{rowId} with { "values": {…} } |
| Upsert | POST /v1/tables/{tableId}/rows/upsert with { "matchOn": ["fld…"], "rows": [...] } |
| Delete | DELETE /v1/tables/{tableId}/rows?ids=row…,row…, or DELETE /v1/tables/{tableId}/rows/{rowId} |
| History | GET /v1/tables/{tableId}/rows/{rowId}/history |
Writing rules
PATCH or PUT. PATCH only touches the fields sent. PUT replaces the row: fields missing from the body are emptied.
Computed fields. They refuse writes (422 READ_ONLY_FIELD): formula, lookup, rollup, count, autonumber, created and modified dates and authors, button.
Conversion with "coerce": true. Added to the body, the option converts text into numbers, dates and checkboxes, and creates unknown select options. Without it, a value of the wrong type answers 422 INVALID_VALUE.
{ "coerce": true, "rows": [{ "values": { "fld01Montant…": "1 250,50", "fld01Signé…": "oui" } }] }Upsert.
matchOnlists 1 to 3 fields, of type text, email, number, autonumber, URL or phone.- Each row sent that matches exactly one existing row updates it. If it finds none, it is created. If it finds several, the call answers
409 UPSERT_AMBIGUOUS. - The response tells created rows (
created) from updated rows (updated).
curl https://api.rangee.io/v1/tables/tbl…/rows/upsert \
-H "Authorization: Bearer $RANGEE_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"matchOn": ["fld01Email…"],
"rows": [
{ "values": { "fld01Email…": "camille@atelier-martin.fr", "fld01Nom…": "Camille Durand" } }
]
}'Deletion. A deleted row goes to the base's trash for 30 days. It can be restored from the app.
Plan limit. A create beyond the number of rows your plan allows answers 422 ROW_LIMIT, with the number of rows left.
Search rows
POST /v1/tables/{tableId}/rows/queryThe body takes the shape of the app's view filters:
| Key | Role |
|---|---|
filter | An and or or group of conditions { field, operator, value }, nestable 5 levels deep |
sort | [{ "field": "fld…", "direction": "asc" | "desc" }] |
search | Text searched in the text fields |
viewId | Starts from a view's filters and sorts |
formula | A Rangee formula that must be true |
fields, limit, cursor, fieldKey | As for the list |
filter, search, viewId and formula combine with “and”.
{
"viewId": "viw…",
"filter": {
"and": [
{ "field": "fld01Statut…", "operator": "isAnyOf", "value": ["En cours", "À valider"] },
{ "or": [
{ "field": "fld01Montant…", "operator": ">=", "value": 10000 },
{ "field": "fld01Prioritaire…", "operator": "is", "value": true }
] }
]
},
"sort": [{ "field": "fld01Echeance…", "direction": "asc" }],
"limit": 50
}Operators: isEmpty, isNotEmpty, is, isNot, contains, doesNotContain, isAnyOf, isNoneOf, hasAnyOf, hasAllOf, linksTo, =, !=, <, <=, >, >=.
Each field type accepts only some of these operators. An operator that is not accepted answers 422 INVALID_FILTER, naming the field at fault. A formula that is too costly is refused before it runs (422 QUERY_TOO_EXPENSIVE).
Values by field type
| Type | Reading | Writing |
|---|---|---|
| Text, long text, rich text, email, URL, phone | string | string; the email is checked |
| Number, currency | number | number; with coerce, "12,5" or "12.5" |
| Percent | number, 0.25 for 25% | same |
| Rating | integer from 1 to the field's maximum | same |
| Duration | integer in seconds | same; with coerce, "1:30:00" |
| Checkbox | true, absent when unchecked | boolean; with coerce, "oui", "yes", "1" |
| Date | "YYYY-MM-DD" | same; with coerce, any ISO date |
| Date and time | ISO 8601 UTC | ISO 8601 with time zone |
| Single select | { "id": "sel…", "name": "Signé" } | ID or name of the option |
| Multiple select | list of { id, name } | list of IDs or names |
| Collaborator, collaborators | { id, email, name }, or a list | ID or e-mail of a workspace member |
| Attachments | list of { id, url, filename, size, type, thumbnails }; signed url, valid for 2 hours | list of { "id" } of an uploaded file |
| Link to rows | list of row IDs | same; each row must exist in the linked table |
| Barcode | { "text", "type"? } | same, or a string |
| AI text | { "state", "value" } | read-only |
| Formula, lookup, rollup, count, aggregation | type of the result | read-only |
| Autonumber, row ID | integer / string | read-only |
| Created, modified (date and author) | ISO 8601 / { id, email, name } | read-only |
| Button | { "label", "url"? } | read-only |
Automations
An automation can be triggered through the API in two ways.
“API call” trigger.
- The automation declares its variables in the editor. Each variable has a name (letters, digits and
_), a label, a type, whether it is required, a default value and a description. - Possible types:
text,number,boolean,date,dateTime,email,rowId,rowIds,json. - Steps read the variables under
trigger.inputs.<name>. - A variable of type
rowIdnames its table. The server checks that the row exists and loads it undertrigger.inputs.<name>.fields.
“Button” trigger. It runs with a rowId, like a click in the app.
| Operation | Request |
|---|---|
| List a base's automations | GET /v1/bases/{baseId}/automations |
| Read an automation and its variables | GET /v1/automations/{automationId} |
| Run | POST /v1/automations/{automationId}/runs |
| Follow a run | GET /v1/runs/{runId} |
| List runs | GET /v1/automations/{automationId}/runs?status&limit&cursor |
curl https://api.rangee.io/v1/automations/aut…/runs \
-H "Authorization: Bearer $RANGEE_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: relance-devis-2026-11-03" \
-d '{
"inputs": { "client": "row…", "montant": 1250, "relance": true },
"wait": 10
}'For a “Button” trigger, send { "rowId": "row…" } instead of inputs.
Rules
Checked before running. A missing required variable, an unknown variable or a value of the wrong type answers 422 INVALID_INPUTS with the list of errors. Such a call counts neither as a run nor against the plan's quota.
Waiting with wait.
- Without
wait: response202, the run is queued (queued). - With
wait(0 to 30 seconds): the server waits for the run to finish, up to that delay. It answers200with the result if it finished,202otherwise.
Result. The “Return values” step computes name and value pairs from the run's data. They are returned in output. Without this step, output is {}.
Run states:
| State | Meaning |
|---|---|
queued | Queued |
running | In progress |
waiting | Stopped on a “Wait” step |
succeeded | Succeeded |
failed | Failed; the run gives the step at fault and its message |
refused | Refused: plan quota reached or automation disabled |
Active automations only. Only published, active automations run; the others answer 422 AUTOMATION_DISABLED.
Traceability. The run carries the token's name as its origin. It shows in the run history and in the history of the rows written.
Services without headers. The “Webhook received” trigger stays available for services that cannot send an Authorization header. Its secret URL accepts the same declared variables.
Webhooks
Rangee sends you a base's changes, without you having to read the data again: each delivery holds the rows concerned.
| Operation | Request |
|---|---|
| Create | POST /v1/bases/{baseId}/webhooks with { url, events, tableIds?, fieldIds?, includeValues? } |
| List | GET /v1/bases/{baseId}/webhooks |
| Read, update, delete | GET, PATCH, DELETE /v1/webhooks/{webhookId} |
| Recent deliveries | GET /v1/webhooks/{webhookId}/deliveries |
| Send a test | POST /v1/webhooks/{webhookId}/test |
Events: row.created, row.updated, row.deleted, field.created, field.updated, field.deleted, table.created, table.updated, table.deleted.
Filters. tableIds limits the webhook to some tables. fieldIds limits row.updated to changes in those fields.
Body of a delivery:
{
"id": "evt…",
"type": "row.updated",
"createdAt": "2026-11-03T09:30:00.000Z",
"baseId": "app…",
"tableId": "tbl…",
"data": {
"row": { "id": "row…", "createdAt": "…", "updatedAt": "…", "values": { "fld01Statut…": { "id": "sel…", "name": "Signé" } } },
"previous": { "values": { "fld01Statut…": { "id": "sel…", "name": "Devis" } } }
}
}previous only holds the fields that changed. With includeValues: false, only IDs are sent.
Check the signature
Each delivery carries the header:
Rangee-Signature: t=<horodatage>,v1=<signature>v1 is the HMAC-SHA256, in hexadecimal, of the string "<timestamp>.<raw body>", computed with the webhook's secret. The secret is shown only once, when the webhook is created.
Refuse any delivery whose timestamp is more than 5 minutes old, so it cannot be replayed.
Node.js:
import crypto from "node:crypto";
export function verifyRangeeSignature(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const t = Number(parts.t);
if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false;
const expected = crypto.createHmac("sha256", secret).update(`${parts.t}.${rawBody}`).digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(parts.v1 || "");
return a.length === b.length && crypto.timingSafeEqual(a, b);
}Python:
import hmac, hashlib, time
def verify_rangee_signature(raw_body: bytes, header: str, secret: str) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
t = parts.get("t", "")
if not t.isdigit() or abs(time.time() - int(t)) > 300:
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, parts.get("v1", ""))Compute the signature on the raw body received, before any JSON parsing.
Delivery
- At least once, in log order for a given row. Deduplicate by event
id. - A
2xxresponse within 10 seconds counts as received. Answer fast, and process afterwards. - Otherwise, Rangee retries for 24 hours, waiting longer each time. After 24 hours of failures, the webhook is disabled and the workspace's Admins are told.
- A webhook does not expire: it lives until it is deleted.
- HTTPS URLs only, no private or local addresses.
- 20 webhooks per base at most.
Schema
| Operation | Request |
|---|---|
| List bases | GET /v1/bases |
| Read a base, its tables, fields and views | GET /v1/bases/{baseId} |
| Create a table | POST /v1/bases/{baseId}/tables |
| Update a table | PATCH /v1/tables/{tableId} |
| Create a field | POST /v1/tables/{tableId}/fields |
| Update a field | PATCH /v1/fields/{fieldId} |
| Read a view with its filters and sorts | GET /v1/views/{viewId} |
| Move to the trash | DELETE /v1/tables/{tableId}, DELETE /v1/fields/{fieldId} |
Describing a field. A field is described by { id, name, type, description, readOnly, options }. The content of options depends on the type, and keeps the same shape for reading and writing.
Type names. They follow the app's, with “row” instead of “record”: multipleRowLinks, rowId. Triggers follow the same rule: rowCreated, rowUpdated, rowMatches, rowEntersView.
Changing the type. A PATCH with a new type changes the field's type and converts the values the way the app does.
Deleting a field.
- It takes the Admin or User role. The field goes to the trash.
- A field used by a formula, an automation or an interface answers
409 FIELD_IN_USEwith the list of its uses. - Add
?force=trueto delete it anyway.
Attachments and comments
| Operation | Request |
|---|---|
| Upload a file | POST /v1/bases/{baseId}/attachments as multipart/form-data, 50 MB at most |
| List a row's comments | GET /v1/tables/{tableId}/rows/{rowId}/comments |
| Add a comment | POST /v1/tables/{tableId}/rows/{rowId}/comments |
| Update, delete a comment | PATCH, DELETE /v1/tables/{tableId}/comments/{commentId} |
Adding an attachment takes two steps:
- Upload the file. You receive
{ id, filename, size, type }. - Put
{ "id": "att…" }in an attachments value within the hour that follows. A file not used within that time is deleted.
curl https://api.rangee.io/v1/bases/app…/attachments \
-H "Authorization: Bearer $RANGEE_TOKEN" \
-F "file=@devis-atelier-martin.pdf"Comments. A comment mentions a member with @[usr…]. Only its author can edit or delete it.
Errors
Every error has the same shape:
{
"error": {
"code": "INVALID_VALUE",
"message": "« Montant » attend un nombre",
"details": { "path": "rows[2].values.fld01Montant…" }
}
}Nothing is ignored silently. An invalid value, an unknown field, a computed field written or a missing variable answers 422, with the exact path of the error in details.path.
| HTTP | Codes |
|---|---|
| 400 | BAD_REQUEST: unreadable JSON |
| 401 | UNAUTHENTICATED: token missing, invalid, revoked or expired |
| 403 | MISSING_SCOPE: the token lacks a scope; FORBIDDEN: the role does not allow the operation |
| 404 | NOT_FOUND: missing, or outside the token's perimeter |
| 409 | IDEMPOTENCY_CONFLICT, UPSERT_AMBIGUOUS, FIELD_IN_USE |
| 413 | PAYLOAD_TOO_LARGE |
| 422 | INVALID_REQUEST, INVALID_VALUE, UNKNOWN_FIELD, READ_ONLY_FIELD, INVALID_FILTER, INVALID_FORMULA, QUERY_TOO_EXPENSIVE, INVALID_INPUTS, AUTOMATION_DISABLED, ROW_LIMIT |
| 429 | RATE_LIMITED: rate exceeded, with a Retry-After header; MONTHLY_LIMIT: plan quota reached, with the reset date |
| 5xx | INTERNAL: replaying with the same Idempotency-Key is safe |
What to do, by code:
| Code | Replay? | What to do |
|---|---|---|
429 RATE_LIMITED | Yes | Wait for Retry-After, then replay |
5xx | Yes | Replay with the same Idempotency-Key, spacing out the attempts |
401 | No | The token needs to be made again or checked |
403 | No | Add the missing scope, or check the role of the token's creator |
422 | No | Fix the element pointed to by details.path |
Limits
| Item | Limit |
|---|---|
| Rate | 50 requests per second per base, 200 per token |
| Calls per month | 1,500 on Free; no cap on paid plans. Counted per organisation; calls refused with a 4xx do not count |
| Rows per page | 1 000 |
| Rows per write | 100 |
| Request body | 1 MB; 50 MB for an upload |
Waiting for a run (wait) | 30 seconds |
| Tokens | 50 active per person |
| Webhooks | 20 per base |
Every response gives your remaining rate in the RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset headers.
Track your usage
Settings › Security › API tokens lists your tokens and when they were last used. GET /v1/me gives the organisation's call quota for the month.
Need help
Write to support quoting the call's Request-Id. It is enough to find the request, its response and its context.