Skip to main content
The REST API Source Connector lets you ingest data from any REST API into Popsink pipelines by providing a declarative JSON configuration. It is the generic counterpart to the dedicated API connectors (Zendesk, Shopify, Pipedrive, …): if a SaaS platform doesn’t have its own connector yet, you can usually integrate it in minutes with this one. The configuration follows the dlt 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, Link header, 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 default paginator and static headers sent 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 a name (which becomes the stream/topic name) and an endpoint:
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.
Start with one resource and a short schedule, check the records in the topic, then add the other endpoints. A mistake in one resource fails the whole run, so it is easier to iterate one endpoint at a time.

Authentication

Authentication is set in client.auth and applies to every request. An endpoint can override it with its own endpoint.auth.
Sends Authorization: Bearer <token>.
Credentials are part of the configuration JSON. Use a dedicated, read-only token scoped to the endpoints you ingest, and rotate it by editing the connector configuration.
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 in client.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.
The response contains the full URL of the next page. Pagination stops when the value is missing or null.
For offset and page_number, total_path defaults to "total". If the API does not return a total field, the run fails with an error about the missing total: set "total_path": null explicitly, and the connector will stop on the first empty page (or on has_more_path).
The page size is usually a regular query parameter: put it in 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

Without incremental, 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:
The first run requests 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.
Pick a cursor the API can actually filter and sort on — typically updated_at, not created_at, so that updated records are picked up too. Records sharing the last cursor value are deduplicated between runs, but an updated record is delivered again in full: deduplicate or merge on the record’s primary key in the target.

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.
Each 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}" }).
A child endpoint issues one request per parent record. On a parent with tens of thousands of items, this can take long and exhaust the API rate limit. Make the parent incremental so only new or changed parents trigger child requests.

Handling errors and empty responses

By default, any HTTP error fails the run. Use response_actions to ignore expected errors — for example a 404 for a parent without children, or a 403 on an optional endpoint:
Rate-limit (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.
This produces two streams, 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 name becomes 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": 0 on the resource, or in resource_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.
  • GET and POST only. GraphQL APIs can be ingested through a POST with the query in json, 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 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.
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.
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.
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 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.
Refer to the dlt REST API source documentation for the full schema reference.

Scheduling

API source connectors run on a schedule rather than streaming continuously. A first run is always triggered immediately after the connector is created; subsequent runs depend on the selected mode: