rest_api_source schema, describing the API client (base URL, authentication, pagination) and the resources (endpoints) to ingest.
Compatible versions
The configuration follows the dlt
rest_api_source schema. When Popsink upgrades dlt, existing configurations keep working — the schema is backwards compatible within the 1.x line.Key Features
- Connect to any REST API: base URL, authentication, pagination, and endpoints are all declared in a single JSON configuration.
- Multiple auth styles: bearer tokens, API keys, HTTP basic, and OAuth 2.0 client credentials — set once for the client or overridden per endpoint.
- Every common pagination scheme: next-URL in the body,
Linkheader, cursor, offset/limit, and page number. - Incremental synchronization: declare a cursor field per resource and only new or updated records are delivered on each run. The cursor is persisted by Popsink and survives restarts.
- Parent/child endpoints: fetch a list, then call a detail or sub-resource endpoint for each item.
- Scheduled ingestion: flexible scheduling by minutes, hours, or days, with an immediate first run on creation.
Configuration
Configuration structure
The configuration JSON has up to three top-level blocks:client—base_url,auth, and optionally a defaultpaginatorand staticheaderssent with every request.resource_defaults— optional. Anything you can put on a resource (endpoint.params,endpoint.paginator,primary_key, …) can be set here once and is merged into every resource.resources— an array of endpoints. Each resource has aname(which becomes the stream/topic name) and anendpoint:
A resource can also be written as a plain string (
"resources": ["customers", "invoices"]): the string is used as both the name and the path, with the client defaults.
Authentication
Authentication is set inclient.auth and applies to every request. An endpoint can override it with its own endpoint.auth.
- Bearer token
- API key (header)
- API key (query)
- HTTP basic
- OAuth 2.0 client credentials
- Custom headers
Sends
Authorization: Bearer <token>.OAuth 2.0 flows that need a user to log in (authorization code with a refresh token) and JWT-signed assertions are not available in the generic REST API source. Use the dedicated connector when one exists (Google Ads, HubSpot, …), or an API key / client-credentials grant if the provider offers one.
Pagination
Set the paginator once inclient.paginator, or per endpoint in endpoint.paginator. When none is given, the connector tries to detect the scheme from the first response ("type": "auto") — fine for a quick test, but declare it explicitly in production so a change in the API response does not silently stop pagination after the first page.
- Next URL in the body
- Link header
- Cursor
- Offset / limit
- Page number
- Single page
The response contains the full URL of the next page. Pagination stops when the value is missing or
null.endpoint.params (for example "per_page": 100), or in resource_defaults.endpoint.params to apply it to every resource. Use the largest page size the API allows — it reduces the number of requests and the risk of hitting rate limits.
Incremental sync
Withoutincremental, every scheduled run re-reads the whole endpoint and delivers all records again. With incremental, the connector tracks the highest value of a cursor field seen so far and only asks the API for newer records on the next run.
There are two ways to pass the cursor to the API.
1. With start_param — the API takes the cursor as a query parameter:
GET /tickets?updated_since=2024-01-01T00:00:00Z. If the highest updated_at received is 2026-10-20T08:12:45Z, the next run requests GET /tickets?updated_since=2026-10-20T08:12:45Z.
2. With a placeholder — when the value must be embedded in a parameter, a path, or a POST body, use {incremental.start_value} and declare the cursor in incremental:
The cursor is stored with the connector after each successful run and restored when the worker restarts.
initial_value is only read on the first run: changing it later does not re-ingest older data. To start over, create a new connector.Parent/child endpoints
Some data is only reachable through a parent: the comments of each ticket, the line items of each order. Reference a field of the parent resource in the child path with{resources.<parent_name>.<field>} — the child endpoint is called once per parent record.
project_tasks record is enriched with _projects_name, copied from its parent (include_from_parent). The placeholder also works in params, for APIs that pass the parent ID as a query parameter ("params": { "project_id": "{resources.projects.id}" }).
Handling errors and empty responses
By default, any HTTP error fails the run. Useresponse_actions to ignore expected errors — for example a 404 for a parent without children, or a 403 on an optional endpoint:
429) and transient server errors (5xx) are retried automatically with exponential backoff, honoring the Retry-After header when the API sends one.
Complete example
Ingesting the issues and issue comments of a GitHub repository: bearer authentication,Link header pagination, an incremental cursor on updated_at, and a child resource.
issues and issue_comments. On each run, only issues updated since the previous run are fetched, and comments are fetched for those issues only.
How records are delivered
- One topic per resource. Each resource
namebecomes a stream that you can select when building the pipeline. Names must be unique within the configuration. - Append-only. Every record read from the API is delivered as a new event. The connector does not detect deletions, and an updated record is delivered again in full. Use the record’s primary key to deduplicate or merge in the target.
- Nested data. Nested objects are flattened into columns (
address.city→address__city). Nested lists are split into separate child tables (customers__tags), which are not selectable as streams. To keep nested lists inside the parent record as JSON, set"max_table_nesting": 0on the resource, or inresource_defaults. - Schema. Columns are inferred from the records; a field that is absent from a record is delivered as
null.
Limitations
- JSON responses only. XML, CSV or binary responses are not supported.
- No custom code. dlt features that need Python — custom paginators, custom authentication classes,
processing_steps(filter,map) — cannot be expressed in the JSON configuration. GETandPOSTonly. GraphQL APIs can be ingested through aPOSTwith the query injson, provided pagination fits one of the schemes above.- Scheduled, not real-time. The connector polls the API on its schedule; for push-based APIs, use the Webhook source.
Troubleshooting
The run only returns the first page
The run only returns the first page
The paginator was not detected or is wrong. Declare it explicitly, and check the field paths against a real response (
next_url_path, cursor_path, total_path). For page_number, check base_page: an API starting at 1 with base_page: 0 re-reads the first page.The stream is empty but the API returns data
The stream is empty but the API returns data
data_selector probably points at the wrong place. Call the endpoint with curl and set data_selector to the path of the records list, e.g. data, results, or response.items.Every run re-delivers all records
Every run re-delivers all records
The resource has no
incremental block, or the API ignores the parameter in start_param. Check the API documentation for the exact parameter name and the expected date format.401 or 403 errors
401 or 403 errors
Check the
auth type expected by the API (bearer vs. API key header name vs. basic), and that the token has read access to every endpoint listed in resources. For an endpoint the token is not allowed to read, either remove it or add a response_actions entry to ignore the status.The run fails on a missing total
The run fails on a missing total
The
offset or page_number paginator looks for a total field by default. Set total_path to the right field, or to null if the API does not return one.