This is an automated email from the ASF dual-hosted git repository.

github-merge-queue[bot] pushed a commit to branch 
gh-readonly-queue/dev/pr-13094-526d06802ef7f00016c5fb94e9e6af1d2b0d60da
in repository https://gitbox.apache.org/repos/asf/seatunnel.git

commit eab2443be5ff98850e6c4c3fe451eee9291ff6a1
Author: Goutam Adwant <[email protected]>
AuthorDate: Wed Nov 4 15:45:55 2026 +0000

    [Feature][Connector-V2] Add Mailchimp source (#13094)
    
    Signed-off-by: Goutam Adwant <[email protected]>
---
 .github/workflows/labeler/label-scope-conf.yml     |  10 +
 config/plugin_config                               |   1 +
 .../changelog/connector-http-mailchimp.md          |  20 +
 docs/en/connectors/source/Mailchimp.md             | 140 ++++++
 .../changelog/connector-http-mailchimp.md          |  20 +
 docs/zh/connectors/source/Mailchimp.md             | 132 +++++
 plugin-mapping.properties                          |   1 +
 .../connector-http-mailchimp/pom.xml               |  51 ++
 .../seatunnel/mailchimp/MailchimpConfig.java       | 104 ++++
 .../seatunnel/mailchimp/MailchimpSource.java       |  72 +++
 .../mailchimp/MailchimpSourceFactory.java          |  75 +++
 .../mailchimp/MailchimpSourceOptions.java          |  71 +++
 .../seatunnel/mailchimp/MailchimpSourceReader.java | 321 ++++++++++++
 .../seatunnel/mailchimp/MailchimpConfigTest.java   | 165 +++++++
 .../mailchimp/MailchimpSourceFactoryTest.java      |  37 ++
 .../mailchimp/MailchimpSourceReaderTest.java       | 543 +++++++++++++++++++++
 seatunnel-connectors-v2/connector-http/pom.xml     |   1 +
 seatunnel-dist/pom.xml                             |   6 +
 .../connector-mailchimp-e2e/pom.xml                |  53 ++
 .../e2e/connector/mailchimp/MailchimpIT.java       | 148 ++++++
 .../test/resources/mailchimp-members-page-1.json   |  23 +
 .../test/resources/mailchimp-members-page-2.json   |  14 +
 .../test/resources/mailchimp_source_to_assert.conf |  59 +++
 seatunnel-e2e/seatunnel-connector-v2-e2e/pom.xml   |   1 +
 24 files changed, 2068 insertions(+)

diff --git a/.github/workflows/labeler/label-scope-conf.yml 
b/.github/workflows/labeler/label-scope-conf.yml
index 098a37ee6b..3df6c438c6 100644
--- a/.github/workflows/labeler/label-scope-conf.yml
+++ b/.github/workflows/labeler/label-scope-conf.yml
@@ -241,6 +241,16 @@ hive:
       - changed-files:
           - any-glob-to-any-file: seatunnel-connectors-v2/connector-hive/**
           - all-globs-to-all-files: 
'!seatunnel-connectors-v2/connector-!(hive)/**'
+mailchimp:
+  - all:
+      - changed-files:
+          - any-glob-to-any-file:
+              - 
seatunnel-connectors-v2/connector-http/connector-http-mailchimp/**
+              - 
seatunnel-e2e/seatunnel-connector-v2-e2e/connector-mailchimp-e2e/**
+          - all-globs-to-all-files:
+              - 
'!seatunnel-connectors-v2/connector-http/connector-http-!(mailchimp)/**'
+              - 
'!seatunnel-e2e/seatunnel-connector-v2-e2e/connector-!(mailchimp)-e2e/**'
+
 http:
   - all:
       - changed-files:
diff --git a/config/plugin_config b/config/plugin_config
index 07fd5a7a43..acf41a0c43 100644
--- a/config/plugin_config
+++ b/config/plugin_config
@@ -74,6 +74,7 @@ connector-mem0
 connector-http-asana
 connector-http-intercom
 connector-http-typeform
+connector-http-mailchimp
 connector-http-feishu
 connector-http-gitlab
 connector-http-github
diff --git a/docs/en/connectors/changelog/connector-http-mailchimp.md 
b/docs/en/connectors/changelog/connector-http-mailchimp.md
new file mode 100644
index 0000000000..7663d21767
--- /dev/null
+++ b/docs/en/connectors/changelog/connector-http-mailchimp.md
@@ -0,0 +1,20 @@
+<!--
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+| Change | Commit | Version |
+| --- | --- | --- |
+| [Feature][connector-http-mailchimp] Add bounded Mailchimp list members 
source | https://github.com/apache/seatunnel | dev |
diff --git a/docs/en/connectors/source/Mailchimp.md 
b/docs/en/connectors/source/Mailchimp.md
new file mode 100644
index 0000000000..9cc4e5f99b
--- /dev/null
+++ b/docs/en/connectors/source/Mailchimp.md
@@ -0,0 +1,140 @@
+import ChangeLog from '../changelog/connector-http-mailchimp.md';
+
+# Http-Mailchimp
+
+<!--
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+> Mailchimp list members source connector
+
+## Description
+
+Reads the members of one configured audience using the Mailchimp Marketing API
+[`GET 
/3.0/lists/{list_id}/members`](https://mailchimp.com/developer/marketing/api/lists/members/list-members/).
+Only bounded batch reads are supported. Campaigns, reports, OAuth and CDC are 
not supported.
+Uses the existing HTTP and JSON dependencies; no Mailchimp SDK is required.
+
+## Key Features
+
+- [x] Batch
+- [ ] Stream
+- [ ] Exactly-once
+- [ ] Column projection
+- [ ] Parallelism
+
+Set source parallelism to 1.
+
+## Options
+
+| Name | Type | Required | Default | Description |
+| --- | --- | --- | --- | --- |
+| token | String | Yes | - | Mailchimp API key including its datacenter 
suffix, for example `<key>-us1`. Not an OAuth token. |
+| list_id | String | Yes | - | One audience/list ID. Letters, digits, 
underscores and hyphens only. |
+| schema | Config | Yes | - | Nonempty output schema using SeaTunnel's 
existing JSON format. |
+| base_url | String | No | `https://<dc>.api.mailchimp.com/3.0` | Explicitly 
trusted API root. The default datacenter comes from `token`. |
+| allow_insecure_http | Boolean | No | `false` | Allow HTTP only for isolated 
test fixtures with dummy credentials. Never enable with real API keys. |
+| page_size | Int | No | `1000` | Members per page, from 1 to 1000. |
+| retry | Int | No | `3` | Additional attempts per page on HTTP 429 or 5xx, 
from 0 to 10. |
+| retry_backoff_ms | Int | No | `1000` | Initial retry delay, from 1 to 60000 
milliseconds. Doubles up to 60000 milliseconds. |
+| request_timeout_ms | Int | No | `30000` | Connection, pool acquisition and 
socket inactivity timeout, from 1 to 300000 milliseconds. Not a total batch 
deadline. |
+
+### Authentication and Endpoint Trust
+
+The `token` option holds an API key and uses SeaTunnel's existing 
sensitive-option masking.
+It is sent only in the HTTP Basic Authorization header (username `seatunnel`, 
password the API key).
+The connector does not log request headers, response bodies or member data, 
and its split state
+contains no credentials or member data. Standard engine configuration and 
secret-management rules
+still apply. Do not enable HTTP wire/header logging with real credentials.
+
+A custom `base_url` is an administrator-selected credential destination: use 
only an API endpoint
+you trust. It must use HTTPS and end in `/3.0` (an optional trailing slash is 
accepted), with a host
+and optional port, but no user information, query, fragment or other path. 
HTTP requires explicit
+`allow_insecure_http = true` and is for isolated dummy-credential fixtures 
only.
+Redirects, cookies and automatic HTTP-client retries are disabled; a redirect 
fails without forwarding
+credentials. Response-provided links are never followed.
+
+### Schema
+
+Each member object is decoded independently using the existing JSON format. 
Typical fields are
+`id`, `email_address`, `status`, `last_changed` and `merge_fields`.
+Field names match the API JSON keys. `last_changed` can be read as a STRING to 
retain its ISO-8601
+offset. Missing optional fields become null; invalid type conversions fail 
without including the
+member data in the error.
+
+`merge_fields` is not flattened. Use a nested ROW for known heterogeneous 
fields or a MAP when
+values have a consistent type. The example uses `map<string,string>`; 
configure a matching schema
+for your actual merge-field definitions. A field declared STRING uses the 
existing JSON format's
+conversion rules, not a connector-specific JSON serializer.
+
+## Example
+
+```hocon
+env {
+  parallelism = 1
+  job.mode = BATCH
+}
+source {
+  Mailchimp {
+    token = "<MAILCHIMP_API_KEY-us1>"
+    list_id = "<LIST_ID>"
+    page_size = 1000
+    schema {
+      fields {
+        id = string
+        email_address = string
+        status = string
+        last_changed = string
+        merge_fields = "map<string,string>"
+      }
+    }
+  }
+}
+sink {
+  Console {}
+}
+```
+
+## Pagination, Recovery and Consistency
+
+Requests send `count` (at most 1000) and a zero-based `offset`. The first 
successful page's
+`total_items` sets the batch's finite upper bound. Later growth cannot extend 
that bound.
+The final request reduces `count` to the remaining number of members.
+
+Each page must contain a `members` array and a nonnegative integer 
`total_items` within the API's
+integer offset range. A page must return the expected number of member 
objects. A short/empty
+page before completion, shrinking total, invalid JSON, unexpected shape or 
schema conversion
+failure fails the batch instead of silently reporting success. Responses are 
limited to 32 MiB
+per page; lower `page_size` for large merge fields.
+
+This is **not a transactional or exact snapshot**. Concurrent additions, 
deletions and updates can
+shift offsets, duplicate members or omit members even when counts match. For a 
stable export, avoid
+audience changes during the run. No `since_last_changed` filter or durable 
incremental cursor is
+provided.
+
+Recovery deliberately replays the **whole batch from offset zero**, including 
previously accepted
+pages. This is at-least-once batch replay, not exactly-once delivery or a 
guarantee of capturing
+every version of a changing member. Use member `id` for downstream 
deduplication/upserts.
+Checkpoints do not persist pagination offsets or a completed-batch marker. 
Network waits do not
+hold the collector's checkpoint lock.
+
+HTTP 429 and 5xx responses are retried at the same offset up to `retry` times. 
`Retry-After`
+seconds or HTTP dates are honored with a maximum wait of 60 seconds; invalid 
values use exponential
+backoff. Other non-200 responses (including 401 and redirects), malformed 
bodies and transport
+failures fail immediately. Restarting the job replays the batch. Cancellation 
during reading aborts active I/O
+and wakes backoff waits without signalling successful completion.
+
+<ChangeLog />
diff --git a/docs/zh/connectors/changelog/connector-http-mailchimp.md 
b/docs/zh/connectors/changelog/connector-http-mailchimp.md
new file mode 100644
index 0000000000..7663d21767
--- /dev/null
+++ b/docs/zh/connectors/changelog/connector-http-mailchimp.md
@@ -0,0 +1,20 @@
+<!--
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+| Change | Commit | Version |
+| --- | --- | --- |
+| [Feature][connector-http-mailchimp] Add bounded Mailchimp list members 
source | https://github.com/apache/seatunnel | dev |
diff --git a/docs/zh/connectors/source/Mailchimp.md 
b/docs/zh/connectors/source/Mailchimp.md
new file mode 100644
index 0000000000..edb61f6e42
--- /dev/null
+++ b/docs/zh/connectors/source/Mailchimp.md
@@ -0,0 +1,132 @@
+import ChangeLog from '../changelog/connector-http-mailchimp.md';
+
+# Http-Mailchimp
+
+<!--
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements. See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License. You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+
+> Mailchimp 列表成员源连接器
+
+## 描述
+
+通过 Mailchimp Marketing API 的
+[`GET 
/3.0/lists/{list_id}/members`](https://mailchimp.com/developer/marketing/api/lists/members/list-members/)
+读取一个指定受众列表的成员。仅支持有界批处理,不支持活动、报表、OAuth 或 CDC。
+复用已有 HTTP 和 JSON 依赖,不引入 Mailchimp SDK。
+
+## 主要特性
+
+- [x] 批处理
+- [ ] 流处理
+- [ ] 精确一次
+- [ ] 列投影
+- [ ] 并行读取
+
+源并行度必须为 1。
+
+## 配置项
+
+| 名称 | 类型 | 必填 | 默认值 | 描述 |
+| --- | --- | --- | --- | --- |
+| token | String | 是 | - | 包含数据中心后缀的 Mailchimp API 密钥,例如 `<key>-us1`。不是 OAuth 
令牌。 |
+| list_id | String | 是 | - | 一个受众列表 ID,仅支持字母、数字、下划线及连字符。 |
+| schema | Config | 是 | - | 非空输出结构,使用 SeaTunnel 已有 JSON 格式。 |
+| base_url | String | 否 | `https://<dc>.api.mailchimp.com/3.0` | 显式信任的 API 
根地址。默认数据中心从 `token` 提取。 |
+| allow_insecure_http | Boolean | 否 | `false` | 仅为隔离测试及虚拟凭据允许 HTTP。禁止用于真实 API 
密钥。 |
+| page_size | Int | 否 | `1000` | 每页成员数,范围 1 到 1000。 |
+| retry | Int | 否 | `3` | 每页遇到 HTTP 429 或 5xx 时额外重试次数,范围 0 到 10。 |
+| retry_backoff_ms | Int | 否 | `1000` | 初始重试间隔,范围 1 到 60000 毫秒,逐次加倍,最多 60000 
毫秒。 |
+| request_timeout_ms | Int | 否 | `30000` | 连接、连接池获取及套接字空闲超时,范围 1 到 300000 
毫秒,并非整个批次的截止时间。 |
+
+### 认证与端点信任
+
+`token` 保存 API 密钥,复用 SeaTunnel 已有的敏感配置脱敏规则。
+密钥仅通过 HTTP Basic Authorization 请求头发送,用户名为 `seatunnel`,密码为 API 密钥。
+连接器不记录请求头、响应正文或成员数据,分片状态不包含凭据及成员数据。
+仍需遵循引擎配置和密钥管理规则,使用真实凭据时不要开启 HTTP wire/header 调试日志。
+
+自定义 `base_url` 是管理员指定的凭据接收端,必须信任该地址。地址必须使用 HTTPS,
+包含主机和可选端口,路径为 `/3.0`(允许末尾斜杠),不允许用户信息、查询、片段或其他路径。
+HTTP 必须显式设置 `allow_insecure_http = true`,且仅用于虚拟凭据的隔离测试。
+连接器禁用重定向、Cookie 和 HTTP 客户端自动重试。重定向直接失败,不转发凭据。
+不访问响应中的链接。
+
+### 输出结构
+
+按成员对象逐条使用已有 JSON 格式解析。常用字段包括 `id`、`email_address`、`status`、
+`last_changed` 和 `merge_fields`。字段名与 API 的 JSON 键一致。
+可将 `last_changed` 设为 STRING,以保留 ISO-8601 时区偏移。
+缺失的可选字段返回 null;类型转换失败会终止读取,错误信息不包含成员数据。
+
+`merge_fields` 不会自动展开。已知且类型不同的字段可使用嵌套 ROW;值类型一致时可使用 MAP。
+示例使用 `map<string,string>`,请根据实际合并字段定义配置结构。
+STRING 类型遵循已有 JSON 格式的转换规则,不提供连接器特有的 JSON 序列化。
+
+## 示例
+
+```hocon
+env {
+  parallelism = 1
+  job.mode = BATCH
+}
+source {
+  Mailchimp {
+    token = "<MAILCHIMP_API_KEY-us1>"
+    list_id = "<LIST_ID>"
+    page_size = 1000
+    schema {
+      fields {
+        id = string
+        email_address = string
+        status = string
+        last_changed = string
+        merge_fields = "map<string,string>"
+      }
+    }
+  }
+}
+sink {
+  Console {}
+}
+```
+
+## 分页、恢复和一致性
+
+请求使用 `count`(最多 1000)及从零开始的 `offset`。
+首个成功页面的 `total_items` 决定本次批处理的有限上界,后续增长不会扩大该上界。
+最后一次请求将 `count` 调整为剩余成员数。
+
+每页必须包含 `members` 数组和 API 整数偏移范围内的非负整数 `total_items`,
+并返回预期数量的成员对象。完成前出现短页或空页、总数减少、JSON 无效、响应结构异常
+或字段转换失败时,批次失败,不会静默报告成功。
+单页响应上限为 32 MiB;合并字段较大时请减小 `page_size`。
+
+这**不是事务快照或精确快照**。并发新增、删除和更新可能移动偏移量,即使总数相同,
+也可能造成重复或遗漏。需要稳定导出时应避免读取期间修改受众列表。
+不提供 `since_last_changed` 过滤或持久化增量游标。
+
+恢复时有意从偏移量零**重放整个批次**,包括已经发送的页面。这是至少一次的批次重放,
+不是精确一次投递,也不保证捕获持续变化成员的所有版本。
+下游可使用成员 `id` 去重或更新写入。
+检查点不保存分页偏移量或已完成标记,网络等待不持有收集器的检查点锁。
+
+HTTP 429 和 5xx 会在相同偏移量最多额外重试 `retry` 次。
+支持秒数或 HTTP 日期格式的 `Retry-After`,每次等待最多 60 秒;无效值使用指数退避。
+其他非 200 响应(包括 401 和重定向)、正文异常及传输错误直接失败。
+重启任务将重放整个批次。读取过程中的取消操作会中止进行中的 I/O 并唤醒退避等待,不报告成功完成。
+
+<ChangeLog />
diff --git a/plugin-mapping.properties b/plugin-mapping.properties
index 5df09428b5..854ab3b7a0 100644
--- a/plugin-mapping.properties
+++ b/plugin-mapping.properties
@@ -210,6 +210,7 @@ seatunnel.source.WebSocket = connector-websocket
 seatunnel.source.Asana = connector-http-asana
 seatunnel.source.Intercom = connector-http-intercom
 seatunnel.source.Typeform = connector-http-typeform
+seatunnel.source.Mailchimp = connector-http-mailchimp
 
 # For custom transforms, make sure to use the 
seatunnel.transform.[PluginIdentifier]=[JarPerfix] naming convention. For 
example:
 # seatunnel.transform.Sql = seatunnel-transforms-v2
diff --git 
a/seatunnel-connectors-v2/connector-http/connector-http-mailchimp/pom.xml 
b/seatunnel-connectors-v2/connector-http/connector-http-mailchimp/pom.xml
new file mode 100644
index 0000000000..5f7b974ae7
--- /dev/null
+++ b/seatunnel-connectors-v2/connector-http/connector-http-mailchimp/pom.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"; 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
http://maven.apache.org/xsd/maven-4.0.0.xsd";>
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.apache.seatunnel</groupId>
+        <artifactId>connector-http</artifactId>
+        <version>${revision}</version>
+    </parent>
+
+    <artifactId>connector-http-mailchimp</artifactId>
+    <name>SeaTunnel : Connectors V2 : Http : Mailchimp</name>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.seatunnel</groupId>
+            <artifactId>connector-http-base</artifactId>
+            <version>${project.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.seatunnel</groupId>
+            <artifactId>seatunnel-common</artifactId>
+            <version>${project.version}</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>com.squareup.okhttp3</groupId>
+            <artifactId>mockwebserver</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+</project>
diff --git 
a/seatunnel-connectors-v2/connector-http/connector-http-mailchimp/src/main/java/org/apache/seatunnel/connectors/seatunnel/mailchimp/MailchimpConfig.java
 
b/seatunnel-connectors-v2/connector-http/connector-http-mailchimp/src/main/java/org/apache/seatunnel/connectors/seatunnel/mailchimp/MailchimpConfig.java
new file mode 100644
index 0000000000..5949025586
--- /dev/null
+++ 
b/seatunnel-connectors-v2/connector-http/connector-http-mailchimp/src/main/java/org/apache/seatunnel/connectors/seatunnel/mailchimp/MailchimpConfig.java
@@ -0,0 +1,104 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.seatunnel.connectors.seatunnel.mailchimp;
+
+import org.apache.seatunnel.api.configuration.ReadonlyConfig;
+import org.apache.seatunnel.api.configuration.util.ConfigValidator;
+
+import java.io.Serializable;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import static 
org.apache.seatunnel.connectors.seatunnel.mailchimp.MailchimpSourceOptions.ALLOW_INSECURE_HTTP;
+import static 
org.apache.seatunnel.connectors.seatunnel.mailchimp.MailchimpSourceOptions.BASE_URL;
+import static 
org.apache.seatunnel.connectors.seatunnel.mailchimp.MailchimpSourceOptions.LIST_ID;
+import static 
org.apache.seatunnel.connectors.seatunnel.mailchimp.MailchimpSourceOptions.PAGE_SIZE;
+import static 
org.apache.seatunnel.connectors.seatunnel.mailchimp.MailchimpSourceOptions.REQUEST_TIMEOUT_MS;
+import static 
org.apache.seatunnel.connectors.seatunnel.mailchimp.MailchimpSourceOptions.RETRY;
+import static 
org.apache.seatunnel.connectors.seatunnel.mailchimp.MailchimpSourceOptions.RETRY_BACKOFF_MS;
+import static 
org.apache.seatunnel.connectors.seatunnel.mailchimp.MailchimpSourceOptions.TOKEN;
+
+final class MailchimpConfig implements Serializable {
+    private static final Pattern KEY_PATTERN = 
Pattern.compile("[A-Za-z0-9]+-(us[0-9]+)");
+    final String apiKey;
+    final URI membersUri;
+    final int pageSize;
+    final int retry;
+    final int backoffMs;
+    final int timeoutMs;
+
+    MailchimpConfig(ReadonlyConfig config) {
+        ConfigValidator.of(config).validate(new 
MailchimpSourceFactory().optionRule());
+        apiKey = config.get(TOKEN);
+        Matcher key = KEY_PATTERN.matcher(apiKey);
+        if (!key.matches()) {
+            throw new IllegalArgumentException(
+                    "Mailchimp token must include a valid datacenter suffix");
+        }
+        String listId = config.get(LIST_ID);
+        if (!listId.matches("[A-Za-z0-9_-]+")) {
+            throw new IllegalArgumentException("Mailchimp list_id must be a 
nonempty identifier");
+        }
+        String base =
+                config.getOptional(BASE_URL)
+                        .orElse("https://"; + key.group(1) + 
".api.mailchimp.com/3.0");
+        try {
+            URI uri = new URI(base);
+            if ("http".equals(uri.getScheme()) && 
!config.get(ALLOW_INSECURE_HTTP)) {
+                throw new IllegalArgumentException(
+                        "Mailchimp base_url requires HTTPS; 
allow_insecure_http is only for isolated fixtures with dummy credentials");
+            }
+            if ((!"https".equals(uri.getScheme()) && 
!"http".equals(uri.getScheme()))
+                    || uri.getHost() == null
+                    || uri.getRawUserInfo() != null
+                    || uri.getRawQuery() != null
+                    || uri.getRawFragment() != null
+                    || (uri.getPort() != -1 && (uri.getPort() < 1 || 
uri.getPort() > 65535))
+                    || (!"/3.0".equals(uri.getRawPath()) && 
!"/3.0/".equals(uri.getRawPath()))) {
+                throw new IllegalArgumentException(
+                        "Mailchimp base_url must be a trusted HTTP(S) API root 
ending in /3.0, without credentials, query or fragment");
+            }
+            membersUri =
+                    new URI(
+                            uri.getScheme(),
+                            null,
+                            uri.getHost(),
+                            uri.getPort(),
+                            "/3.0/lists/" + listId + "/members",
+                            null,
+                            null);
+        } catch (URISyntaxException e) {
+            // URI parser messages can contain credentials from invalid user 
input.
+            throw new IllegalArgumentException("Mailchimp base_url is not a 
valid API root");
+        }
+        pageSize = range(config.get(PAGE_SIZE), 1, 1000, PAGE_SIZE.key());
+        retry = range(config.get(RETRY), 0, 10, RETRY.key());
+        backoffMs = range(config.get(RETRY_BACKOFF_MS), 1, 60000, 
RETRY_BACKOFF_MS.key());
+        timeoutMs = range(config.get(REQUEST_TIMEOUT_MS), 1, 300000, 
REQUEST_TIMEOUT_MS.key());
+    }
+
+    private static int range(int value, int min, int max, String option) {
+        if (value < min || value > max) {
+            throw new IllegalArgumentException(
+                    "Mailchimp " + option + " must be between " + min + " and 
" + max);
+        }
+        return value;
+    }
+}
diff --git 
a/seatunnel-connectors-v2/connector-http/connector-http-mailchimp/src/main/java/org/apache/seatunnel/connectors/seatunnel/mailchimp/MailchimpSource.java
 
b/seatunnel-connectors-v2/connector-http/connector-http-mailchimp/src/main/java/org/apache/seatunnel/connectors/seatunnel/mailchimp/MailchimpSource.java
new file mode 100644
index 0000000000..3858de2a36
--- /dev/null
+++ 
b/seatunnel-connectors-v2/connector-http/connector-http-mailchimp/src/main/java/org/apache/seatunnel/connectors/seatunnel/mailchimp/MailchimpSource.java
@@ -0,0 +1,72 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.seatunnel.connectors.seatunnel.mailchimp;
+
+import org.apache.seatunnel.api.common.JobContext;
+import org.apache.seatunnel.api.configuration.ReadonlyConfig;
+import org.apache.seatunnel.api.source.Boundedness;
+import org.apache.seatunnel.api.table.catalog.CatalogTable;
+import org.apache.seatunnel.api.table.catalog.CatalogTableUtil;
+import org.apache.seatunnel.api.table.type.SeaTunnelRow;
+import org.apache.seatunnel.common.constants.JobMode;
+import 
org.apache.seatunnel.connectors.seatunnel.common.source.AbstractSingleSplitReader;
+import 
org.apache.seatunnel.connectors.seatunnel.common.source.AbstractSingleSplitSource;
+import 
org.apache.seatunnel.connectors.seatunnel.common.source.SingleSplitReaderContext;
+
+import java.util.Collections;
+import java.util.List;
+
+public class MailchimpSource extends AbstractSingleSplitSource<SeaTunnelRow> {
+    private final MailchimpConfig config;
+    private final CatalogTable table;
+
+    public MailchimpSource(ReadonlyConfig options) {
+        config = new MailchimpConfig(options);
+        table = CatalogTableUtil.buildWithConfig(options);
+        if (table.getSeaTunnelRowType().getTotalFields() == 0) {
+            throw new IllegalArgumentException("Mailchimp schema must contain 
at least one field");
+        }
+    }
+
+    @Override
+    public String getPluginName() {
+        return "Mailchimp";
+    }
+
+    @Override
+    public void setJobContext(JobContext context) {
+        if (context.getJobMode() != JobMode.BATCH) {
+            throw new IllegalArgumentException("Mailchimp source only supports 
BATCH mode");
+        }
+    }
+
+    @Override
+    public Boundedness getBoundedness() {
+        return Boundedness.BOUNDED;
+    }
+
+    @Override
+    public List<CatalogTable> getProducedCatalogTables() {
+        return Collections.singletonList(table);
+    }
+
+    @Override
+    public AbstractSingleSplitReader<SeaTunnelRow> 
createReader(SingleSplitReaderContext context) {
+        return new MailchimpSourceReader(config, table, context);
+    }
+}
diff --git 
a/seatunnel-connectors-v2/connector-http/connector-http-mailchimp/src/main/java/org/apache/seatunnel/connectors/seatunnel/mailchimp/MailchimpSourceFactory.java
 
b/seatunnel-connectors-v2/connector-http/connector-http-mailchimp/src/main/java/org/apache/seatunnel/connectors/seatunnel/mailchimp/MailchimpSourceFactory.java
new file mode 100644
index 0000000000..8481bb569a
--- /dev/null
+++ 
b/seatunnel-connectors-v2/connector-http/connector-http-mailchimp/src/main/java/org/apache/seatunnel/connectors/seatunnel/mailchimp/MailchimpSourceFactory.java
@@ -0,0 +1,75 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.seatunnel.connectors.seatunnel.mailchimp;
+
+import org.apache.seatunnel.api.configuration.util.OptionRule;
+import org.apache.seatunnel.api.options.ConnectorCommonOptions;
+import org.apache.seatunnel.api.source.SeaTunnelSource;
+import org.apache.seatunnel.api.source.SourceSplit;
+import org.apache.seatunnel.api.table.connector.TableSource;
+import org.apache.seatunnel.api.table.factory.Factory;
+import org.apache.seatunnel.api.table.factory.TableSourceFactory;
+import org.apache.seatunnel.api.table.factory.TableSourceFactoryContext;
+
+import com.google.auto.service.AutoService;
+
+import java.io.Serializable;
+
+import static 
org.apache.seatunnel.connectors.seatunnel.mailchimp.MailchimpSourceOptions.ALLOW_INSECURE_HTTP;
+import static 
org.apache.seatunnel.connectors.seatunnel.mailchimp.MailchimpSourceOptions.BASE_URL;
+import static 
org.apache.seatunnel.connectors.seatunnel.mailchimp.MailchimpSourceOptions.LIST_ID;
+import static 
org.apache.seatunnel.connectors.seatunnel.mailchimp.MailchimpSourceOptions.PAGE_SIZE;
+import static 
org.apache.seatunnel.connectors.seatunnel.mailchimp.MailchimpSourceOptions.REQUEST_TIMEOUT_MS;
+import static 
org.apache.seatunnel.connectors.seatunnel.mailchimp.MailchimpSourceOptions.RETRY;
+import static 
org.apache.seatunnel.connectors.seatunnel.mailchimp.MailchimpSourceOptions.RETRY_BACKOFF_MS;
+import static 
org.apache.seatunnel.connectors.seatunnel.mailchimp.MailchimpSourceOptions.TOKEN;
+
+@AutoService(Factory.class)
+public class MailchimpSourceFactory implements TableSourceFactory {
+    @Override
+    public String factoryIdentifier() {
+        return "Mailchimp";
+    }
+
+    @Override
+    public OptionRule optionRule() {
+        return OptionRule.builder()
+                .required(TOKEN, LIST_ID, ConnectorCommonOptions.SCHEMA)
+                .optional(
+                        BASE_URL,
+                        ALLOW_INSECURE_HTTP,
+                        PAGE_SIZE,
+                        RETRY,
+                        RETRY_BACKOFF_MS,
+                        REQUEST_TIMEOUT_MS)
+                .build();
+    }
+
+    @Override
+    @SuppressWarnings("unchecked")
+    public <T, SplitT extends SourceSplit, StateT extends Serializable>
+            TableSource<T, SplitT, StateT> 
createSource(TableSourceFactoryContext context) {
+        MailchimpSource source = new MailchimpSource(context.getOptions());
+        return () -> (SeaTunnelSource<T, SplitT, StateT>) source;
+    }
+
+    @Override
+    public Class<? extends SeaTunnelSource> getSourceClass() {
+        return MailchimpSource.class;
+    }
+}
diff --git 
a/seatunnel-connectors-v2/connector-http/connector-http-mailchimp/src/main/java/org/apache/seatunnel/connectors/seatunnel/mailchimp/MailchimpSourceOptions.java
 
b/seatunnel-connectors-v2/connector-http/connector-http-mailchimp/src/main/java/org/apache/seatunnel/connectors/seatunnel/mailchimp/MailchimpSourceOptions.java
new file mode 100644
index 0000000000..dc7d60f620
--- /dev/null
+++ 
b/seatunnel-connectors-v2/connector-http/connector-http-mailchimp/src/main/java/org/apache/seatunnel/connectors/seatunnel/mailchimp/MailchimpSourceOptions.java
@@ -0,0 +1,71 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.seatunnel.connectors.seatunnel.mailchimp;
+
+import org.apache.seatunnel.api.configuration.Option;
+import org.apache.seatunnel.api.configuration.Options;
+
+public final class MailchimpSourceOptions {
+    public static final Option<String> TOKEN =
+            Options.key("token")
+                    .stringType()
+                    .noDefaultValue()
+                    .withDescription("Mailchimp API key, with its datacenter 
suffix.");
+    public static final Option<String> LIST_ID =
+            Options.key("list_id")
+                    .stringType()
+                    .noDefaultValue()
+                    .withDescription("Audience/list identifier.");
+    public static final Option<String> BASE_URL =
+            Options.key("base_url")
+                    .stringType()
+                    .noDefaultValue()
+                    .withDescription(
+                            "Explicitly trusted API root including /3.0; 
defaults to the API key datacenter HTTPS endpoint.");
+    public static final Option<Boolean> ALLOW_INSECURE_HTTP =
+            Options.key("allow_insecure_http")
+                    .booleanType()
+                    .defaultValue(false)
+                    .withDescription(
+                            "Allow cleartext HTTP only for isolated fixtures 
with dummy credentials. Never use with real API keys.");
+    public static final Option<Integer> PAGE_SIZE =
+            Options.key("page_size")
+                    .intType()
+                    .defaultValue(1000)
+                    .withDescription("Number of members per request, from 1 to 
1000.");
+    public static final Option<Integer> RETRY =
+            Options.key("retry")
+                    .intType()
+                    .defaultValue(3)
+                    .withDescription(
+                            "Maximum retries per page for HTTP 429 and 5xx, 
from 0 to 10.");
+    public static final Option<Integer> RETRY_BACKOFF_MS =
+            Options.key("retry_backoff_ms")
+                    .intType()
+                    .defaultValue(1000)
+                    .withDescription(
+                            "Initial exponential retry delay in milliseconds, 
from 1 to 60000; capped at 60000.");
+    public static final Option<Integer> REQUEST_TIMEOUT_MS =
+            Options.key("request_timeout_ms")
+                    .intType()
+                    .defaultValue(30000)
+                    .withDescription(
+                            "Connection, pool and socket timeout in 
milliseconds, from 1 to 300000.");
+
+    private MailchimpSourceOptions() {}
+}
diff --git 
a/seatunnel-connectors-v2/connector-http/connector-http-mailchimp/src/main/java/org/apache/seatunnel/connectors/seatunnel/mailchimp/MailchimpSourceReader.java
 
b/seatunnel-connectors-v2/connector-http/connector-http-mailchimp/src/main/java/org/apache/seatunnel/connectors/seatunnel/mailchimp/MailchimpSourceReader.java
new file mode 100644
index 0000000000..679cb052f8
--- /dev/null
+++ 
b/seatunnel-connectors-v2/connector-http/connector-http-mailchimp/src/main/java/org/apache/seatunnel/connectors/seatunnel/mailchimp/MailchimpSourceReader.java
@@ -0,0 +1,321 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.seatunnel.connectors.seatunnel.mailchimp;
+
+import 
org.apache.seatunnel.shade.com.fasterxml.jackson.databind.DeserializationFeature;
+import org.apache.seatunnel.shade.com.fasterxml.jackson.databind.JsonNode;
+import org.apache.seatunnel.shade.com.fasterxml.jackson.databind.ObjectMapper;
+
+import org.apache.seatunnel.api.source.Collector;
+import org.apache.seatunnel.api.table.catalog.CatalogTable;
+import org.apache.seatunnel.api.table.type.SeaTunnelRow;
+import 
org.apache.seatunnel.connectors.seatunnel.common.source.AbstractSingleSplitReader;
+import 
org.apache.seatunnel.connectors.seatunnel.common.source.SingleSplitReaderContext;
+import org.apache.seatunnel.format.json.JsonDeserializationSchema;
+
+import org.apache.http.client.config.RequestConfig;
+import org.apache.http.client.methods.CloseableHttpResponse;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.utils.URIBuilder;
+import org.apache.http.impl.DefaultConnectionReuseStrategy;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+import java.util.Base64;
+
+final class MailchimpSourceReader extends 
AbstractSingleSplitReader<SeaTunnelRow> {
+    private static final int MAX_PAGE_BYTES = 32 * 1024 * 1024;
+    private final MailchimpConfig config;
+    private final SingleSplitReaderContext context;
+    private final JsonDeserializationSchema deserializer;
+    private final String tableId;
+    private final ObjectMapper mapper =
+            new ObjectMapper()
+                    .enable(DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS)
+                    .enable(DeserializationFeature.FAIL_ON_TRAILING_TOKENS);
+    private final Object lifecycle = new Object();
+    private volatile boolean closed;
+    private CloseableHttpClient client;
+    private HttpGet activeRequest;
+
+    MailchimpSourceReader(
+            MailchimpConfig config, CatalogTable table, 
SingleSplitReaderContext context) {
+        this.config = config;
+        this.context = context;
+        deserializer = new JsonDeserializationSchema(table, false, false);
+        tableId = table.getTablePath().toString();
+    }
+
+    @Override
+    public void open() throws IOException {
+        synchronized (lifecycle) {
+            if (closed) {
+                throw new IOException("Mailchimp reader is closed");
+            }
+            if (client == null) {
+                RequestConfig timeouts =
+                        RequestConfig.custom()
+                                .setConnectTimeout(config.timeoutMs)
+                                .setSocketTimeout(config.timeoutMs)
+                                .setConnectionRequestTimeout(config.timeoutMs)
+                                .setRedirectsEnabled(false)
+                                .build();
+                client =
+                        HttpClients.custom()
+                                .setDefaultRequestConfig(timeouts)
+                                .setConnectionReuseStrategy(
+                                        (response, context) ->
+                                                
response.getStatusLine().getStatusCode() < 400
+                                                        && 
DefaultConnectionReuseStrategy.INSTANCE
+                                                                
.keepAlive(response, context))
+                                .disableRedirectHandling()
+                                .disableAutomaticRetries()
+                                .disableCookieManagement()
+                                .build();
+            }
+        }
+    }
+
+    /**
+     * One bounded batch. Checkpoints deliberately retain an empty split, so a 
recovered reader
+     * replays from offset zero instead of treating offsets in a mutable 
audience as durable
+     * cursors.
+     */
+    @Override
+    public void pollNext(Collector<SeaTunnelRow> output) throws Exception {
+        if (closed || noMoreSplits) {
+            return;
+        }
+        long offset = 0;
+        long target = -1;
+        while (!closed) {
+            int count =
+                    target < 0 ? config.pageSize : (int) 
Math.min(config.pageSize, target - offset);
+            JsonNode page = requestPage(offset, count);
+            JsonNode members = page.get("members");
+            JsonNode total = page.get("total_items");
+            if (!page.isObject()
+                    || members == null
+                    || !members.isArray()
+                    || total == null
+                    || !total.isIntegralNumber()
+                    || !total.canConvertToInt()
+                    || total.intValue() < 0) {
+                throw new IOException(
+                        "Mailchimp page must contain members array and 
nonnegative integer total_items");
+            }
+            if (target < 0) {
+                target = total.intValue();
+            }
+            if (total.intValue() < target || members.size() != Math.min(count, 
target - offset)) {
+                throw new IOException(
+                        "Mailchimp pagination is incomplete or audience 
changed; retry the batch");
+            }
+            // Validate and convert the entire page before accepting any of 
its rows.
+            SeaTunnelRow[] rows = new SeaTunnelRow[members.size()];
+            for (int i = 0; i < rows.length; i++) {
+                if (!members.get(i).isObject()) {
+                    throw new IOException("Mailchimp members must be JSON 
objects");
+                }
+                try {
+                    rows[i] = deserializer.convertToRowData(members.get(i));
+                    rows[i].setTableId(tableId);
+                } catch (Exception e) {
+                    // JSON conversion errors may include member data. Do not 
retain their causes.
+                    throw new IOException("Mailchimp member does not match the 
configured schema");
+                }
+            }
+            for (SeaTunnelRow row : rows) {
+                synchronized (output.getCheckpointLock()) {
+                    if (closed) {
+                        return;
+                    }
+                    output.collect(row);
+                }
+            }
+            offset += rows.length;
+            if (offset >= target) {
+                boolean finished;
+                synchronized (lifecycle) {
+                    finished = !closed;
+                    if (finished) {
+                        noMoreSplits = true;
+                    }
+                }
+                // Completion may wait for a remote acknowledgement; never 
block cancel on it.
+                if (finished) {
+                    context.signalNoMoreElement();
+                }
+                return;
+            }
+        }
+    }
+
+    private JsonNode requestPage(long offset, int count) throws Exception {
+        for (int attempt = 0; ; attempt++) {
+            HttpGet request =
+                    new HttpGet(
+                            new URIBuilder(config.membersUri)
+                                    .addParameter("count", 
Integer.toString(count))
+                                    .addParameter("offset", 
Long.toString(offset))
+                                    .build());
+            String basic =
+                    Base64.getEncoder()
+                            .encodeToString(
+                                    ("seatunnel:" + config.apiKey)
+                                            .getBytes(StandardCharsets.UTF_8));
+            request.setHeader("Authorization", "Basic " + basic);
+            request.setHeader("Accept", "application/json");
+            CloseableHttpClient httpClient;
+            synchronized (lifecycle) {
+                ensureOpen();
+                activeRequest = request;
+                httpClient = client;
+            }
+            int status;
+            long delay = Math.min(60000L, (long) config.backoffMs << attempt);
+            byte[] body = null;
+            try (CloseableHttpResponse response = httpClient.execute(request)) 
{
+                status = response.getStatusLine().getStatusCode();
+                if (status == 200) {
+                    if (response.getEntity() == null) {
+                        throw new IOException("Missing response");
+                    }
+                    try (InputStream in = response.getEntity().getContent()) {
+                        boolean consumed = false;
+                        try {
+                            body = readPage(in);
+                            consumed = true;
+                        } finally {
+                            if (!consumed) {
+                                // Entity close can drain an unfinished tail. 
Abort before close.
+                                request.abort();
+                            }
+                        }
+                    }
+                } else if (response.getFirstHeader("Retry-After") != null) {
+                    delay = 
retryDelay(response.getFirstHeader("Retry-After").getValue(), delay);
+                }
+            } catch (IOException | RuntimeException e) {
+                // Transport errors can expose URI values. No response/error 
content is surfaced.
+                throw new IOException("Mailchimp request failed or reader was 
closed");
+            } finally {
+                synchronized (lifecycle) {
+                    activeRequest = null;
+                }
+                request.releaseConnection();
+            }
+            if (status == 200) {
+                try {
+                    JsonNode page = mapper.readTree(body);
+                    if (page == null) {
+                        throw new IOException("Empty response");
+                    }
+                    return page;
+                } catch (IOException e) {
+                    throw new IOException("Mailchimp returned invalid JSON");
+                }
+            }
+            if ((status != 429 && (status < 500 || status > 599)) || attempt 
>= config.retry) {
+                throw new IOException("Mailchimp request failed with HTTP " + 
status);
+            }
+            synchronized (lifecycle) {
+                ensureOpen();
+                long deadline = System.nanoTime() + delay * 1_000_000L;
+                long remaining;
+                while ((remaining = deadline - System.nanoTime()) > 0) {
+                    try {
+                        lifecycle.wait(Math.max(1, remaining / 1_000_000L));
+                    } catch (InterruptedException e) {
+                        Thread.currentThread().interrupt();
+                        throw new IOException("Mailchimp retry interrupted");
+                    }
+                    ensureOpen();
+                }
+            }
+        }
+    }
+
+    private byte[] readPage(InputStream in) throws IOException {
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        byte[] buffer = new byte[8192];
+        int n;
+        while ((n = in.read(buffer)) != -1) {
+            if (out.size() > MAX_PAGE_BYTES - n) {
+                throw new IOException("Mailchimp page exceeds size limit");
+            }
+            out.write(buffer, 0, n);
+        }
+        return out.toByteArray();
+    }
+
+    private static long retryDelay(String value, long fallback) {
+        try {
+            long seconds = Long.parseLong(value);
+            return seconds < 0 ? fallback : Math.min(seconds, 60) * 1000L;
+        } catch (NumberFormatException e) {
+            try {
+                long millis =
+                        ZonedDateTime.parse(value, 
DateTimeFormatter.RFC_1123_DATE_TIME)
+                                        .toInstant()
+                                        .toEpochMilli()
+                                - System.currentTimeMillis();
+                return Math.max(1, Math.min(60000, millis));
+            } catch (DateTimeParseException ignored) {
+                return fallback;
+            }
+        }
+    }
+
+    private void ensureOpen() throws IOException {
+        if (closed || client == null) {
+            throw new IOException("Mailchimp reader is not open");
+        }
+    }
+
+    @Override
+    public void cancel() {
+        try {
+            close();
+        } catch (IOException e) {
+            throw new IllegalStateException("Mailchimp reader could not close 
its HTTP client");
+        }
+    }
+
+    /** Abort in-flight I/O and wake retry waits without waiting for the 
checkpoint lock. */
+    @Override
+    public void close() throws IOException {
+        synchronized (lifecycle) {
+            closed = true;
+            if (activeRequest != null) {
+                activeRequest.abort();
+            }
+            lifecycle.notifyAll();
+            if (client != null) {
+                client.close();
+            }
+        }
+    }
+}
diff --git 
a/seatunnel-connectors-v2/connector-http/connector-http-mailchimp/src/test/java/org/apache/seatunnel/connectors/seatunnel/mailchimp/MailchimpConfigTest.java
 
b/seatunnel-connectors-v2/connector-http/connector-http-mailchimp/src/test/java/org/apache/seatunnel/connectors/seatunnel/mailchimp/MailchimpConfigTest.java
new file mode 100644
index 0000000000..172826c618
--- /dev/null
+++ 
b/seatunnel-connectors-v2/connector-http/connector-http-mailchimp/src/test/java/org/apache/seatunnel/connectors/seatunnel/mailchimp/MailchimpConfigTest.java
@@ -0,0 +1,165 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.seatunnel.connectors.seatunnel.mailchimp;
+
+import org.apache.seatunnel.shade.com.typesafe.config.ConfigFactory;
+
+import org.apache.seatunnel.api.common.JobContext;
+import org.apache.seatunnel.api.configuration.ReadonlyConfig;
+import org.apache.seatunnel.api.source.Boundedness;
+import org.apache.seatunnel.api.source.SourceReader;
+import org.apache.seatunnel.common.constants.JobMode;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+class MailchimpConfigTest {
+    static Map<String, Object> options() {
+        return new HashMap<>(
+                ConfigFactory.parseString(
+                                "token=testkey-us1\nlist_id=audience1\nschema 
{ fields { "
+                                        + "id=string, email_address=string, 
status=string, last_changed=string, "
+                                        + "merge_fields=\"map<string,string>\" 
} }")
+                        .root()
+                        .unwrapped());
+    }
+
+    static ReadonlyConfig config(Map<String, Object> options) {
+        return ReadonlyConfig.fromMap(options);
+    }
+
+    @Test
+    void defaultsAndBoundedMode() throws Exception {
+        MailchimpConfig cfg = new MailchimpConfig(config(options()));
+        Assertions.assertEquals(
+                "https://us1.api.mailchimp.com/3.0/lists/audience1/members";,
+                cfg.membersUri.toString());
+        Assertions.assertEquals(1000, cfg.pageSize);
+        MailchimpSource source = new MailchimpSource(config(options()));
+        Assertions.assertEquals(Boundedness.BOUNDED, source.getBoundedness());
+        Assertions.assertEquals(
+                5, 
source.getProducedCatalogTables().get(0).getSeaTunnelRowType().getTotalFields());
+        JobContext streaming = new JobContext();
+        streaming.setJobMode(JobMode.STREAMING);
+        Assertions.assertThrows(
+                IllegalArgumentException.class, () -> 
source.setJobContext(streaming));
+        SourceReader.Context context = mock(SourceReader.Context.class);
+        when(context.getIndexOfSubtask()).thenReturn(1);
+        Assertions.assertThrows(IllegalArgumentException.class, () -> 
source.createReader(context));
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = {"token", "list_id", "schema"})
+    void missingRequired(String key) {
+        Map<String, Object> opts = options();
+        opts.remove(key);
+        Assertions.assertThrows(Exception.class, () -> new 
MailchimpSource(config(opts)));
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = {"", " ", "secret", "secret-us1\r\nInjected", 
"secret-us1.evil"})
+    void invalidKeysAreNotEchoed(String value) {
+        Map<String, Object> opts = options();
+        opts.put("token", value);
+        Exception error =
+                Assertions.assertThrows(
+                        IllegalArgumentException.class, () -> new 
MailchimpConfig(config(opts)));
+        Assertions.assertNull(error.getCause());
+        Assertions.assertFalse(error.getMessage().contains("secret"));
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = {"", "../other", "x?fields=email_address", "x/y", 
"x%2fy", "x#fragment"})
+    void listCannotChangeRequestTarget(String value) {
+        Map<String, Object> opts = options();
+        opts.put("list_id", value);
+        Assertions.assertThrows(
+                IllegalArgumentException.class, () -> new 
MailchimpConfig(config(opts)));
+    }
+
+    @ParameterizedTest
+    @ValueSource(
+            strings = {
+                "https://secret@host/3.0";,
+                "https://host/3.0?secret=1";,
+                "https://host/3.0#secret";,
+                "file:///3.0",
+                "https://host/3.0/evil";,
+                "https://host:0/3.0";,
+                "https://host:65536/3.0";,
+                "https://host/%33.0";,
+                "https://host/3.0\\evil";,
+                "not a secret uri"
+            })
+    void rejectsUnsafeUriShapesWithoutEcho(String url) {
+        Map<String, Object> opts = options();
+        opts.put("base_url", url);
+        Exception error =
+                Assertions.assertThrows(
+                        IllegalArgumentException.class, () -> new 
MailchimpConfig(config(opts)));
+        Assertions.assertFalse(error.getMessage().contains("secret"));
+        Assertions.assertNull(error.getCause());
+    }
+
+    @Test
+    void explicitEndpointAndNumericBounds() {
+        Map<String, Object> opts = options();
+        opts.put("base_url", "https://trusted.example:8443/3.0/";);
+        opts.put("page_size", 1);
+        opts.put("retry", 0);
+        Assertions.assertEquals(
+                "https://trusted.example:8443/3.0/lists/audience1/members";,
+                new MailchimpConfig(config(opts)).membersUri.toString());
+        Map<String, int[]> bad = new HashMap<>();
+        bad.put("page_size", new int[] {0, 1001});
+        bad.put("retry", new int[] {-1, 11});
+        bad.put("retry_backoff_ms", new int[] {0, 60001});
+        bad.put("request_timeout_ms", new int[] {0, 300001});
+        bad.forEach(
+                (key, values) -> {
+                    for (int value : values) {
+                        Map<String, Object> invalid = options();
+                        invalid.put(key, value);
+                        Assertions.assertThrows(
+                                IllegalArgumentException.class,
+                                () -> new MailchimpConfig(config(invalid)));
+                    }
+                });
+        opts.put("schema", new HashMap<>());
+        Assertions.assertThrows(
+                IllegalArgumentException.class, () -> new 
MailchimpSource(config(opts)));
+    }
+
+    @Test
+    void httpRequiresExplicitFixtureOptIn() {
+        Map<String, Object> opts = options();
+        opts.put("base_url", "http://localhost:8080/3.0";);
+        Assertions.assertThrows(
+                IllegalArgumentException.class, () -> new 
MailchimpConfig(config(opts)));
+        opts.put("allow_insecure_http", true);
+        Assertions.assertEquals("http", new 
MailchimpConfig(config(opts)).membersUri.getScheme());
+    }
+}
diff --git 
a/seatunnel-connectors-v2/connector-http/connector-http-mailchimp/src/test/java/org/apache/seatunnel/connectors/seatunnel/mailchimp/MailchimpSourceFactoryTest.java
 
b/seatunnel-connectors-v2/connector-http/connector-http-mailchimp/src/test/java/org/apache/seatunnel/connectors/seatunnel/mailchimp/MailchimpSourceFactoryTest.java
new file mode 100644
index 0000000000..3aadfe1ddb
--- /dev/null
+++ 
b/seatunnel-connectors-v2/connector-http/connector-http-mailchimp/src/test/java/org/apache/seatunnel/connectors/seatunnel/mailchimp/MailchimpSourceFactoryTest.java
@@ -0,0 +1,37 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.seatunnel.connectors.seatunnel.mailchimp;
+
+import org.apache.seatunnel.api.table.factory.FactoryUtil;
+import org.apache.seatunnel.api.table.factory.TableSourceFactory;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+class MailchimpSourceFactoryTest {
+    @Test
+    void discoversMailchimpByIdentifier() {
+        ClassLoader loader = getClass().getClassLoader();
+        Assertions.assertNotNull(
+                FactoryUtil.discoverFactory(loader, TableSourceFactory.class, 
"Http"));
+        Assertions.assertEquals(
+                "Mailchimp",
+                FactoryUtil.discoverFactory(loader, TableSourceFactory.class, 
"Mailchimp")
+                        .factoryIdentifier());
+    }
+}
diff --git 
a/seatunnel-connectors-v2/connector-http/connector-http-mailchimp/src/test/java/org/apache/seatunnel/connectors/seatunnel/mailchimp/MailchimpSourceReaderTest.java
 
b/seatunnel-connectors-v2/connector-http/connector-http-mailchimp/src/test/java/org/apache/seatunnel/connectors/seatunnel/mailchimp/MailchimpSourceReaderTest.java
new file mode 100644
index 0000000000..0c842d0f2e
--- /dev/null
+++ 
b/seatunnel-connectors-v2/connector-http/connector-http-mailchimp/src/test/java/org/apache/seatunnel/connectors/seatunnel/mailchimp/MailchimpSourceReaderTest.java
@@ -0,0 +1,543 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.seatunnel.connectors.seatunnel.mailchimp;
+
+import org.apache.seatunnel.api.source.Collector;
+import org.apache.seatunnel.api.source.SourceReader;
+import org.apache.seatunnel.api.table.catalog.CatalogTableUtil;
+import org.apache.seatunnel.api.table.type.SeaTunnelRow;
+import org.apache.seatunnel.connectors.seatunnel.common.source.SingleSplit;
+import 
org.apache.seatunnel.connectors.seatunnel.common.source.SingleSplitReaderContext;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import com.sun.net.httpserver.HttpServer;
+import okhttp3.mockwebserver.MockResponse;
+import okhttp3.mockwebserver.MockWebServer;
+import okhttp3.mockwebserver.RecordedRequest;
+import okhttp3.mockwebserver.SocketPolicy;
+
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.net.InetSocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Base64;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+class MailchimpSourceReaderTest {
+    private MockWebServer server;
+    private MailchimpSourceReader reader;
+    private SourceReader.Context context;
+    private Collector<SeaTunnelRow> output;
+    private final List<SeaTunnelRow> rows = new ArrayList<>();
+
+    static String member(String id) {
+        return "{\"id\":\""
+                + id
+                + "\",\"email_address\":\""
+                + id
+                + 
"@example.invalid\",\"status\":\"subscribed\",\"last_changed\":\"2026-01-01T00:00:00Z\","
+                + "\"merge_fields\":{\"FNAME\":\"Example\"}}";
+    }
+
+    static String page(int total, String... members) {
+        return "{\"total_items\":" + total + ",\"members\":[" + 
String.join(",", members) + "]}";
+    }
+
+    @BeforeEach
+    @SuppressWarnings("unchecked")
+    void setUp() throws Exception {
+        server = new MockWebServer();
+        server.start();
+        context = mock(SourceReader.Context.class);
+        output = mock(Collector.class);
+        when(output.getCheckpointLock()).thenReturn(new Object());
+        doAnswer(
+                        call -> {
+                            rows.add(call.getArgument(0));
+                            return null;
+                        })
+                .when(output)
+                .collect(any(SeaTunnelRow.class));
+        reader = newReader(2, 1);
+        reader.open();
+    }
+
+    private MailchimpSourceReader newReader(int retry, int backoff) {
+        return newReader(retry, backoff, "map<string,string>");
+    }
+
+    @SuppressWarnings("unchecked")
+    private MailchimpSourceReader newReader(int retry, int backoff, String 
mergeType) {
+        Map<String, Object> opts = MailchimpConfigTest.options();
+        Map<String, Object> schema = new LinkedHashMap<>((Map<String, Object>) 
opts.get("schema"));
+        Map<String, Object> fields =
+                new LinkedHashMap<>((Map<String, Object>) 
schema.get("fields"));
+        fields.put("merge_fields", mergeType);
+        schema.put("fields", fields);
+        opts.put("schema", schema);
+        opts.put("base_url", server.url("/3.0").toString());
+        opts.put("allow_insecure_http", true);
+        opts.put("page_size", 2);
+        opts.put("retry", retry);
+        opts.put("retry_backoff_ms", backoff);
+        opts.put("request_timeout_ms", 30000);
+        return new MailchimpSourceReader(
+                new MailchimpConfig(MailchimpConfigTest.config(opts)),
+                
CatalogTableUtil.buildWithConfig(MailchimpConfigTest.config(opts)),
+                new SingleSplitReaderContext(context));
+    }
+
+    @AfterEach
+    void tearDown() throws Exception {
+        reader.close();
+        server.shutdown();
+    }
+
+    @Test
+    void readsMultiplePagesAndSchemaWithoutExtraRequest() throws Exception {
+        server.enqueue(new MockResponse().setBody(page(3, member("one"), 
member("two"))));
+        server.enqueue(new MockResponse().setBody(page(3, member("three"))));
+        reader.pollNext(output);
+        reader.pollNext(output);
+        Assertions.assertEquals(3, rows.size());
+        Assertions.assertEquals("one", rows.get(0).getField(0));
+        Assertions.assertEquals("[email protected]", 
rows.get(2).getField(1));
+        Assertions.assertEquals(
+                Collections.singletonMap("FNAME", "Example"), 
rows.get(0).getField(4));
+        Assertions.assertEquals("default.default.default", 
rows.get(0).getTableId());
+        Assertions.assertEquals(2, server.getRequestCount());
+        RecordedRequest first = server.takeRequest();
+        Assertions.assertEquals("GET", first.getMethod());
+        
Assertions.assertEquals("/3.0/lists/audience1/members?count=2&offset=0", 
first.getPath());
+        Assertions.assertEquals(
+                "Basic "
+                        + Base64.getEncoder()
+                                .encodeToString(
+                                        
"seatunnel:testkey-us1".getBytes(StandardCharsets.UTF_8)),
+                first.getHeader("Authorization"));
+        Assertions.assertEquals(
+                "/3.0/lists/audience1/members?count=1&offset=2", 
server.takeRequest().getPath());
+        verify(context).signalNoMoreElement();
+    }
+
+    @Test
+    void emptyAudienceCompletes() throws Exception {
+        server.enqueue(new MockResponse().setBody(page(0)));
+        reader.pollNext(output);
+        Assertions.assertTrue(rows.isEmpty());
+        verify(context).signalNoMoreElement();
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = {"map<string,decimal(24,18)>", 
"{VALUE=\"decimal(24,18)\"}"})
+    void preservesNestedDecimalPrecision(String mergeType) throws Exception {
+        reader.close();
+        reader = newReader(0, 1, mergeType);
+        reader.open();
+        server.enqueue(
+                new MockResponse()
+                        .setBody(
+                                page(
+                                        1,
+                                        
"{\"id\":\"one\",\"merge_fields\":{\"VALUE\":0.123456789012345678}}")));
+        reader.pollNext(output);
+        Object merge = rows.get(0).getField(4);
+        Object value =
+                merge instanceof Map
+                        ? ((Map<?, ?>) merge).get("VALUE")
+                        : ((SeaTunnelRow) merge).getField(0);
+        Assertions.assertEquals(new BigDecimal("0.123456789012345678"), value);
+        verify(context).signalNoMoreElement();
+    }
+
+    @Test
+    void exactFullPageCompletesAndMissingOptionalFieldsAreNull() throws 
Exception {
+        server.enqueue(new MockResponse().setBody(page(2, "{\"id\":\"one\"}", 
"{\"id\":\"two\"}")));
+        reader.pollNext(output);
+        Assertions.assertEquals(2, rows.size());
+        Assertions.assertNull(rows.get(0).getField(1));
+        Assertions.assertEquals(1, server.getRequestCount());
+        verify(context).signalNoMoreElement();
+    }
+
+    @Test
+    void growthCannotExtendInitialBound() throws Exception {
+        server.enqueue(new MockResponse().setBody(page(3, member("one"), 
member("two"))));
+        server.enqueue(new MockResponse().setBody(page(100, member("three"))));
+        reader.pollNext(output);
+        Assertions.assertEquals(3, rows.size());
+        Assertions.assertEquals(2, server.getRequestCount());
+    }
+
+    @ParameterizedTest
+    @ValueSource(
+            strings = {
+                "{}",
+                "[]",
+                "null",
+                "{\"members\":{},\"total_items\":0}",
+                "{\"members\":[],\"total_items\":\"0\"}",
+                "{\"members\":[],\"total_items\":-1}",
+                "{\"members\":[],\"total_items\":2147483648}",
+                "{\"members\":[],\"total_items\":1}",
+                "{\"members\":[{}],\"total_items\":2}",
+                "{\"members\":[null],\"total_items\":1}",
+                "{\"members\":[1],\"total_items\":1}",
+                "{\"members\":[],\"total_items\":0} {}",
+                "{\"members\":[{\"merge_fields\":42}],\"total_items\":1}",
+                "[email protected]"
+            })
+    void rejectsMalformedOrTruncatedPages(String body) {
+        server.enqueue(new MockResponse().setBody(body));
+        IOException error =
+                Assertions.assertThrows(IOException.class, () -> 
reader.pollNext(output));
+        Assertions.assertNull(error.getCause());
+        Assertions.assertFalse(error.getMessage().contains("PII"));
+        Assertions.assertTrue(rows.isEmpty());
+        verify(context, never()).signalNoMoreElement();
+    }
+
+    @Test
+    void retries429And5xxAtSameOffset() throws Exception {
+        server.enqueue(
+                new MockResponse()
+                        .setResponseCode(429)
+                        .setHeader("Retry-After", "0")
+                        .setBody("private"));
+        server.enqueue(
+                new MockResponse()
+                        .setResponseCode(503)
+                        .setHeader("Retry-After", "invalid")
+                        .setBody("private"));
+        server.enqueue(new MockResponse().setBody(page(1, member("one"))));
+        reader.pollNext(output);
+        Assertions.assertEquals(1, rows.size());
+        for (int i = 0; i < 3; i++) {
+            Assertions.assertEquals(
+                    "/3.0/lists/audience1/members?count=2&offset=0",
+                    server.takeRequest().getPath());
+        }
+    }
+
+    @Test
+    void retriesBodylessErrorsAndReusesSuccessfulPageConnection() throws 
Exception {
+        HttpServer fixture = HttpServer.create(new 
InetSocketAddress("127.0.0.1", 0), 0);
+        AtomicInteger requests = new AtomicInteger();
+        List<String> paths = Collections.synchronizedList(new ArrayList<>());
+        List<Integer> ports = Collections.synchronizedList(new ArrayList<>());
+        fixture.createContext(
+                "/3.0/lists/audience1/members",
+                exchange -> {
+                    int request = requests.incrementAndGet();
+                    paths.add(exchange.getRequestURI().toString());
+                    ports.add(exchange.getRemoteAddress().getPort());
+                    try {
+                        if (request <= 2) {
+                            exchange.getResponseHeaders().set("Retry-After", 
"0");
+                            exchange.sendResponseHeaders(request == 1 ? 429 : 
503, -1);
+                        } else {
+                            byte[] body =
+                                    (request == 3
+                                                    ? page(3, member("one"), 
member("two"))
+                                                    : page(3, member("three")))
+                                            .getBytes(StandardCharsets.UTF_8);
+                            exchange.sendResponseHeaders(200, body.length);
+                            exchange.getResponseBody().write(body);
+                        }
+                    } finally {
+                        exchange.close();
+                    }
+                });
+        fixture.start();
+        try {
+            reader.close();
+            Map<String, Object> opts = MailchimpConfigTest.options();
+            opts.put("base_url", "http://127.0.0.1:"; + 
fixture.getAddress().getPort() + "/3.0");
+            opts.put("allow_insecure_http", true);
+            opts.put("page_size", 2);
+            opts.put("retry", 2);
+            opts.put("retry_backoff_ms", 1);
+            opts.put("request_timeout_ms", 3000);
+            reader =
+                    new MailchimpSourceReader(
+                            new 
MailchimpConfig(MailchimpConfigTest.config(opts)),
+                            
CatalogTableUtil.buildWithConfig(MailchimpConfigTest.config(opts)),
+                            new SingleSplitReaderContext(context));
+            reader.open();
+            Assertions.assertDoesNotThrow(
+                    () -> reader.pollNext(output),
+                    () -> "Bodyless retry sequence failed after " + 
requests.get() + " requests");
+            Assertions.assertEquals(3, rows.size());
+            Assertions.assertEquals(4, requests.get());
+            for (int i = 0; i < 3; i++) {
+                Assertions.assertEquals(
+                        "/3.0/lists/audience1/members?count=2&offset=0", 
paths.get(i));
+            }
+            
Assertions.assertEquals("/3.0/lists/audience1/members?count=1&offset=2", 
paths.get(3));
+            Assertions.assertEquals(
+                    ports.get(2), ports.get(3), "Successful pages reuse a 
connection");
+            verify(context).signalNoMoreElement();
+        } finally {
+            fixture.stop(0);
+        }
+    }
+
+    @ParameterizedTest
+    @ValueSource(ints = {400, 401, 403, 404, 204, 301, 302, 307, 308})
+    void permanentErrorsAndRedirectsNeverRetryOrExposeBodies(int status) 
throws Exception {
+        server.enqueue(
+                new MockResponse()
+                        .setResponseCode(status)
+                        .setHeader("Location", server.url("/stolen"))
+                        .setBody("testkey-us1 [email protected]"));
+        IOException error =
+                Assertions.assertThrows(IOException.class, () -> 
reader.pollNext(output));
+        Assertions.assertTrue(error.getMessage().contains("HTTP " + status));
+        Assertions.assertNull(error.getCause());
+        Assertions.assertFalse(error.getMessage().contains("testkey"));
+        Assertions.assertFalse(error.getMessage().contains("private"));
+        Assertions.assertEquals(1, server.getRequestCount());
+        verify(context, never()).signalNoMoreElement();
+    }
+
+    @ParameterizedTest
+    @ValueSource(ints = {429, 500, 503})
+    void retryBudgetIsFinite(int status) {
+        for (int i = 0; i < 3; i++) {
+            server.enqueue(new 
MockResponse().setResponseCode(status).setBody("private"));
+        }
+        Assertions.assertThrows(IOException.class, () -> 
reader.pollNext(output));
+        Assertions.assertEquals(3, server.getRequestCount());
+        verify(context, never()).signalNoMoreElement();
+    }
+
+    @Test
+    void redirectCannotForwardCredentialsToAnotherOrigin() throws Exception {
+        try (MockWebServer destination = new MockWebServer()) {
+            destination.start();
+            server.enqueue(
+                    new MockResponse()
+                            .setResponseCode(302)
+                            .setHeader("Location", 
destination.url("/credential-target")));
+            Assertions.assertThrows(IOException.class, () -> 
reader.pollNext(output));
+            Assertions.assertNull(destination.takeRequest(100, 
TimeUnit.MILLISECONDS));
+            Assertions.assertEquals(1, server.getRequestCount());
+            verify(context, never()).signalNoMoreElement();
+        }
+    }
+
+    @Test
+    void httpDateRetryAfterIsAccepted() throws Exception {
+        server.enqueue(
+                new MockResponse()
+                        .setResponseCode(429)
+                        .setHeader("Retry-After", "Thu, 01 Jan 1970 00:00:00 
GMT"));
+        server.enqueue(new MockResponse().setBody(page(0)));
+        reader.pollNext(output);
+        Assertions.assertEquals(2, server.getRequestCount());
+        verify(context).signalNoMoreElement();
+    }
+
+    @Test
+    void brokenTransportDoesNotComplete() {
+        server.enqueue(
+                new MockResponse()
+                        .setBody(page(1, member("one")))
+                        
.setSocketPolicy(SocketPolicy.DISCONNECT_DURING_RESPONSE_BODY));
+        Assertions.assertThrows(IOException.class, () -> 
reader.pollNext(output));
+        Assertions.assertTrue(rows.isEmpty());
+        verify(context, never()).signalNoMoreElement();
+    }
+
+    @Test
+    void oversizedUnfinishedBodyFailsPromptlyWithoutCancellation() throws 
Exception {
+        server.enqueue(
+                new MockResponse()
+                        .setBody("x".repeat(32 * 1024 * 1024 + 16384))
+                        .setHeader("Content-Length", 64 * 1024 * 1024));
+        CompletableFuture<Exception> task = pollInBackground();
+        Assertions.assertInstanceOf(IOException.class, task.get(5, 
TimeUnit.SECONDS));
+        Assertions.assertTrue(rows.isEmpty());
+        verify(context, never()).signalNoMoreElement();
+    }
+
+    @Test
+    void failureAfterAcceptedPageReplaysWholeBatchOnRestore() throws Exception 
{
+        server.enqueue(new MockResponse().setBody(page(3, member("one"), 
member("two"))));
+        server.enqueue(new 
MockResponse().setResponseCode(401).setBody("private"));
+        Assertions.assertThrows(IOException.class, () -> 
reader.pollNext(output));
+        Assertions.assertEquals(2, rows.size());
+        List<SingleSplit> state = reader.snapshotState(1);
+        Assertions.assertNull(
+                state.get(0).getState(), "No offset, credentials or PII in 
split state");
+        verify(context, never()).signalNoMoreElement();
+        reader.close();
+        reader = newReader(0, 1);
+        reader.addSplits(state);
+        reader.open();
+        server.enqueue(new MockResponse().setBody(page(3, member("one"), 
member("two"))));
+        server.enqueue(new MockResponse().setBody(page(3, member("three"))));
+        reader.pollNext(output);
+        Assertions.assertEquals(5, rows.size());
+        server.takeRequest();
+        server.takeRequest();
+        
Assertions.assertTrue(server.takeRequest().getPath().endsWith("offset=0"));
+        verify(context).signalNoMoreElement();
+    }
+
+    @Test
+    void audienceShrinkIsFailureNotSuccess() {
+        server.enqueue(new MockResponse().setBody(page(3, member("one"), 
member("two"))));
+        server.enqueue(new MockResponse().setBody(page(2)));
+        Assertions.assertThrows(IOException.class, () -> 
reader.pollNext(output));
+        Assertions.assertEquals(2, rows.size());
+        verify(context, never()).signalNoMoreElement();
+    }
+
+    @ParameterizedTest
+    @ValueSource(booleans = {false, true})
+    void closeOrCancelWakesRetryAndIsIdempotent(boolean cancel) throws 
Exception {
+        reader.close();
+        reader = newReader(10, 60000);
+        reader.open();
+        server.enqueue(
+                new 
MockResponse().setResponseCode(429).setHeader("Retry-After", "9999999999999"));
+        CompletableFuture<Exception> task = pollInBackground();
+        Assertions.assertNotNull(server.takeRequest(2, TimeUnit.SECONDS));
+        if (cancel) {
+            reader.cancel();
+        } else {
+            reader.close();
+        }
+        Assertions.assertInstanceOf(IOException.class, task.get(2, 
TimeUnit.SECONDS));
+        reader.close();
+        Assertions.assertThrows(IOException.class, () -> reader.open());
+        Assertions.assertEquals(1, server.getRequestCount());
+        verify(context, never()).signalNoMoreElement();
+    }
+
+    @ParameterizedTest
+    @ValueSource(booleans = {false, true})
+    void closeOrCancelAbortsActiveIo(boolean cancel) throws Exception {
+        server.enqueue(new 
MockResponse().setSocketPolicy(SocketPolicy.NO_RESPONSE));
+        CompletableFuture<Exception> task = pollInBackground();
+        Assertions.assertNotNull(server.takeRequest(2, TimeUnit.SECONDS));
+        Assertions.assertTrue(
+                CompletableFuture.supplyAsync(
+                                () -> {
+                                    synchronized (output.getCheckpointLock()) {
+                                        return true;
+                                    }
+                                })
+                        .get(1, TimeUnit.SECONDS),
+                "Network I/O must not hold the checkpoint lock");
+        if (cancel) {
+            reader.cancel();
+        } else {
+            reader.close();
+        }
+        Assertions.assertInstanceOf(IOException.class, task.get(2, 
TimeUnit.SECONDS));
+        verify(context, never()).signalNoMoreElement();
+    }
+
+    @Test
+    void cancellationBeforeOpenIsSticky() throws Exception {
+        reader.close();
+        reader = newReader(0, 1);
+        reader.cancel();
+        reader.cancel();
+        Assertions.assertThrows(IOException.class, () -> reader.open());
+        reader.pollNext(output);
+        Assertions.assertEquals(0, server.getRequestCount());
+        verify(context, never()).signalNoMoreElement();
+    }
+
+    @Test
+    void cancelDoesNotWaitForCompletionAcknowledgement() throws Exception {
+        server.enqueue(new MockResponse().setBody(page(0)));
+        CountDownLatch entered = new CountDownLatch(1);
+        CountDownLatch release = new CountDownLatch(1);
+        doAnswer(
+                        call -> {
+                            entered.countDown();
+                            release.await();
+                            return null;
+                        })
+                .when(context)
+                .signalNoMoreElement();
+        CompletableFuture<Exception> poll = pollInBackground();
+        Assertions.assertTrue(entered.await(2, TimeUnit.SECONDS));
+        CompletableFuture<Void> cancel = 
CompletableFuture.runAsync(reader::cancel);
+        try {
+            cancel.get(1, TimeUnit.SECONDS);
+        } finally {
+            release.countDown();
+            poll.get(2, TimeUnit.SECONDS);
+            cancel.get(2, TimeUnit.SECONDS);
+        }
+    }
+
+    @Test
+    void closeDuringCollectionDoesNotSignalCompletion() throws Exception {
+        server.enqueue(new MockResponse().setBody(page(2, member("one"), 
member("two"))));
+        doAnswer(
+                        call -> {
+                            rows.add(call.getArgument(0));
+                            reader.close();
+                            return null;
+                        })
+                .when(output)
+                .collect(any(SeaTunnelRow.class));
+        reader.pollNext(output);
+        Assertions.assertEquals(1, rows.size());
+        verify(context, never()).signalNoMoreElement();
+    }
+
+    private CompletableFuture<Exception> pollInBackground() {
+        return CompletableFuture.supplyAsync(
+                () -> {
+                    try {
+                        reader.pollNext(output);
+                        return null;
+                    } catch (Exception e) {
+                        return e;
+                    }
+                });
+    }
+}
diff --git a/seatunnel-connectors-v2/connector-http/pom.xml 
b/seatunnel-connectors-v2/connector-http/pom.xml
index 264bd42087..87d301a9fc 100644
--- a/seatunnel-connectors-v2/connector-http/pom.xml
+++ b/seatunnel-connectors-v2/connector-http/pom.xml
@@ -53,6 +53,7 @@
         <module>connector-http-asana</module>
         <module>connector-http-intercom</module>
         <module>connector-http-typeform</module>
+        <module>connector-http-mailchimp</module>
         <module>connector-mem0</module>
     </modules>
 
diff --git a/seatunnel-dist/pom.xml b/seatunnel-dist/pom.xml
index f93cd8346e..9697e98641 100644
--- a/seatunnel-dist/pom.xml
+++ b/seatunnel-dist/pom.xml
@@ -275,6 +275,12 @@
                     <version>${project.version}</version>
                     <scope>provided</scope>
                 </dependency>
+                <dependency>
+                    <groupId>org.apache.seatunnel</groupId>
+                    <artifactId>connector-http-mailchimp</artifactId>
+                    <version>${project.version}</version>
+                    <scope>provided</scope>
+                </dependency>
                 <dependency>
                     <groupId>org.apache.seatunnel</groupId>
                     <artifactId>connector-kafka</artifactId>
diff --git 
a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-mailchimp-e2e/pom.xml 
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-mailchimp-e2e/pom.xml
new file mode 100644
index 0000000000..e461dd8d69
--- /dev/null
+++ b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-mailchimp-e2e/pom.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+    Licensed to the Apache Software Foundation (ASF) under one or more
+    contributor license agreements.  See the NOTICE file distributed with
+    this work for additional information regarding copyright ownership.
+    The ASF licenses this file to You under the Apache License, Version 2.0
+    (the "License"); you may not use this file except in compliance with
+    the License.  You may obtain a copy of the License at
+       http://www.apache.org/licenses/LICENSE-2.0
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"; 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
http://maven.apache.org/xsd/maven-4.0.0.xsd";>
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.apache.seatunnel</groupId>
+        <artifactId>seatunnel-connector-v2-e2e</artifactId>
+        <version>${revision}</version>
+    </parent>
+    <artifactId>connector-mailchimp-e2e</artifactId>
+    <name>SeaTunnel : E2E : Connector V2 : Mailchimp</name>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.seatunnel</groupId>
+            <artifactId>connector-http-mailchimp</artifactId>
+            <version>${project.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.seatunnel</groupId>
+            <artifactId>connector-http-base</artifactId>
+            <version>${project.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.seatunnel</groupId>
+            <artifactId>connector-assert</artifactId>
+            <version>${project.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.seatunnel</groupId>
+            <artifactId>connector-common</artifactId>
+            <version>${project.version}</version>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+</project>
diff --git 
a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-mailchimp-e2e/src/test/java/org/apache/seatunnel/e2e/connector/mailchimp/MailchimpIT.java
 
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-mailchimp-e2e/src/test/java/org/apache/seatunnel/e2e/connector/mailchimp/MailchimpIT.java
new file mode 100644
index 0000000000..2bdd79103e
--- /dev/null
+++ 
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-mailchimp-e2e/src/test/java/org/apache/seatunnel/e2e/connector/mailchimp/MailchimpIT.java
@@ -0,0 +1,148 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.seatunnel.e2e.connector.mailchimp;
+
+import org.apache.seatunnel.e2e.common.TestResource;
+import org.apache.seatunnel.e2e.common.TestSuiteBase;
+import org.apache.seatunnel.e2e.common.container.EngineType;
+import org.apache.seatunnel.e2e.common.container.TestContainer;
+
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.TestTemplate;
+import org.testcontainers.Testcontainers;
+import org.testcontainers.containers.Container;
+
+import com.sun.net.httpserver.HttpServer;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.InetSocketAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+import java.util.Collections;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.regex.Pattern;
+
+class MailchimpIT extends TestSuiteBase implements TestResource {
+    private HttpServer server;
+    private final AtomicInteger requests = new AtomicInteger();
+    private final AtomicReference<String> invalidRequest = new 
AtomicReference<>();
+
+    @BeforeAll
+    @Override
+    public void startUp() throws IOException {
+        server = HttpServer.create(new InetSocketAddress(0), 0);
+        Testcontainers.exposeHostPorts(server.getAddress().getPort());
+        server.createContext(
+                "/3.0/lists/audience1/members",
+                exchange -> {
+                    int request = requests.incrementAndGet();
+                    String auth =
+                            "Basic "
+                                    + Base64.getEncoder()
+                                            .encodeToString(
+                                                    "seatunnel:fixturekey-us1"
+                                                            
.getBytes(StandardCharsets.UTF_8));
+                    String expected = request <= 3 ? "count=2&offset=0" : 
"count=1&offset=2";
+                    int code = 200;
+                    String body;
+                    if (!"GET".equals(exchange.getRequestMethod())
+                            || 
!auth.equals(exchange.getRequestHeaders().getFirst("Authorization"))
+                            || 
!expected.equals(exchange.getRequestURI().getRawQuery())
+                            || !"/3.0/lists/audience1/members"
+                                    
.equals(exchange.getRequestURI().getPath())) {
+                        invalidRequest.set(
+                                "Unexpected request method, authentication or 
pagination");
+                        code = 400;
+                        body = "{}";
+                    } else if (request == 1 || request == 2) {
+                        code = request == 1 ? 429 : 503;
+                        exchange.getResponseHeaders().set("Retry-After", "0");
+                        body = "{}";
+                    } else {
+                        String resource =
+                                request == 3
+                                        ? "/mailchimp-members-page-1.json"
+                                        : "/mailchimp-members-page-2.json";
+                        try (InputStream input = 
getClass().getResourceAsStream(resource)) {
+                            if (input == null) {
+                                throw new IOException("Missing Mailchimp 
fixture");
+                            }
+                            body = new String(input.readAllBytes(), 
StandardCharsets.UTF_8);
+                        }
+                    }
+                    byte[] bytes = body.getBytes(StandardCharsets.UTF_8);
+                    exchange.getResponseHeaders().set("Content-Type", 
"application/json");
+                    if (code == 429 || code == 503) {
+                        exchange.sendResponseHeaders(code, -1);
+                        exchange.close();
+                        return;
+                    }
+                    exchange.sendResponseHeaders(code, bytes.length);
+                    try (OutputStream out = exchange.getResponseBody()) {
+                        out.write(bytes);
+                    }
+                });
+        server.start();
+    }
+
+    @BeforeEach
+    void reset() {
+        requests.set(0);
+        invalidRequest.set(null);
+    }
+
+    @AfterAll
+    @Override
+    public void tearDown() {
+        if (server != null) {
+            server.stop(0);
+        }
+    }
+
+    @TestTemplate
+    void listMembersToAssert(TestContainer container) throws Exception {
+        Container.ExecResult result =
+                container.executeJob(
+                        "/mailchimp_source_to_assert.conf",
+                        Collections.singletonList(
+                                "mailchimp.test.port=" + 
server.getAddress().getPort()));
+        Assertions.assertEquals(0, result.getExitCode(), result.getStderr());
+        if (container.identifier().getEngineType() == EngineType.SEATUNNEL) {
+            String clientLogs = result.getStdout() + "\n" + result.getStderr();
+            Assertions.assertTrue(
+                    Pattern.compile("Job \\([0-9]+\\) end with state FINISHED")
+                            .matcher(clientLogs)
+                            .find(),
+                    clientLogs);
+            Assertions.assertFalse(
+                    Pattern.compile("Job \\([0-9]+\\) end with state 
FAILED|jobStatus=FAILED")
+                            .matcher(clientLogs)
+                            .find(),
+                    clientLogs);
+        }
+        Assertions.assertNull(invalidRequest.get());
+        Assertions.assertEquals(
+                4, requests.get(), "Retries stay at offset zero, followed by 
the final page");
+    }
+}
diff --git 
a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-mailchimp-e2e/src/test/resources/mailchimp-members-page-1.json
 
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-mailchimp-e2e/src/test/resources/mailchimp-members-page-1.json
new file mode 100644
index 0000000000..20287920ea
--- /dev/null
+++ 
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-mailchimp-e2e/src/test/resources/mailchimp-members-page-1.json
@@ -0,0 +1,23 @@
+{
+  "members": [
+    {
+      "id": "member1",
+      "email_address": "[email protected]",
+      "status": "subscribed",
+      "last_changed": "2026-01-01T00:00:00Z",
+      "merge_fields": {
+        "FNAME": "Example"
+      }
+    },
+    {
+      "id": "member2",
+      "email_address": "[email protected]",
+      "status": "subscribed",
+      "last_changed": "2026-01-01T00:00:00Z",
+      "merge_fields": {
+        "FNAME": "Example"
+      }
+    }
+  ],
+  "total_items": 3
+}
diff --git 
a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-mailchimp-e2e/src/test/resources/mailchimp-members-page-2.json
 
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-mailchimp-e2e/src/test/resources/mailchimp-members-page-2.json
new file mode 100644
index 0000000000..536999c960
--- /dev/null
+++ 
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-mailchimp-e2e/src/test/resources/mailchimp-members-page-2.json
@@ -0,0 +1,14 @@
+{
+  "members": [
+    {
+      "id": "member3",
+      "email_address": "[email protected]",
+      "status": "subscribed",
+      "last_changed": "2026-01-01T00:00:00Z",
+      "merge_fields": {
+        "FNAME": "Example"
+      }
+    }
+  ],
+  "total_items": 3
+}
diff --git 
a/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-mailchimp-e2e/src/test/resources/mailchimp_source_to_assert.conf
 
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-mailchimp-e2e/src/test/resources/mailchimp_source_to_assert.conf
new file mode 100644
index 0000000000..f448360eb1
--- /dev/null
+++ 
b/seatunnel-e2e/seatunnel-connector-v2-e2e/connector-mailchimp-e2e/src/test/resources/mailchimp_source_to_assert.conf
@@ -0,0 +1,59 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+env {
+  parallelism = 1
+  job.mode = "BATCH"
+  job.retry.times = 0
+}
+
+source {
+  Mailchimp {
+    base_url = "http://host.testcontainers.internal:${mailchimp.test.port}/3.0";
+    allow_insecure_http = true
+    token = "fixturekey-us1"
+    list_id = "audience1"
+    page_size = 2
+    retry_backoff_ms = 10
+    schema {
+      fields {
+        id = string
+        email_address = string
+        status = string
+        last_changed = string
+        merge_fields = "map<string,string>"
+      }
+    }
+  }
+}
+
+sink {
+  Assert {
+    rules {
+      row_rules = [
+        { rule_type = "MAX_ROW", rule_value = 3 },
+        { rule_type = "MIN_ROW", rule_value = 3 }
+      ]
+      field_rules = [
+        { field_name = "id", field_type = "string", field_value = [{ rule_type 
= "NOT_NULL" }] },
+        { field_name = "email_address", field_type = "string", field_value = 
[{ rule_type = "NOT_NULL" }] },
+        { field_name = "status", field_type = "string", field_value = [{ 
rule_type = "NOT_NULL" }] },
+        { field_name = "last_changed", field_type = "string", field_value = [{ 
rule_type = "NOT_NULL" }] }
+      ]
+    }
+  }
+}
diff --git a/seatunnel-e2e/seatunnel-connector-v2-e2e/pom.xml 
b/seatunnel-e2e/seatunnel-connector-v2-e2e/pom.xml
index 7368584279..ec1e0adba3 100644
--- a/seatunnel-e2e/seatunnel-connector-v2-e2e/pom.xml
+++ b/seatunnel-e2e/seatunnel-connector-v2-e2e/pom.xml
@@ -54,6 +54,7 @@
         <module>connector-asana-e2e</module>
         <module>connector-intercom-e2e</module>
         <module>connector-typeform-e2e</module>
+        <module>connector-mailchimp-e2e</module>
         <module>connector-http-e2e</module>
         <module>connector-paypal-e2e</module>
         <module>connector-tiktok-ads-e2e</module>

Reply via email to