Skill Tracer
Skills › Scientific Agent Skills › benchling-integration › api_endpoints.md

api_endpoints.md

315 lines · skills/benchling-integration/references/api_endpoints.md at 5f2bc03

Open on GitHub
1# Benchling REST API Reference
2
3Reviewed 2026-09-30 against the [official reference](https://benchling.com/api/reference),
4platform guides, and generated **benchling-api-client 2.0.434**, resolved alongside SDK **1.25.0**.
5The public reference is a JavaScript application; the published Python client supplied
6the endpoint/parameter/model definitions for this audit. Check your tenant's interactive
7reference before executing writes, especially on a validated/restricted release.
8No authenticated endpoint calls were made. Payloads below are illustrative.
9

Benchling REST API Reference

Reviewed 2026-09-30 against the official reference, platform guides, and generated benchling-api-client 2.0.434, resolved alongside SDK 1.25.0. The public reference is a JavaScript application; the published Python client supplied the endpoint/parameter/model definitions for this audit. Check your tenant's interactive reference before executing writes, especially on a validated/restricted release. No authenticated endpoint calls were made. Payloads below are illustrative.

Raw lines 1 to 9
10## Version and authentication
12This reference covers `https://{tenant}.benchling.com/api/v2`. V3 also exists;
13consult its [availability/stability guide](https://docs.benchling.com/docs/v3-api-overview)
14and tenant documentation rather than replacing `/v2` with `/v3` in these paths.
16Use HTTPS and either Basic auth (API key as username, empty password) or OAuth Bearer
17auth. JSON writes use `Content-Type: application/json`. Token exchanges are form encoded
18at `/oauth/token`; the SDK's legacy `/api/v2/token` path still works. See
19[authentication](authentication.md) for identity, expiry, and token acquisition.

Version and authentication

This reference covers https://{tenant}.benchling.com/api/v2. V3 also exists; consult its availability/stability guide and tenant documentation rather than replacing /v2 with /v3 in these paths.

Use HTTPS and either Basic auth (API key as username, empty password) or OAuth Bearer auth. JSON writes use Content-Type: application/json. Token exchanges are form encoded at /oauth/token; the SDK's legacy /api/v2/token path still works. See authentication for identity, expiry, and token acquisition.

Raw lines 10 to 20
21## Responses, pagination, and filters
23Single-resource endpoints generally return a resource object; `GET /entries/{id}` is an
24exception with an `entry` wrapper. Creation usually returns HTTP 201 and the created
25resource. List responses use a **resource-specific collection key**, never a universal `results` key:
27```json
29 "dnaSequences": [{"id": "seq_example", "name": "Construct 001", "bases": "ATCG"}],
30 "nextToken": "opaque-token"
32```
34Most paginated lists accept `pageSize` (default 50, maximum 100) and `nextToken`. Preserve
35filters when requesting the next page; stop on an empty token. Some lists differ:
36`GET /registries` returns `registries` without the standard pagination arguments.
37`GET /events` has additional polling semantics described below.
39Filters are endpoint-specific. On DNA sequences, `name` matches the full name;
40`nameIncludes` supports substring search. `modifiedAt` and `createdAt` are string filters;
41consult the tenant reference for their comparison syntax rather than assuming every
42timestamp filter is inclusive. Use `archiveReason` for archival filtering according to
43the endpoint definition; `archived=false` is not a supported replacement.

Responses, pagination, and filters

Single-resource endpoints generally return a resource object; GET /entries/{id} is an exception with an entry wrapper. Creation usually returns HTTP 201 and the created resource. List responses use a resource-specific collection key, never a universal results key:

{
  "dnaSequences": [{"id": "seq_example", "name": "Construct 001", "bases": "ATCG"}],
  "nextToken": "opaque-token"
}

Most paginated lists accept pageSize (default 50, maximum 100) and nextToken. Preserve filters when requesting the next page; stop on an empty token. Some lists differ: GET /registries returns registries without the standard pagination arguments. GET /events has additional polling semantics described below.

Filters are endpoint-specific. On DNA sequences, name matches the full name; nameIncludes supports substring search. modifiedAt and createdAt are string filters; consult the tenant reference for their comparison syntax rather than assuming every timestamp filter is inclusive. Use archiveReason for archival filtering according to the endpoint definition; archived=false is not a supported replacement.

Raw lines 21 to 44
45### Collection and resource routes
47All paths in this table are relative to `/api/v2`. A `GET` collection returns the named
48collection; `POST` creates one resource with the type's create model. A resource `GET`
49returns one object (with the entry wrapper noted above) and `PATCH` applies its update model.
51| Collection | Collection methods | Collection key | Resource path and methods |
52|---|---|---|---|
53| `/dna-sequences` | GET, POST | `dnaSequences` | `/dna-sequences/{dnaSequenceId}` GET, PATCH |
54| `/rna-sequences` | GET, POST | `rnaSequences` | `/rna-sequences/{rnaSequenceId}` GET, PATCH |
55| `/aa-sequences` | GET, POST | `aaSequences` | `/aa-sequences/{aaSequenceId}` GET, PATCH |
56| `/custom-entities` | GET, POST | `customEntities` | `/custom-entities/{customEntityId}` GET, PATCH |
57| `/mixtures` | GET, POST | `mixtures` | `/mixtures/{mixtureId}` GET, PATCH |
58| `/containers` | GET, POST | `containers` | `/containers/{containerId}` GET, PATCH |
59| `/boxes` | GET, POST | `boxes` | `/boxes/{boxId}` GET, PATCH |
60| `/locations` | GET, POST | `locations` | `/locations/{locationId}` GET, PATCH |
61| `/plates` | GET, POST | `plates` | `/plates/{plateId}` GET, PATCH |
62| `/entries` | GET, POST | `entries` | `/entries/{entryId}` GET, PATCH |
63| `/workflow-tasks` | GET, POST | `workflowTasks` | `/workflow-tasks/{workflowTaskId}` GET, PATCH |
64| `/folders` | GET, POST | `folders` | `/folders/{folderId}` GET |
65| `/projects` | GET | `projects` | `/projects/{projectId}` GET |
66| `/users` | GET | `users` | `/users/{userId}` GET |
67| `/teams` | GET | `teams` | `/teams/{teamId}` GET |
68| `/registries` | GET | `registries` | `/registries/{registryId}` GET |
70This is a task-oriented subset, not a claim that these are each service's only methods.
71There is no documented `/users/me` endpoint; test auth against an accessible resource.

Collection and resource routes

All paths in this table are relative to /api/v2. A GET collection returns the named collection; POST creates one resource with the type's create model. A resource GET returns one object (with the entry wrapper noted above) and PATCH applies its update model.

CollectionCollection methodsCollection keyResource path and methods
/dna-sequencesGET, POSTdnaSequences/dna-sequences/{dnaSequenceId} GET, PATCH
/rna-sequencesGET, POSTrnaSequences/rna-sequences/{rnaSequenceId} GET, PATCH
/aa-sequencesGET, POSTaaSequences/aa-sequences/{aaSequenceId} GET, PATCH
/custom-entitiesGET, POSTcustomEntities/custom-entities/{customEntityId} GET, PATCH
/mixturesGET, POSTmixtures/mixtures/{mixtureId} GET, PATCH
/containersGET, POSTcontainers/containers/{containerId} GET, PATCH
/boxesGET, POSTboxes/boxes/{boxId} GET, PATCH
/locationsGET, POSTlocations/locations/{locationId} GET, PATCH
/platesGET, POSTplates/plates/{plateId} GET, PATCH
/entriesGET, POSTentries/entries/{entryId} GET, PATCH
/workflow-tasksGET, POSTworkflowTasks/workflow-tasks/{workflowTaskId} GET, PATCH
/foldersGET, POSTfolders/folders/{folderId} GET
/projectsGETprojects/projects/{projectId} GET
/usersGETusers/users/{userId} GET
/teamsGETteams/teams/{teamId} GET
/registriesGETregistries/registries/{registryId} GET

This is a task-oriented subset, not a claim that these are each service's only methods. There is no documented /users/me endpoint; test auth against an accessible resource.

Raw lines 45 to 72
73### Schema routes
75Use `GET` on each collection or append `/{schemaId}` for one schema:
77| Collection | Collection key |
78|---|---|
79| `/entity-schemas` | `entitySchemas` |
80| `/entry-schemas` | `entrySchemas` |
81| `/container-schemas` | `containerSchemas` |
82| `/box-schemas` | `boxSchemas` |
83| `/location-schemas` | `locationSchemas` |
84| `/plate-schemas` | `plateSchemas` |
85| `/workflow-task-schemas` | `workflowTaskSchemas` |
87There is no generic stable-v2 `/schemas?entityType=...` route. The SDK exposes these as
88`benchling.schemas.list_entity_schemas()`, `get_entity_schema_by_id(id)`, and analogous
89resource-specific methods. Schema definitions determine field names, value types,
90required values, units, and valid workflow statuses.

Schema routes

Use GET on each collection or append /{schemaId} for one schema:

CollectionCollection key
/entity-schemasentitySchemas
/entry-schemasentrySchemas
/container-schemascontainerSchemas
/box-schemasboxSchemas
/location-schemaslocationSchemas
/plate-schemasplateSchemas
/workflow-task-schemasworkflowTaskSchemas

There is no generic stable-v2 /schemas?entityType=... route. The SDK exposes these as benchling.schemas.list_entity_schemas(), get_entity_schema_by_id(id), and analogous resource-specific methods. Schema definitions determine field names, value types, required values, units, and valid workflow statuses.

Raw lines 73 to 91
92## Entity create/update/archive
94A registered DNA sequence create body for `POST /dna-sequences`:
96```json
98 "name": "Construct 001",
99 "bases": "ATCGATCG",
100 "isCircular": true,
101 "folderId": "lib_example",
102 "schemaId": "ts_example",
103 "registryId": "src_example",
104 "namingStrategy": "NEW_IDS",
105 "fields": {"gene_name": {"value": "GFP"}}
107```
109Omit registration arguments to create an unregistered entity. To assign a human registry
110identifier explicitly, supply `entityRegistryId` **instead of** `namingStrategy`, together
111with `registryId`. These are different concepts, not interchangeable IDs.
113`POST /rna-sequences` uses `bases` (RNA alphabet) and `isCircular`.
114`POST /aa-sequences` uses `aminoAcids`. `POST /custom-entities` uses `name`, `schemaId`,
115`folderId`, and applicable fields. Do not coerce numeric fields to strings globally.
116A partial update uses the same field-value envelope, for example:
118```json
119{"fields": {"passage_number": {"value": 16}}}
120```
122Sequence filters include `folderId`, `schemaId`, `name`, `nameIncludes`, `modifiedAt`,
123`createdAt`, `registryId`, and `archiveReason`; `schemaId` is an optional custom-entity
124filter rather than a requirement to list all accessible entities.
126Archival endpoints are `POST /dna-sequences:archive`, `/rna-sequences:archive`,
127`/aa-sequences:archive`, `/custom-entities:archive`, and `/mixtures:archive`.
128Use the appropriate array key (`dnaSequenceIds`, `rnaSequenceIds`, `aaSequenceIds`,
129`customEntityIds`, or `mixtureIds`) and an allowed reason:
131```json
132{"dnaSequenceIds": ["seq_example"], "reason": "Retired"}
133```
135Allowed `EntityArchiveReason` values in this release include `Made in error`, `Retired`,
136`Expended`, `Shipped`, `Contaminated`, `Expired`, `Missing`, and `Other`. Free-form
137reasons such as `Cleanup` are not the enum. Read the archival response rather than
138assuming deletion; archival preserves records and can affect related data.
140For mixture creation, each ingredient uses a separate amount and unit:
142```json
144 "name": "Example formulation",
145 "schemaId": "ts_mixture",
146 "folderId": "lib_example",
147 "ingredients": [{
148 "componentEntityId": "bfi_component",
149 "amount": "100",
150 "units": "mg",
151 "catalogIdentifier": null,
152 "componentLotContainerId": null,
153 "componentLotEntityId": null,
154 "componentLotText": null,
155 "notes": null
156 }]
158```

Entity create/update/archive

A registered DNA sequence create body for POST /dna-sequences:

{
  "name": "Construct 001",
  "bases": "ATCGATCG",
  "isCircular": true,
  "folderId": "lib_example",
  "schemaId": "ts_example",
  "registryId": "src_example",
  "namingStrategy": "NEW_IDS",
  "fields": {"gene_name": {"value": "GFP"}}
}

Omit registration arguments to create an unregistered entity. To assign a human registry identifier explicitly, supply entityRegistryId instead of namingStrategy, together with registryId. These are different concepts, not interchangeable IDs.

POST /rna-sequences uses bases (RNA alphabet) and isCircular. POST /aa-sequences uses aminoAcids. POST /custom-entities uses name, schemaId, folderId, and applicable fields. Do not coerce numeric fields to strings globally. A partial update uses the same field-value envelope, for example:

{"fields": {"passage_number": {"value": 16}}}

Sequence filters include folderId, schemaId, name, nameIncludes, modifiedAt, createdAt, registryId, and archiveReason; schemaId is an optional custom-entity filter rather than a requirement to list all accessible entities.

Archival endpoints are POST /dna-sequences:archive, /rna-sequences:archive, /aa-sequences:archive, /custom-entities:archive, and /mixtures:archive. Use the appropriate array key (dnaSequenceIds, rnaSequenceIds, aaSequenceIds, customEntityIds, or mixtureIds) and an allowed reason:

{"dnaSequenceIds": ["seq_example"], "reason": "Retired"}

Allowed EntityArchiveReason values in this release include Made in error, Retired, Expended, Shipped, Contaminated, Expired, Missing, and Other. Free-form reasons such as Cleanup are not the enum. Read the archival response rather than assuming deletion; archival preserves records and can affect related data.

For mixture creation, each ingredient uses a separate amount and unit:

{
  "name": "Example formulation",
  "schemaId": "ts_mixture",
  "folderId": "lib_example",
  "ingredients": [{
    "componentEntityId": "bfi_component",
    "amount": "100",
    "units": "mg",
    "catalogIdentifier": null,
    "componentLotContainerId": null,
    "componentLotEntityId": null,
    "componentLotText": null,
    "notes": null
  }]
}
Raw lines 92 to 159
160## Inventory payloads and actions
162For container/box/plate creation, `schemaId` is required. Location creation requires
163`name` and `schemaId`. Depending on storage type, add `parentStorageId`, `barcode`,
164`name`, or schema `fields`. Query containers and boxes by `ancestorStorageId` and
165`barcodes` (comma-separated); `parentStorageId` and singular `barcode` are not the list
166filters shown in this SDK. Ancestor filters include nested descendants.
168Move a container physically with `PATCH /containers/{containerId}`:
170```json
171{"parentStorageId": "loc_destination"}
172```
174Check out with `POST /containers:check-out`:
176```json
177{"containerIds": ["con_example"], "assigneeId": "usr_example", "comment": "At bench"}
178```
180Check in with `POST /containers:check-in`:
182```json
183{"containerIds": ["con_example"], "comments": "Returned"}
184```
186These operations return HTTP 200 with an empty JSON object `{}` on success. There is no `locationId` parameter
187for check-in. Notice the hyphen in each route and different `comment`/`comments` keys.
189**Material transfer** has two distinct operations:
191- `POST /containers/{destinationContainerId}:transfer`: `ContainerTransfer` body,
192 including `destinationContents` plus the applicable source/quantity fields; HTTP 200 with `{}`
193 on success.
194- `POST /transfers`: a `transfers` array of `MultipleContainersTransfer` objects;
195 returns HTTP 202 with a `taskId` for async processing. Limit: 5000 transfers per request.
197Illustrative batch body:
199```json
201 "transfers": [{
202 "sourceContainerId": "con_source",
203 "destinationContainerId": "con_destination",
204 "transferQuantity": {"value": 5.0, "units": "uL"}
205 }]
207```
209Neither `/containers:transfer` nor `/containers:bulk-transfer` is the corresponding
210stable-v2 route. Never use material transfer to represent moving a tube to a box.
212Plate creation `wells` is a mapping of positions to well properties, not an array of
213`{"position": "A1", "entityId": ...}`. Create/read the plate first, resolve its well
214container IDs, then load biological contents through transfer APIs. Storage schema and
215well positions must match the actual plate configuration.

Inventory payloads and actions

For container/box/plate creation, schemaId is required. Location creation requires name and schemaId. Depending on storage type, add parentStorageId, barcode, name, or schema fields. Query containers and boxes by ancestorStorageId and barcodes (comma-separated); parentStorageId and singular barcode are not the list filters shown in this SDK. Ancestor filters include nested descendants.

Move a container physically with PATCH /containers/{containerId}:

{"parentStorageId": "loc_destination"}

Check out with POST /containers:check-out:

{"containerIds": ["con_example"], "assigneeId": "usr_example", "comment": "At bench"}

Check in with POST /containers:check-in:

{"containerIds": ["con_example"], "comments": "Returned"}

These operations return HTTP 200 with an empty JSON object {} on success. There is no locationId parameter for check-in. Notice the hyphen in each route and different comment/comments keys.

Material transfer has two distinct operations:

  • POST /containers/{destinationContainerId}:transfer: ContainerTransfer body, including destinationContents plus the applicable source/quantity fields; HTTP 200 with {} on success.
  • POST /transfers: a transfers array of MultipleContainersTransfer objects; returns HTTP 202 with a taskId for async processing. Limit: 5000 transfers per request.

Illustrative batch body:

{
  "transfers": [{
    "sourceContainerId": "con_source",
    "destinationContainerId": "con_destination",
    "transferQuantity": {"value": 5.0, "units": "uL"}
  }]
}

Neither /containers:transfer nor /containers:bulk-transfer is the corresponding stable-v2 route. Never use material transfer to represent moving a tube to a box.

Plate creation wells is a mapping of positions to well properties, not an array of {"position": "A1", "entityId": ...}. Create/read the plate first, resolve its well container IDs, then load biological contents through transfer APIs. Storage schema and well positions must match the actual plate configuration.

Raw lines 160 to 216
217## Entries, workflow tasks, folders
219`POST /entries` requires `name` and `folderId`; `entryTemplateId`, `initialTables`,
220`schemaId`, and `fields` support appropriate template/schema workflows. `PATCH /entries/{id}`
221updates supported metadata and schema fields; it is not a general rich-text editor.
222`GET /entries` supports `projectId`, `schemaId`, `modifiedAt`, and other documented
223filters, but does not expose `folderId` in the stable SDK's list method.
224`GET /entry-templates` and `/entry-templates/{entryTemplateId}` discover templates.
225There is no general `entry_links` service to attach arbitrary objects to an entry.
227`POST /workflow-tasks` requires a task group:
229```json
230{"workflowTaskGroupId": "wtg_example", "assigneeId": "usr_example"}
231```
233`GET /workflow-tasks` accepts comma-separated `workflowTaskGroupIds`, `statusIds`, and
234`assigneeIds`, plus `schemaId` and pagination. `PATCH /workflow-tasks/{workflowTaskId}`
235can set `statusId`, `assigneeId`, `scheduledOn`, and `fields`. Status IDs come from the
236workflow task schema; there is no `workflowId` create/list field here.
238`POST /folders` takes `name` and `parentFolderId`; the parent determines the project.
239Do not add a `projectId` to `FolderCreate`. Folder listing does accept `projectId` and
240`parentFolderId` filters.

Entries, workflow tasks, folders

POST /entries requires name and folderId; entryTemplateId, initialTables, schemaId, and fields support appropriate template/schema workflows. PATCH /entries/{id} updates supported metadata and schema fields; it is not a general rich-text editor. GET /entries supports projectId, schemaId, modifiedAt, and other documented filters, but does not expose folderId in the stable SDK's list method. GET /entry-templates and /entry-templates/{entryTemplateId} discover templates. There is no general entry_links service to attach arbitrary objects to an entry.

POST /workflow-tasks requires a task group:

{"workflowTaskGroupId": "wtg_example", "assigneeId": "usr_example"}

GET /workflow-tasks accepts comma-separated workflowTaskGroupIds, statusIds, and assigneeIds, plus schemaId and pagination. PATCH /workflow-tasks/{workflowTaskId} can set statusId, assigneeId, scheduledOn, and fields. Status IDs come from the workflow task schema; there is no workflowId create/list field here.

POST /folders takes name and parentFolderId; the parent determines the project. Do not add a projectId to FolderCreate. Folder listing does accept projectId and parentFolderId filters.

Raw lines 217 to 241
242## Async jobs and events
244`GET /tasks/{taskId}` polls an asynchronous server job; it is not the workflow-task
245CRUD endpoint. Responses contain `status` (`RUNNING`, `SUCCEEDED`, `FAILED`) and
246operation-specific `response`, `errors`, or `message` when applicable. Timeout on the
247client does not cancel the job. Keep the task ID and check its final outcome.
249`GET /events` returns `events` with `nextToken`. Filters include `createdAt.gte`,
250`startingAfter` (event ID), `eventTypes` (comma-separated), `poll`, and `pageSize`.
251The SDK spelling is `created_atgte`. Events are sorted by processing order, not by
252`createdAt`; never terminate recovery early merely because a timestamp is out of order.
253See [event recovery](eventbridge.md) for retention and deduplication.

Async jobs and events

GET /tasks/{taskId} polls an asynchronous server job; it is not the workflow-task CRUD endpoint. Responses contain status (RUNNING, SUCCEEDED, FAILED) and operation-specific response, errors, or message when applicable. Timeout on the client does not cancel the job. Keep the task ID and check its final outcome.

GET /events returns events with nextToken. Filters include createdAt.gte, startingAfter (event ID), eventTypes (comma-separated), poll, and pageSize. The SDK spelling is created_atgte. Events are sorted by processing order, not by createdAt; never terminate recovery early merely because a timestamp is out of order. See event recovery for retention and deduplication.

Raw lines 242 to 254
255## Field values
257The `fields` mapping is keyed by actual schema field names, with a `value` member:
259```json
261 "passage_number": {"value": 15},
262 "comment": {"value": "QC passed"},
263 "linked_entities": {"value": ["seq_example"]},
264 "selection": {"value": ["sfso_example"]}
266```
268Use schema-defined scalar/list multiplicity. Dropdown values are option IDs, not
269human option labels. Numeric fields accept numeric values; date/datetime formats depend
270on the field definition. `null` is distinct from omission and may clear a value.
271See [schema field types](https://docs.benchling.com/docs/schemas).

Field values

The fields mapping is keyed by actual schema field names, with a value member:

{
  "passage_number": {"value": 15},
  "comment": {"value": "QC passed"},
  "linked_entities": {"value": ["seq_example"]},
  "selection": {"value": ["sfso_example"]}
}

Use schema-defined scalar/list multiplicity. Dropdown values are option IDs, not human option labels. Numeric fields accept numeric values; date/datetime formats depend on the field definition. null is distinct from omission and may clear a value. See schema field types.

Raw lines 255 to 272
273## Errors and limits
275Failures normally use an `error` object with `message`, `type`, and sometimes
276`userMessage`. Do not assume exception-class names like `RateLimitError` appear in JSON.
277Check HTTP status before reading success data: 400 invalid request, 401 authentication,
278403 permission, 404 missing/inaccessible resource, 429 throttling, and 5xx server errors.
279A successful check-in or single-container transfer returns an empty JSON object.
281The [V2 limits guide](https://docs.benchling.com/docs/rate-limiting) currently lists
28260 requests/30 seconds across user keys per tenant, 300/30 seconds per app, and
2831000/30 seconds across apps per tenant. There are separate dynamic throughput limits.
284The response headers are `x-rate-limit-limit`, `x-rate-limit-remaining`, and
285`x-rate-limit-reset`; **reset is seconds remaining, not an epoch timestamp**.
287Use bounded exponential backoff with jitter for 429/transient server failures and honor
288server retry guidance when present. Do not invent a JSON `retryAfter` field or recurse
289without an attempt limit. Reconcile ambiguous write failures before repeating creates.

Errors and limits

Failures normally use an error object with message, type, and sometimes userMessage. Do not assume exception-class names like RateLimitError appear in JSON. Check HTTP status before reading success data: 400 invalid request, 401 authentication, 403 permission, 404 missing/inaccessible resource, 429 throttling, and 5xx server errors. A successful check-in or single-container transfer returns an empty JSON object.

The V2 limits guide currently lists 60 requests/30 seconds across user keys per tenant, 300/30 seconds per app, and 1000/30 seconds across apps per tenant. There are separate dynamic throughput limits. The response headers are x-rate-limit-limit, x-rate-limit-remaining, and x-rate-limit-reset; reset is seconds remaining, not an epoch timestamp.

Use bounded exponential backoff with jitter for 429/transient server failures and honor server retry guidance when present. Do not invent a JSON retryAfter field or recurse without an attempt limit. Reconcile ambiguous write failures before repeating creates.

Raw lines 273 to 290
291## Direct pagination example
293This illustrative read-only example uses `httpx`, checks status, and preserves filters:
295```python
296import os
297import httpx
299with httpx.Client(
300 base_url=os.environ["BENCHLING_TENANT_URL"].rstrip("/") + "/api/v2/",
301 auth=(os.environ["BENCHLING_API_KEY"], ""),
302 timeout=30.0,
303) as client:
304 params = {"pageSize": 100, "schemaId": "ts_example"}
305 while True:
306 response = client.get("dna-sequences", params=params)
307 response.raise_for_status()
308 data = response.json()
309 for sequence in data["dnaSequences"]:
310 print(sequence["id"], sequence["name"])
311 next_token = data.get("nextToken")
312 if not next_token:
313 break
314 params["nextToken"] = next_token
315```

Direct pagination example

This illustrative read-only example uses httpx, checks status, and preserves filters:

import os
import httpx

with httpx.Client(
    base_url=os.environ["BENCHLING_TENANT_URL"].rstrip("/") + "/api/v2/",
    auth=(os.environ["BENCHLING_API_KEY"], ""),
    timeout=30.0,
) as client:
    params = {"pageSize": 100, "schemaId": "ts_example"}
    while True:
        response = client.get("dna-sequences", params=params)
        response.raise_for_status()
        data = response.json()
        for sequence in data["dnaSequences"]:
            print(sequence["id"], sequence["name"])
        next_token = data.get("nextToken")
        if not next_token:
            break
        params["nextToken"] = next_token
Raw lines 291 to 315