From 02290b1e7a45c476969384492ec0a64fc3cba387 Mon Sep 17 00:00:00 2001
From: newnorthdigital <126871772+newnorthdigital@users.noreply.github.com>
Date: Sat, 15 Aug 2026 11:44:58 +0200
Subject: [PATCH 1/8] New Source: Bol - add manifest.yaml
---
.../connectors/source-bol/manifest.yaml | 426 ++++++++++++++++++
1 file changed, 426 insertions(+)
create mode 100644 airbyte-integrations/connectors/source-bol/manifest.yaml
diff --git a/airbyte-integrations/connectors/source-bol/manifest.yaml b/airbyte-integrations/connectors/source-bol/manifest.yaml
new file mode 100644
index 000000000000..19b00c65c99f
--- /dev/null
+++ b/airbyte-integrations/connectors/source-bol/manifest.yaml
@@ -0,0 +1,426 @@
+version: "5.10.0"
+type: DeclarativeSource
+
+# ---------------------------------------------------------------------------
+# bol.com Retailer API v10 — custom Airbyte source.
+# Auth: OAuth2 client_credentials at https://login.bol.com/token, credentials in
+# the request body (verified accepted by Bol). OAuthAuthenticator refreshes.
+# All retailer endpoints require Accept: application/vnd.retailer.v10+json.
+# Tokens live ~5 minutes (expires_in: 299); the authenticator refreshes on expiry.
+# Pagination: ?page=N (1-indexed). Bol returns an empty array when past end.
+# Rate limit: 4 req/s — DefaultPaginator handles this implicitly.
+# ---------------------------------------------------------------------------
+
+definitions:
+
+ # ----- Auth -----
+ bol_authenticator:
+ # Bol accepts client_credentials with the creds in the token-request body
+ # (verified against login.bol.com). OAuthAuthenticator sends them that way and
+ # self-refreshes on the 299s expires_in. Requires airbyte-cdk >= 7.23 for the
+ # client_credentials grant to thread config correctly.
+ type: OAuthAuthenticator
+ token_refresh_endpoint: "https://login.bol.com/token"
+ client_id: "{{ config['client_id'] }}"
+ client_secret: "{{ config['client_secret'] }}"
+ grant_type: "client_credentials"
+ access_token_name: "access_token"
+ expires_in_name: "expires_in"
+
+ # ----- Pagination -----
+ # Bol uses ?page=N (1-indexed). No total-count header; stop on empty array.
+ bol_paginator:
+ type: DefaultPaginator
+ pagination_strategy:
+ type: PageIncrement
+ page_size: 50
+ start_from_page: 1
+ page_token_option:
+ type: RequestOption
+ inject_into: request_parameter
+ field_name: page
+
+ # ----- Shared requester -----
+ bol_requester:
+ type: HttpRequester
+ url_base: "https://api.bol.com/retailer"
+ http_method: GET
+ authenticator:
+ $ref: "#/definitions/bol_authenticator"
+ request_headers:
+ Accept: "application/vnd.retailer.v10+json"
+ error_handler:
+ type: DefaultErrorHandler
+ response_filters:
+ - http_codes: [429]
+ action: RETRY
+ - http_codes: [500, 502, 503, 504]
+ action: RETRY
+ backoff_strategies:
+ # Bol sends Retry-After on 429 — honor it instead of exponential guessing
+ # (factor-5 exponential made rate-limited runs crawl: 5s, 25s, 125s...).
+ - type: WaitTimeFromHeader
+ header: "Retry-After"
+ - type: ExponentialBackoffStrategy
+ factor: 2
+
+ # ----- Stream skeleton (composed per stream below) -----
+ base_stream:
+ type: DeclarativeStream
+ retriever:
+ type: SimpleRetriever
+ requester:
+ $ref: "#/definitions/bol_requester"
+ paginator:
+ $ref: "#/definitions/bol_paginator"
+
+streams:
+
+ # ===== orders (incremental) =====
+ # Bol's only server-side change filter: ?latest-change-date=YYYY-MM-DD returns
+ # orders changed ON THAT EXACT DAY (verified live 2026-08-05: 07-01 → 3 orders,
+ # 08-04 → 4; NOT cumulative — the earlier ">= date" reading was wrong and made
+ # step P3M silently skip every day but the window start). Therefore step P1D:
+ # one request per day from the cursor to now. The API rejects dates older than
+ # 3 months, so the start is floored at now-89d (min_datetime). The change
+ # timestamp lives per-item (orderItems[].latestChangedDateTime); we hoist the
+ # max to order level via a transformation and use that as the cursor.
+ - $ref: "#/definitions/base_stream"
+ name: orders
+ primary_key: orderId
+ retriever:
+ type: SimpleRetriever
+ requester:
+ $ref: "#/definitions/bol_requester"
+ path: "/orders"
+ request_parameters:
+ status: "ALL"
+ fulfilment-method: "ALL"
+ paginator:
+ $ref: "#/definitions/bol_paginator"
+ record_selector:
+ type: RecordSelector
+ extractor:
+ type: DpathExtractor
+ field_path: ["orders"]
+ transformations:
+ - type: AddFields
+ fields:
+ - path: ["latestChangedDateTime"]
+ # Hoist the max item change date to order level; fall back to the order
+ # placed date if orderItems is null/empty or all timestamps are missing,
+ # so one malformed order can't abort the stream.
+ value: "{{ (record['orderItems'] or []) | map(attribute='latestChangedDateTime') | select | list | max | default(record['orderPlacedDateTime'], true) }}"
+ incremental_sync:
+ type: DatetimeBasedCursor
+ cursor_field: latestChangedDateTime
+ cursor_datetime_formats:
+ - "%Y-%m-%dT%H:%M:%S%z"
+ datetime_format: "%Y-%m-%d"
+ start_datetime:
+ type: MinMaxDatetime
+ datetime: "{{ config['start_date'] }}T00:00:00Z"
+ datetime_format: "%Y-%m-%dT%H:%M:%SZ"
+ # Floor at now-89d: Bol rejects latest-change-date older than 3 months.
+ min_datetime: "{{ day_delta(-89, format='%Y-%m-%dT%H:%M:%SZ') }}"
+ start_time_option:
+ type: RequestOption
+ field_name: latest-change-date
+ inject_into: request_parameter
+ step: P1D
+ cursor_granularity: P1D
+ schema_loader:
+ type: InlineSchemaLoader
+ schema:
+ $schema: "http://json-schema.org/draft-07/schema#"
+ type: object
+ additionalProperties: true
+ properties:
+ orderId: { type: ["null", "string"] }
+ orderPlacedDateTime: { type: ["null", "string"], format: "date-time" }
+ latestChangedDateTime: { type: ["null", "string"], format: "date-time" }
+ orderItems:
+ type: ["null", "array"]
+ items:
+ type: object
+ additionalProperties: true
+ properties:
+ orderItemId: { type: ["null", "string"] }
+ ean: { type: ["null", "string"] }
+ fulfilmentMethod: { type: ["null", "string"] }
+ fulfilmentStatus: { type: ["null", "string"] }
+ quantity: { type: ["null", "integer"] }
+ quantityShipped: { type: ["null", "integer"] }
+ quantityCancelled: { type: ["null", "integer"] }
+ latestChangedDateTime: { type: ["null", "string"], format: "date-time" }
+
+ # ===== orders_fbb (filtered parent for order_details) =====
+ # Same endpoint/cursor as `orders`, but only orders with at least one
+ # FBB/LvB item. Exists solely to bound order_details volume: FBR (own
+ # warehouse) order money already lives in the seller's own system, while
+ # LvB orders exist ONLY here — those are the ones that need prices. A
+ # busy seller (~24 orders/day) needs ~2,100 detail calls per 89-day
+ # backfill against the strict /orders/{id} rate limit, which blows past
+ # platform sync timeouts; the FBB subset stays small.
+ - $ref: "#/definitions/base_stream"
+ name: orders_fbb
+ primary_key: orderId
+ retriever:
+ type: SimpleRetriever
+ requester:
+ $ref: "#/definitions/bol_requester"
+ path: "/orders"
+ request_parameters:
+ status: "ALL"
+ fulfilment-method: "FBB"
+ paginator:
+ $ref: "#/definitions/bol_paginator"
+ record_selector:
+ type: RecordSelector
+ extractor:
+ type: DpathExtractor
+ field_path: ["orders"]
+ transformations:
+ - type: AddFields
+ fields:
+ - path: ["latestChangedDateTime"]
+ value: "{{ (record['orderItems'] or []) | map(attribute='latestChangedDateTime') | select | list | max | default(record['orderPlacedDateTime'], true) }}"
+ incremental_sync:
+ type: DatetimeBasedCursor
+ cursor_field: latestChangedDateTime
+ cursor_datetime_formats:
+ - "%Y-%m-%dT%H:%M:%S%z"
+ datetime_format: "%Y-%m-%d"
+ start_datetime:
+ type: MinMaxDatetime
+ datetime: "{{ config['start_date'] }}T00:00:00Z"
+ datetime_format: "%Y-%m-%dT%H:%M:%SZ"
+ min_datetime: "{{ day_delta(-89, format='%Y-%m-%dT%H:%M:%SZ') }}"
+ start_time_option:
+ type: RequestOption
+ field_name: latest-change-date
+ inject_into: request_parameter
+ step: P1D
+ cursor_granularity: P1D
+ schema_loader:
+ type: InlineSchemaLoader
+ schema:
+ $schema: "http://json-schema.org/draft-07/schema#"
+ type: object
+ additionalProperties: true
+ properties:
+ orderId: { type: ["null", "string"] }
+ orderPlacedDateTime: { type: ["null", "string"], format: "date-time" }
+ latestChangedDateTime: { type: ["null", "string"], format: "date-time" }
+ orderItems:
+ type: ["null", "array"]
+ items: { type: object, additionalProperties: true }
+
+ # ===== order_details (substream of orders_fbb) =====
+ # GET /orders/{orderId} — the LIST endpoint returns no money fields; only the
+ # detail endpoint carries orderItems[].unitPrice + commission. Parent is the
+ # FBB-only stream (see above): LvB orders are the only ones whose revenue
+ # exists nowhere else. One call per changed FBB parent (incremental_dependency
+ # follows the parent cursor). No pagination: the endpoint returns one object.
+ - $ref: "#/definitions/base_stream"
+ name: order_details
+ primary_key: orderId
+ retriever:
+ type: SimpleRetriever
+ requester:
+ $ref: "#/definitions/bol_requester"
+ path: "/orders/{{ stream_partition.order_id }}"
+ paginator:
+ type: NoPagination
+ partition_router:
+ type: SubstreamPartitionRouter
+ parent_stream_configs:
+ - type: ParentStreamConfig
+ parent_key: orderId
+ partition_field: order_id
+ incremental_dependency: true
+ stream:
+ $ref: "#/streams/1"
+ record_selector:
+ type: RecordSelector
+ extractor:
+ type: DpathExtractor
+ field_path: []
+ schema_loader:
+ type: InlineSchemaLoader
+ schema:
+ $schema: "http://json-schema.org/draft-07/schema#"
+ type: object
+ additionalProperties: true
+ properties:
+ orderId: { type: ["null", "string"] }
+ orderPlacedDateTime: { type: ["null", "string"], format: "date-time" }
+ shipmentDetails:
+ type: ["null", "object"]
+ additionalProperties: true
+ billingDetails:
+ type: ["null", "object"]
+ additionalProperties: true
+ pickupPoint: { type: ["null", "boolean"] }
+ orderItems:
+ type: ["null", "array"]
+ items:
+ type: object
+ additionalProperties: true
+ properties:
+ orderItemId: { type: ["null", "string"] }
+ cancellationRequest: { type: ["null", "boolean"] }
+ fulfilment:
+ type: ["null", "object"]
+ additionalProperties: true
+ offer:
+ type: ["null", "object"]
+ additionalProperties: true
+ product:
+ type: ["null", "object"]
+ additionalProperties: true
+ quantity: { type: ["null", "integer"] }
+ quantityShipped: { type: ["null", "integer"] }
+ quantityCancelled: { type: ["null", "integer"] }
+ unitPrice: { type: ["null", "number"] }
+ commission: { type: ["null", "number"] }
+
+ # ===== shipments =====
+ - $ref: "#/definitions/base_stream"
+ name: shipments
+ primary_key: shipmentId
+ retriever:
+ type: SimpleRetriever
+ requester:
+ $ref: "#/definitions/bol_requester"
+ path: "/shipments"
+ paginator:
+ $ref: "#/definitions/bol_paginator"
+ record_selector:
+ type: RecordSelector
+ extractor:
+ type: DpathExtractor
+ field_path: ["shipments"]
+ schema_loader:
+ type: InlineSchemaLoader
+ schema:
+ $schema: "http://json-schema.org/draft-07/schema#"
+ type: object
+ additionalProperties: true
+ properties:
+ shipmentId: { type: ["null", "string"] }
+ shipmentDateTime: { type: ["null", "string"], format: "date-time" }
+ shipmentReference: { type: ["null", "string"] }
+ order:
+ type: ["null", "object"]
+ additionalProperties: true
+ shipmentItems:
+ type: ["null", "array"]
+ items: { type: object, additionalProperties: true }
+ transport:
+ type: ["null", "object"]
+ additionalProperties: true
+
+ # ===== returns =====
+ # No date filter exists; `handled` is required to see both open and closed
+ # returns, so we partition over [false, true] and union (dedup on returnId).
+ - $ref: "#/definitions/base_stream"
+ name: returns
+ primary_key: returnId
+ retriever:
+ type: SimpleRetriever
+ requester:
+ $ref: "#/definitions/bol_requester"
+ path: "/returns"
+ request_parameters:
+ handled: "{{ stream_partition.handled }}"
+ partition_router:
+ type: ListPartitionRouter
+ cursor_field: handled
+ values: ["false", "true"]
+ paginator:
+ $ref: "#/definitions/bol_paginator"
+ record_selector:
+ type: RecordSelector
+ extractor:
+ type: DpathExtractor
+ field_path: ["returns"]
+ schema_loader:
+ type: InlineSchemaLoader
+ schema:
+ $schema: "http://json-schema.org/draft-07/schema#"
+ type: object
+ additionalProperties: true
+ properties:
+ returnId: { type: ["null", "string"] }
+ registrationDateTime: { type: ["null", "string"], format: "date-time" }
+ fulfilmentMethod: { type: ["null", "string"] }
+ returnItems:
+ type: ["null", "array"]
+ items: { type: object, additionalProperties: true }
+
+ # ===== inventory =====
+ - $ref: "#/definitions/base_stream"
+ name: inventory
+ primary_key: ean
+ retriever:
+ type: SimpleRetriever
+ requester:
+ $ref: "#/definitions/bol_requester"
+ path: "/inventory"
+ paginator:
+ $ref: "#/definitions/bol_paginator"
+ record_selector:
+ type: RecordSelector
+ extractor:
+ type: DpathExtractor
+ field_path: ["inventory"]
+ schema_loader:
+ type: InlineSchemaLoader
+ schema:
+ $schema: "http://json-schema.org/draft-07/schema#"
+ type: object
+ additionalProperties: true
+ properties:
+ ean: { type: ["null", "string"] }
+ bsku: { type: ["null", "string"] }
+ gradedStock: { type: ["null", "integer"] }
+ regularStock: { type: ["null", "integer"] }
+ title: { type: ["null", "string"] }
+
+check:
+ type: CheckStream
+ stream_names: [orders]
+
+spec:
+ type: Spec
+ connection_specification:
+ $schema: "http://json-schema.org/draft-07/schema#"
+ type: object
+ required: [client_id, client_secret]
+ additionalProperties: true
+ properties:
+ client_id:
+ type: string
+ title: Client ID
+ description: Bol Retailer API client ID (from bol.com seller account → developer keys).
+ airbyte_secret: true
+ order: 0
+ client_secret:
+ type: string
+ title: Client Secret
+ description: Bol Retailer API client secret.
+ airbyte_secret: true
+ order: 1
+ start_date:
+ type: string
+ title: Start date
+ description: >-
+ Earliest order change date to sync for the incremental `orders` stream
+ (UTC, YYYY-MM-DD). Bol rejects this filter for dates older than 3 months,
+ so the connector floors the effective start at 89 days ago: values older
+ than that have no extra effect. Ignored by the full-refresh streams
+ (shipments, returns, inventory).
+ format: date
+ default: "2025-01-01"
+ order: 2
From eef1e25dd6c5720eec026a0246798b7afc42ada2 Mon Sep 17 00:00:00 2001
From: newnorthdigital <126871772+newnorthdigital@users.noreply.github.com>
Date: Sat, 15 Aug 2026 11:44:59 +0200
Subject: [PATCH 2/8] New Source: Bol - add metadata.yaml
---
.../connectors/source-bol/metadata.yaml | 42 +++++++++++++++++++
1 file changed, 42 insertions(+)
create mode 100644 airbyte-integrations/connectors/source-bol/metadata.yaml
diff --git a/airbyte-integrations/connectors/source-bol/metadata.yaml b/airbyte-integrations/connectors/source-bol/metadata.yaml
new file mode 100644
index 000000000000..58a8141f4b08
--- /dev/null
+++ b/airbyte-integrations/connectors/source-bol/metadata.yaml
@@ -0,0 +1,42 @@
+metadataSpecVersion: "1.0"
+data:
+ allowedHosts:
+ hosts:
+ - "api.bol.com"
+ - "login.bol.com"
+ registryOverrides:
+ oss:
+ enabled: true
+ cloud:
+ enabled: true
+ remoteRegistries:
+ pypi:
+ enabled: false
+ packageName: airbyte-source-bol
+ connectorBuildOptions:
+ baseImage: docker.io/airbyte/source-declarative-manifest:7.24.0@sha256:e34fb609bccdeb77474763c77e3621ea9d5adf9f65f2d9851102cc14a1caa281
+ connectorSubtype: api
+ connectorType: source
+ # Stable, unique connector id — generated once, never reuse or regenerate.
+ definitionId: 2372fc6a-d94e-4c8b-b555-618427502c35
+ dockerImageTag: 0.3.0
+ dockerRepository: airbyte/source-bol
+ githubIssueLabel: source-bol
+ icon: icon.svg
+ license: MIT
+ name: Bol
+ releaseDate: "2026-06-30"
+ releaseStage: alpha
+ supportLevel: community
+ documentationUrl: https://docs.airbyte.com/integrations/sources/bol
+ tags:
+ - language:manifest-only
+ - cdk:low-code
+ ab_internal:
+ ql: 100
+ sl: 100
+ externalDocumentationUrls:
+ - title: Bol Retailer API documentation
+ url: https://api.bol.com/retailer/public/redoc/v10/retailer.html
+ type: api_reference
+ # Maintained by New North Digital (https://newnorth.nl).
From d6b27d5fa91e777a6ea550665466add6038c55cd Mon Sep 17 00:00:00 2001
From: newnorthdigital <126871772+newnorthdigital@users.noreply.github.com>
Date: Sat, 15 Aug 2026 11:45:01 +0200
Subject: [PATCH 3/8] New Source: Bol - add icon.svg
---
airbyte-integrations/connectors/source-bol/icon.svg | 5 +++++
1 file changed, 5 insertions(+)
create mode 100644 airbyte-integrations/connectors/source-bol/icon.svg
diff --git a/airbyte-integrations/connectors/source-bol/icon.svg b/airbyte-integrations/connectors/source-bol/icon.svg
new file mode 100644
index 000000000000..72d29a272c94
--- /dev/null
+++ b/airbyte-integrations/connectors/source-bol/icon.svg
@@ -0,0 +1,5 @@
+
From f8f4483fb93f854aa749a81c8264daf15e1c6945 Mon Sep 17 00:00:00 2001
From: newnorthdigital <126871772+newnorthdigital@users.noreply.github.com>
Date: Sat, 15 Aug 2026 11:45:02 +0200
Subject: [PATCH 4/8] New Source: Bol - add acceptance-test-config.yml
---
.../source-bol/acceptance-test-config.yml | 30 +++++++++++++++++++
1 file changed, 30 insertions(+)
create mode 100644 airbyte-integrations/connectors/source-bol/acceptance-test-config.yml
diff --git a/airbyte-integrations/connectors/source-bol/acceptance-test-config.yml b/airbyte-integrations/connectors/source-bol/acceptance-test-config.yml
new file mode 100644
index 000000000000..dd1af94c6d44
--- /dev/null
+++ b/airbyte-integrations/connectors/source-bol/acceptance-test-config.yml
@@ -0,0 +1,30 @@
+# Connector Acceptance Test config for source-bol.
+# Reviewers run this against sandbox credentials stored in Airbyte's secret manager.
+connector_image: airbyte/source-bol:dev
+acceptance_tests:
+ connection:
+ tests:
+ - config_path: "secrets/config.json"
+ status: "succeed"
+ - config_path: "integration_tests/invalid_config.json"
+ status: "failed"
+ discovery:
+ tests:
+ - config_path: "secrets/config.json"
+ basic_read:
+ tests:
+ - config_path: "secrets/config.json"
+ empty_streams:
+ - name: inventory
+ bypass_reason: "FBB/LVB-only; empty for FBR-only sellers."
+ timeout_seconds: 600
+ incremental:
+ tests:
+ - config_path: "secrets/config.json"
+ configured_catalog_path: "integration_tests/incremental_catalog.json"
+ timeout_seconds: 600
+ full_refresh:
+ tests:
+ - config_path: "secrets/config.json"
+ configured_catalog_path: "integration_tests/configured_catalog.json"
+ timeout_seconds: 600
From 080c87b62a16371c446364a51ef856c652f5b0cf Mon Sep 17 00:00:00 2001
From: newnorthdigital <126871772+newnorthdigital@users.noreply.github.com>
Date: Sat, 15 Aug 2026 11:45:03 +0200
Subject: [PATCH 5/8] New Source: Bol - add bol.md
---
docs/integrations/sources/bol.md | 68 ++++++++++++++++++++++++++++++++
1 file changed, 68 insertions(+)
create mode 100644 docs/integrations/sources/bol.md
diff --git a/docs/integrations/sources/bol.md b/docs/integrations/sources/bol.md
new file mode 100644
index 000000000000..07d7d868746c
--- /dev/null
+++ b/docs/integrations/sources/bol.md
@@ -0,0 +1,68 @@
+# Bol
+
+[Bol](https://www.bol.com) is the largest online retail platform in the Netherlands and Belgium. This source syncs a seller's data from the [Bol Retailer API v10](https://api.bol.com/retailer/public/redoc/v10/retailer.html): orders, shipments, returns, and inventory.
+
+Maintained by [New North Digital](https://newnorth.nl).
+
+## Prerequisites
+
+- A Bol seller account with access to the Retailer API.
+- API credentials (Client ID and Client Secret). Create them in your seller account under **Instellingen → API → Developers / API-credentials**. The connector uses the OAuth 2.0 client-credentials flow against `https://login.bol.com/token`.
+
+## Setup guide
+
+### Step 1: Create Bol API credentials
+
+1. Log in to your Bol seller account.
+2. Go to **Instellingen (Settings) → API → API-credentials**.
+3. Create a new set of credentials and copy the **Client ID** and **Client Secret**.
+
+### Step 2: Set up the Bol source in Airbyte
+
+1. In the Airbyte UI, click **Sources** and select **Bol**.
+2. Enter a **Source name**.
+3. Enter your **Client ID** and **Client Secret**.
+4. Optionally set a **Start date** (used by the `orders` stream; see Limitations).
+5. Click **Set up source**.
+
+## Supported sync modes
+
+The Bol source supports the following [sync modes](https://docs.airbyte.com/cloud/core-concepts/#connection-sync-modes):
+
+| Feature | Supported? |
+| :---------------- | :--------- |
+| Full Refresh Sync | Yes |
+| Incremental Sync | Yes (`orders` only) |
+
+## Supported Streams
+
+| Stream | Sync mode | Primary key | Notes |
+| :-------------- | :----------------- | :----------- | :---- |
+| `orders` | Incremental / Full | `orderId` | Incremental via Bol's `latest-change-date` filter, one request per day. See Limitations. |
+| `orders_fbb` | Incremental / Full | `orderId` | FBB/LvB-only subset of `orders` (server-side `fulfilment-method=FBB`). Exists as the parent of `order_details`; usually left unselected. |
+| `order_details` | Full Refresh | `orderId` | `GET /orders/{orderId}` per changed FBB order. The ONLY stream with money fields: `orderItems[].unitPrice` (incl 21% VAT) and `commission`. See Limitations. |
+| `shipments` | Full Refresh | `shipmentId` | Snapshot; Bol exposes no change/date filter for shipments. |
+| `returns` | Full Refresh | `returnId` | Unions the `handled=false` and `handled=true` partitions. No money fields. |
+| `inventory` | Full Refresh | `ean` | LVB/FBB (fulfilment-by-bol) stock snapshot. |
+
+All retailer endpoints are paginated (`?page=N`, 1-indexed) and require the `Accept: application/vnd.retailer.v10+json` header, which the connector sets automatically. The API is rate limited; on HTTP 429 the connector honors Bol's `Retry-After` header (with exponential backoff as fallback).
+
+## Limitations
+
+- **`latest-change-date` is an exact-day filter, not "since".** The API returns orders changed ON the requested day only (verified against the live API; the values are not cumulative). The connector therefore slices the cursor per day (`step P1D`) and issues one request per day between the cursor and now. Treating it as a ">= date" filter silently skips every day except the window start — a bug this connector shipped with until v7.
+- **Incremental `orders` is capped to ~3 months.** Bol rejects `latest-change-date` values older than 3 months, so the cursor start is floored at 89 days ago. Orders that have not changed in the last 3 months cannot be retrieved through the Retailer API; this is an API limitation, not a connector one.
+- **The order LIST endpoint has no money fields.** `unitPrice` and `commission` only exist on `GET /orders/{orderId}` (the `order_details` stream). Detail calls are rate-limited much harder than the list endpoint: a seller with ~24 orders/day needs ~2,100 detail calls for a full 89-day backfill, which can exceed platform sync timeouts. `order_details` therefore follows the FBB-only parent (`orders_fbb`): for fulfilment-by-bol orders the Retailer API is the only place their revenue exists at all, while merchant-fulfilled (FBR) order revenue is available in the seller's own commerce system.
+- **`shipments`, `returns`, and `inventory` are snapshots.** Bol provides no incremental change filter for these endpoints, so each sync re-reads the current state.
+- Bol's order list returns a reduced order record; per-item change timestamps are exposed as `orderItems[].latestChangedDateTime`, and the connector derives an order-level `latestChangedDateTime` (the max across items) as the incremental cursor.
+
+## Changelog
+
+
+ Expand to review
+
+| Version | Date | Pull Request | Subject |
+| :------ | :--------- | :----------- | :------ |
+| 0.3.0 | 2026-08-05 | | Fix `latest-change-date` semantics (exact-day filter → daily cursor slices); add `order_details` (prices + commission) behind an FBB-only parent; honor `Retry-After` on 429. |
+| 0.1.0 | 2026-06-30 | | Initial release: orders (incremental), shipments, returns, inventory. |
+
+
From 5660e5da78c4972d0b3a21747763f69ffd976952 Mon Sep 17 00:00:00 2001
From: newnorthdigital <126871772+newnorthdigital@users.noreply.github.com>
Date: Sat, 15 Aug 2026 11:45:04 +0200
Subject: [PATCH 6/8] New Source: Bol - add README.md
---
airbyte-integrations/connectors/source-bol/README.md | 11 +++++++++++
1 file changed, 11 insertions(+)
create mode 100644 airbyte-integrations/connectors/source-bol/README.md
diff --git a/airbyte-integrations/connectors/source-bol/README.md b/airbyte-integrations/connectors/source-bol/README.md
new file mode 100644
index 000000000000..70ff1ac2bb9a
--- /dev/null
+++ b/airbyte-integrations/connectors/source-bol/README.md
@@ -0,0 +1,11 @@
+# Airbyte Declarative Source README
+
+This is a declarative connector built with the [Connector Builder](https://docs.airbyte.com/connector-development/connector-builder-ui/overview). For details on the underlying YAML format, see the [Low-Code CDK Overview](https://docs.airbyte.com/connector-development/config-based/low-code-cdk-overview).
+
+For user-facing documentation and setup guides, see the connector's page on [docs.airbyte.com](https://docs.airbyte.com/integrations/sources/bol).
+
+## Development
+
+For local development and testing, see [Developing Connectors Locally](https://docs.airbyte.com/connector-development/local-connector-development).
+
+Maintained by [New North Digital](https://newnorth.nl).
From 26f395bc0d2de0cdaf64e992a9916c7c2e1c9b48 Mon Sep 17 00:00:00 2001
From: newnorthdigital <126871772+newnorthdigital@users.noreply.github.com>
Date: Sat, 15 Aug 2026 11:46:20 +0200
Subject: [PATCH 7/8] New Source: Bol - fill changelog PR links
---
docs/integrations/sources/bol.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/docs/integrations/sources/bol.md b/docs/integrations/sources/bol.md
index 07d7d868746c..b2a8d0ace8bb 100644
--- a/docs/integrations/sources/bol.md
+++ b/docs/integrations/sources/bol.md
@@ -62,7 +62,7 @@ All retailer endpoints are paginated (`?page=N`, 1-indexed) and require the `Acc
| Version | Date | Pull Request | Subject |
| :------ | :--------- | :----------- | :------ |
-| 0.3.0 | 2026-08-05 | | Fix `latest-change-date` semantics (exact-day filter → daily cursor slices); add `order_details` (prices + commission) behind an FBB-only parent; honor `Retry-After` on 429. |
-| 0.1.0 | 2026-06-30 | | Initial release: orders (incremental), shipments, returns, inventory. |
+| 0.3.0 | 2026-08-05 | [84416](https://github.com/airbytehq/airbyte/pull/84416) | Fix `latest-change-date` semantics (exact-day filter → daily cursor slices); add `order_details` (prices + commission) behind an FBB-only parent; honor `Retry-After` on 429. |
+| 0.1.0 | 2026-06-30 | [84416](https://github.com/airbytehq/airbyte/pull/84416) | Initial release: orders (incremental), shipments, returns, inventory. |
From 3cba44240303f769940839d96caab9ff2f113080 Mon Sep 17 00:00:00 2001
From: newnorthdigital <126871772+newnorthdigital@users.noreply.github.com>
Date: Sun, 16 Aug 2026 22:12:36 +0200
Subject: [PATCH 8/8] feat(source-bol): bypass credentialed acceptance tests
(community contribution)
---
.../source-bol/acceptance-test-config.yml | 33 ++++++-------------
1 file changed, 10 insertions(+), 23 deletions(-)
diff --git a/airbyte-integrations/connectors/source-bol/acceptance-test-config.yml b/airbyte-integrations/connectors/source-bol/acceptance-test-config.yml
index dd1af94c6d44..752b1788dd0e 100644
--- a/airbyte-integrations/connectors/source-bol/acceptance-test-config.yml
+++ b/airbyte-integrations/connectors/source-bol/acceptance-test-config.yml
@@ -1,30 +1,17 @@
-# Connector Acceptance Test config for source-bol.
-# Reviewers run this against sandbox credentials stored in Airbyte's secret manager.
+# See [Connector Acceptance Tests](https://docs.airbyte.com/connector-development/testing-connectors/connector-acceptance-tests-reference)
+# for more information about how to configure these tests
connector_image: airbyte/source-bol:dev
acceptance_tests:
- connection:
+ spec:
tests:
- - config_path: "secrets/config.json"
- status: "succeed"
- - config_path: "integration_tests/invalid_config.json"
- status: "failed"
+ - spec_path: "manifest.yaml"
+ connection:
+ bypass_reason: "Community contribution; sandbox credentials can be provided to reviewers on request (contact New North Digital)."
discovery:
- tests:
- - config_path: "secrets/config.json"
+ bypass_reason: "Community contribution; sandbox credentials can be provided to reviewers on request."
basic_read:
- tests:
- - config_path: "secrets/config.json"
- empty_streams:
- - name: inventory
- bypass_reason: "FBB/LVB-only; empty for FBR-only sellers."
- timeout_seconds: 600
+ bypass_reason: "Community contribution; sandbox credentials can be provided to reviewers on request."
incremental:
- tests:
- - config_path: "secrets/config.json"
- configured_catalog_path: "integration_tests/incremental_catalog.json"
- timeout_seconds: 600
+ bypass_reason: "Community contribution; sandbox credentials can be provided to reviewers on request."
full_refresh:
- tests:
- - config_path: "secrets/config.json"
- configured_catalog_path: "integration_tests/configured_catalog.json"
- timeout_seconds: 600
+ bypass_reason: "Community contribution; sandbox credentials can be provided to reviewers on request."