Repository: amp-labs/connectors
Pull request #500 (2024-06-10 14:10:12)

scrapper: Salesloft, collect Statistics on query params
# Description

Salesloft has some query parameters that can be used for performing Read method using `Since` qurying.

Added a method that can be executed at any time to see stats on frequency usage of query params in Salesloft APIs.


Patch:
diff --git a/.github/workflows/generate-json-catalog.yaml b/.github/workflows/generate-json-catalog.yaml
new file mode 100644
index 00000000..d81ab12f
--- /dev/null
+++ b/.github/workflows/generate-json-catalog.yaml
@@ -0,0 +1,38 @@
+name: Publish JSON catalog
+
+on:
+  pull_request:
+    types: [closed]
+    branches:
+      - 'main'
+
+jobs:
+  build:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+
+      - name: Check for changes in providers/catalog.go
+        uses: dorny/paths-filter@v3
+        id: filter
+        with:
+          filters: |
+            catalog:
+              - 'providers/catalog.go'
+
+      - name: Run script if catalog has changed
+        if: steps.filter.outputs.catalog == 'true'
+        run: go run scripts/catalog/json.go
+        shell: bash
+
+      - name: Push changes
+        if: steps.filter.outputs.catalog == 'true'
+        run: |
+          git config --global user.email "no-reply@github.com"
+          git config --global user.name "GitHub Actions"
+          git add providers/catalog.json
+          git commit -m "Update catalog.json"
+          git remote set-url origin https://x-access-token:${{ secrets.GITHUB_TOKEN }}@github.com/${{ github.repository }}
+          git push
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/common/apiKey.go b/common/apiKey.go
index a5569c82..d424e8c4 100644
--- a/common/apiKey.go
+++ b/common/apiKey.go
@@ -4,10 +4,10 @@ import (
 	"context"
 )
 
-// NewApiKeyAuthHTTPClient returns a new http client, with automatic API key
+// NewApiKeyHeaderAuthHTTPClient returns a new http client, with automatic API key
 // authentication. Specifically this means that the client will automatically
 // add the API key (as a header) to every request.
-func NewApiKeyAuthHTTPClient( //nolint:ireturn
+func NewApiKeyHeaderAuthHTTPClient( //nolint:ireturn
 	ctx context.Context,
 	headerName, apiKey string,
 	opts ...HeaderAuthClientOption,
@@ -17,3 +17,17 @@ func NewApiKeyAuthHTTPClient( //nolint:ireturn
 		Value: apiKey,
 	}))...)
 }
+
+// NewApiKeyQueryParamAuthHTTPClient returns a new http client, with automatic API key
+// authentication. Specifically this means that the client will automatically
+// add the API key (as a query param) to every request.
+func NewApiKeyQueryParamAuthHTTPClient( //nolint:ireturn
+	ctx context.Context,
+	queryParamName, apiKey string,
+	opts ...QueryParamAuthClientOption,
+) (AuthenticatedHTTPClient, error) {
+	return NewQueryParamAuthHTTPClient(ctx, append(opts, WithQueryParams(QueryParam{
+		Key:   queryParamName,
+		Value: apiKey,
+	}))...)
+}
diff --git a/common/query.go b/common/query.go
new file mode 100644
index 00000000..c671517f
--- /dev/null
+++ b/common/query.go
@@ -0,0 +1,111 @@
+package common
+
+import (
+	"context"
+	"net/http"
+)
+
+type QueryParam struct {
+	Key   string
+	Value string
+}
+
+type QueryParamAuthClientOption func(params *queryParamClientParams)
+
+// NewQueryParamAuthHTTPClient returns a new http client, which will
+// do generic query-param-based authentication. It does this by automatically
+// adding the provided query params to every request. There's no additional
+// logic for refreshing tokens or anything like that. This is appropriate
+// for APIs that use keys encoded in the query params.
+func NewQueryParamAuthHTTPClient( //nolint:ireturn
+	ctx context.Context,
+	opts ...QueryParamAuthClientOption,
+) (AuthenticatedHTTPClient, error) {
+	params := &queryParamClientParams{}
+	for _, opt := range opts {
+		opt(params)
+	}
+
+	return newQueryParamAuthClient(ctx, params.prepare()), nil
+}
+
+// WithQueryParamClient sets the http client to use for the connector. Its usage is optional.
+func WithQueryParamClient(client *http.Client) QueryParamAuthClientOption {
+	return func(params *queryParamClientParams) {
+		params.client = client
+	}
+}
+
+func WithQueryParams(ps ...QueryParam) QueryParamAuthClientOption {
+	return func(params *queryParamClientParams) {
+		params.params = append(params.params, ps...)
+	}
+}
+
+// WithQueryParamDebug sets a debug function to be called on every request and response,
+// after the response has been received from the downstream API.
+func WithQueryParamDebug(f func(req *http.Request, rsp *http.Response)) QueryParamAuthClientOption {
+	return func(params *queryParamClientParams) {
+		params.debug = f
+	}
+}
+
+// queryParamClientParams is the internal configuration for the oauth http client.
+type queryParamClientParams struct {
+	client *http.Client
+	params []QueryParam
+	debug  func(req *http.Request, rsp *http.Response)
+}
+
+func (p *queryParamClientParams) prepare() *queryParamClientParams {
+	if p.client == nil {
+		p.client = http.DefaultClient
+	}
+
+	return p
+}
+
+// newQueryParamAuthClient returns a new http client for the connector, with automatic OAuth authentication.
+func newQueryParamAuthClient( //nolint:ireturn
+	_ context.Context,
+	params *queryParamClientParams,
+) AuthenticatedHTTPClient {
+	return &queryParamAuthClient{
+		client: params.client,
+		params: params.params,
+		debug:  params.debug,
+	}
+}
+
+type queryParamAuthClient struct {
+	client *http.Client
+	params []QueryParam
+	debug  func(req *http.Request, rsp *http.Response)
+}
+
+func (c *queryParamAuthClient) Do(req *http.Request) (*http.Response, error) {
+	// This allows us to modify query params without mutating the input
+	req = req.Clone(req.Context())
+
+	query := req.URL.Query()
+	for _, p := range c.params {
+		query.Add(p.Key, p.Value)
+	}
+
+	req.URL.RawQuery = query.Encode()
+
+	rsp, err := c.client.Do(req)
+	if err != nil {
+		return rsp, err
+	}
+
+	if c.debug != nil {
+		c.debug(req, cloneResponse(rsp))
+	}
+
+	return rsp, nil
+}
+
+func (c *queryParamAuthClient) CloseIdleConnections() {
+	c.client.CloseIdleConnections()
+}
diff --git a/go.mod b/go.mod
index eda48727..b2e6db3b 100644
--- a/go.mod
+++ b/go.mod
@@ -10,9 +10,9 @@ require (
 	github.com/go-test/deep v1.1.0
 	github.com/iancoleman/strcase v0.3.0
 	github.com/joho/godotenv v1.5.1
-	github.com/spf13/cobra v1.8.0
+	github.com/spf13/cobra v1.8.1
 	github.com/spf13/viper v1.19.0
-	github.com/spyzhov/ajson v0.9.1
+	github.com/spyzhov/ajson v0.9.3
 	github.com/stretchr/testify v1.9.0
 	github.com/subchen/go-xmldom v1.1.2
 	golang.org/x/oauth2 v0.21.0
diff --git a/go.sum b/go.sum
index a4988d12..cb4661f7 100644
--- a/go.sum
+++ b/go.sum
@@ -6,7 +6,7 @@ github.com/antchfx/xpath v0.0.0-20170515025933-1f3266e77307 h1:C735MoY/X+UOx6SEC
 github.com/antchfx/xpath v0.0.0-20170515025933-1f3266e77307/go.mod h1:Yee4kTMuNiPYJ7nSNorELQMr1J33uOpXDMByNYhvtNk=
 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/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
+github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
@@ -63,14 +63,14 @@ 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/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
-github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
+github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
+github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
 github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
 github.com/spf13/pflag v1.0.5/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/spyzhov/ajson v0.9.1 h1:izsDkOC9yznlpA2BxJcrkr8Q5Gyjf43yeSc2Wz0g/aU=
-github.com/spyzhov/ajson v0.9.1/go.mod h1:a6oSw0MMb7Z5aD2tPoPO+jq11ETKgXUr2XktHdT8Wt8=
+github.com/spyzhov/ajson v0.9.3 h1://kaIRii6TMRA4zgqQnEdZTURVWlbFinUUCGHJ8BjSg=
+github.com/spyzhov/ajson v0.9.3/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=
diff --git a/gong/methods.go b/gong/methods.go
deleted file mode 100644
index a83052f8..00000000
--- a/gong/methods.go
+++ /dev/null
@@ -1,16 +0,0 @@
-package gong
-
-import (
-	"context"
-
-	"github.com/amp-labs/connectors/common"
-)
-
-func (c *Connector) get(ctx context.Context, url string) (*common.JSONHTTPResponse, error) {
-	res, err := c.Client.Get(ctx, url)
-	if err != nil {
-		return nil, c.HandleError(err)
-	}
-
-	return res, nil
-}
diff --git a/gong/read.go b/gong/read.go
index d4e319f0..0473439c 100644
--- a/gong/read.go
+++ b/gong/read.go
@@ -28,7 +28,7 @@ func (c *Connector) Read(ctx context.Context, config common.ReadParams) (*common
 		fields = config.Fields
 	}
 
-	res, err = c.get(ctx, fullURL)
+	res, err = c.Client.Get(ctx, fullURL)
 	if err != nil {
 		return nil, err
 	}
diff --git a/providers/catalog.go b/providers/catalog.go
index c3da0afa..ea402178 100644
--- a/providers/catalog.go
+++ b/providers/catalog.go
@@ -47,6 +47,7 @@ const (
 	Ironclad                Provider = "ironclad"
 	IroncladDemo            Provider = "ironcladDemo"
 	IroncladEU              Provider = "ironcladEU"
+	Iterable                Provider = "iterable"
 	Keap                    Provider = "keap"
 	Klaviyo                 Provider = "klaviyo"
 	LinkedIn                Provider = "linkedIn"
@@ -93,7 +94,7 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 	Salesforce: {
 		AuthType: Oauth2,
 		BaseURL:  "https://{{.workspace}}.my.salesforce.com",
-		OauthOpts: &OauthOpts{
+		Oauth2Opts: &Oauth2Opts{
 			GrantType:                 AuthorizationCode,
 			AuthURL:                   "https://{{.workspace}}.my.salesforce.com/services/oauth2/authorize",
 			TokenURL:                  "https://{{.workspace}}.my.salesforce.com/services/oauth2/token",
@@ -127,7 +128,7 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 	Hubspot: {
 		AuthType: Oauth2,
 		BaseURL:  "https://api.hubapi.com",
-		OauthOpts: &OauthOpts{
+		Oauth2Opts: &Oauth2Opts{
 			GrantType:                 AuthorizationCode,
 			AuthURL:                   "https://app.hubspot.com/oauth/authorize",
 			TokenURL:                  "https://api.hubapi.com/oauth/v1/token",
@@ -152,7 +153,7 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 	LinkedIn: {
 		AuthType: Oauth2,
 		BaseURL:  "https://api.linkedin.com",
-		OauthOpts: &OauthOpts{
+		Oauth2Opts: &Oauth2Opts{
 			GrantType:                 AuthorizationCode,
 			AuthURL:                   "https://www.linkedin.com/oauth/v2/authorization",
 			TokenURL:                  "https://www.linkedin.com/oauth/v2/accessToken",
@@ -180,7 +181,7 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 	Salesloft: {
 		AuthType: Oauth2,
 		BaseURL:  "https://api.salesloft.com",
-		OauthOpts: &OauthOpts{
+		Oauth2Opts: &Oauth2Opts{
 			AuthURL:                   "https://accounts.salesloft.com/oauth/authorize",
 			TokenURL:                  "https://accounts.salesloft.com/oauth/token",
 			ExplicitScopesRequired:    false,
@@ -208,7 +209,7 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 	Outreach: {
 		AuthType: Oauth2,
 		BaseURL:  "https://api.outreach.io",
-		OauthOpts: &OauthOpts{
+		Oauth2Opts: &Oauth2Opts{
 			AuthURL:                   "https://api.outreach.io/oauth/authorize",
 			TokenURL:                  "https://api.outreach.io/oauth/token",
 			ExplicitScopesRequired:    true,
@@ -240,7 +241,7 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 	RingCentral: {
 		AuthType: Oauth2,
 		BaseURL:  "https://platform.ringcentral.com",
-		OauthOpts: &OauthOpts{
+		Oauth2Opts: &Oauth2Opts{
 			GrantType:                 PKCE,
 			AuthURL:                   "https://platform.ringcentral.com/restapi/oauth/authorize",
 			TokenURL:                  "https://platform.ringcentral.com/restapi/oauth/token",
@@ -269,7 +270,7 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 	Pipedrive: {
 		AuthType: Oauth2,
 		BaseURL:  "https://api.pipedrive.com",
-		OauthOpts: &OauthOpts{
+		Oauth2Opts: &Oauth2Opts{
 			GrantType:                 AuthorizationCode,
 			AuthURL:                   "https://oauth.pipedrive.com/oauth/authorize",
 			TokenURL:                  "https://oauth.pipedrive.com/oauth/token",
@@ -294,7 +295,7 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 	Capsule: {
 		AuthType: Oauth2,
 		BaseURL:  "https://api.capsulecrm.com/api",
-		OauthOpts: &OauthOpts{
+		Oauth2Opts: &Oauth2Opts{
 			GrantType:                 AuthorizationCode,
 			AuthURL:                   "https://api.capsulecrm.com/oauth/authorise",
 			TokenURL:                  "https://api.capsulecrm.com/oauth/token",
@@ -319,7 +320,7 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 	Wrike: {
 		AuthType: Oauth2,
 		BaseURL:  "https://www.wrike.com/api",
-		OauthOpts: &OauthOpts{
+		Oauth2Opts: &Oauth2Opts{
 			GrantType:                 AuthorizationCode,
 			AuthURL:                   "https://www.wrike.com/oauth2/authorize",
 			TokenURL:                  "https://www.wrike.com/oauth2/token",
@@ -344,7 +345,7 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 	Copper: {
 		AuthType: Oauth2,
 		BaseURL:  "https://api.copper.com/developer_api",
-		OauthOpts: &OauthOpts{
+		Oauth2Opts: &Oauth2Opts{
 			GrantType:                 AuthorizationCode,
 			AuthURL:                   "https://app.copper.com/oauth/authorize",
 			TokenURL:                  "https://app.copper.com/oauth/token",
@@ -370,7 +371,7 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 		DisplayName: "Zoho CRM",
 		AuthType:    Oauth2,
 		BaseURL:     "https://www.zohoapis.com",
-		OauthOpts: &OauthOpts{
+		Oauth2Opts: &Oauth2Opts{
 			GrantType:                 AuthorizationCode,
 			AuthURL:                   "https://accounts.zoho.com/oauth/v2/auth",
 			TokenURL:                  "https://accounts.zoho.com/oauth/v2/token",
@@ -399,7 +400,7 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 	Mural: {
 		AuthType: Oauth2,
 		BaseURL:  "https://api.mural.co/api",
-		OauthOpts: &OauthOpts{
+		Oauth2Opts: &Oauth2Opts{
 			GrantType:                 AuthorizationCode,
 			AuthURL:                   "https://api.mural.co/oauth/authorize",
 			TokenURL:                  "https://api.mural.co/oauth/token",
@@ -427,7 +428,7 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 	Klaviyo: {
 		AuthType: Oauth2,
 		BaseURL:  "https://a.klaviyo.com",
-		OauthOpts: &OauthOpts{
+		Oauth2Opts: &Oauth2Opts{
 			GrantType:                 PKCE,
 			AuthURL:                   "https://www.klaviyo.com/oauth/authorize",
 			TokenURL:                  "https://a.klaviyo.com/oauth/token",
@@ -455,7 +456,7 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 	Sellsy: {
 		AuthType: Oauth2,
 		BaseURL:  "https://api.sellsy.com",
-		OauthOpts: &OauthOpts{
+		Oauth2Opts: &Oauth2Opts{
 			GrantType:                 PKCE,
 			AuthURL:                   "https://login.sellsy.com/oauth2/authorization",
 			TokenURL:                  "https://login.sellsy.com/oauth2/access-tokens",
@@ -480,7 +481,7 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 	Attio: {
 		AuthType: Oauth2,
 		BaseURL:  "https://api.attio.com",
-		OauthOpts: &OauthOpts{
+		Oauth2Opts: &Oauth2Opts{
 			GrantType:                 AuthorizationCode,
 			AuthURL:                   "https://app.attio.com/authorize",
 			TokenURL:                  "https://app.attio.com/oauth/token",
@@ -505,7 +506,7 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 	Close: {
 		AuthType: Oauth2,
 		BaseURL:  "https://api.close.com/api",
-		OauthOpts: &OauthOpts{
+		Oauth2Opts: &Oauth2Opts{
 			GrantType:                 AuthorizationCode,
 			AuthURL:                   "https://app.close.com/oauth2/authorize",
 			TokenURL:                  "https://api.close.com/oauth2/token",
@@ -535,7 +536,7 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 		AuthType: Oauth2,
 		BaseURL:  "https://api.infusionsoft.com",
 
-		OauthOpts: &OauthOpts{
+		Oauth2Opts: &Oauth2Opts{
 			GrantType:                 AuthorizationCode,
 			AuthURL:                   "https://accounts.infusionsoft.com/app/oauth/authorize",
 			TokenURL:                  "https://api.infusionsoft.com/token",
@@ -560,7 +561,7 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 	Asana: {
 		AuthType: Oauth2,
 		BaseURL:  "https://app.asana.com/api",
-		OauthOpts: &OauthOpts{
+		Oauth2Opts: &Oauth2Opts{
 			AuthURL:                   "https://app.asana.com/-/oauth_authorize",
 			TokenURL:                  "https://app.asana.com/-/oauth_token",
 			ExplicitScopesRequired:    false,
@@ -588,7 +589,7 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 	Dropbox: {
 		AuthType: Oauth2,
 		BaseURL:  "https://api.dropboxapi.com",
-		OauthOpts: &OauthOpts{
+		Oauth2Opts: &Oauth2Opts{
 			GrantType:                 AuthorizationCode,
 			AuthURL:                   "https://www.dropbox.com/oauth2/authorize",
 			TokenURL:                  "https://api.dropboxapi.com/oauth2/token",
@@ -617,7 +618,7 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 	Notion: {
 		AuthType: Oauth2,
 		BaseURL:  "https://api.notion.com",
-		OauthOpts: &OauthOpts{
+		Oauth2Opts: &Oauth2Opts{
 			AuthURL:                   "https://api.notion.com/v1/oauth/authorize",
 			TokenURL:                  "https://api.notion.com/v1/oauth/token",
 			ExplicitScopesRequired:    false,
@@ -646,7 +647,7 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 	Gong: {
 		AuthType: Oauth2,
 		BaseURL:  "https://api.gong.io",
-		OauthOpts: &OauthOpts{
+		Oauth2Opts: &Oauth2Opts{
 			AuthURL:                   "https://app.gong.io/oauth2/authorize",
 			TokenURL:                  "https://app.gong.io/oauth2/generate-customer-token",
 			ExplicitScopesRequired:    true,
@@ -674,7 +675,7 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 	Zoom: {
 		AuthType: Oauth2,
 		BaseURL:  "https://api.zoom.us",
-		OauthOpts: &OauthOpts{
+		Oauth2Opts: &Oauth2Opts{
 			AuthURL:                   "https://zoom.us/oauth/authorize",
 			TokenURL:                  "https://zoom.us/oauth/token",
 			ExplicitScopesRequired:    false,
@@ -702,7 +703,7 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 	Intercom: {
 		AuthType: Oauth2,
 		BaseURL:  "https://api.intercom.io",
-		OauthOpts: &OauthOpts{
+		Oauth2Opts: &Oauth2Opts{
 			AuthURL:                   "https://app.intercom.com/oauth",
 			TokenURL:                  "https://api.intercom.io/auth/eagle/token",
 			ExplicitScopesRequired:    false,
@@ -727,7 +728,7 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 	Docusign: {
 		AuthType: Oauth2,
 		BaseURL:  "https://{{.server}}.docusign.net",
-		OauthOpts: &OauthOpts{
+		Oauth2Opts: &Oauth2Opts{
 			AuthURL:                   "https://account.docusign.com/oauth/auth",
 			TokenURL:                  "https://account.docusign.com/oauth/token",
 			ExplicitScopesRequired:    true,
@@ -753,7 +754,7 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 	DocusignDeveloper: {
 		AuthType: Oauth2,
 		BaseURL:  "https://demo.docusign.net",
-		OauthOpts: &OauthOpts{
+		Oauth2Opts: &Oauth2Opts{
 			GrantType:                 AuthorizationCode,
 			AuthURL:                   "https://account-d.docusign.com/oauth/auth",
 			TokenURL:                  "https://account-d.docusign.com/oauth/token",
@@ -781,7 +782,7 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 	Calendly: {
 		AuthType: Oauth2,
 		BaseURL:  "https://api.calendly.com",
-		OauthOpts: &OauthOpts{
+		Oauth2Opts: &Oauth2Opts{
 			GrantType:                 AuthorizationCode,
 			AuthURL:                   "https://auth.calendly.com/oauth/authorize",
 			TokenURL:                  "https://auth.calendly.com/oauth/token",
@@ -806,7 +807,7 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 	CampaignMonitor: {
 		AuthType: Oauth2,
 		BaseURL:  "https://api.createsend.com",
-		OauthOpts: &OauthOpts{
+		Oauth2Opts: &Oauth2Opts{
 			AuthURL:                   "https://api.createsend.com/oauth",
 			TokenURL:                  "https://api.createsend.com/oauth/token",
 			ExplicitScopesRequired:    true,
@@ -831,7 +832,7 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 	GetResponse: {
 		AuthType: Oauth2,
 		BaseURL:  "https://api.getresponse.com",
-		OauthOpts: &OauthOpts{
+		Oauth2Opts: &Oauth2Opts{
 			AuthURL:                   "https://app.getresponse.com/oauth2_authorize.html",
 			TokenURL:                  "https://api.getresponse.com/v3/token",
 			ExplicitScopesRequired:    false,
@@ -856,7 +857,7 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 	AWeber: {
 		AuthType: Oauth2,
 		BaseURL:  "https://api.aweber.com",
-		OauthOpts: &OauthOpts{
+		Oauth2Opts: &Oauth2Opts{
 			AuthURL:                   "https://auth.aweber.com/oauth2/authorize",
 			TokenURL:                  "https://auth.aweber.com/oauth2/token",
 			ExplicitScopesRequired:    true,
@@ -881,7 +882,7 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 		DisplayName: "Microsoft Dynamics CRM",
 		AuthType:    Oauth2,
 		BaseURL:     "https://{{.workspace}}.api.crm.dynamics.com/api/data",
-		OauthOpts: &OauthOpts{
+		Oauth2Opts: &Oauth2Opts{
 			GrantType:              AuthorizationCode,
 			AuthURL:                "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
 			TokenURL:               "https://login.microsoftonline.com/common/oauth2/v2.0/token",
@@ -909,7 +910,7 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 		DisplayName: "Constant Contact",
 		AuthType:    Oauth2,
 		BaseURL:     "https://api.cc.email",
-		OauthOpts: &OauthOpts{
+		Oauth2Opts: &Oauth2Opts{
 			GrantType:                 AuthorizationCode,
 			AuthURL:                   "https://authz.constantcontact.com/oauth2/default/v1/authorize",
 			TokenURL:                  "https://authz.constantcontact.com/oauth2/default/v1/token",
@@ -935,7 +936,7 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 		DisplayName: "Microsoft Dynamics Business Central",
 		AuthType:    Oauth2,
 		BaseURL:     "https://api.businesscentral.dynamics.com",
-		OauthOpts: &OauthOpts{
+		Oauth2Opts: &Oauth2Opts{
 			AuthURL:                   "https://login.microsoftonline.com/{{.workspace}}/oauth2/v2.0/authorize",
 			TokenURL:                  "https://login.microsoftonline.com/{{.workspace}}/oauth2/v2.0/token",
 			ExplicitScopesRequired:    true,
@@ -963,7 +964,7 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 	Gainsight: {
 		AuthType: Oauth2,
 		BaseURL:  "https://{{.workspace}}.gainsightcloud.com",
-		OauthOpts: &OauthOpts{
+		Oauth2Opts: &Oauth2Opts{
 			AuthURL:                   "https://{{.workspace}}.gainsightcloud.com/v1/authorize",
 			TokenURL:                  "https://{{.workspace}}.gainsightcloud.com/v1/users/oauth/token",
 			ExplicitScopesRequired:    false,
@@ -988,7 +989,7 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 	Box: {
 		AuthType: Oauth2,
 		BaseURL:  "https://api.box.com",
-		OauthOpts: &OauthOpts{
+		Oauth2Opts: &Oauth2Opts{
 			GrantType:                 AuthorizationCode,
 			AuthURL:                   "https://account.box.com/api/oauth2/authorize",
 			TokenURL:                  "https://api.box.com/oauth2/token",
@@ -1014,7 +1015,7 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 		DisplayName: "Zendesk Support",
 		AuthType:    Oauth2,
 		BaseURL:     "https://{{.workspace}}.zendesk.com",
-		OauthOpts: &OauthOpts{
+		Oauth2Opts: &Oauth2Opts{
 			GrantType:                 AuthorizationCode,
 			AuthURL:                   "https://{{.workspace}}.zendesk.com/oauth/authorizations/new",
 			TokenURL:                  "https://{{.workspace}}.zendesk.com/oauth/tokens",
@@ -1039,7 +1040,7 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 		DisplayName: "Zendesk Chat",
 		AuthType:    Oauth2,
 		BaseURL:     "https://www.zopim.com",
-		OauthOpts: &OauthOpts{
+		Oauth2Opts: &Oauth2Opts{
 			GrantType:                 AuthorizationCode,
 			AuthURL:                   "https://www.zopim.com/oauth2/authorizations/new?subdomain={{.workspace}}",
 			TokenURL:                  "https://www.zopim.com/oauth2/token",
@@ -1064,7 +1065,7 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 	WordPress: {
 		AuthType: Oauth2,
 		BaseURL:  "https://public-api.wordpress.com",
-		OauthOpts: &OauthOpts{
+		Oauth2Opts: &Oauth2Opts{
 			AuthURL:                   "https://public-api.wordpress.com/oauth2/authorize",
 			TokenURL:                  "https://public-api.wordpress.com/oauth2/token",
 			ExplicitScopesRequired:    true,
@@ -1092,7 +1093,7 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 	Airtable: {
 		AuthType: Oauth2,
 		BaseURL:  "https://api.airtable.com",
-		OauthOpts: &OauthOpts{
+		Oauth2Opts: &Oauth2Opts{
 			GrantType:                 PKCE,
 			AuthURL:                   "https://airtable.com/oauth2/v1/authorize",
 			TokenURL:                  "https://airtable.com/oauth2/v1/token",
@@ -1120,7 +1121,7 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 	Ironclad: {
 		AuthType: Oauth2,
 		BaseURL:  "https://ironcladapp.com",
-		OauthOpts: &OauthOpts{
+		Oauth2Opts: &Oauth2Opts{
 			AuthURL:                   "https://ironcladapp.com/oauth/authorize",
 			TokenURL:                  "https://ironcladapp.com/oauth/token",
 			ExplicitScopesRequired:    true,
@@ -1148,7 +1149,7 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 	Slack: {
 		AuthType: Oauth2,
 		BaseURL:  "https://slack.com/api",
-		OauthOpts: &OauthOpts{
+		Oauth2Opts: &Oauth2Opts{
 			GrantType:                 AuthorizationCode,
 			AuthURL:                   "https://slack.com/oauth/v2/authorize",
 			TokenURL:                  "https://slack.com/api/oauth.v2.access",
@@ -1177,7 +1178,7 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 		DisplayName: "Help Scout Mailbox",
 		AuthType:    Oauth2,
 		BaseURL:     "https://api.helpscout.net",
-		OauthOpts: &OauthOpts{
+		Oauth2Opts: &Oauth2Opts{
 			GrantType:                 AuthorizationCode,
 			AuthURL:                   "https://secure.helpscout.net/authentication/authorizeClientApplication",
 			TokenURL:                  "https://api.helpscout.net/v2/oauth2/token",
@@ -1202,7 +1203,7 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 	Timely: {
 		AuthType: Oauth2,
 		BaseURL:  "https://api.timelyapp.com",
-		OauthOpts: &OauthOpts{
+		Oauth2Opts: &Oauth2Opts{
 			GrantType:                 AuthorizationCode,
 			AuthURL:                   "https://api.timelyapp.com/1.1/oauth/authorize",
 			TokenURL:                  "https://api.timelyapp.com/1.1/oauth/token",
@@ -1231,7 +1232,7 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 		DisplayName: "Atlassian Jira",
 		AuthType:    Oauth2,
 		BaseURL:     "https://api.atlassian.com",
-		OauthOpts: &OauthOpts{
+		Oauth2Opts: &Oauth2Opts{
 			GrantType:                 AuthorizationCode,
 			AuthURL:                   "https://auth.atlassian.com/authorize",
 			TokenURL:                  "https://auth.atlassian.com/oauth/token",
@@ -1256,7 +1257,7 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 	Webflow: {
 		AuthType: Oauth2,
 		BaseURL:  "https://api.webflow.com",
-		OauthOpts: &OauthOpts{
+		Oauth2Opts: &Oauth2Opts{
 			GrantType:                 AuthorizationCode,
 			AuthURL:                   "https://webflow.com/oauth/authorize",
 			TokenURL:                  "https://api.webflow.com/oauth/access_token",
@@ -1284,7 +1285,7 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 	Smartsheet: {
 		AuthType: Oauth2,
 		BaseURL:  "https://api.smartsheet.com",
-		OauthOpts: &OauthOpts{
+		Oauth2Opts: &Oauth2Opts{
 			GrantType:                 AuthorizationCode,
 			AuthURL:                   "https://app.smartsheet.com/b/authorize",
 			TokenURL:                  "https://api.smartsheet.com/2.0/token",
@@ -1309,7 +1310,7 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 	StackExchange: {
 		AuthType: Oauth2,
 		BaseURL:  "https://api.stackexchange.com",
-		OauthOpts: &OauthOpts{
+		Oauth2Opts: &Oauth2Opts{
 			GrantType:                 AuthorizationCode,
 			AuthURL:                   "https://stackoverflow.com/oauth",
 			TokenURL:                  "https://stackoverflow.com/oauth/access_token/json",
@@ -1334,7 +1335,7 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 	Google: {
 		AuthType: Oauth2,
 		BaseURL:  "https://www.googleapis.com",
-		OauthOpts: &OauthOpts{
+		Oauth2Opts: &Oauth2Opts{
 			GrantType:                 AuthorizationCode,
 			AuthURL:                   "https://accounts.google.com/o/oauth2/v2/auth",
 			TokenURL:                  "https://oauth2.googleapis.com/token",
@@ -1363,7 +1364,7 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 		DisplayName: "Google Contacts",
 		AuthType:    Oauth2,
 		BaseURL:     "https://people.googleapis.com",
-		OauthOpts: &OauthOpts{
+		Oauth2Opts: &Oauth2Opts{
 			GrantType:                 AuthorizationCode,
 			AuthURL:                   "https://accounts.google.com/o/oauth2/v2/auth",
 			TokenURL:                  "https://oauth2.googleapis.com/token",
@@ -1391,7 +1392,7 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 	Gmail: {
 		AuthType: Oauth2,
 		BaseURL:  "https://gmail.googleapis.com",
-		OauthOpts: &OauthOpts{
+		Oauth2Opts: &Oauth2Opts{
 			GrantType:                 AuthorizationCode,
 			AuthURL:                   "https://accounts.google.com/o/oauth2/v2/auth",
 			TokenURL:                  "https://oauth2.googleapis.com/token",
@@ -1418,7 +1419,7 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 	Monday: {
 		AuthType: Oauth2,
 		BaseURL:  "https://api.monday.com",
-		OauthOpts: &OauthOpts{
+		Oauth2Opts: &Oauth2Opts{
 			GrantType:                 AuthorizationCode,
 			AuthURL:                   "https://auth.monday.com/oauth2/authorize",
 			TokenURL:                  "https://auth.monday.com/oauth2/token",
@@ -1445,7 +1446,7 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 	Figma: {
 		AuthType: Oauth2,
 		BaseURL:  "https://api.figma.com",
-		OauthOpts: &OauthOpts{
+		Oauth2Opts: &Oauth2Opts{
 			GrantType:                 AuthorizationCode,
 			AuthURL:                   "https://www.figma.com/oauth",
 			TokenURL:                  "https://www.figma.com/api/oauth/token",
@@ -1472,7 +1473,7 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 	Miro: {
 		AuthType: Oauth2,
 		BaseURL:  "https://api.miro.com",
-		OauthOpts: &OauthOpts{
+		Oauth2Opts: &Oauth2Opts{
 			GrantType:                 AuthorizationCode,
 			AuthURL:                   "https://miro.com/oauth/authorize",
 			TokenURL:                  "https://api.miro.com/v1/oauth/token",
@@ -1500,7 +1501,7 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 	Typeform: {
 		AuthType: Oauth2,
 		BaseURL:  "https://api.typeform.com",
-		OauthOpts: &OauthOpts{
+		Oauth2Opts: &Oauth2Opts{
 			GrantType:                 AuthorizationCode,
 			AuthURL:                   "https://api.typeform.com/oauth/authorize",
 			TokenURL:                  "https://api.typeform.com/oauth/token",
@@ -1525,9 +1526,8 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 	Zuora: {
 		AuthType: Oauth2,
 		BaseURL:  "https://{{.workspace}}.zuora.com",
-		OauthOpts: &OauthOpts{
+		Oauth2Opts: &Oauth2Opts{
 			GrantType:                 ClientCredentials,
-			AuthURL:                   "https://{{.workspace}}.zuora.com/oauth/auth_mock",
 			TokenURL:                  "https://{{.workspace}}.zuora.com/oauth/token",
 			ExplicitScopesRequired:    false,
 			ExplicitWorkspaceRequired: true,
@@ -1551,7 +1551,7 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 		DisplayName: "Dropbox Sign",
 		AuthType:    Oauth2,
 		BaseURL:     "https://api.hellosign.com",
-		OauthOpts: &OauthOpts{
+		Oauth2Opts: &Oauth2Opts{
 			GrantType:                 AuthorizationCode,
 			AuthURL:                   "https://app.hellosign.com/oauth/authorize",
 			TokenURL:                  "https://app.hellosign.com/oauth/token",
@@ -1576,7 +1576,7 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 	Facebook: {
 		AuthType: Oauth2,
 		BaseURL:  "https://graph.facebook.com",
-		OauthOpts: &OauthOpts{
+		Oauth2Opts: &Oauth2Opts{
 			GrantType:                 AuthorizationCode,
 			AuthURL:                   "https://www.facebook.com/v19.0/dialog/oauth",
 			TokenURL:                  "https://graph.facebook.com/v19.0/oauth/access_token",
@@ -1601,7 +1601,7 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 	ClickUp: {
 		AuthType: Oauth2,
 		BaseURL:  "https://api.clickup.com",
-		OauthOpts: &OauthOpts{
+		Oauth2Opts: &Oauth2Opts{
 			GrantType:                 AuthorizationCode,
 			AuthURL:                   "https://app.clickup.com/api",
 			TokenURL:                  "https://api.clickup.com/api/v2/oauth/token",
@@ -1626,7 +1626,7 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 	Discord: {
 		AuthType: Oauth2,
 		BaseURL:  "https://discord.com",
-		OauthOpts: &OauthOpts{
+		Oauth2Opts: &Oauth2Opts{
 			GrantType:                 AuthorizationCode,
 			AuthURL:                   "https://discord.com/oauth2/authorize",
 			TokenURL:                  "https://discord.com/api/oauth2/token",
@@ -1654,7 +1654,7 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 	Drift: {
 		AuthType: Oauth2,
 		BaseURL:  "https://driftapi.com",
-		OauthOpts: &OauthOpts{
+		Oauth2Opts: &Oauth2Opts{
 			GrantType:                 AuthorizationCode,
 			AuthURL:                   "https://dev.drift.com/authorize",
 			TokenURL:                  "https://driftapi.com/oauth2/token",
@@ -1681,7 +1681,7 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 	IroncladDemo: {
 		AuthType: Oauth2,
 		BaseURL:  "https://demo.ironcladapp.com",
-		OauthOpts: &OauthOpts{
+		Oauth2Opts: &Oauth2Opts{
 			AuthURL:                   "https://demo.ironcladapp.com/oauth/authorize",
 			TokenURL:                  "https://demo.ironcladapp.com/oauth/token",
 			ExplicitScopesRequired:    true,
@@ -1709,7 +1709,7 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 		DisplayName: "Ironclad Europe",
 		AuthType:    Oauth2,
 		BaseURL:     "https://eu1.ironcladapp.com",
-		OauthOpts: &OauthOpts{
+		Oauth2Opts: &Oauth2Opts{
 			AuthURL:                   "https://eu1.ironcladapp.com/oauth/authorize",
 			TokenURL:                  "https://eu1.ironcladapp.com/oauth/token",
 			ExplicitScopesRequired:    true,
@@ -1737,7 +1737,7 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 	Aircall: {
 		AuthType: Oauth2,
 		BaseURL:  "https://api.aircall.io",
-		OauthOpts: &OauthOpts{
+		Oauth2Opts: &Oauth2Opts{
 			GrantType:                 AuthorizationCode,
 			AuthURL:                   "https://dashboard.aircall.io/oauth/authorize",
 			TokenURL:                  "https://api.aircall.io/v1/oauth/token",
@@ -1762,7 +1762,7 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 	Microsoft: {
 		AuthType: Oauth2,
 		BaseURL:  "https://graph.microsoft.com",
-		OauthOpts: &OauthOpts{
+		Oauth2Opts: &Oauth2Opts{
 			GrantType:                 AuthorizationCode,
 			AuthURL:                   "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
 			TokenURL:                  "https://login.microsoftonline.com/common/oauth2/v2.0/token",
@@ -1787,7 +1787,7 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 	Formstack: {
 		AuthType: Oauth2,
 		BaseURL:  "https://www.formstack.com/api",
-		OauthOpts: &OauthOpts{
+		Oauth2Opts: &Oauth2Opts{
 			GrantType:                 AuthorizationCode,
 			AuthURL:                   "https://www.formstack.com/api/v2/oauth2/authorize",
 			TokenURL:                  "https://www.formstack.com/api/v2/oauth2/token",
@@ -1815,7 +1815,7 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 	Pinterest: {
 		AuthType: Oauth2,
 		BaseURL:  "https://api.pinterest.com",
-		OauthOpts: &OauthOpts{
+		Oauth2Opts: &Oauth2Opts{
 			GrantType:                 AuthorizationCode,
 			AuthURL:                   "https://www.pinterest.com/oauth",
 			TokenURL:                  "https://api.pinterest.com/v5/oauth/token",
@@ -1842,7 +1842,7 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 	Aha: {
 		AuthType: Oauth2,
 		BaseURL:  "https://{{.workspace}}.aha.io/api",
-		OauthOpts: &OauthOpts{
+		Oauth2Opts: &Oauth2Opts{
 			GrantType:                 AuthorizationCode,
 			AuthURL:                   "https://{{.workspace}}.aha.io/oauth/authorize",
 			TokenURL:                  "https://{{.workspace}}.aha.io/oauth/token",
@@ -1867,7 +1867,7 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 	SnapchatAds: {
 		AuthType: Oauth2,
 		BaseURL:  "https://adsapi.snapchat.com",
-		OauthOpts: &OauthOpts{
+		Oauth2Opts: &Oauth2Opts{
 			AuthURL:                   "https://accounts.snapchat.com/login/oauth2/authorize",
 			TokenURL:                  "https://accounts.snapchat.com/login/oauth2/access_token",
 			ExplicitScopesRequired:    true,
@@ -1891,7 +1891,7 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 	SharePoint: {
 		AuthType: Oauth2,
 		BaseURL:  "https://{{.workspace}}.sharepoint.com/_api",
-		OauthOpts: &OauthOpts{
+		Oauth2Opts: &Oauth2Opts{
 			GrantType:              AuthorizationCode,
 			AuthURL:                "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
 			TokenURL:               "https://login.microsoftonline.com/common/oauth2/v2.0/token",
@@ -1921,7 +1921,7 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 	Instagram: {
 		AuthType: Oauth2,
 		BaseURL:  "https://graph.instagram.com",
-		OauthOpts: &OauthOpts{
+		Oauth2Opts: &Oauth2Opts{
 			GrantType:                 AuthorizationCode,
 			AuthURL:                   "https://api.instagram.com/oauth/authorize",
 			TokenURL:                  "https://api.instagram.com/oauth/access_token",
@@ -1949,7 +1949,7 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 	TeamleaderCRM: {
 		AuthType: Oauth2,
 		BaseURL:  "https://api.focus.teamleader.eu",
-		OauthOpts: &OauthOpts{
+		Oauth2Opts: &Oauth2Opts{
 			GrantType:                 AuthorizationCode,
 			AuthURL:                   "https://focus.teamleader.eu/oauth2/authorize",
 			TokenURL:                  "https://focus.teamleader.eu/oauth2/access_token",
@@ -1974,12 +1974,15 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 	Github: {
 		AuthType: Oauth2,
 		BaseURL:  "https://api.github.com",
-		OauthOpts: &OauthOpts{
+		Oauth2Opts: &Oauth2Opts{
 			GrantType:                 AuthorizationCode,
 			AuthURL:                   "https://github.com/login/oauth/authorize",
 			TokenURL:                  "https://github.com/login/oauth/access_token",
-			ExplicitScopesRequired:    false,
+			ExplicitScopesRequired:    true,
 			ExplicitWorkspaceRequired: false,
+			TokenMetadataFields: TokenMetadataFields{
+				ScopesField: "scope",
+			},
 		},
 		Support: Support{
 			BulkWrite: BulkWriteSupport{
@@ -1998,7 +2001,7 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 	Seismic: {
 		AuthType: Oauth2,
 		BaseURL:  "https://api.seismic.com",
-		OauthOpts: &OauthOpts{
+		Oauth2Opts: &Oauth2Opts{
 			GrantType:                 AuthorizationCode,
 			AuthURL:                   "https://auth.seismic.com/tenants/{{.workspace}}/connect/authorize",
 			TokenURL:                  "https://auth.seismic.com/tenants/{{.workspace}}/connect/token",
@@ -2023,7 +2026,7 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 	ServiceNow: {
 		AuthType: Oauth2,
 		BaseURL:  "https://{{.workspace}}.service-now.com",
-		OauthOpts: &OauthOpts{
+		Oauth2Opts: &Oauth2Opts{
 			AuthURL:                   "https://{{.workspace}}.service-now.com/oauth_auth.do",
 			TokenURL:                  "https://{{.workspace}}.service-now.com/oauth_token.do",
 			ExplicitScopesRequired:    false,
@@ -2051,7 +2054,7 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 	AcuityScheduling: {
 		AuthType: Oauth2,
 		BaseURL:  "https://acuityscheduling.com",
-		OauthOpts: &OauthOpts{
+		Oauth2Opts: &Oauth2Opts{
 			GrantType:                 AuthorizationCode,
 			AuthURL:                   "https://acuityscheduling.com/oauth2/authorize",
 			TokenURL:                  "https://acuityscheduling.com/oauth2/token",
@@ -2077,7 +2080,7 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 	Basecamp: {
 		AuthType: Oauth2,
 		BaseURL:  "https://3.basecampapi.com/{{.workspace}}",
-		OauthOpts: &OauthOpts{
+		Oauth2Opts: &Oauth2Opts{
 			GrantType:                 AuthorizationCode,
 			AuthURL:                   "https://launchpad.37signals.com/authorization/new?type=web_server",
 			TokenURL:                  "https://launchpad.37signals.com/authorization/token?type=refresh",
@@ -2102,7 +2105,7 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 	SurveyMonkey: {
 		AuthType: Oauth2,
 		BaseURL:  "https://api.surveymonkey.com",
-		OauthOpts: &OauthOpts{
+		Oauth2Opts: &Oauth2Opts{
 			AuthURL:                   "https://api.surveymonkey.com/oauth/authorize",
 			TokenURL:                  "https://api.surveymonkey.com/oauth/token",
 			ExplicitScopesRequired:    false,
@@ -2122,4 +2125,27 @@ var catalog = CatalogType{ // nolint:gochecknoglobals
 			Write:     false,
 		},
 	},
+
+	// Iterable API Key authentication
+	Iterable: {
+		AuthType: ApiKey,
+		BaseURL:  "https://api.iterable.com",
+		ApiKeyOpts: &ApiKeyOpts{
+			Type:       InHeader,
+			HeaderName: "Api-Key",
+			DocsURL:    "https://app.iterable.com/settings/apiKeys",
+		},
+		Support: Support{
+			BulkWrite: BulkWriteSupport{
+				Insert: false,
+				Update: false,
+				Upsert: false,
+				Delete: false,
+			},
+			Proxy:     false,
+			Read:      false,
+			Subscribe: false,
+			Write:     false,
+		},
+	},
 }
diff --git a/providers/types.gen.go b/providers/types.gen.go
index bb44e512..40ce49b2 100644
--- a/providers/types.gen.go
+++ b/providers/types.gen.go
@@ -3,6 +3,12 @@
 // Code generated by github.com/deepmap/oapi-codegen version v1.16.2 DO NOT EDIT.
 package providers
 
+// Defines values for ApiKeyOptsType.
+const (
+	InHeader ApiKeyOptsType = "in-header"
+	InQuery  ApiKeyOptsType = "in-query"
+)
+
 // Defines values for AuthType.
 const (
 	ApiKey AuthType = "api_key"
@@ -11,21 +17,26 @@ const (
 	Oauth2 AuthType = "oauth2"
 )
 
-// Defines values for OauthOptsGrantType.
+// Defines values for Oauth2OptsGrantType.
 const (
-	AuthorizationCode OauthOptsGrantType = "authorizationCode"
-	ClientCredentials OauthOptsGrantType = "clientCredentials"
-	PKCE              OauthOptsGrantType = "PKCE"
+	AuthorizationCode Oauth2OptsGrantType = "authorizationCode"
+	ClientCredentials Oauth2OptsGrantType = "clientCredentials"
+	PKCE              Oauth2OptsGrantType = "PKCE"
 )
 
 // ApiKeyOpts defines model for ApiKeyOpts.
 type ApiKeyOpts struct {
 	// DocsURL URL with more information about how to get or use an API key.
-	DocsURL     string `json:"docsURL,omitempty"`
-	HeaderName  string `json:"headerName" validate:"required"`
-	ValuePrefix string `json:"valuePrefix,omitempty"`
+	DocsURL        string         `json:"docsURL,omitempty"`
+	HeaderName     string         `json:"headerName,omitempty"`
+	QueryParamName string         `json:"queryParamName,omitempty"`
+	Type           ApiKeyOptsType `json:"type" validate:"required"`
+	ValuePrefix    string         `json:"valuePrefix,omitempty"`
 }
 
+// ApiKeyOptsType defines model for ApiKeyOpts.Type.
+type ApiKeyOptsType string
+
 // AuthType defines model for AuthType.
 type AuthType string
 
@@ -40,19 +51,19 @@ type BulkWriteSupport struct {
 // CatalogType defines model for CatalogType.
 type CatalogType map[string]ProviderInfo
 
-// OauthOpts defines model for OauthOpts.
-type OauthOpts struct {
+// Oauth2Opts defines model for Oauth2Opts.
+type Oauth2Opts struct {
 	Audience                  string              `json:"audience,omitempty"`
 	AuthURL                   string              `json:"authURL,omitempty"`
 	ExplicitScopesRequired    bool                `json:"explicitScopesRequired"`
 	ExplicitWorkspaceRequired bool                `json:"explicitWorkspaceRequired"`
-	GrantType                 OauthOptsGrantType  `json:"grantType"`
+	GrantType                 Oauth2OptsGrantType `json:"grantType"`
 	TokenMetadataFields       TokenMetadataFields `json:"tokenMetadataFields"`
 	TokenURL                  string              `json:"tokenURL" validate:"required"`
 }
 
-// OauthOptsGrantType defines model for OauthOpts.GrantType.
-type OauthOptsGrantType string
+// Oauth2OptsGrantType defines model for Oauth2Opts.GrantType.
+type Oauth2OptsGrantType string
 
 // Provider defines model for Provider.
 type Provider = string
@@ -64,9 +75,9 @@ type ProviderInfo struct {
 	BaseURL    string      `json:"baseURL" validate:"required"`
 
 	// DisplayName The display name of the provider, if omitted, defaults to provider name.
-	DisplayName string     `json:"displayName,omitempty"`
-	Name        string     `json:"name"`
-	OauthOpts   *OauthOpts `json:"oauthOpts,omitempty"`
+	DisplayName string      `json:"displayName,omitempty"`
+	Name        string      `json:"name"`
+	Oauth2Opts  *Oauth2Opts `json:"oauth2Opts,omitempty"`
 
 	// PostAuthInfoNeeded If true, we require additional information after auth to start making requests.
 	PostAuthInfoNeeded bool         `json:"postAuthInfoNeeded,omitempty"`
diff --git a/providers/types.yaml b/providers/types.yaml
index 1fc9e8e1..ab1468f1 100644
--- a/providers/types.yaml
+++ b/providers/types.yaml
@@ -75,7 +75,7 @@ components:
           example: scopes
           x-go-type-skip-optional-pointer: true
 
-    OauthOpts:
+    Oauth2Opts:
       type: object
       required:
           - grantType
@@ -112,13 +112,21 @@ components:
     ApiKeyOpts:
       type: object
       required:
-        - headerName
+        - type
       properties:
-        headerName:
+        type:
           type: string
-          example: X-Api-Key
+          enum: [in-query, in-header]
           x-oapi-codegen-extra-tags:
             validate: required
+        queryParamName:
+          type: string
+          example: api_key
+          x-go-type-skip-optional-pointer: true
+        headerName:
+          type: string
+          example: X-Api-Key
+          x-go-type-skip-optional-pointer: true
         valuePrefix:
           type: string
           example: "Bearer "
@@ -157,8 +165,8 @@ components:
           type: string
           x-oapi-codegen-extra-tags:
             validate: required
-        oauthOpts:
-          $ref: '#/components/schemas/OauthOpts'
+        oauth2Opts:
+          $ref: '#/components/schemas/Oauth2Opts'
         apiKeyOpts:
           $ref: '#/components/schemas/ApiKeyOpts'
         support:
diff --git a/providers/utils.go b/providers/utils.go
index 39519160..d2fe6058 100644
--- a/providers/utils.go
+++ b/providers/utils.go
@@ -24,8 +24,31 @@ var (
 	ErrClient                  = errors.New("client creation failed")
 )
 
-func ReadCatalog() (CatalogType, error) {
-	catalog, err := clone[CatalogType](catalog)
+type CatalogOption func(params *catalogParams)
+
+type catalogParams struct {
+	catalog CatalogType
+}
+
+// WithCatalog is an option that can be used to override the default catalog.
+func WithCatalog(c CatalogType) CatalogOption {
+	return func(params *catalogParams) {
+		params.catalog = c
+	}
+}
+
+func ReadCatalog(opts ...CatalogOption) (CatalogType, error) {
+	params := &catalogParams{catalog: catalog}
+
+	for _, opt := range opts {
+		opt(params)
+	}
+
+	if params.catalog == nil {
+		return nil, ErrProviderCatalogNotFound
+	}
+
+	catalog, err := clone[CatalogType](params.catalog)
 	if err != nil {
 		return nil, err
 	}
@@ -53,8 +76,18 @@ func SetInfo(provider Provider, info ProviderInfo) {
 
 // ReadInfo reads the information from the catalog for specific provider. It also performs string substitution
 // on the values in the config that are surrounded by {{}}.
-func ReadInfo(provider Provider, substitutions *map[string]string) (*ProviderInfo, error) {
-	pInfo, ok := catalog[provider]
+func ReadInfo(provider Provider, substitutions *map[string]string, opts ...CatalogOption) (*ProviderInfo, error) {
+	params := &catalogParams{catalog: catalog}
+
+	for _, opt := range opts {
+		opt(params)
+	}
+
+	if params.catalog == nil {
+		return nil, ErrProviderCatalogNotFound
+	}
+
+	pInfo, ok := params.catalog[provider]
 	if !ok {
 		return nil, ErrProviderCatalogNotFound
 	}
@@ -214,11 +247,11 @@ func (i *ProviderInfo) NewClient(ctx context.Context, params *NewClientParams) (
 	case None:
 		return createUnauthenticatedClient(ctx, params.Client, params.Debug)
 	case Oauth2:
-		if i.OauthOpts == nil {
+		if i.Oauth2Opts == nil {
 			return nil, fmt.Errorf("%w: %s", ErrClient, "oauth2 options not found")
 		}
 
-		switch i.OauthOpts.GrantType {
+		switch i.Oauth2Opts.GrantType {
 		case AuthorizationCode:
 			return createOAuth2AuthCodeHTTPClient(ctx, params.Client, params.Debug, params.OAuth2AuthCodeCreds)
 		case ClientCredentials:
@@ -226,7 +259,7 @@ func (i *ProviderInfo) NewClient(ctx context.Context, params *NewClientParams) (
 		case PKCE:
 			return nil, fmt.Errorf("%w: %s", ErrClient, "PKCE grant type not supported")
 		default:
-			return nil, fmt.Errorf("%w: unsupported grant type %q", ErrClient, i.OauthOpts.GrantType)
+			return nil, fmt.Errorf("%w: unsupported grant type %q", ErrClient, i.Oauth2Opts.GrantType)
 		}
 	case Basic:
 		if params.BasicCreds == nil {
@@ -354,24 +387,43 @@ func createApiKeyHTTPClient( //nolint:ireturn
 	info *ProviderInfo,
 	apiKey string,
 ) (common.AuthenticatedHTTPClient, error) {
-	if info.ApiKeyOpts.ValuePrefix != "" {
-		apiKey = info.ApiKeyOpts.ValuePrefix + apiKey
-	}
+	if info.ApiKeyOpts.Type == InHeader { //nolint:nestif
+		if info.ApiKeyOpts.ValuePrefix != "" {
+			apiKey = info.ApiKeyOpts.ValuePrefix + apiKey
+		}
 
-	opts := []common.HeaderAuthClientOption{
-		common.WithHeaderClient(getClient(client)),
-	}
+		opts := []common.HeaderAuthClientOption{
+			common.WithHeaderClient(getClient(client)),
+		}
 
-	if dbg {
-		opts = append(opts, common.WithHeaderDebug(common.PrintRequestAndResponse))
-	}
+		if dbg {
+			opts = append(opts, common.WithHeaderDebug(common.PrintRequestAndResponse))
+		}
 
-	c, err := common.NewApiKeyAuthHTTPClient(ctx, info.ApiKeyOpts.HeaderName, apiKey, opts...)
-	if err != nil {
-		panic(err)
+		c, err := common.NewApiKeyHeaderAuthHTTPClient(ctx, info.ApiKeyOpts.HeaderName, apiKey, opts...)
+		if err != nil {
+			return nil, fmt.Errorf("%w: failed to create api key client: %w", ErrClient, err)
+		}
+
+		return c, nil
+	} else if info.ApiKeyOpts.Type == InQuery {
+		opts := []common.QueryParamAuthClientOption{
+			common.WithQueryParamClient(getClient(client)),
+		}
+
+		if dbg {
+			opts = append(opts, common.WithQueryParamDebug(common.PrintRequestAndResponse))
+		}
+
+		c, err := common.NewApiKeyQueryParamAuthHTTPClient(ctx, info.ApiKeyOpts.QueryParamName, apiKey, opts...)
+		if err != nil {
+			return nil, fmt.Errorf("%w: failed to create api key client: %w", ErrClient, err)
+		}
+
+		return c, nil
 	}
 
-	return c, nil
+	return nil, fmt.Errorf("%w: unsupported api key type %q", ErrClient, info.ApiKeyOpts.Type)
 }
 
 // clone uses gob to deep copy objects.
diff --git a/salesloft/metadata/queryParamStats.json b/salesloft/metadata/queryParamStats.json
new file mode 100755
index 00000000..b513abc0
--- /dev/null
+++ b/salesloft/metadata/queryParamStats.json
@@ -0,0 +1,308 @@
+{
+  "meta": {
+    "totalObjects": 48,
+    "collectedAt": "2024-06-10"
+  },
+  "queryParams": [
+    {
+      "name": "updated_at[gt]",
+      "frequency": 0.3125,
+      "totalObjects": 15,
+      "objects": [
+        "account_stages",
+        "accounts",
+        "actions",
+        "activities_calls",
+        "activities_emails",
+        "activity_histories",
+        "cadence_memberships",
+        "cadences",
+        "crm_activities",
+        "email_templates",
+        "notes",
+        "people",
+        "successes",
+        "tasks",
+        "team_templates"
+      ]
+    },
+    {
+      "name": "updated_at[gte]",
+      "frequency": 0.3125,
+      "totalObjects": 15,
+      "objects": [
+        "account_stages",
+        "accounts",
+        "actions",
+        "activities_calls",
+        "activities_emails",
+        "activity_histories",
+        "cadence_memberships",
+        "cadences",
+        "crm_activities",
+        "email_templates",
+        "notes",
+        "people",
+        "successes",
+        "tasks",
+        "team_templates"
+      ]
+    },
+    {
+      "name": "updated_at[lt]",
+      "frequency": 0.3125,
+      "totalObjects": 15,
+      "objects": [
+        "account_stages",
+        "accounts",
+        "actions",
+        "activities_calls",
+        "activities_emails",
+        "activity_histories",
+        "cadence_memberships",
+        "cadences",
+        "crm_activities",
+        "email_templates",
+        "notes",
+        "people",
+        "successes",
+        "tasks",
+        "team_templates"
+      ]
+    },
+    {
+      "name": "updated_at[lte]",
+      "frequency": 0.3125,
+      "totalObjects": 15,
+      "objects": [
+        "account_stages",
+        "accounts",
+        "actions",
+        "activities_calls",
+        "activities_emails",
+        "activity_histories",
+        "cadence_memberships",
+        "cadences",
+        "crm_activities",
+        "email_templates",
+        "notes",
+        "people",
+        "successes",
+        "tasks",
+        "team_templates"
+      ]
+    },
+    {
+      "name": "created_at[gt]",
+      "frequency": 0.1041,
+      "totalObjects": 5,
+      "objects": [
+        "accounts",
+        "activities_calls",
+        "imports",
+        "meetings",
+        "people"
+      ]
+    },
+    {
+      "name": "created_at[gte]",
+      "frequency": 0.1041,
+      "totalObjects": 5,
+      "objects": [
+        "accounts",
+        "activities_calls",
+        "imports",
+        "meetings",
+        "people"
+      ]
+    },
+    {
+      "name": "created_at[lt]",
+      "frequency": 0.1041,
+      "totalObjects": 5,
+      "objects": [
+        "accounts",
+        "activities_calls",
+        "imports",
+        "meetings",
+        "people"
+      ]
+    },
+    {
+      "name": "created_at[lte]",
+      "frequency": 0.1041,
+      "totalObjects": 5,
+      "objects": [
+        "accounts",
+        "activities_calls",
+        "imports",
+        "meetings",
+        "people"
+      ]
+    },
+    {
+      "name": "last_contacted[gt]",
+      "frequency": 0.0416,
+      "totalObjects": 2,
+      "objects": [
+        "accounts",
+        "people"
+      ]
+    },
+    {
+      "name": "last_contacted[gte]",
+      "frequency": 0.0416,
+      "totalObjects": 2,
+      "objects": [
+        "accounts",
+        "people"
+      ]
+    },
+    {
+      "name": "last_contacted[lt]",
+      "frequency": 0.0416,
+      "totalObjects": 2,
+      "objects": [
+        "accounts",
+        "people"
+      ]
+    },
+    {
+      "name": "last_contacted[lte]",
+      "frequency": 0.0416,
+      "totalObjects": 2,
+      "objects": [
+        "accounts",
+        "people"
+      ]
+    },
+    {
+      "name": "due_on[gt]",
+      "frequency": 0.0208,
+      "totalObjects": 1,
+      "objects": [
+        "actions"
+      ]
+    },
+    {
+      "name": "due_on[gte]",
+      "frequency": 0.0208,
+      "totalObjects": 1,
+      "objects": [
+        "actions"
+      ]
+    },
+    {
+      "name": "due_on[lt]",
+      "frequency": 0.0208,
+      "totalObjects": 1,
+      "objects": [
+        "actions"
+      ]
+    },
+    {
+      "name": "due_on[lte]",
+      "frequency": 0.0208,
+      "totalObjects": 1,
+      "objects": [
+        "actions"
+      ]
+    },
+    {
+      "name": "occurred_at[gt]",
+      "frequency": 0.0208,
+      "totalObjects": 1,
+      "objects": [
+        "activity_histories"
+      ]
+    },
+    {
+      "name": "occurred_at[gte]",
+      "frequency": 0.0208,
+      "totalObjects": 1,
+      "objects": [
+        "activity_histories"
+      ]
+    },
+    {
+      "name": "occurred_at[lt]",
+      "frequency": 0.0208,
+      "totalObjects": 1,
+      "objects": [
+        "activity_histories"
+      ]
+    },
+    {
+      "name": "occurred_at[lte]",
+      "frequency": 0.0208,
+      "totalObjects": 1,
+      "objects": [
+        "activity_histories"
+      ]
+    },
+    {
+      "name": "sent_at[gt]",
+      "frequency": 0.0208,
+      "totalObjects": 1,
+      "objects": [
+        "activities_emails"
+      ]
+    },
+    {
+      "name": "sent_at[gte]",
+      "frequency": 0.0208,
+      "totalObjects": 1,
+      "objects": [
+        "activities_emails"
+      ]
+    },
+    {
+      "name": "sent_at[lt]",
+      "frequency": 0.0208,
+      "totalObjects": 1,
+      "objects": [
+        "activities_emails"
+      ]
+    },
+    {
+      "name": "sent_at[lte]",
+      "frequency": 0.0208,
+      "totalObjects": 1,
+      "objects": [
+        "activities_emails"
+      ]
+    },
+    {
+      "name": "start_time[gt]",
+      "frequency": 0.0208,
+      "totalObjects": 1,
+      "objects": [
+        "meetings"
+      ]
+    },
+    {
+      "name": "start_time[gte]",
+      "frequency": 0.0208,
+      "totalObjects": 1,
+      "objects": [
+        "meetings"
+      ]
+    },
+    {
+      "name": "start_time[lt]",
+      "frequency": 0.0208,
+      "totalObjects": 1,
+      "objects": [
+        "meetings"
+      ]
+    },
+    {
+      "name": "start_time[lte]",
+      "frequency": 0.0208,
+      "totalObjects": 1,
+      "objects": [
+        "meetings"
+      ]
+    }
+  ]
+}
\ No newline at end of file
diff --git a/salesloft/read.go b/salesloft/read.go
index 02a96e37..916dbdf9 100644
--- a/salesloft/read.go
+++ b/salesloft/read.go
@@ -3,6 +3,7 @@ package salesloft
 import (
 	"context"
 	"strconv"
+	"time"
 
 	"github.com/amp-labs/connectors/common"
 	"github.com/amp-labs/connectors/common/urlbuilder"
@@ -43,5 +44,14 @@ func (c *Connector) buildReadURL(config common.ReadParams) (*urlbuilder.URL, err
 
 	link.WithQueryParam("per_page", strconv.Itoa(DefaultPageSize))
 
+	if !config.Since.IsZero() {
+		// Documentation states ISO8601, while server accepts different formats
+		// but for consistency we are sticking to one format to be sent.
+		// For the reference any API resource that includes time data type mentions iso8601 string format.
+		// One example, say accounts is https://developers.salesloft.com/docs/api/accounts-index
+		updatedSince := config.Since.Format(time.RFC3339Nano)
+		link.WithQueryParam("updated_at[gte]", updatedSince)
+	}
+
 	return link, nil
 }
diff --git a/scripts/catalog/json.go b/scripts/catalog/json.go
new file mode 100644
index 00000000..7b8f3abe
--- /dev/null
+++ b/scripts/catalog/json.go
@@ -0,0 +1,32 @@
+package main
+
+import (
+	"encoding/json"
+	"log"
+	"os"
+
+	"github.com/amp-labs/connectors/providers"
+)
+
+const writePerm = 0o644
+
+func main() {
+	catalog, err := providers.ReadCatalog()
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	bytes, err := json.Marshal(catalog)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	tempFile := "providers/catalog.json"
+
+	err = os.WriteFile(tempFile, bytes, writePerm)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	log.Printf("Catalog successfully written to: %s\n", tempFile)
+}
diff --git a/scripts/oauth/token.go b/scripts/oauth/token.go
index 905649ce..a32ce078 100644
--- a/scripts/oauth/token.go
+++ b/scripts/oauth/token.go
@@ -99,7 +99,7 @@ var readers = []utils.Reader{
 
 // OAuthApp is a simple OAuth app that can be used to get an OAuth token.
 type OAuthApp struct {
-	GrantType         providers.OauthOptsGrantType
+	GrantType         providers.Oauth2OptsGrantType
 	Callback          string
 	Port              int
 	Config            *oauth2.Config
@@ -200,6 +200,7 @@ func (a *OAuthApp) processCallback(writer http.ResponseWriter, request *http.Req
 func (a *OAuthApp) Run() error {
 	if a.GrantType == providers.ClientCredentials {
 		src := a.ClientCredsConfig.TokenSource(context.Background())
+
 		tok, err := src.Token()
 		if err != nil {
 			return err
@@ -297,7 +298,7 @@ func setup() *OAuthApp {
 		os.Exit(1)
 	}
 
-	if providerInfo.OauthOpts == nil {
+	if providerInfo.Oauth2Opts == nil {
 		slog.Error("provider does not have OAuth2 options, not compatible with this script", "provider", provider)
 
 		os.Exit(1)
@@ -314,9 +315,9 @@ func setup() *OAuthApp {
 
 	oauthScopes := strings.Split(scopes, ",")
 
-	switch providerInfo.OauthOpts.GrantType {
+	switch providerInfo.Oauth2Opts.GrantType {
 	case providers.AuthorizationCode:
-		if providerInfo.OauthOpts.AuthURL == "" {
+		if providerInfo.Oauth2Opts.AuthURL == "" {
 			slog.Error("provider does not have an AuthURL, not compatible with this script", "provider", provider)
 
 			os.Exit(1)
@@ -351,8 +352,8 @@ func setup() *OAuthApp {
 
 		// Set up the OAuth config based on the provider.
 		app.Config.Endpoint = oauth2.Endpoint{
-			AuthURL:   providerInfo.OauthOpts.AuthURL,
-			TokenURL:  providerInfo.OauthOpts.TokenURL,
+			AuthURL:   providerInfo.Oauth2Opts.AuthURL,
+			TokenURL:  providerInfo.Oauth2Opts.TokenURL,
 			AuthStyle: oauth2.AuthStyleAutoDetect,
 		}
 
@@ -369,7 +370,7 @@ func setup() *OAuthApp {
 			ClientCredsConfig: &clientcredentials.Config{
 				ClientID:     clientId,
 				ClientSecret: clientSecret,
-				TokenURL:     providerInfo.OauthOpts.TokenURL,
+				TokenURL:     providerInfo.Oauth2Opts.TokenURL,
 				Scopes:       oauthScopes,
 				AuthStyle:    oauth2.AuthStyleAutoDetect,
 			},
@@ -378,8 +379,8 @@ func setup() *OAuthApp {
 			app.State = state
 		}
 
-		if providerInfo.OauthOpts.Audience != "" {
-			aud := providerInfo.OauthOpts.Audience
+		if providerInfo.Oauth2Opts.Audience != "" {
+			aud := providerInfo.Oauth2Opts.Audience
 			app.ClientCredsConfig.EndpointParams = url.Values{"audience": {aud}}
 		}
 
diff --git a/scripts/proxy/proxy.go b/scripts/proxy/proxy.go
index 8c36137b..15836461 100644
--- a/scripts/proxy/proxy.go
+++ b/scripts/proxy/proxy.go
@@ -158,17 +158,17 @@ func main() {
 
 	switch info.AuthType {
 	case providers.Oauth2:
-		if info.OauthOpts == nil {
+		if info.Oauth2Opts == nil {
 			log.Fatalf("Missing OAuth options for provider %s", provider)
 		}
 
-		switch info.OauthOpts.GrantType {
+		switch info.Oauth2Opts.GrantType {
 		case providers.ClientCredentials:
 			mainOAuth2ClientCreds(ctx, provider, substitutionsMap)
 		case providers.AuthorizationCode:
 			mainOAuth2AuthCode(ctx, provider, substitutionsMap)
 		default:
-			log.Fatalf("Unsupported OAuth2 grant type: %s", info.OauthOpts.GrantType)
+			log.Fatalf("Unsupported OAuth2 grant type: %s", info.Oauth2Opts.GrantType)
 		}
 	case providers.ApiKey:
 		mainApiKey(ctx, provider, substitutionsMap)
@@ -190,7 +190,7 @@ func mainOAuth2ClientCreds(ctx context.Context, provider string, substitutionsMa
 
 	oauthScopes := strings.Split(scopes, ",")
 
-	validateRequiredOAuth2Flags(ctx, provider, clientId, clientSecret)
+	validateRequiredOAuth2Flags(provider, clientId, clientSecret)
 	startOAuthClientCredsProxy(ctx, provider, oauthScopes, clientId, clientSecret, substitutionsMap, DefaultPort)
 }
 
@@ -206,7 +206,7 @@ func mainOAuth2AuthCode(ctx context.Context, provider string, substitutionsMap m
 
 	oauthScopes := strings.Split(scopes, ",")
 
-	validateRequiredOAuth2Flags(ctx, provider, clientId, clientSecret)
+	validateRequiredOAuth2Flags(provider, clientId, clientSecret)
 	startOAuthAuthCodeProxy(ctx, provider, oauthScopes, clientId, clientSecret, substitutionsMap, DefaultPort, tokens)
 }
 
@@ -236,6 +236,7 @@ func mainBasic(ctx context.Context, provider string, substitutionsMap map[string
 func getTokensFromRegistry() *oauth2.Token {
 	accessToken := registry.MustString("AccessToken")
 	refreshToken, err := registry.GetString("RefreshToken")
+
 	if err != nil {
 		// we are working without refresh token
 		return &oauth2.Token{
@@ -286,7 +287,7 @@ func parseAccessTokenExpiry(expiryStr, timeFormat string) time.Time {
 	return expiry
 }
 
-func validateRequiredOAuth2Flags(ctx context.Context, provider, clientId, clientSecret string) {
+func validateRequiredOAuth2Flags(provider, clientId, clientSecret string) {
 	if provider == "" || clientId == "" || clientSecret == "" {
 		_, _ = fmt.Fprintln(os.Stderr, "Missing required flags: -provider, -client-id, -client-secret")
 
@@ -436,12 +437,15 @@ func configureOAuthClientCredentials(clientId, clientSecret string, scopes []str
 	cfg := &clientcredentials.Config{
 		ClientID:     clientId,
 		ClientSecret: clientSecret,
-		Scopes:       scopes,
-		TokenURL:     providerInfo.OauthOpts.TokenURL,
+		TokenURL:     providerInfo.Oauth2Opts.TokenURL,
+	}
+
+	if providerInfo.Oauth2Opts.ExplicitScopesRequired {
+		cfg.Scopes = scopes
 	}
 
-	if providerInfo.OauthOpts.Audience != "" {
-		aud := providerInfo.OauthOpts.Audience
+	if providerInfo.Oauth2Opts.Audience != "" {
+		aud := providerInfo.Oauth2Opts.Audience
 		cfg.EndpointParams = url.Values{"audience": {aud}}
 	}
 
@@ -454,8 +458,8 @@ func configureOAuthAuthCode(clientId, clientSecret string, scopes []string, prov
 		ClientSecret: clientSecret,
 		Scopes:       scopes,
 		Endpoint: oauth2.Endpoint{
-			AuthURL:   providerInfo.OauthOpts.AuthURL,
-			TokenURL:  providerInfo.OauthOpts.TokenURL,
+			AuthURL:   providerInfo.Oauth2Opts.AuthURL,
+			TokenURL:  providerInfo.Oauth2Opts.TokenURL,
 			AuthStyle: oauth2.AuthStyleAutoDetect,
 		},
 	}
diff --git a/scripts/scrapper/salesloft/metadata/main.go b/scripts/scrapper/salesloft/metadata/main.go
index aab96f77..f92a4ce1 100644
--- a/scripts/scrapper/salesloft/metadata/main.go
+++ b/scripts/scrapper/salesloft/metadata/main.go
@@ -1,7 +1,9 @@
 package main
 
 import (
+	"flag"
 	"log"
+	"sort"
 	"strings"
 
 	"github.com/PuerkitoBio/goquery"
@@ -17,11 +19,23 @@ const (
 	ModelIndexURL = "https://developers.salesloft.com/docs/api"
 )
 
+var withQueryParamStats bool // nolint:gochecknoglobals
+
+func init() {
+	flag.BoolVar(&withQueryParamStats, "queryParamStats", false,
+		"collect statistics on query parameters")
+	flag.Parse()
+}
+
 func main() {
-	// index will have URLs for every schema
-	createIndex()
-	// using index file collect response fields for every object
-	createSchemas()
+	if withQueryParamStats {
+		createQueryParamStats()
+	} else {
+		// index will have URLs for every schema
+		createIndex()
+		// using index file collect response fields for every object
+		createSchemas()
+	}
 
 	log.Println("Completed.")
 }
@@ -77,6 +91,63 @@ func createSchemas() {
 	must(metadata.FileManager.SaveSchemas(schemas))
 }
 
+func createQueryParamStats() {
+	index, err := metadata.FileManager.LoadIndex()
+	must(err)
+
+	registry := make(map[string][]string)
+
+	filteredListDocs := getFilteredListDocs(index)
+	numObjects := len(filteredListDocs)
+
+	for i, model := range filteredListDocs { // nolint:varnamelen
+		doc := scrapper.QueryHTML(model.URL)
+
+		modelName := strcase.ToSnake(model.Name)
+
+		doc.Find(`.openapi-params__list-item .openapi-schema__property`).Each(func(i int, element *goquery.Selection) {
+			prop := element.Text()
+			if _, found := registry[prop]; !found {
+				registry[prop] = make([]string, 0)
+			}
+
+			registry[prop] = append(registry[prop], modelName)
+		})
+
+		log.Printf("Query param schemas completed %.2f%% [%v]\n", getPercentage(i, numObjects), modelName)
+	}
+
+	// create set of query parameters
+	properties := make([]string, 0)
+
+	for prop := range registry {
+		if strings.Contains(prop, "[") {
+			properties = append(properties, prop)
+		}
+	}
+	// sort query parameters, where most occurred come first
+	sort.SliceStable(properties, func(i, j int) bool {
+		a := properties[i]
+		b := properties[j]
+		l1 := len(registry[a])
+		l2 := len(registry[b])
+
+		if l1 == l2 {
+			return a < b
+		}
+
+		return l1 > l2
+	})
+
+	// finally prepare to write to file
+	stats := scrapper.NewQueryParamStats(numObjects)
+	for _, prop := range properties {
+		stats.Add(prop, registry[prop])
+	}
+
+	must(metadata.FileManager.SaveQueryParamStats(stats))
+}
+
 /*
 Index file has these suffixes for model name (Total 134):
   - destroy 2
diff --git a/tools/scrapper/files.go b/tools/scrapper/files.go
index 594efe02..763bbc0c 100644
--- a/tools/scrapper/files.go
+++ b/tools/scrapper/files.go
@@ -3,8 +3,9 @@ package scrapper
 import "encoding/json"
 
 const (
-	IndexFile   = "index.json"
-	SchemasFile = "schemas.json"
+	IndexFile           = "index.json"
+	SchemasFile         = "schemas.json"
+	QueryParamStatsFile = "queryParamStats.json"
 )
 
 // MetadataFileLocator locates index and schema files.
@@ -56,3 +57,7 @@ func (m MetadataFileManager) LoadSchemas() (*ObjectMetadataResult, error) {
 
 	return result, nil
 }
+
+func (m MetadataFileManager) SaveQueryParamStats(stats *QueryParamStats) error {
+	return FlushToFile(m.locator.AbsPathTo(QueryParamStatsFile), stats)
+}
diff --git a/tools/scrapper/models.go b/tools/scrapper/models.go
index da9a9618..a26e7dbd 100644
--- a/tools/scrapper/models.go
+++ b/tools/scrapper/models.go
@@ -1,8 +1,12 @@
 package scrapper
 
 import (
+	"errors"
+	"fmt"
+	"math"
 	"sort"
 	"strings"
+	"time"
 )
 
 type ModelDocLinks []ModelDocLink
@@ -87,3 +91,76 @@ func (r *ObjectMetadataResult) Add(objectName string, objectDisplayName string,
 
 	data.FieldsMap[fieldName] = fieldName
 }
+
+type QueryParamStats struct {
+	Meta queryParamStatsMeta     `json:"meta"`
+	Data []queryParamObjectStats `json:"queryParams"`
+}
+
+type queryParamStatsMeta struct {
+	TotalObjects int      `json:"totalObjects"`
+	CollectedAt  DateTime `json:"collectedAt"`
+}
+
+type queryParamObjectStats struct {
+	Name         string   `json:"name"`
+	Frequency    float64  `json:"frequency"`
+	TotalObjects int      `json:"totalObjects"`
+	Objects      []string `json:"objects"`
+}
+
+func NewQueryParamStats(totalObjects int) *QueryParamStats {
+	return &QueryParamStats{
+		Meta: queryParamStatsMeta{
+			TotalObjects: totalObjects,
+			CollectedAt:  DateTime{Time: time.Now()},
+		},
+		Data: make([]queryParamObjectStats, 0),
+	}
+}
+
+func (s *QueryParamStats) Add(queryParamName string, objectNames []string) {
+	num := len(objectNames)
+	freq := float64(num) / float64(s.Meta.TotalObjects)
+	s.Data = append(s.Data, queryParamObjectStats{
+		Name:         queryParamName,
+		Frequency:    roundFloat(freq, 4), // nolint:gomnd
+		TotalObjects: num,
+		Objects:      objectNames,
+	})
+}
+
+func roundFloat(f float64, decPlaces int) float64 {
+	target := math.Pow(10, float64(decPlaces)) // nolint:gomnd
+
+	return float64(int(f*target)) / target
+}
+
+type DateTime struct {
+	Time time.Time
+}
+
+func (s DateTime) MarshalJSON() ([]byte, error) {
+	format := s.Time.Format(time.DateOnly)
+
+	return []byte(fmt.Sprintf(`"%v"`, format)), nil
+}
+
+func (s *DateTime) UnmarshalJSON(bytes []byte) error {
+	str := string(bytes)
+	// remove string quotes
+	if len(str) < 2 { // nolint:gomnd
+		return errors.New("date time has no quotes") // nolint:goerr113
+	}
+
+	format := str[1 : len(str)-1]
+
+	t, err := time.Parse(time.DateTime, format)
+	if err != nil {
+		return err
+	}
+
+	s.Time = t
+
+	return nil
+}


Test patch:
diff --git a/salesloft/read_test.go b/salesloft/read_test.go
index 8f36650a..a0bb2ed5 100644
--- a/salesloft/read_test.go
+++ b/salesloft/read_test.go
@@ -5,9 +5,11 @@ import (
 	"errors"
 	"net/http"
 	"net/http/httptest"
+	"net/url"
 	"reflect"
 	"strings"
 	"testing"
+	"time"
 
 	"github.com/amp-labs/connectors/common"
 	"github.com/amp-labs/connectors/common/interpreter"
@@ -22,6 +24,10 @@ func TestRead(t *testing.T) { //nolint:funlen,gocognit,cyclop,maintidx
 	responseEmptyRead := mockutils.DataFromFile(t, "read-empty.json")
 	responseListPeople := mockutils.DataFromFile(t, "read-list-people.json")
 	responseListUsers := mockutils.DataFromFile(t, "read-list-users.json")
+	responseListAccounts := mockutils.DataFromFile(t, "read-list-accounts.json")
+	responseListAccountsSince := mockutils.DataFromFile(t, "read-list-accounts-since.json")
+	accountsSince, err := time.Parse(time.RFC3339Nano, "2024-06-07T10:51:20.851224-04:00")
+	mockutils.NoErrors(t, err)
 
 	tests := []struct {
 		name         string
@@ -193,6 +199,47 @@ func TestRead(t *testing.T) { //nolint:funlen,gocognit,cyclop,maintidx
 			},
 			expectedErrs: nil,
 		},
+		{
+			name:  "Successful read accounts without since query",
+			input: common.ReadParams{ObjectName: "accounts"},
+			server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+				w.Header().Set("Content-Type", "application/json")
+				mockutils.RespondToMissingQueryParameters(w, r, []string{"updated_at[gte]"}, func() {
+					w.WriteHeader(http.StatusOK)
+					_, _ = w.Write(responseListAccounts)
+				})
+			})),
+			comparator: func(baseURL string, actual, expected *common.ReadResult) bool {
+				return actual.Rows == expected.Rows
+			},
+			expected: &common.ReadResult{
+				Rows: 4,
+			},
+			expectedErrs: nil,
+		},
+		{
+			name: "Successful read accounts since point in time",
+			input: common.ReadParams{
+				ObjectName: "accounts",
+				Since:      accountsSince,
+			},
+			server: httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+				w.Header().Set("Content-Type", "application/json")
+				mockutils.RespondToQueryParameters(w, r, url.Values{
+					"updated_at[gte]": []string{"2024-06-07T10:51:20.851224-04:00"},
+				}, func() {
+					w.WriteHeader(http.StatusOK)
+					_, _ = w.Write(responseListAccountsSince)
+				})
+			})),
+			comparator: func(baseURL string, actual, expected *common.ReadResult) bool {
+				return actual.Rows == expected.Rows
+			},
+			expected: &common.ReadResult{
+				Rows: 2,
+			},
+			expectedErrs: nil,
+		},
 	}
 
 	for _, tt := range tests {
diff --git a/salesloft/test/read-list-accounts-since.json b/salesloft/test/read-list-accounts-since.json
new file mode 100644
index 00000000..4c2f3568
--- /dev/null
+++ b/salesloft/test/read-list-accounts-since.json
@@ -0,0 +1,67 @@
+{
+  "metadata": {
+    "filtering": {
+      "updated_at": {
+        "gte": "2024-06-07T14:51:20.851224Z"
+      }
+    },
+    "paging": {
+      "per_page": 25,
+      "current_page": 1,
+      "next_page": null,
+      "prev_page": null
+    },
+    "sorting": {
+      "sort_by": "updated_at",
+      "sort_direction": "DESC NULLS LAST"
+    }
+  },
+  "data": [
+    {
+      "id": 48371814,
+      "created_at": "2024-06-07T10:51:48.966580-04:00",
+      "updated_at": "2024-06-07T10:51:48.966580-04:00",
+      "name": "Reebok",
+      "domain": "https://www.reebok.eu/en-gb/",
+      "crm_object_type": "account",
+      "do_not_contact": false,
+      "custom_fields": {},
+      "user_relationships": [],
+      "tags": [],
+      "counts": {
+        "people": null
+      },
+      "owner": {
+        "_href": "https://api.salesloft.com/v2/users/49067",
+        "id": 49067
+      },
+      "creator": {
+        "_href": "https://api.salesloft.com/v2/users/49067",
+        "id": 49067
+      }
+    },
+    {
+      "id": 48371806,
+      "created_at": "2024-06-07T10:51:20.851224-04:00",
+      "updated_at": "2024-06-07T10:51:20.851224-04:00",
+      "name": "Asics",
+      "domain": "https://www.asics.com/gb/en-gb/",
+      "crm_object_type": "account",
+      "do_not_contact": false,
+      "custom_fields": {},
+      "user_relationships": [],
+      "tags": [],
+      "counts": {
+        "people": null
+      },
+      "owner": {
+        "_href": "https://api.salesloft.com/v2/users/49067",
+        "id": 49067
+      },
+      "creator": {
+        "_href": "https://api.salesloft.com/v2/users/49067",
+        "id": 49067
+      }
+    }
+  ]
+}
\ No newline at end of file
diff --git a/salesloft/test/read-list-accounts.json b/salesloft/test/read-list-accounts.json
new file mode 100644
index 00000000..9fea1223
--- /dev/null
+++ b/salesloft/test/read-list-accounts.json
@@ -0,0 +1,109 @@
+{
+  "metadata": {
+    "filtering": {},
+    "paging": {
+      "per_page": 25,
+      "current_page": 1,
+      "next_page": null,
+      "prev_page": null
+    },
+    "sorting": {
+      "sort_by": "updated_at",
+      "sort_direction": "DESC NULLS LAST"
+    }
+  },
+  "data": [
+    {
+      "id": 48371814,
+      "created_at": "2024-06-07T10:51:48.966580-04:00",
+      "updated_at": "2024-06-07T10:51:48.966580-04:00",
+      "name": "Reebok",
+      "domain": "https://www.reebok.eu/en-gb/",
+      "crm_object_type": "account",
+      "do_not_contact": false,
+      "custom_fields": {},
+      "user_relationships": [],
+      "tags": [],
+      "counts": {
+        "people": null
+      },
+      "owner": {
+        "_href": "https://api.salesloft.com/v2/users/49067",
+        "id": 49067
+      },
+      "creator": {
+        "_href": "https://api.salesloft.com/v2/users/49067",
+        "id": 49067
+      }
+    },
+    {
+      "id": 48371806,
+      "created_at": "2024-06-07T10:51:20.851224-04:00",
+      "updated_at": "2024-06-07T10:51:20.851224-04:00",
+      "name": "Asics",
+      "domain": "https://www.asics.com/gb/en-gb/",
+      "crm_object_type": "account",
+      "do_not_contact": false,
+      "custom_fields": {},
+      "user_relationships": [],
+      "tags": [],
+      "counts": {
+        "people": null
+      },
+      "owner": {
+        "_href": "https://api.salesloft.com/v2/users/49067",
+        "id": 49067
+      },
+      "creator": {
+        "_href": "https://api.salesloft.com/v2/users/49067",
+        "id": 49067
+      }
+    },
+    {
+      "id": 48371772,
+      "created_at": "2024-06-07T10:50:43.502576-04:00",
+      "updated_at": "2024-06-07T10:50:43.502576-04:00",
+      "name": "Puma",
+      "domain": "https://us.puma.com/us/en",
+      "crm_object_type": "account",
+      "do_not_contact": false,
+      "custom_fields": {},
+      "user_relationships": [],
+      "tags": [],
+      "counts": {
+        "people": null
+      },
+      "owner": {
+        "_href": "https://api.salesloft.com/v2/users/49067",
+        "id": 49067
+      },
+      "creator": {
+        "_href": "https://api.salesloft.com/v2/users/49067",
+        "id": 49067
+      }
+    },
+    {
+      "id": 48316458,
+      "created_at": "2024-06-06T12:35:41.051972-04:00",
+      "updated_at": "2024-06-06T12:35:41.051972-04:00",
+      "name": "Nike",
+      "domain": "https://www.nike.com",
+      "crm_object_type": "account",
+      "do_not_contact": false,
+      "custom_fields": {},
+      "user_relationships": [],
+      "tags": [],
+      "counts": {
+        "people": null
+      },
+      "owner": {
+        "_href": "https://api.salesloft.com/v2/users/49067",
+        "id": 49067
+      },
+      "creator": {
+        "_href": "https://api.salesloft.com/v2/users/49067",
+        "id": 49067
+      }
+    }
+  ]
+}
\ No newline at end of file
diff --git a/test/utils/mockutils/checks.go b/test/utils/mockutils/checks.go
index 0c20b837..e1b8d5a1 100644
--- a/test/utils/mockutils/checks.go
+++ b/test/utils/mockutils/checks.go
@@ -1,6 +1,9 @@
 package mockutils
 
-import "fmt"
+import (
+	"fmt"
+	"testing"
+)
 
 func DoesObjectCorrespondToString(object any, correspondent string) bool {
 	if object == nil && len(correspondent) == 0 {
@@ -9,3 +12,9 @@ func DoesObjectCorrespondToString(object any, correspondent string) bool {
 
 	return fmt.Sprintf("%v", object) == correspondent
 }
+
+func NoErrors(t *testing.T, err error) {
+	if err != nil {
+		t.Fatalf("failed to start test, %v", err)
+	}
+}
diff --git a/test/utils/mockutils/server.go b/test/utils/mockutils/server.go
index f546bcfc..9743a7e7 100644
--- a/test/utils/mockutils/server.go
+++ b/test/utils/mockutils/server.go
@@ -3,6 +3,7 @@ package mockutils
 import (
 	"fmt"
 	"net/http"
+	"net/url"
 )
 
 func RespondNoContentForMethod(w http.ResponseWriter, r *http.Request, methodName string) {
@@ -42,6 +43,36 @@ func RespondToHeader(w http.ResponseWriter, r *http.Request, header http.Header,
 	}
 }
 
+func RespondToQueryParameters(w http.ResponseWriter, r *http.Request, queries url.Values, onSuccess func()) {
+	// if some query parameters are mismatching return error code so the test will fail
+	if queryParam, ok := queryParamsAreSubset(r.URL.Query(), queries); ok {
+		// if method is matching headers
+		onSuccess()
+	} else {
+		w.WriteHeader(http.StatusBadRequest)
+		WriteBody(w, fmt.Sprintf(`{
+			"error": {
+				"code": "from test",
+				"message": "test server mismatching [%v] query parameter"
+			}}`, queryParam))
+	}
+}
+
+func RespondToMissingQueryParameters(w http.ResponseWriter, r *http.Request, missingQueries []string, onSuccess func()) {
+	// if at least one query parameter exists return error code so the test will fail
+	if queryParam, ok := queryParamsMissing(r.URL.Query(), missingQueries); ok {
+		// if method is matching headers
+		onSuccess()
+	} else {
+		w.WriteHeader(http.StatusBadRequest)
+		WriteBody(w, fmt.Sprintf(`{
+			"error": {
+				"code": "from test",
+				"message": "test server found [%v] query parameter"
+			}}`, queryParam))
+	}
+}
+
 func WriteBody(w http.ResponseWriter, body string) {
 	_, _ = w.Write([]byte(body))
 }
@@ -62,3 +93,37 @@ func headerIsSubset(superset, subset http.Header) (string, bool) {
 
 	return "", true
 }
+
+func queryParamsAreSubset(superset, subset url.Values) (string, bool) {
+	for param, values := range subset {
+		superValues := make(map[string]bool)
+
+		strings, ok := superset[param]
+		if !ok {
+			return param, false
+		}
+
+		for _, v := range strings {
+			superValues[v] = true
+		}
+
+		for _, value := range values {
+			if _, found := superValues[value]; !found {
+				return param, false
+			}
+		}
+	}
+
+	return "", true
+}
+
+func queryParamsMissing(superset url.Values, missing []string) (string, bool) {
+	for _, param := range missing {
+		if superset.Has(param) {
+			// query was found, while should be missing
+			return param, false
+		}
+	}
+
+	return "", true
+}