Install
cargo install faucet-cli --features source-rest faucet init --source rest
# Cargo.toml — use it as a library cargo add faucet-source-rest
Starter config
source:
type: rest
config:
base_url: "" # REQUIRED — Base URL of the API, e.g.
# path: "" # URL path, relative to `base_url`.
# method: "GET" # HTTP method for the data request.
# auth: { type: none } # one of: none, bearer, basic, api_key, api_key_query, oauth2, token_endpoint, custom
# --- Alternative variants — replace the block above with one of these ---
# type: bearer
# config:
# token: "" # REQUIRED
#
# type: basic
# config:
# username: "" # REQUIRED
# password: "" # REQUIRED
#
# type: api_key
# config:
# header: "" # REQUIRED
# value: "" # REQUIRED
#
# type: api_key_query
# config:
# param: "" # REQUIRED
# value: "" # REQUIRED
#
# type: oauth2
# config:
# token_url: "" # REQUIRED
# client_id: "" # REQUIRED
# client_secret: "" # REQUIRED
# scopes: [] # REQUIRED
# expiry_ratio: 0.9
#
# type: token_endpoint
# config:
# url: "" # REQUIRED
# method: "" # REQUIRED
# body: null
# token_path: "" # REQUIRED
# expiry_path: ""
# expiry_ratio: 0.9
# encoding: null
#
# type: custom
# config:
# headers: {} # REQUIRED
# headers: {} # Static request headers sent on **every** request (data pages, async-job submit/poll/fetch requests, and OData `$metadata` discovery probes).
# trusted_hosts: [] # Extra hosts that may receive this source's credentials when a server-given URL (a next-page link, an async-job URL, a discovery listing's next link) points at them, or a redirect leads to them.
# query_params: {} # Static query-string parameters, rendered as `?k=v`.
# query_params_multi: {} # Repeated / array-valued query params (#536), rendered as repeated keys — e.g.
# body: null # JSON request body, for `POST`/`PUT`-style reads.
# pagination: { type: None } # one of: None, Cursor, CursorInBody, LinkHeader, NextLinkInBody, PageNumber, Offset, OffsetInBody, RecordFieldCursor
# --- Alternative variants — replace the block above with one of these ---
# type: Cursor
# next_token_path: "" # REQUIRED
# param_name: "" # REQUIRED
#
# type: CursorInBody
# next_token_path: "" # REQUIRED
# body_cursor_field: "" # REQUIRED
#
# type: LinkHeader
#
# type: NextLinkInBody
# next_link_path: "" # REQUIRED
#
# type: PageNumber
# param_name: "" # REQUIRED
# start_page: 0 # REQUIRED
# page_size: 0
# page_size_param: ""
#
# type: Offset
# offset_param: "" # REQUIRED
# limit_param: "" # REQUIRED
# limit: 0 # REQUIRED
# total_path: ""
# rows_path: ""
# stop_when_short: false
#
# type: OffsetInBody
# offset_field: "" # REQUIRED
# limit_field: "" # REQUIRED
# limit: 0 # REQUIRED
# stop_when_short: false
# rows_path: ""
# total_path: ""
#
# type: RecordFieldCursor
# field: "" # REQUIRED
# into: null
# param: "" # REQUIRED
# agg: null
# stop_when_short: true
# page_size: 0 # REQUIRED
# inclusive: false
# records_path: null # JSONPath to the array of records inside the response body, e.g.
# allow_missing_records_path: false # Accept a response in which `records_path` resolves to nothing (the key is absent) as an empty page.
# max_pages: null # Optional cap on pages fetched per pass, across every pagination style.
# allow_repeated_pages: false # Keep paging when a `PageNumber` / `Offset` / `OffsetInBody` page holds the same records as an earlier, non-adjacent page of the pass, instead of failing the run.
# assume_ordered: false # The feed returns records in ascending `replication_key` order, so an incremental pass that `max_pages` cuts short may persist the largest key it read and resume from it next run.
# request_delay: null # Fixed delay between page requests, in seconds — a politeness knob for APIs that rate-limit by request frequency.
# timeout: 30 # Per-request timeout in seconds.
# max_retries: 3 # Number of retries (after the first attempt) for transient request failures.
# retry_backoff: 1 # Base exponential-backoff delay between retries.
# tolerated_http_errors: [] # HTTP status codes that should **not** cause an error.
# retry_on_response: [] # Responses to treat as throttling and retry, for APIs that signal a rate limit with a 4xx other than 429 plus an error code in the body (e.g.
# replication_method: { type: FullTable } # one of: FullTable, Incremental
# --- Alternative variants — replace the block above with one of these ---
# type: Incremental
# replication_key: null # Field used for incremental replication bookmarking: a top-level field name (`updated_at`), a dot path into nested objects (`fields.updated`; numeric segments index arrays, and a literal top-level field of that exact name wins), or a JSON Pointer (`/fields/updated`) for field names that contain dots.
# on_missing_key: null # What to do with a record whose replication key is missing or `null` (#747).
# start_replication_value: null # Bookmark value: records where `record[replication_key] <= start_replication_value` are filtered out when `replication_me…
# state_key: null # Opt-in identifier used by `Pipeline::with_state_store` to persist this stream's bookmark across runs.
# name: null # Human-readable stream name (used in logging and Singer SCHEMA messages).
# primary_keys: [] # Field names that uniquely identify a record (Singer `key_properties`).
# schema: null # JSON Schema describing the structure of each record.
# schema_sample_size: 100 # Maximum number of records to sample when inferring the schema via `crate::stream::RestStream::infer_schema`.
# requests: [] # `requests:` — one entry per request: a context map whose values are substituted into `path` placeholders.
# request_concurrency: null # `request_concurrency:` — maximum number of `requests` entries fetched concurrently (`partition_concurrency:` is still accepted).
# tls: null # Optional client-certificate (mutual TLS) config.
# response_format: null # How to parse the response body into records.
# csv_delimiter: 44 # CSV field delimiter byte (default `,`).
# csv_has_headers: true # Whether the first CSV row is a header row supplying field names (default `true`).
# csv_quote: 34 # CSV quote character (default `"`).
# csv_null_values: [] # CSV fields equal to any of these strings decode as JSON `null` instead of a string (default: none).
# excel_sheet: null # Excel worksheet to read: a sheet name, or a 0-based index as a string.
# excel_header_row: 0 # 0-based index of the Excel header row (default `0`).
# replication_bind: null # Bind the stored bookmark into the outgoing request (query param / header / body field / path) so the server returns only new rows.
# odata: null # Speak the OData protocol: `@odata.nextLink` paging, the `$.value` envelope, `$select`/`$filter`/`$expand`/`$orderby` sugar, and `$metadata` (EDMX) → schema discovery.
# drop_key_prefixes: [] # Drop per-record keys starting with any of these prefixes (#654 M24).
# decode: [] # Decode the response body before record extraction: a chain of `extract` (JSONPath) / `base64` / `gunzip` / `unzip` / `parse` (json|csv|xlsx|xml) steps.
# async_job: null # Run a submit→poll→fetch job lifecycle instead of a single GET, for bulk/export/report-run APIs (bulk export jobs, report runs, …).
# window: null # Bound each request to a rolling `[start, end)` window between the stored bookmark and `now`, iterating the windows within one run (each `step` wide) with per-window bookmark durability.
# record_ancestors: {} # When `records_path` selects a **nested** array element (e.g.
# records_multi: [] # Emit several record arrays from one response in a single page (sharing one pagination advance), each stamped with a user-defined op marker under `op_field`.
# op_field: "" # Field name each `records_multi` record is stamped with its spec's `op` value.
# records_route: null # Route each record to a named stream by its GID type or a discriminator field, stamping the stream into `stream_field` — so one bulk job's parents and children reach separate sinks.
# persist_cursor: false # Persist the terminal pagination cursor as this run's bookmark (riding the existing `StreamPage.bookmark` / `StateStore` path — no core trait change) and, on resume, seed the stored bookmark back into the first request (query param for `cursor`, request body field for `cursor_in_body`) before paging.
# discovery: null # Generic, vendor-neutral discovery recipe: declare which API calls to make and how to extract datasets from their JSON responses (reusing this source's `auth` / `base_url` / `headers`).
Capabilities
| Replay | Non-deterministic replay |
|---|---|
| Exactly-once replay | No |
| Incremental | Yes |
| Dataset discovery | Yes |
| Reports lag | No |
| Compression | No |
Config fields
| Field | Type | Default | Description |
|---|---|---|---|
base_urlrequired | string | — | Base URL of the API, e.g. https://api.example.com/v2. Required. Joined with path; any trailing slash is handled either way. |
path | string | "" | URL path, relative to base_url. May contain {key} placeholders that are substituted per-partition (e.g. "/orgs/{org_id}/users"). |
method | string | GET | HTTP method for the data request. Defaults to GET; use POST for search-style APIs that take a query body. |
auth | object | type: none | Authentication: either inline ({ type, config }) or a { ref: <name> } pointer to a shared provider in the CLI's top-level auth: catalog. |
headers | object | — | Static request headers sent on **every** request (data pages, async-job submit/poll/fetch requests, and OData $metadata discovery probes). Applied *before* the auth provider's header placements, so an auth header of the same name always wins on a clash. Values honor ${env:} / ${param.*} load-time interpolation and pass through the secrets/redaction boundary like other config strings. Invalid header names/values are rejected at config load (FaucetError::Config), never a mid-run panic. |
trusted_hosts | array | — | Extra hosts that may receive this source's credentials when a server-given URL (a next-page link, an async-job URL, a discovery listing's next link) points at them, or a redirect leads to them. Credentials — the auth block and a shared provider's headers — are otherwise sent only to the scheme, host and port of base_url; a link or redirect elsewhere is fetched without them and with only standard headers (Accept, User-Agent, …). An entry is a host on any port (files.example.com), a host and port (files.example.com:8443, that port only), or a *.example.com wildcard; a link and a redirect follow the same rule, and neither keeps credentials when it changes the scheme. An https base never follows a link to plain http. |
query_params | object | — | Static query-string parameters, rendered as ?k=v. Values honor {placeholder} context substitution for child sources. Empty by default. |
query_params_multi | object | — | Repeated / array-valued query params (#536), rendered as repeated keys — e.g. { "group_by[]": ["api_key_id", "model"] } → ?group_by[]=api_key_id&group_by[]=model. Applied alongside (in addition to) query_params; use this for APIs that need a key to appear more than once (group_by[], repeated expand/fields). Values honor {placeholder} context substitution for child sources, like query_params. Empty by default. Query parameters that may repeat, e.g. { "fields": ["id", "name"] } renders ?fields=id&fields=name. Use this rather than query_params when the API expects a key more than once. |
body | any | — | JSON request body, for POST/PUT-style reads. Ignored for GET. |
pagination | object | type: None | How to walk pages. Defaults to none (a single request). Each style carries its own termination guard — see the crate README's pagination table. |
records_path | string | — | JSONPath to the array of records inside the response body, e.g. $.data.items. When unset the body is expected to *be* the array (or a single object, which is emitted as one record). |
allow_missing_records_path | boolean | — | Accept a response in which records_path resolves to nothing (the key is absent) as an empty page. Off by default: a missing key usually means an error body served with HTTP 200 or a renamed field, and treating it as "no more records" ends the run green with rows unread. Enable it only for APIs that omit the records key on an empty result. |
max_pages | integer | — | Optional cap on pages fetched per pass, across every pagination style. Unset by default: each pagination style already stops on its own end signal and loop guards. When the cap cuts a pass short, only a persisted cursor bookmark (persist_cursor) is kept. An incremental or windowed pass the cap cuts short fails the run instead — its record-derived or window bookmark would skip the unread pages, and keeping none would re-read the same pages forever — unless assume_ordered is set. |
allow_repeated_pages | boolean | — | Keep paging when a PageNumber / Offset / OffsetInBody page holds the same records as an earlier, non-adjacent page of the pass, instead of failing the run. For feeds whose pages can legitimately repeat (a sparse report returning [{"count": 0}] on several pages). Requires max_pages, which then bounds a server that cycles pages. A page identical to the page just before it still ends paging. |
assume_ordered | boolean | — | The feed returns records in ascending replication_key order, so an incremental pass that max_pages cuts short may persist the largest key it read and resume from it next run. Without it such a pass fails the run. Requires a replication_bind when max_pages is set: only a bookmark the server filters on moves the next run past the pages already read. The bind must compare **inclusively** (>=, e.g. a gte|${bookmark} template): rows sharing the last key read may sit on the first unread page, and an exclusive (>) server filter would skip them; the client-side filter is inclusive, so the re-read rows at that key arrive again (pair with an upsert sink). Ignored for windowed passes, with persist_cursor, and across requests: entries (a truncated entry fails the run). |
request_delay | integer | — | Fixed delay between page requests, in seconds — a politeness knob for APIs that rate-limit by request frequency. Applied *in addition* to any Retry-After honoured by the retry path. |
timeout | integer | 30 | Per-request timeout in seconds. Covers one HTTP request, not the whole run, so a paginated extract is bounded per page. A streamed async_job result download is bounded per connect and per idle read instead. |
max_retries | integer | 3 | Number of retries (after the first attempt) for transient request failures. Default 3. |
retry_backoff | integer | 1 | Base exponential-backoff delay between retries. Default 1s. Shares the legacy-field precedence rule documented on max_retries. |
tolerated_http_errors | array | [] | HTTP status codes that should **not** cause an error. Responses with these codes are treated as empty pages (no records, no further pages). |
retry_on_response | array | [] | Responses to treat as throttling and retry, for APIs that signal a rate limit with a 4xx other than 429 plus an error code in the body (e.g. HTTP 400 with error.code 17 / 80004), or inside a 2xx (match_success). A rule's backoff_from reads the wait from the response (a reset header, a JSON header, the body). A matching response counts as a rate-limit response (faucet_source_throttled_total, faucet_source_retries_total{class="rate_limited"}) and is retried up to max_retries times in a row before the original error is surfaced. Checked before tolerated_http_errors. Applies to data pages, async_job submit/poll requests and discovery requests. |
replication_method | object | type: FullTable | full_table (default) re-reads everything each run; incremental filters on replication_key and emits a bookmark so the next run resumes. |
replication_key | string | — | Field used for incremental replication bookmarking: a top-level field name (updated_at), a dot path into nested objects (fields.updated; numeric segments index arrays, and a literal top-level field of that exact name wins), or a JSON Pointer (/fields/updated) for field names that contain dots. Not a JSONPath: it must resolve to one value. |
on_missing_key | object | keep | What to do with a record whose replication_key is missing or null once a start bookmark exists: keep (default — kept, counted in faucet_source_replication_key_missing_total, warned once per run), drop (counted and warned), or fail the run. |
start_replication_value | any | — | Bookmark value: records where record[replication_key] <= start_replication_value are filtered out when replication_method is Incremental. |
state_key | string | — | Opt-in identifier used by Pipeline::with_state_store to persist this stream's bookmark across runs. When set, the pipeline will load any previously-stored bookmark before fetching and write the new bookmark only after the sink confirms the batch. |
name | string | — | Human-readable stream name (used in logging and Singer SCHEMA messages). |
primary_keys | array | [] | Field names that uniquely identify a record (Singer key_properties). |
schema | any | — | JSON Schema describing the structure of each record. |
schema_sample_size | integer | 100 | Maximum number of records to sample when inferring the schema via crate::stream::RestStream::infer_schema. 0 means sample all available records (up to max_pages, when set). Defaults to 100. |
requests | array | [] | requests: — one entry per request: a context map whose values are substituted into path placeholders. The stream is executed once per entry and results are concatenated. Empty means run once with no substitution. The pre-#670 spelling partitions: is still accepted. |
request_concurrency | integer | — | request_concurrency: — maximum number of requests entries fetched concurrently (partition_concurrency: is still accepted). None (and 0/1) means sequential processing — the default. |
tls | object | — | Optional client-certificate (mutual TLS) config. When set, the source presents a client certificate on **every** request — data requests and any inline auth token request (both go through the same HTTP client). Requires the crate's mtls feature; a tls block on a build without it is a load-time error rather than being silently ignored. |
response_format | object | json | How to parse the response body. json (default) uses JSONPath extraction (records_path); csv / excel parse a tabular **file** body into records — for authenticated file endpoints such as a cloud-drive …/content download or any signed export URL. In file mode a single response is fetched (pagination must be none) and records_path does not apply. excel requires the crate's excel feature. |
csv_delimiter | integer | 44 | CSV field delimiter byte (default ,). Used only when response_format: csv. |
csv_has_headers | boolean | true | Whether the first CSV row is a header row supplying field names (default true). When false, fields are named column_0, column_1, … |
csv_quote | integer | 34 | CSV quote character (default "). Set it when the export quotes with something else — without this knob a '-quoted export parsed as literal quote characters inside every field (#670 L29). response_format: csv and the parse: csv decode step both honour it. |
csv_null_values | array | [] | CSV fields equal to any of these strings decode as JSON null instead of a string (default: none). Some bulk-export APIs write a null as an empty field, so their templates set [""]; other APIs list sentinels such as "NULL" or "#N/A". Headers are never mapped, and a quoted empty field ("") is mapped like an unquoted one — the CSV reader does not report quoting per field. response_format: csv only. |
excel_sheet | string | — | Excel worksheet to read: a sheet name, or a 0-based index as a string. When omitted, the first worksheet is used. response_format: excel only. |
excel_header_row | integer | 0 | 0-based index of the Excel header row (default 0). Rows above it are skipped; the header row supplies field names. response_format: excel only. |
replication_bind | object | — | Bind the stored bookmark into the outgoing request (query param / header / body field / path) so the server returns only new rows. Composes with the existing replication_key client-side filter, which stays active as a safety net. Requires replication_method: incremental + replication_key. |
odata | object | — | Speak the OData protocol: @odata.nextLink paging, the $.value envelope, $select/$filter/$expand/$orderby sugar, and $metadata (EDMX) → schema discovery. When set, it derives the pagination, records_path, query params, and Prefer header at load time (explicit values still win). See ODataConfig. |
drop_key_prefixes | array | — | Drop per-record keys starting with any of these prefixes (#654 M24). |
decode | array | — | Decode the response body before record extraction: a chain of extract (JSONPath) / base64 / gunzip / unzip / parse (json|csv|xlsx|xml) steps. Lets a source consume base64/compressed/file payloads (e.g. a base64 XLSX inside a SOAP body, or a gzipped-CSV export). When set, it replaces the response_format body parsing, and pagination must be none. See DecodeStep. |
async_job | object | — | Run a submit→poll→fetch job lifecycle instead of a single GET, for bulk/export/report-run APIs (bulk export jobs, report runs, …). The fetched result flows through decode: / response_format. When set, pagination must be none. See AsyncJobConfig. |
window | object | — | Bound each request to a rolling [start, end) window between the stored bookmark and now, iterating the windows within one run (each step wide) with per-window bookmark durability. For APIs that require — or cap — a bounded date range (analytics/ads/reporting feeds). Parity with Airbyte's DatetimeBasedCursor. Requires replication_method: incremental + replication_key, and a start bookmark (from state, or start_replication_value). See WindowSpec. |
record_ancestors | object | — | When records_path selects a **nested** array element (e.g. $.data[*].data.object), copy fields from the enclosing [*] array-element ancestor onto each emitted record. The map is dest_field: ancestor_relative_path — for each matched leaf, the source walks up to the array-element ancestor and copies the named path onto the record under dest_field. Absent ⇒ records are emitted unchanged. |
records_multi | array | — | Emit several record arrays from one response in a single page (sharing one pagination advance), each stamped with a user-defined op marker under op_field. Composes with a downstream write_mode: upsert + delete_marker so added/modified/removed feeds route correctly. Mutually exclusive with records_path / record_ancestors, and requires response_format: json with no decode: pipeline. |
op_field | string | — | Field name each records_multi record is stamped with its spec's op value. Defaults to _op when omitted. |
records_route | object | — | Route each record to a named stream by its GID type or a discriminator field, stamping the stream into stream_field — so one bulk job's parents and children reach separate sinks. See RecordsRoute. |
persist_cursor | boolean | false | Persist the terminal pagination cursor as this run's bookmark (riding the existing StreamPage.bookmark / StateStore path — no core trait change) and, on resume, seed the stored bookmark back into the first request (query param for cursor, request body field for cursor_in_body) before paging. Only meaningful with pagination: cursor / cursor_in_body; mutually exclusive with window slicing. Default false. |
discovery | object | — | Generic, vendor-neutral discovery recipe: declare which API calls to make and how to extract datasets from their JSON responses (reusing this source's auth / base_url / headers). Powers faucet discover and run-time fan-out for any REST API with a listing / describe shape, with no connector-specific code. See DiscoverySpec. |
Get started
Your first pipeline runs in five minutes.
Install the CLI, scaffold a config, and move real data — nothing external to stand up.
curl -LsSf https://github.com/faucet-hq/faucet-stream/releases/latest/download/faucet-cli-installer.sh | sh brew install faucet-hq/faucet-stream/faucet-cli