Repository: amp-labs/connectors
Pull request #1505 (2025-03-20 13:14:40)

[CON-1508] feat: Add Hunter Metadata Connector
Tests:
<img width="1220" alt="Screenshot 2025-03-20 at 16 11 38" src="https://github.com/user-attachments/assets/8176d2b4-a9aa-40c7-8eba-c30a1250bc91" />



Patch:
diff --git a/go.mod b/go.mod
index f99dc6c5..303f6c80 100644
--- a/go.mod
+++ b/go.mod
@@ -6,10 +6,10 @@ require (
 	github.com/PuerkitoBio/goquery v1.10.2
 	github.com/antchfx/xmlquery v1.4.3
 	github.com/brianvoe/gofakeit/v6 v6.28.0
-	github.com/chromedp/chromedp v0.13.1
+	github.com/chromedp/chromedp v0.13.3
 	github.com/deiu/linkparser v0.0.0-20170608193052-9b6849e15168
 	github.com/gertd/go-pluralize v0.2.1
-	github.com/getkin/kin-openapi v0.129.0
+	github.com/getkin/kin-openapi v0.131.0
 	github.com/go-playground/validator v9.31.0+incompatible
 	github.com/go-test/deep v1.1.1
 	github.com/gobwas/glob v0.2.3
@@ -17,7 +17,7 @@ require (
 	github.com/iancoleman/strcase v0.3.0
 	github.com/invopop/yaml v0.3.1
 	github.com/spf13/cobra v1.9.1
-	github.com/spf13/viper v1.19.0
+	github.com/spf13/viper v1.20.0
 	github.com/spyzhov/ajson v0.9.6
 	github.com/stretchr/testify v1.10.0
 	gitlab.com/c0b/go-ordered-json v0.0.0-20201030195603-febf46534d5a
@@ -29,36 +29,33 @@ require (
 require (
 	github.com/andybalholm/cascadia v1.3.3 // indirect
 	github.com/antchfx/xpath v1.3.3 // indirect
-	github.com/chromedp/cdproto v0.0.0-20250222051814-50c6cb17f10a // indirect
+	github.com/chromedp/cdproto v0.0.0-20250319231242-a755498943c8 // indirect
 	github.com/chromedp/sysutil v1.1.0 // indirect
-	github.com/fsnotify/fsnotify v1.7.0 // indirect
+	github.com/fsnotify/fsnotify v1.8.0 // indirect
 	github.com/go-json-experiment/json v0.0.0-20250211171154-1ae217ad3535 // indirect
 	github.com/go-openapi/jsonpointer v0.21.0 // indirect
 	github.com/go-openapi/swag v0.23.0 // indirect
 	github.com/go-playground/locales v0.14.1 // indirect
 	github.com/go-playground/universal-translator v0.18.1 // indirect
+	github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
 	github.com/gobwas/httphead v0.1.0 // indirect
 	github.com/gobwas/pool v0.2.1 // indirect
 	github.com/gobwas/ws v1.4.0 // indirect
 	github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
 	github.com/google/go-cmp v0.6.0 // indirect
-	github.com/hashicorp/hcl v1.0.0 // indirect
 	github.com/inconshreveable/mousetrap v1.1.0 // indirect
 	github.com/josharian/intern v1.0.0 // indirect
 	github.com/leodido/go-urn v1.4.0 // indirect
-	github.com/magiconair/properties v1.8.7 // indirect
 	github.com/mailru/easyjson v0.7.7 // indirect
-	github.com/mitchellh/mapstructure v1.5.0 // indirect
 	github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
-	github.com/oasdiff/yaml v0.0.0-20241210131133-6b86fb107d80 // indirect
-	github.com/oasdiff/yaml3 v0.0.0-20241210130736-a94c01f36349 // indirect
-	github.com/pelletier/go-toml/v2 v2.2.2 // indirect
+	github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect
+	github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect
+	github.com/pelletier/go-toml/v2 v2.2.3 // indirect
 	github.com/perimeterx/marshmallow v1.1.5 // indirect
-	github.com/sagikazarmark/locafero v0.4.0 // indirect
-	github.com/sagikazarmark/slog-shim v0.1.0 // indirect
+	github.com/sagikazarmark/locafero v0.7.0 // indirect
 	github.com/sourcegraph/conc v0.3.0 // indirect
-	github.com/spf13/afero v1.11.0 // indirect
-	github.com/spf13/cast v1.6.0 // indirect
+	github.com/spf13/afero v1.12.0 // indirect
+	github.com/spf13/cast v1.7.1 // indirect
 	github.com/spf13/pflag v1.0.6 // indirect
 	github.com/subosito/gotenv v1.6.0 // indirect
 	go.uber.org/atomic v1.9.0 // indirect
@@ -66,7 +63,6 @@ require (
 	golang.org/x/net v0.35.0 // indirect
 	golang.org/x/sys v0.30.0 // indirect
 	gopkg.in/go-playground/assert.v1 v1.2.1 // indirect
-	gopkg.in/ini.v1 v1.67.0 // indirect
 )
 
 require (
diff --git a/go.sum b/go.sum
index bba3c264..b8432af2 100644
--- a/go.sum
+++ b/go.sum
@@ -8,10 +8,10 @@ github.com/antchfx/xpath v1.3.3 h1:tmuPQa1Uye0Ym1Zn65vxPgfltWb/Lxu2jeqIGteJSRs=
 github.com/antchfx/xpath v1.3.3/go.mod h1:i54GszH55fYfBmoZXapTHN8T8tkcHfRgLyVwwqzXNcs=
 github.com/brianvoe/gofakeit/v6 v6.28.0 h1:Xib46XXuQfmlLS2EXRuJpqcw8St6qSZz75OUo0tgAW4=
 github.com/brianvoe/gofakeit/v6 v6.28.0/go.mod h1:Xj58BMSnFqcn/fAQeSK+/PLtC5kSb7FJIq4JyGa8vEs=
-github.com/chromedp/cdproto v0.0.0-20250222051814-50c6cb17f10a h1:EnkQjhmp/MxhDB4KOTssv6xC20aQ9rhFRCfGHTsTqmE=
-github.com/chromedp/cdproto v0.0.0-20250222051814-50c6cb17f10a/go.mod h1:NItd7aLkcfOA/dcMXvl8p1u+lQqioRMq/SqDp71Pb/k=
-github.com/chromedp/chromedp v0.13.1 h1:FDh9CfaAt0w70gl69Hb69M/xgZrWuppH9AW22aGa+iU=
-github.com/chromedp/chromedp v0.13.1/go.mod h1:O3nO4Lno7iLoVX+7GdqQkehhKG7DtLf/zFRyJo0AhXY=
+github.com/chromedp/cdproto v0.0.0-20250319231242-a755498943c8 h1:AqW2bDQf67Zbq6Tpop/+yJSIknxhiQecO2B8jNYTAPs=
+github.com/chromedp/cdproto v0.0.0-20250319231242-a755498943c8/go.mod h1:NItd7aLkcfOA/dcMXvl8p1u+lQqioRMq/SqDp71Pb/k=
+github.com/chromedp/chromedp v0.13.3 h1:c6nTn97XQBykzcXiGYL5LLebw3h3CEyrCihm4HquYh0=
+github.com/chromedp/chromedp v0.13.3/go.mod h1:khsDP9OP20GrowpJfZ7N05iGCwcAYxk7qf9AZBzR3Qw=
 github.com/chromedp/sysutil v1.1.0 h1:PUFNv5EcprjqXZD9nJb9b/c9ibAbxiYo4exNWZyipwM=
 github.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8=
 github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
@@ -23,12 +23,12 @@ github.com/deiu/linkparser v0.0.0-20170608193052-9b6849e15168 h1:faQ0lJ7RbfOyHSV
 github.com/deiu/linkparser v0.0.0-20170608193052-9b6849e15168/go.mod h1:EPdXetNGTVpWsQ9wn8LQzqNByQOjMeFhYEvNOZNGtwg=
 github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
 github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
-github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
-github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
+github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
+github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
 github.com/gertd/go-pluralize v0.2.1 h1:M3uASbVjMnTsPb0PNqg+E/24Vwigyo/tvyMTtAlLgiA=
 github.com/gertd/go-pluralize v0.2.1/go.mod h1:rbYaKDbsXxmRfr8uygAEKhOWsjyrrqrkHVpZvoOp8zk=
-github.com/getkin/kin-openapi v0.129.0 h1:QGYTNcmyP5X0AtFQ2Dkou9DGBJsUETeLH9rFrJXZh30=
-github.com/getkin/kin-openapi v0.129.0/go.mod h1:gmWI+b/J45xqpyK5wJmRRZse5wefA5H0RDMK46kLUtI=
+github.com/getkin/kin-openapi v0.131.0 h1:NO2UeHnFKRYhZ8wg6Nyh5Cq7dHk4suQQr72a4pMrDxE=
+github.com/getkin/kin-openapi v0.131.0/go.mod h1:3OlG51PCYNsPByuiMB0t4fjnNlIDnaEDsjiKUV8nL58=
 github.com/go-json-experiment/json v0.0.0-20250211171154-1ae217ad3535 h1:yE7argOs92u+sSCRgqqe6eF+cDaVhSPlioy1UkA0p/w=
 github.com/go-json-experiment/json v0.0.0-20250211171154-1ae217ad3535/go.mod h1:BWmvoE1Xia34f3l/ibJweyhrT+aROb/FQ6d+37F0e2s=
 github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
@@ -43,6 +43,8 @@ github.com/go-playground/validator v9.31.0+incompatible h1:UA72EPEogEnq76ehGdEDp
 github.com/go-playground/validator v9.31.0+incompatible/go.mod h1:yrEkQXlcI+PugkyDjY2bRrL/UBU4f3rvrgkN3V8JEig=
 github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
 github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
+github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
+github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
 github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
 github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
 github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
@@ -57,8 +59,6 @@ github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l
 github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
 github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
 github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
-github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
-github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
 github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI=
 github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
 github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
@@ -75,22 +75,18 @@ github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kUL
 github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs=
 github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
 github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
-github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
-github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
 github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
 github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
-github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
-github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
 github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9M+97sNutRR1RKhG96O6jWumTTnw=
 github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
-github.com/oasdiff/yaml v0.0.0-20241210131133-6b86fb107d80 h1:nZspmSkneBbtxU9TopEAE0CY+SBJLxO8LPUlw2vG4pU=
-github.com/oasdiff/yaml v0.0.0-20241210131133-6b86fb107d80/go.mod h1:7tFDb+Y51LcDpn26GccuUgQXUk6t0CXZsivKjyimYX8=
-github.com/oasdiff/yaml3 v0.0.0-20241210130736-a94c01f36349 h1:t05Ww3DxZutOqbMN+7OIuqDwXbhl32HiZGpLy26BAPc=
-github.com/oasdiff/yaml3 v0.0.0-20241210130736-a94c01f36349/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o=
+github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY=
+github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw=
+github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c=
+github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o=
 github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw=
 github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
-github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
-github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
+github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
+github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
 github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s=
 github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@@ -99,33 +95,24 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH
 github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
 github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
-github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
-github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
-github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
-github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
+github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo=
+github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k=
 github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
 github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
-github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
-github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
-github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
-github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
+github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs=
+github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4=
+github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
+github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
 github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
 github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
 github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
 github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
-github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
-github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
+github.com/spf13/viper v1.20.0 h1:zrxIyR3RQIOsarIrgL8+sAvALXul9jeEPa06Y0Ph6vY=
+github.com/spf13/viper v1.20.0/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
 github.com/spyzhov/ajson v0.9.6 h1:iJRDaLa+GjhCDAt1yFtU/LKMtLtsNVKkxqlpvrHHlpQ=
 github.com/spyzhov/ajson v0.9.6/go.mod h1:a6oSw0MMb7Z5aD2tPoPO+jq11ETKgXUr2XktHdT8Wt8=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
-github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
-github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
-github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
-github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
-github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
 github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
 github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
@@ -218,9 +205,6 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
 gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM=
 gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE=
-gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
-gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
-gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
diff --git a/providers/hunter/connector.go b/providers/hunter/connector.go
new file mode 100644
index 00000000..487d53c0
--- /dev/null
+++ b/providers/hunter/connector.go
@@ -0,0 +1,42 @@
+package hunter
+
+import (
+	"github.com/amp-labs/connectors/common"
+	"github.com/amp-labs/connectors/internal/components"
+	"github.com/amp-labs/connectors/internal/components/operations"
+	"github.com/amp-labs/connectors/internal/components/schema"
+	"github.com/amp-labs/connectors/providers"
+)
+
+const restAPIVersion = "v2"
+
+type Connector struct {
+	// Basic connector
+	*components.Connector
+
+	// Require authenticated client
+	common.RequireAuthenticatedClient
+
+	// Supported operations
+	components.SchemaProvider
+}
+
+func NewConnector(params common.Parameters) (*Connector, error) {
+	return components.Initialize(providers.Hunter, params, constructor)
+}
+
+func constructor(base *components.Connector) (*Connector, error) {
+	connector := &Connector{Connector: base}
+
+	// Set the metadata provider for the connector
+	connector.SchemaProvider = schema.NewObjectSchemaProvider(
+		connector.HTTPClient().Client,
+		schema.FetchModeParallel,
+		operations.SingleObjectMetadataHandlers{
+			BuildRequest:  connector.buildSingleObjectMetadataRequest,
+			ParseResponse: connector.parseSingleObjectMetadataResponse,
+		},
+	)
+
+	return connector, nil
+}
diff --git a/providers/hunter/handlers.go b/providers/hunter/handlers.go
new file mode 100644
index 00000000..6cc44af2
--- /dev/null
+++ b/providers/hunter/handlers.go
@@ -0,0 +1,70 @@
+package hunter
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+
+	"github.com/amp-labs/connectors/common"
+	"github.com/amp-labs/connectors/common/naming"
+	"github.com/amp-labs/connectors/common/urlbuilder"
+)
+
+const (
+	limitQuery       = "limit"
+	metadataPageSize = "1"
+)
+
+type readResponse struct {
+	Data map[string]any `json:"data"`
+	Meta map[string]any `json:"meta"`
+}
+
+func (c *Connector) buildSingleObjectMetadataRequest(ctx context.Context, objectName string) (*http.Request, error) {
+	url, err := urlbuilder.New(c.ProviderInfo().BaseURL, restAPIVersion, objectName)
+	if err != nil {
+		return nil, err
+	}
+
+	// Limit response to 1 record data.
+	url.WithQueryParam(limitQuery, metadataPageSize)
+
+	return http.NewRequestWithContext(ctx, http.MethodGet, url.String(), nil)
+}
+
+func (c *Connector) parseSingleObjectMetadataResponse(
+	ctx context.Context,
+	objectName string,
+	request *http.Request,
+	response *common.JSONHTTPResponse,
+) (*common.ObjectMetadata, error) {
+	objectMetadata := common.ObjectMetadata{
+		FieldsMap:   make(map[string]string),
+		DisplayName: naming.CapitalizeFirstLetterEveryWord(objectName),
+	}
+
+	resp, err := common.UnmarshalJSON[readResponse](response)
+	if err != nil {
+		return nil, common.ErrFailedToUnmarshalBody
+	}
+
+	records, okay := resp.Data[objectName].([]any)
+	if !okay {
+		return nil, fmt.Errorf("couldn't convert the data response field data to an array: %w", common.ErrMissingExpectedValues) // nolint:lll
+	}
+
+	if len(records) == 0 {
+		return nil, fmt.Errorf("%w: could not find a record to sample fields from", common.ErrMissingExpectedValues)
+	}
+
+	firstRecord, okay := records[0].(map[string]any)
+	if !okay {
+		return nil, fmt.Errorf("couldn't convert the data response field data to a map: %w", common.ErrMissingExpectedValues) // nolint:lll
+	}
+
+	for fld := range firstRecord {
+		objectMetadata.FieldsMap[fld] = fld
+	}
+
+	return &objectMetadata, nil
+}
diff --git a/providers/lemlist/connector.go b/providers/lemlist/connector.go
new file mode 100644
index 00000000..3728340f
--- /dev/null
+++ b/providers/lemlist/connector.go
@@ -0,0 +1,80 @@
+package lemlist
+
+import (
+	"github.com/amp-labs/connectors/common"
+	"github.com/amp-labs/connectors/internal/components"
+	"github.com/amp-labs/connectors/internal/components/operations"
+	"github.com/amp-labs/connectors/internal/components/reader"
+	"github.com/amp-labs/connectors/internal/components/schema"
+	"github.com/amp-labs/connectors/internal/components/writer"
+	"github.com/amp-labs/connectors/internal/staticschema"
+	"github.com/amp-labs/connectors/providers"
+)
+
+const (
+	restAPIPrefix = "api"
+	object        = "object"
+	list          = "list"
+)
+
+type Connector struct {
+	// Basic connector
+	*components.Connector
+
+	// Require authenticated client
+	common.RequireAuthenticatedClient
+
+	// Supported operations
+	components.SchemaProvider
+	components.Reader
+	components.Writer
+}
+
+func NewConnector(params common.Parameters) (*Connector, error) {
+	return components.Initialize(providers.Lemlist, params, constructor)
+}
+
+func constructor(base *components.Connector) (*Connector, error) {
+	connector := &Connector{Connector: base}
+
+	// Set the metadata provider for the connector
+	connector.SchemaProvider = schema.NewObjectSchemaProvider(
+		connector.HTTPClient().Client,
+		schema.FetchModeParallel,
+		operations.SingleObjectMetadataHandlers{
+			BuildRequest:  connector.buildSingleObjectMetadataRequest,
+			ParseResponse: connector.parseSingleObjectMetadataResponse,
+		},
+	)
+
+	registry, err := components.NewEndpointRegistry(supportedOperations())
+	if err != nil {
+		return nil, err
+	}
+
+	// Set the read provider for the connector
+	connector.Reader = reader.NewHTTPReader(
+		connector.HTTPClient().Client,
+		registry,
+		staticschema.RootModuleID,
+		operations.ReadHandlers{
+			BuildRequest:  connector.buildReadRequest,
+			ParseResponse: connector.parseReadResponse,
+			ErrorHandler:  common.InterpretError,
+		},
+	)
+
+	// Set the write provider for the connector
+	connector.Writer = writer.NewHTTPWriter(
+		connector.HTTPClient().Client,
+		registry,
+		staticschema.RootModuleID,
+		operations.WriteHandlers{
+			BuildRequest:  connector.buildWriteRequest,
+			ParseResponse: connector.parseWriteResponse,
+			ErrorHandler:  common.InterpretError,
+		},
+	)
+
+	return connector, nil
+}
diff --git a/providers/lemlist/handlers.go b/providers/lemlist/handlers.go
new file mode 100644
index 00000000..b8086527
--- /dev/null
+++ b/providers/lemlist/handlers.go
@@ -0,0 +1,202 @@
+package lemlist
+
+import (
+	"bytes"
+	"context"
+	"encoding/json"
+	"fmt"
+	"net/http"
+
+	"github.com/amp-labs/connectors/common"
+	"github.com/amp-labs/connectors/common/naming"
+	"github.com/amp-labs/connectors/common/urlbuilder"
+	"github.com/amp-labs/connectors/internal/jsonquery"
+)
+
+func (c *Connector) buildSingleObjectMetadataRequest(ctx context.Context, objectName string) (*http.Request, error) {
+	url, err := urlbuilder.New(c.ProviderInfo().BaseURL, restAPIPrefix, objectName)
+	if err != nil {
+		return nil, err
+	}
+
+	url.WithQueryParam("version", "v2")
+
+	return http.NewRequestWithContext(ctx, http.MethodGet, url.String(), nil)
+}
+
+func (c *Connector) parseSingleObjectMetadataResponse(
+	ctx context.Context,
+	objectName string,
+	request *http.Request,
+	response *common.JSONHTTPResponse,
+) (*common.ObjectMetadata, error) {
+	var (
+		firstRecord map[string]any
+		err         error
+	)
+
+	objectMetadata := common.ObjectMetadata{
+		FieldsMap:   make(map[string]string),
+		DisplayName: naming.CapitalizeFirstLetterEveryWord(objectName),
+	}
+
+	schema, fld := responseSchema(objectName)
+
+	switch schema {
+	case object:
+		firstRecord, err = parseObject(response, fld)
+		if err != nil {
+			return nil, err
+		}
+
+	case list:
+		firstRecord, err = parseList(response)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	for fld := range firstRecord {
+		objectMetadata.FieldsMap[fld] = fld
+	}
+
+	return &objectMetadata, nil
+}
+
+func parseObject(response *common.JSONHTTPResponse, fld string) (map[string]any, error) {
+	// We're unmarshaling the data to map[string]any,
+	// all supported objects returns this data type.
+	data, err := common.UnmarshalJSON[map[string]any](response)
+	if err != nil {
+		return nil, common.ErrFailedToUnmarshalBody
+	}
+
+	if len(*data) == 0 {
+		return nil, common.ErrMissingExpectedValues
+	}
+
+	firstRecord := *data
+
+	if fld != "" {
+		// If this is the case, we're expecting the data in a certain field
+		// in this current map.
+		records, okay := (*data)[fld].([]any)
+		if !okay {
+			return nil, fmt.Errorf("couldn't convert the data response field data to an array: %w", common.ErrMissingExpectedValues) // nolint:lll
+		}
+
+		if len(records) == 0 {
+			return nil, fmt.Errorf("%w: could not find a record to sample fields from", common.ErrMissingExpectedValues)
+		}
+
+		// Iterate over the first record.
+		firstRecord, okay = records[0].(map[string]any)
+		if !okay {
+			return nil, fmt.Errorf("couldn't convert the first record data to a map: %w", common.ErrMissingExpectedValues)
+		}
+	}
+
+	return firstRecord, nil
+}
+
+func parseList(response *common.JSONHTTPResponse) (map[string]any, error) {
+	// We're unmarshaling the data to []map[string]any,
+	// all supported objects returns this data type.
+	data, err := common.UnmarshalJSON[[]map[string]any](response)
+	if err != nil {
+		return nil, common.ErrFailedToUnmarshalBody
+	}
+
+	if len(*data) == 0 {
+		return nil, common.ErrMissingExpectedValues
+	}
+
+	return (*data)[0], nil
+}
+
+func (c *Connector) buildReadRequest(ctx context.Context, params common.ReadParams) (*http.Request, error) {
+	var (
+		url *urlbuilder.URL
+		err error
+	)
+
+	url, err = urlbuilder.New(c.ProviderInfo().BaseURL, restAPIPrefix, params.ObjectName)
+	if err != nil {
+		return nil, err
+	}
+
+	url.WithQueryParam("version", "v2")
+
+	if params.NextPage != "" {
+		url.WithQueryParam("page", params.NextPage.String())
+	}
+
+	return http.NewRequestWithContext(ctx, http.MethodGet, url.String(), nil)
+}
+
+func (c *Connector) parseReadResponse(
+	ctx context.Context,
+	params common.ReadParams,
+	request *http.Request,
+	response *common.JSONHTTPResponse,
+) (*common.ReadResult, error) {
+	return common.ParseResult(
+		response,
+		records(params.ObjectName),
+		nextRecordsURL(params.ObjectName),
+		common.GetMarshaledData,
+		params.Fields,
+	)
+}
+
+func (c *Connector) buildWriteRequest(ctx context.Context, params common.WriteParams) (*http.Request, error) {
+	method := http.MethodPost
+
+	url, err := urlbuilder.New(c.ProviderInfo().BaseURL, restAPIPrefix, params.ObjectName)
+	if err != nil {
+		return nil, err
+	}
+
+	if params.RecordId != "" {
+		url.AddPath(params.RecordId)
+
+		method = http.MethodPatch
+	}
+
+	jsonData, err := json.Marshal(params.RecordData)
+	if err != nil {
+		return nil, err
+	}
+
+	return http.NewRequestWithContext(ctx, method, url.String(), bytes.NewReader(jsonData))
+}
+
+func (c *Connector) parseWriteResponse(
+	ctx context.Context,
+	params common.WriteParams,
+	request *http.Request,
+	response *common.JSONHTTPResponse,
+) (*common.WriteResult, error) {
+	body, ok := response.Body()
+	if !ok {
+		return &common.WriteResult{
+			Success: true,
+		}, nil
+	}
+
+	data, err := jsonquery.Convertor.ObjectToMap(body)
+	if err != nil {
+		return nil, err
+	}
+
+	recordID, okay := data["_id"].(string)
+	if !okay {
+		recordID = ""
+	}
+
+	return &common.WriteResult{
+		Success:  true,
+		RecordId: recordID,
+		Data:     data,
+	}, nil
+}
diff --git a/providers/lemlist/parse.go b/providers/lemlist/parse.go
new file mode 100644
index 00000000..aea592e3
--- /dev/null
+++ b/providers/lemlist/parse.go
@@ -0,0 +1,71 @@
+package lemlist
+
+import (
+	"strconv"
+
+	"github.com/amp-labs/connectors/common"
+	"github.com/amp-labs/connectors/internal/jsonquery"
+	"github.com/spyzhov/ajson"
+)
+
+func records(objectName string) common.RecordsFunc {
+	return func(node *ajson.Node) ([]map[string]any, error) {
+		schema, fld := responseSchema(objectName)
+
+		switch schema {
+		case object:
+			if fld != "" {
+				rcds, err := jsonquery.New(node).ArrayOptional(fld)
+				if err != nil {
+					return nil, err
+				}
+
+				return jsonquery.Convertor.ArrayToMap(rcds)
+			}
+
+			record, err := jsonquery.Convertor.ObjectToMap(node)
+
+			return []map[string]any{record}, err
+
+		default:
+			return common.GetRecordsUnderJSONPath(fld)(node)
+		}
+	}
+}
+
+func nextRecordsURL(objectName string) common.NextPageFunc {
+	return func(node *ajson.Node) (string, error) {
+		schema, _ := responseSchema(objectName)
+		switch schema {
+		case object:
+			paginationQuery := jsonquery.New(node, "pagination")
+
+			currentPage, err := paginationQuery.IntegerOptional("currentPage")
+			if err != nil {
+				return "", err
+			}
+
+			totalPage, err := paginationQuery.IntegerOptional("totalPage")
+			if err != nil {
+				return "", err
+			}
+
+			nextPage, err := paginationQuery.IntegerOptional("nextPage")
+			if err != nil {
+				return "", err
+			}
+
+			if nextPage == nil || currentPage == nil || totalPage == nil {
+				return "", nil
+			}
+
+			if *currentPage < *totalPage {
+				return strconv.Itoa(int(*nextPage)), nil
+			}
+		default:
+			return "", nil
+		}
+
+		return "", nil
+	}
+}
diff --git a/providers/lemlist/supports.go b/providers/lemlist/supports.go
new file mode 100644
index 00000000..a60e66b0
--- /dev/null
+++ b/providers/lemlist/supports.go
@@ -0,0 +1,56 @@
+package lemlist
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/amp-labs/connectors/internal/components"
+	"github.com/amp-labs/connectors/internal/staticschema"
+)
+
+func responseSchema(objectName string) (string, string) {
+	// team --> an object
+	// api/team/senders --> an array of objects
+	// api/team/credits --> object
+	// api/campaigns --> object campaigns array
+	// api/activities --> an array of objects
+	// api/unsubscirbes --> an array of objects
+	// api/hooks --> an array of objects
+	// api/database/filters -->array of objects
+	// api/schema/people --> an object
+	// api/schema/companies --> an object
+	switch objectName {
+	case "campaigns", "schedules":
+		return object, objectName
+	case "team/senders", "activities", "unsubscirbes", "hooks", "database/filters":
+		return list, ""
+	default:
+		return object, ""
+	}
+}
+
+func supportedOperations() components.EndpointRegistryInput {
+	readSupport := []string{
+		"team", "team/senders", "team/credits", "campaigns",
+		"activities", "unsubscribes", "hooks", "database/filters",
+		"schema/people", "schema/companies", "schedules",
+	}
+
+	writeSupport := []string{
+		"campaigns", "schedules", "unsubscribes/*", "hooks", "database/people",
+		"database/companies", "tasks", "tasks/ignore",
+	}
+
+	return components.EndpointRegistryInput{
+		staticschema.RootModuleID: {
+			{
+				Endpoint: fmt.Sprintf("{%s}", strings.Join(readSupport, ",")),
+				Support:  components.ReadSupport,
+			},
+			{
+				Endpoint: fmt.Sprintf("{%s}", strings.Join(writeSupport, ",")),
+				Support:  components.WriteSupport,
+			},
+		},
+	}
+}
diff --git a/providers/salesforce/subscribe.go b/providers/salesforce/subscribe.go
index 2e5cb100..cf45a904 100644
--- a/providers/salesforce/subscribe.go
+++ b/providers/salesforce/subscribe.go
@@ -218,8 +218,11 @@ func (c *Connector) UpdateSubscription(
 		delete(params.SubscriptionEvents, objName)
 	}
 
+	channelMembersToKeep := make(map[common.ObjectName]*EventChannelMember)
+
 	// remove objects to exclude from delete
 	for _, objName := range objectsExcludeFromDelete {
+		channelMembersToKeep[objName] = prevState.EventChannelMembers[objName]
 		delete(prevState.EventChannelMembers, objName)
 	}
 
@@ -230,7 +233,16 @@ func (c *Connector) UpdateSubscription(
 		objectsToDelete = append(objectsToDelete, objName)
 	}
 
-	if err := c.DeleteSubscription(ctx, *previousResult); err != nil {
+	// rename the previous result to deleteParam for clarity
+	// we will use this to delete the previous subscription
+	deleteParams := *previousResult
+	deleteParams.Result = prevState
+	deleteParams.Objects = objectsToDelete
+
+	// this is the delete step, but it looks for only object that were selected to delete
+	// in objectsToDelete array, so we are still preserving some objects
+	// that needs to remain in the subscription
+	if err := c.DeleteSubscription(ctx, deleteParams); err != nil {
 		return nil, fmt.Errorf("failed to delete previous subscription: %w", err)
 	}
 
@@ -242,6 +254,8 @@ func (c *Connector) UpdateSubscription(
 
 	// for clarity, rename the state since we will return the object as the result of update
 	newState := prevState
+	// reset the ChannelMembers that was not deleted
+	newState.EventChannelMembers = channelMembersToKeep
 
 	//nolint:forcetypeassert
 	// update the previous result with the new subscription result
diff --git a/scripts/docs/salesloft/main.go b/scripts/docs/salesloft/main.go
new file mode 100644
index 00000000..466fea54
--- /dev/null
+++ b/scripts/docs/salesloft/main.go
@@ -0,0 +1,16 @@
+package main
+
+import (
+	"fmt"
+
+	"github.com/amp-labs/connectors/internal/staticschema"
+	"github.com/amp-labs/connectors/providers/salesloft/metadata"
+)
+
+// Prints supported read objects in random order formatted for https://github.com/amp-labs/docs.
+// nolint:forbidigo
+func main() {
+	for objectName, object := range metadata.Schemas.Modules[staticschema.RootModuleID].Objects {
+		fmt.Printf("- [%v](%v) (%v)\n", object.DisplayName, *object.DocsURL, objectName)
+	}
+}


Test patch:
diff --git a/test/github/read/main.go b/test/github/read/main.go
index 5b27ab87..c0a06cc6 100644
--- a/test/github/read/main.go
+++ b/test/github/read/main.go
@@ -49,5 +49,4 @@ func main() {
 	}
 
 	utils.DumpJSON(res, os.Stdout)
-
 }
diff --git a/test/hunter/connector.go b/test/hunter/connector.go
new file mode 100644
index 00000000..a0a7328c
--- /dev/null
+++ b/test/hunter/connector.go
@@ -0,0 +1,30 @@
+package hunter
+
+import (
+	"context"
+
+	"github.com/amp-labs/connectors/common"
+	"github.com/amp-labs/connectors/common/scanning/credscanning"
+	"github.com/amp-labs/connectors/providers"
+	"github.com/amp-labs/connectors/providers/hunter"
+	"github.com/amp-labs/connectors/test/utils"
+)
+
+func GetHunterConnector(ctx context.Context) *hunter.Connector {
+	filePath := credscanning.LoadPath(providers.Hunter)
+	reader := utils.MustCreateProvCredJSON(filePath, false, false)
+
+	client, err := common.NewApiKeyQueryParamAuthHTTPClient(ctx, "api_key", reader.Get(credscanning.Fields.ApiKey))
+	if err != nil {
+		utils.Fail("error creating client", "error", err)
+	}
+
+	conn, err := hunter.NewConnector(
+		common.Parameters{AuthenticatedClient: client},
+	)
+	if err != nil {
+		utils.Fail("error creating connector", "error", err)
+	}
+
+	return conn
+}
diff --git a/test/hunter/metadata/main.go b/test/hunter/metadata/main.go
new file mode 100644
index 00000000..a790853e
--- /dev/null
+++ b/test/hunter/metadata/main.go
@@ -0,0 +1,31 @@
+package main
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/amp-labs/connectors/test/hunter"
+	"github.com/amp-labs/connectors/test/utils"
+)
+
+func main() {
+	if err := run(); err != nil {
+		utils.Fail(err.Error())
+	}
+}
+
+func run() error {
+	ctx := context.Background()
+	connector := hunter.GetHunterConnector(ctx)
+
+	m, err := connector.ListObjectMetadata(ctx, []string{"leads", "leads_lists"})
+	if err != nil {
+		return err
+	}
+
+	// Print the results
+	fmt.Println("Results: ", m.Result)
+	fmt.Println("Errors: ", m.Errors)
+
+	return nil
+}
diff --git a/test/lemlist/connector.go b/test/lemlist/connector.go
new file mode 100644
index 00000000..495506cf
--- /dev/null
+++ b/test/lemlist/connector.go
@@ -0,0 +1,30 @@
+package lemlist
+
+import (
+	"context"
+
+	"github.com/amp-labs/connectors/common"
+	"github.com/amp-labs/connectors/common/scanning/credscanning"
+	"github.com/amp-labs/connectors/providers"
+	"github.com/amp-labs/connectors/providers/lemlist"
+	"github.com/amp-labs/connectors/test/utils"
+)
+
+func GetLemlistConnector(ctx context.Context) *lemlist.Connector {
+	filePath := credscanning.LoadPath(providers.Lemlist)
+	reader := utils.MustCreateProvCredJSON(filePath, false, false)
+
+	client, err := common.NewApiKeyQueryParamAuthHTTPClient(ctx, "access_token", reader.Get(credscanning.Fields.ApiKey))
+	if err != nil {
+		utils.Fail("error creating client", "error", err)
+	}
+
+	conn, err := lemlist.NewConnector(
+		common.Parameters{AuthenticatedClient: client},
+	)
+	if err != nil {
+		utils.Fail("error creating connector", "error", err)
+	}
+
+	return conn
+}
diff --git a/test/lemlist/metadata/main.go b/test/lemlist/metadata/main.go
new file mode 100644
index 00000000..eb7d66df
--- /dev/null
+++ b/test/lemlist/metadata/main.go
@@ -0,0 +1,31 @@
+package main
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/amp-labs/connectors/test/lemlist"
+	"github.com/amp-labs/connectors/test/utils"
+)
+
+func main() {
+	if err := run(); err != nil {
+		utils.Fail(err.Error())
+	}
+}
+
+func run() error {
+	ctx := context.Background()
+	connector := lemlist.GetLemlistConnector(ctx)
+
+	m, err := connector.ListObjectMetadata(ctx, []string{"campaigns", "team", "schedules", "schema/people"})
+	if err != nil {
+		return err
+	}
+
+	// Print the results
+	fmt.Println("Results: ", m.Result)
+	fmt.Println("Errors: ", m.Errors)
+
+	return nil
+}
diff --git a/test/lemlist/read/main.go b/test/lemlist/read/main.go
new file mode 100644
index 00000000..37911428
--- /dev/null
+++ b/test/lemlist/read/main.go
@@ -0,0 +1,68 @@
+package main
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"log/slog"
+	"os"
+	"os/signal"
+	"syscall"
+
+	"github.com/amp-labs/connectors"
+	"github.com/amp-labs/connectors/common"
+	ll "github.com/amp-labs/connectors/providers/lemlist"
+	"github.com/amp-labs/connectors/test/lemlist"
+	"github.com/amp-labs/connectors/test/utils"
+)
+
+func main() {
+	// Handle Ctrl-C gracefully.
+	ctx, done := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
+	defer done()
+
+	// Set up slog logging.
+	utils.SetupLogging()
+
+	conn := lemlist.GetLemlistConnector(ctx)
+
+	if err := testRead(ctx, conn, "schedules", []string{"id", "name", "start"}); err != nil {
+		slog.Error(err.Error())
+	}
+
+	if err := testRead(ctx, conn, "campaigns", []string{"id", "name"}); err != nil {
+		slog.Error(err.Error())
+	}
+
+	if err := testRead(ctx, conn, "hooks", []string{"targetUrl", "campaignId", "_id"}); err != nil {
+		slog.Error(err.Error())
+	}
+}
+
+func testRead(ctx context.Context, conn *ll.Connector, objectName string, fields []string) error {
+	params := common.ReadParams{
+		ObjectName: objectName,
+		Fields:     connectors.Fields(fields...),
+		// NextPage:   "3",
+	}
+
+	res, err := conn.Read(ctx, params)
+	if err != nil {
+		return fmt.Errorf("failed to read %s: %w", objectName, err)
+	}
+
+	jsonStr, err := json.MarshalIndent(res, "", "  ")
+	if err != nil {
+		return fmt.Errorf("error marshalling JSON: %w", err)
+	}
+
+	if _, err := os.Stdout.Write(jsonStr); err != nil {
+		return fmt.Errorf("error writing to stdout: %w", err)
+	}
+
+	if _, err := os.Stdout.WriteString("\n"); err != nil {
+		return fmt.Errorf("error writing to stdout: %w", err)
+	}
+
+	return nil
+}
diff --git a/test/lemlist/write/main.go b/test/lemlist/write/main.go
new file mode 100644
index 00000000..39a3d7f2
--- /dev/null
+++ b/test/lemlist/write/main.go
@@ -0,0 +1,87 @@
+package main
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"log/slog"
+	"os"
+
+	"github.com/amp-labs/connectors/common"
+	ll "github.com/amp-labs/connectors/providers/lemlist"
+	"github.com/amp-labs/connectors/test/lemlist"
+)
+
+func main() {
+	if err := run(); err != nil {
+		slog.Error(err.Error())
+	}
+}
+
+func run() error {
+	ctx := context.Background()
+	conn := lemlist.GetLemlistConnector(ctx)
+
+	err := testCreatingCampaigns(ctx, conn)
+	if err != nil {
+		return err
+	}
+
+	err = testCreatingWebhooks(ctx, conn)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func testCreatingCampaigns(ctx context.Context, conn *ll.Connector) error {
+	params := common.WriteParams{
+		ObjectName: "campaigns",
+		RecordData: map[string]any{
+			"name": "Eclipse of Empires",
+		},
+	}
+
+	res, err := conn.Write(ctx, params)
+	if err != nil {
+		return err
+	}
+
+	// Print the results
+	jsonStr, err := json.MarshalIndent(res, "", "  ")
+	if err != nil {
+		return fmt.Errorf("error marshalling JSON: %w", err)
+	}
+
+	_, _ = os.Stdout.Write(jsonStr)
+	_, _ = os.Stdout.WriteString("\n")
+
+	return nil
+}
+
+func testCreatingWebhooks(ctx context.Context, conn *ll.Connector) error {
+	params := common.WriteParams{
+		ObjectName: "hooks",
+		RecordData: map[string]any{
+			"targetUrl": "https://webhook.site/f748c494-e583-4b47-b9ea",
+			"type":      "emailsOpened",
+		},
+	}
+
+	res, err := conn.Write(ctx, params)
+	if err != nil {
+		return err
+	}
+
+	// Print the results
+	jsonStr, err := json.MarshalIndent(res, "", "  ")
+	if err != nil {
+		return fmt.Errorf("error marshalling JSON: %w", err)
+	}
+
+	_, _ = os.Stdout.Write(jsonStr)
+	_, _ = os.Stdout.WriteString("\n")
+
+	return nil
+}