rangee
Developers

API documentation

Read, search, create and update your rows, trigger your automations, receive changes by webhook and manage your bases' schema. With the app's words: workspace, base, table, field, view, row, automation.

Base URLhttps://api.rangee.io/v1REST · JSONPersonal tokensBeta
During the betaThe /v1 API is open to beta members. Tokens are created in Settings › Security › API tokens, and each base has its API tester in the base menu › API. Choosing bases and an expiry in Settings, the “API call” trigger in the automation editor and writing an attachment by URL arrive with the next waves.

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.

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

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

Terminal
curl "https://api.rangee.io/v1/tables/tbl…/rows?limit=10&fieldKey=name" \
  -H "Authorization: Bearer $RANGEE_TOKEN"

5. Create a row.

Terminal
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:

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:

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()
In production, prefer field IDs (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:

Text
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

ScopeGives access to
rows:readRead and search rows, their history and comments; download attachments
rows:writeCreate, update, delete and upsert rows; write comments; upload attachments
schema:readRead bases, tables, fields and views
schema:writeCreate and update tables, fields and views; move them to the trash
automations:readList automations, their variables and their runs
automations:runTrigger an automation and follow the run
webhooks:manageCreate, 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

Text
GET /v1/me

Returns the person, the token's name, its scopes, its perimeter and the month's call quota.

Vocabulary and IDs

TermMeaningID
WorkspaceThe organisation's space, which holds the baseswsp…
BaseA set of linked tablesapp…
TableA list of rows with the same schematbl…
FieldA typed column of a tablefld…
ViewA saved presentation of a table (filters, sorts, visible fields)viw…
RowAn item of a table; its values are in valuesrow…
ValueA row's content for a field—
AutomationA trigger and a series of stepsaut…
RunOne pass of an automationrun…
VariableA piece of data that an API-triggered automation asks for at call timename of your choice
WebhookAn address told about a base's changeswhk…
EventA change sent to a webhookevt…
AttachmentAn uploaded file, placed in a valueatt…
CommentA message on a rowcom…
OptionA choice of a select fieldsel…
MemberA person in the workspaceusr…
TokenA person's access key to the APIpat…

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 as YYYY-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) and cursor. A cursor stays valid for 24 hours. When nextCursor is null, 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=name to any row read to receive them keyed by name.
  • Replay safely. Any POST, PATCH, PUT or DELETE request accepts an Idempotency-Key header (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 answers 409 IDEMPOTENCY_CONFLICT.
  • Support. Every response carries a Request-Id header. Quote it to support: it is enough to find the call.

Paginate

JavaScript
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

JSON
{
  "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.

OperationRequest
Read a rowGET /v1/tables/{tableId}/rows/{rowId}
ListGET /v1/tables/{tableId}/rows?viewId&fields&limit&cursor&fieldKey
SearchPOST /v1/tables/{tableId}/rows/query (details)
CreatePOST /v1/tables/{tableId}/rows with { "rows": [{ "values": {…} }] }
UpdatePATCH /v1/tables/{tableId}/rows with { "rows": [{ "id": "row…", "values": {…} }] }, or PATCH /v1/tables/{tableId}/rows/{rowId} with { "values": {…} }
ReplacePUT /v1/tables/{tableId}/rows/{rowId} with { "values": {…} }
UpsertPOST /v1/tables/{tableId}/rows/upsert with { "matchOn": ["fld…"], "rows": [...] }
DeleteDELETE /v1/tables/{tableId}/rows?ids=row…,row…, or DELETE /v1/tables/{tableId}/rows/{rowId}
HistoryGET /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.

JSON
{ "coerce": true, "rows": [{ "values": { "fld01Montant…": "1 250,50", "fld01Signé…": "oui" } }] }

Upsert.

  • matchOn lists 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).
Terminal
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

Text
POST /v1/tables/{tableId}/rows/query

The body takes the shape of the app's view filters:

KeyRole
filterAn and or or group of conditions { field, operator, value }, nestable 5 levels deep
sort[{ "field": "fld…", "direction": "asc" | "desc" }]
searchText searched in the text fields
viewIdStarts from a view's filters and sorts
formulaA Rangee formula that must be true
fields, limit, cursor, fieldKeyAs for the list

filter, search, viewId and formula combine with “and”.

JSON
{
  "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

TypeReadingWriting
Text, long text, rich text, email, URL, phonestringstring; the email is checked
Number, currencynumbernumber; with coerce, "12,5" or "12.5"
Percentnumber, 0.25 for 25%same
Ratinginteger from 1 to the field's maximumsame
Durationinteger in secondssame; with coerce, "1:30:00"
Checkboxtrue, absent when uncheckedboolean; with coerce, "oui", "yes", "1"
Date"YYYY-MM-DD"same; with coerce, any ISO date
Date and timeISO 8601 UTCISO 8601 with time zone
Single select{ "id": "sel…", "name": "Signé" }ID or name of the option
Multiple selectlist of { id, name }list of IDs or names
Collaborator, collaborators{ id, email, name }, or a listID or e-mail of a workspace member
Attachmentslist of { id, url, filename, size, type, thumbnails }; signed url, valid for 2 hourslist of { "id" } of an uploaded file
Link to rowslist of row IDssame; 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, aggregationtype of the resultread-only
Autonumber, row IDinteger / stringread-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 rowId names its table. The server checks that the row exists and loads it under trigger.inputs.<name>.fields.

“Button” trigger. It runs with a rowId, like a click in the app.

OperationRequest
List a base's automationsGET /v1/bases/{baseId}/automations
Read an automation and its variablesGET /v1/automations/{automationId}
RunPOST /v1/automations/{automationId}/runs
Follow a runGET /v1/runs/{runId}
List runsGET /v1/automations/{automationId}/runs?status&limit&cursor
Terminal
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: response 202, the run is queued (queued).
  • With wait (0 to 30 seconds): the server waits for the run to finish, up to that delay. It answers 200 with the result if it finished, 202 otherwise.

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:

StateMeaning
queuedQueued
runningIn progress
waitingStopped on a “Wait” step
succeededSucceeded
failedFailed; the run gives the step at fault and its message
refusedRefused: 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.

OperationRequest
CreatePOST /v1/bases/{baseId}/webhooks with { url, events, tableIds?, fieldIds?, includeValues? }
ListGET /v1/bases/{baseId}/webhooks
Read, update, deleteGET, PATCH, DELETE /v1/webhooks/{webhookId}
Recent deliveriesGET /v1/webhooks/{webhookId}/deliveries
Send a testPOST /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:

JSON
{
  "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:

Text
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:

JavaScript
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:

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 2xx response 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

OperationRequest
List basesGET /v1/bases
Read a base, its tables, fields and viewsGET /v1/bases/{baseId}
Create a tablePOST /v1/bases/{baseId}/tables
Update a tablePATCH /v1/tables/{tableId}
Create a fieldPOST /v1/tables/{tableId}/fields
Update a fieldPATCH /v1/fields/{fieldId}
Read a view with its filters and sortsGET /v1/views/{viewId}
Move to the trashDELETE /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_USE with the list of its uses.
  • Add ?force=true to delete it anyway.

Attachments and comments

OperationRequest
Upload a filePOST /v1/bases/{baseId}/attachments as multipart/form-data, 50 MB at most
List a row's commentsGET /v1/tables/{tableId}/rows/{rowId}/comments
Add a commentPOST /v1/tables/{tableId}/rows/{rowId}/comments
Update, delete a commentPATCH, DELETE /v1/tables/{tableId}/comments/{commentId}

Adding an attachment takes two steps:

  1. Upload the file. You receive { id, filename, size, type }.
  2. Put { "id": "att…" } in an attachments value within the hour that follows. A file not used within that time is deleted.
Terminal
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:

JSON
{
  "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.

HTTPCodes
400BAD_REQUEST: unreadable JSON
401UNAUTHENTICATED: token missing, invalid, revoked or expired
403MISSING_SCOPE: the token lacks a scope; FORBIDDEN: the role does not allow the operation
404NOT_FOUND: missing, or outside the token's perimeter
409IDEMPOTENCY_CONFLICT, UPSERT_AMBIGUOUS, FIELD_IN_USE
413PAYLOAD_TOO_LARGE
422INVALID_REQUEST, INVALID_VALUE, UNKNOWN_FIELD, READ_ONLY_FIELD, INVALID_FILTER, INVALID_FORMULA, QUERY_TOO_EXPENSIVE, INVALID_INPUTS, AUTOMATION_DISABLED, ROW_LIMIT
429RATE_LIMITED: rate exceeded, with a Retry-After header; MONTHLY_LIMIT: plan quota reached, with the reset date
5xxINTERNAL: replaying with the same Idempotency-Key is safe

What to do, by code:

CodeReplay?What to do
429 RATE_LIMITEDYesWait for Retry-After, then replay
5xxYesReplay with the same Idempotency-Key, spacing out the attempts
401NoThe token needs to be made again or checked
403NoAdd the missing scope, or check the role of the token's creator
422NoFix the element pointed to by details.path

Limits

ItemLimit
Rate50 requests per second per base, 200 per token
Calls per month1,500 on Free; no cap on paid plans. Counted per organisation; calls refused with a 4xx do not count
Rows per page1 000
Rows per write100
Request body1 MB; 50 MB for an upload
Waiting for a run (wait)30 seconds
Tokens50 active per person
Webhooks20 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.