Repository: cloudflare/cloudflare-go
Pull request #1316 (2023-06-22 14:32:30)

Support for OS Version Extra in posture rule creation

## Description
We want to give our users ability to enter os_version_extra while creating posture rule. This directly maps to product version extra that mac os uses in some instances for versioning. 

## Has your change been tested?
Yes

## Screenshots (if appropriate):

## Types of changes

What sort of change does your code introduce/modify?

- [ ] Bug fix (non-breaking change which fixes an issue)
- [X] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)

## Checklist:

- [x] My code follows the code style of this project.
- [x] My change requires a change to the documentation.
- [x] I have updated the documentation accordingly.
- [x] I have added tests to cover my changes.
- [x] All new and existing tests passed.
- [x] This change is using publicly documented in [cloudflare/api-schemas](https://github.com/cloudflare/api-schemas) 
      and relies on stable APIs.

[1]: https://help.github.com/articles/closing-issues-using-keywords/


github-actions[bot]: Oops! It looks like no changelog entry is attached to this PR. Please include a release note as described in https://github.com/cloudflare/cloudflare-go/blob/master/docs/changelog-process.md.

Example: 

~~~
```release-note:TYPE
Release note
```
~~~

If you do not require a release note to be included, please add the `workflow/skip-changelog-entry` label.

Patch:
diff --git a/.changelog/1088.txt b/.changelog/1088.txt
deleted file mode 100644
index ac1759d7a..000000000
--- a/.changelog/1088.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-```release-note:enhancement
-stream: added metadata support
-```
\ No newline at end of file
diff --git a/.changelog/1190.txt b/.changelog/1190.txt
deleted file mode 100644
index 6e12a08b1..000000000
--- a/.changelog/1190.txt
+++ /dev/null
@@ -1,7 +0,0 @@
-```release-note:bug
-stream: Fix a bug that cannot unmarshal video duration number.
-```
-
-```release-note:breaking-change
-stream: StreamVideo.Duration has changed from int to float64.
-```
diff --git a/.changelog/1281.txt b/.changelog/1281.txt
deleted file mode 100644
index c11f54ae4..000000000
--- a/.changelog/1281.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-```release-note:enhancement
-access: Added `self_hosted_domains` support to access applications
-```
diff --git a/.changelog/1293.txt b/.changelog/1293.txt
deleted file mode 100644
index 4c865f5bf..000000000
--- a/.changelog/1293.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-```release-note:enhancement
-load_balancing: extend documentation for least_outstanding_requests steering policy
-```
\ No newline at end of file
diff --git a/.changelog/1295.txt b/.changelog/1295.txt
deleted file mode 100644
index feff3ce19..000000000
--- a/.changelog/1295.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-```release-note:dependency
-deps: bumps github.com/urfave/cli/v2 from 2.25.3 to 2.25.5
-```
diff --git a/.changelog/1296.txt b/.changelog/1296.txt
deleted file mode 100644
index 5eb7ddda1..000000000
--- a/.changelog/1296.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-```release-note:dependency
-deps: bumps github.com/stretchr/testify from 1.8.3 to 1.8.4
-```
diff --git a/.changelog/1297.txt b/.changelog/1297.txt
deleted file mode 100644
index cee310173..000000000
--- a/.changelog/1297.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-```release-note:bug
-email_routing_destination: return encountered error, not `ErrMissingAccountID` all the time
-```
diff --git a/.changelog/1298.txt b/.changelog/1298.txt
deleted file mode 100644
index fff1d2a2b..000000000
--- a/.changelog/1298.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-```release-note:enhancement
-custom_hostname: add support for `bundle_method` TLS configuration
-```
diff --git a/.changelog/1300.txt b/.changelog/1300.txt
deleted file mode 100644
index b065b8252..000000000
--- a/.changelog/1300.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-```release-note:dependency
-deps: bumps github.com/hashicorp/go-retryablehttp from 0.7.2 to 0.7.3
-```
diff --git a/.changelog/1301.txt b/.changelog/1301.txt
deleted file mode 100644
index a0982af62..000000000
--- a/.changelog/1301.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-```release-note:dependency
-deps: bumps github.com/hashicorp/go-retryablehttp from 0.7.3 to 0.7.4
-```
diff --git a/.changelog/1303.txt b/.changelog/1303.txt
deleted file mode 100644
index 50a9b97a3..000000000
--- a/.changelog/1303.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-```release-note:breaking-change
-tunnel: swap `ConnectTimeout`, `TLSTimeout`, `TCPKeepAlive` and `KeepAliveTimeout` to `TunnelDuration` instead of `time.Duration`
-```
diff --git a/.changelog/1304.txt b/.changelog/1304.txt
deleted file mode 100644
index f3e8693e2..000000000
--- a/.changelog/1304.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-```release-note:enhancement
-custom_nameservers: add support for managing custom nameservers
-```
diff --git a/.changelog/1305.txt b/.changelog/1305.txt
deleted file mode 100644
index 1919a14e3..000000000
--- a/.changelog/1305.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-```release-note:dependency
-deps: bumps github.com/urfave/cli/v2 from 2.25.5 to 2.25.6
-```
diff --git a/.changelog/1306.txt b/.changelog/1306.txt
deleted file mode 100644
index a655ab47b..000000000
--- a/.changelog/1306.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-```release-note:dependency
-deps: bumps goreleaser/goreleaser-action from 4.2.0 to 4.3.0
-```
diff --git a/.changelog/1307.txt b/.changelog/1307.txt
deleted file mode 100644
index ce939ea0c..000000000
--- a/.changelog/1307.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-```release-note:dependency
-deps: bumps golang.org/x/net from 0.10.0 to 0.11.0
-```
diff --git a/.changelog/1311.txt b/.changelog/1311.txt
deleted file mode 100644
index 46639f877..000000000
--- a/.changelog/1311.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-```release-note:enhancement
-waiting_room: add support for `additional_routes` and `cookie_suffix`
-```
diff --git a/.changelog/1312.txt b/.changelog/1312.txt
deleted file mode 100644
index 9d3d216d1..000000000
--- a/.changelog/1312.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-```release-note:breaking-change
-railgun: remove support for railgun
-```
diff --git a/.changelog/1313.txt b/.changelog/1313.txt
deleted file mode 100644
index ace0376ab..000000000
--- a/.changelog/1313.txt
+++ /dev/null
@@ -1,7 +0,0 @@
-```release-note:breaking-change
-virtualdns: remove support in favour of newer DNS firewall methods
-```
-
-```release-note:breaking-change
-dns_firewall: modernise method signatures and conventions to align with the experimental client
-```
diff --git a/.changelog/1314.txt b/.changelog/1314.txt
deleted file mode 100644
index 4eb0d0acb..000000000
--- a/.changelog/1314.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-```release-note:dependency
-deps: bumps github.com/urfave/cli/v2 from 2.25.6 to 2.25.7
-```
diff --git a/.changelog/1315.txt b/.changelog/1315.txt
deleted file mode 100644
index c5dedfd9a..000000000
--- a/.changelog/1315.txt
+++ /dev/null
@@ -1,7 +0,0 @@
-```release-note:breaking-change
-cloudflare: remove `api.AccountID` from client struct
-```
-
-```release-note:breaking-change
-cloudflare: remove `UsingAccount` in favour of resource specific attributes
-```
diff --git a/.changelog/1316.txt b/.changelog/1316.txt
new file mode 100644
index 000000000..d2588ba25
--- /dev/null
+++ b/.changelog/1316.txt
@@ -0,0 +1,3 @@
+```release-note:enhancement
+device_posture_rule: support os_version_extra
+```
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 542ceda88..d93438671 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -17,7 +17,7 @@ jobs:
         with:
           go-version-file: 'go.mod'
       - name: Run GoReleaser
-        uses: goreleaser/goreleaser-action@v4.3.0
+        uses: goreleaser/goreleaser-action@v4.2.0
         with:
           version: latest
           args: release --rm-dist
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 66e277472..83d7a6a95 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,54 +1,8 @@
-## 0.71.0 (Unreleased)
-
-## 0.70.0 (June 21st, 2023)
-
-BREAKING CHANGES:
-
-* cloudflare: remove `UsingAccount` in favour of resource specific attributes ([#1315](https://github.com/cloudflare/cloudflare-go/issues/1315))
-* cloudflare: remove `api.AccountID` from client struct ([#1315](https://github.com/cloudflare/cloudflare-go/issues/1315))
-* dns_firewall: modernise method signatures and conventions to align with the experimental client ([#1313](https://github.com/cloudflare/cloudflare-go/issues/1313))
-* railgun: remove support for railgun ([#1312](https://github.com/cloudflare/cloudflare-go/issues/1312))
-* tunnel: swap `ConnectTimeout`, `TLSTimeout`, `TCPKeepAlive` and `KeepAliveTimeout` to `TunnelDuration` instead of `time.Duration` ([#1303](https://github.com/cloudflare/cloudflare-go/issues/1303))
-* virtualdns: remove support in favour of newer DNS firewall methods ([#1313](https://github.com/cloudflare/cloudflare-go/issues/1313))
-
-ENHANCEMENTS:
-
-* custom_nameservers: add support for managing custom nameservers ([#1304](https://github.com/cloudflare/cloudflare-go/issues/1304))
-* load_balancing: extend documentation for least_outstanding_requests steering policy ([#1293](https://github.com/cloudflare/cloudflare-go/issues/1293))
-* waiting_room: add support for `additional_routes` and `cookie_suffix` ([#1311](https://github.com/cloudflare/cloudflare-go/issues/1311))
-
-DEPENDENCIES:
-
-* deps: bumps github.com/hashicorp/go-retryablehttp from 0.7.3 to 0.7.4 ([#1301](https://github.com/cloudflare/cloudflare-go/issues/1301))
-* deps: bumps github.com/urfave/cli/v2 from 2.25.5 to 2.25.6 ([#1305](https://github.com/cloudflare/cloudflare-go/issues/1305))
-* deps: bumps github.com/urfave/cli/v2 from 2.25.6 to 2.25.7 ([#1314](https://github.com/cloudflare/cloudflare-go/issues/1314))
-* deps: bumps golang.org/x/net from 0.10.0 to 0.11.0 ([#1307](https://github.com/cloudflare/cloudflare-go/issues/1307))
-* deps: bumps goreleaser/goreleaser-action from 4.2.0 to 4.3.0 ([#1306](https://github.com/cloudflare/cloudflare-go/issues/1306))
-
-## 0.69.0 (June 7th, 2023)
-
-BREAKING CHANGES:
-
-* stream: StreamVideo.Duration has changed from int to float64. ([#1190](https://github.com/cloudflare/cloudflare-go/issues/1190))
-
-ENHANCEMENTS:
-
-* access: Added `self_hosted_domains` support to access applications ([#1281](https://github.com/cloudflare/cloudflare-go/issues/1281))
-* custom_hostname: add support for `bundle_method` TLS configuration ([#1298](https://github.com/cloudflare/cloudflare-go/issues/1298))
-* devices_policy: Add missing description field to policy ([#1294](https://github.com/cloudflare/cloudflare-go/issues/1294))
-* stream: added metadata support ([#1088](https://github.com/cloudflare/cloudflare-go/issues/1088))
-
-BUG FIXES:
-
-* email_routing_destination: return encountered error, not `ErrMissingAccountID` all the time ([#1297](https://github.com/cloudflare/cloudflare-go/issues/1297))
-* stream: Fix a bug that cannot unmarshal video duration number. ([#1190](https://github.com/cloudflare/cloudflare-go/issues/1190))
+## 0.69.0 (Unreleased)
 
 DEPENDENCIES:
 
 * deps: bumps dependabot/fetch-metadata from 1.5.0 to 1.5.1 ([#1292](https://github.com/cloudflare/cloudflare-go/issues/1292))
-* deps: bumps github.com/hashicorp/go-retryablehttp from 0.7.2 to 0.7.3 ([#1300](https://github.com/cloudflare/cloudflare-go/issues/1300))
-* deps: bumps github.com/stretchr/testify from 1.8.3 to 1.8.4 ([#1296](https://github.com/cloudflare/cloudflare-go/issues/1296))
-* deps: bumps github.com/urfave/cli/v2 from 2.25.3 to 2.25.5 ([#1295](https://github.com/cloudflare/cloudflare-go/issues/1295))
 
 ## 0.68.0 (May 24th, 2023)
 
diff --git a/access_application.go b/access_application.go
index bd49cf2c3..e627fca27 100644
--- a/access_application.go
+++ b/access_application.go
@@ -31,7 +31,6 @@ type AccessApplication struct {
 	LogoURL                 string                         `json:"logo_url,omitempty"`
 	AUD                     string                         `json:"aud,omitempty"`
 	Domain                  string                         `json:"domain"`
-	SelfHostedDomains       []string                       `json:"self_hosted_domains"`
 	Type                    AccessApplicationType          `json:"type,omitempty"`
 	SessionDuration         string                         `json:"session_duration,omitempty"`
 	SameSiteCookieAttribute string                         `json:"same_site_cookie_attribute,omitempty"`
diff --git a/cloudflare.go b/cloudflare.go
index 2231bd79f..8e5afe369 100644
--- a/cloudflare.go
+++ b/cloudflare.go
@@ -45,6 +45,7 @@ type API struct {
 	APIUserServiceKey string
 	APIToken          string
 	BaseURL           string
+	AccountID         string
 	UserAgent         string
 	headers           http.Header
 	httpClient        *http.Client
@@ -146,7 +147,7 @@ func (api *API) SetAuthType(authType int) {
 // ZoneIDByName retrieves a zone's ID from the name.
 func (api *API) ZoneIDByName(zoneName string) (string, error) {
 	zoneName = normalizeZoneName(zoneName)
-	res, err := api.ListZonesContext(context.Background(), WithZoneFilters(zoneName, "", ""))
+	res, err := api.ListZonesContext(context.Background(), WithZoneFilters(zoneName, api.AccountID, ""))
 	if err != nil {
 		return "", fmt.Errorf("ListZonesContext command failed: %w", err)
 	}
@@ -415,6 +416,19 @@ func (api *API) request(ctx context.Context, method, uri string, reqBody io.Read
 	return resp, nil
 }
 
+// Returns the base URL to use for API endpoints that exist for accounts.
+// If an account option was used when creating the API instance, returns
+// the account URL.
+//
+// accountBase is the base URL for endpoints referring to the current user.
+// It exists as a parameter because it is not consistent across APIs.
+func (api *API) userBaseURL(accountBase string) string {
+	if api.AccountID != "" {
+		return "/accounts/" + api.AccountID
+	}
+	return accountBase
+}
+
 // copyHeader copies all headers for `source` and sets them on `target`.
 // based on https://godoc.org/github.com/golang/gddo/httputil/header#Copy
 func copyHeader(target, source http.Header) {
diff --git a/cmd/flarectl/misc.go b/cmd/flarectl/misc.go
index 4f8372c3d..ddfeeb6cf 100644
--- a/cmd/flarectl/misc.go
+++ b/cmd/flarectl/misc.go
@@ -44,6 +44,10 @@ func initializeAPI(c *cli.Context) error {
 		return err
 	}
 
+	if c.IsSet("account-id") {
+		cloudflare.UsingAccount(c.String("account-id"))(api) // nolint
+	}
+
 	return nil
 }
 
diff --git a/custom_hostname.go b/custom_hostname.go
index 0404a0745..29bbdb9fe 100644
--- a/custom_hostname.go
+++ b/custom_hostname.go
@@ -79,7 +79,6 @@ type CustomHostnameSSL struct {
 	SSLValidationRecord
 	ValidationRecords []SSLValidationRecord `json:"validation_records,omitempty"`
 	ValidationErrors  []SSLValidationError  `json:"validation_errors,omitempty"`
-	BundleMethod      string                `json:"bundle_method,omitempty"`
 }
 
 // CustomMetadata defines custom metadata for the hostname. This requires logic to be implemented by Cloudflare to act on the data provided.
diff --git a/custom_nameservers.go b/custom_nameservers.go
deleted file mode 100644
index 45150a211..000000000
--- a/custom_nameservers.go
+++ /dev/null
@@ -1,209 +0,0 @@
-package cloudflare
-
-import (
-	"context"
-	"encoding/json"
-	"errors"
-	"fmt"
-	"net/http"
-)
-
-type CustomNameserverRecord struct {
-	Type  string `json:"type"`
-	Value string `json:"value"`
-}
-
-type CustomNameserver struct {
-	NSName string `json:"ns_name"`
-	NSSet  int    `json:"ns_set"`
-}
-
-type CustomNameserverResult struct {
-	DNSRecords []CustomNameserverRecord `json:"dns_records"`
-	NSName     string                   `json:"ns_name"`
-	NSSet      int                      `json:"ns_set"`
-	Status     string                   `json:"status"`
-	ZoneTag    string                   `json:"zone_tag"`
-}
-
-type CustomNameserverZoneMetadata struct {
-	Type    string `json:"type"`
-	NSSet   string `json:"ns_set"`
-	Enabled bool   `json:"enabled"`
-}
-
-type customNameserverListResponse struct {
-	Response
-	Result []CustomNameserverResult `json:"result"`
-}
-
-type customNameserverCreateResponse struct {
-	Response
-	Result CustomNameserverResult `json:"result"`
-}
-
-type getEligibleZonesAccountCustomNameserversResponse struct {
-	Result []string `json:"result"`
-}
-
-type customNameserverZoneMetadata struct {
-	Response
-	Result CustomNameserverZoneMetadata
-}
-
-type GetCustomNameserversParams struct{}
-
-type CreateCustomNameserversParams struct {
-	NSName string `json:"ns_name"`
-	NSSet  int    `json:"ns_set"`
-}
-
-type DeleteCustomNameserversParams struct {
-	NSName string
-}
-
-type GetEligibleZonesAccountCustomNameserversParams struct{}
-
-type GetCustomNameserverZoneMetadataParams struct{}
-
-type UpdateCustomNameserverZoneMetadataParams struct {
-	Type    string `json:"type"`
-	NSSet   string `json:"ns_set"`
-	Enabled bool   `json:"enabled"`
-}
-
-// GetCustomNameservers lists custom nameservers.
-//
-// API documentation: https://developers.cloudflare.com/api/operations/account-level-custom-nameservers-list-account-custom-nameservers
-func (api *API) GetCustomNameservers(ctx context.Context, rc *ResourceContainer, params GetCustomNameserversParams) ([]CustomNameserverResult, error) {
-	if rc.Level != AccountRouteLevel {
-		return []CustomNameserverResult{}, ErrRequiredAccountLevelResourceContainer
-	}
-	uri := fmt.Sprintf("/%s/%s/custom_ns", rc.Level, rc.Identifier)
-
-	res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
-	if err != nil {
-		return nil, err
-	}
-
-	var response customNameserverListResponse
-	err = json.Unmarshal(res, &response)
-	if err != nil {
-		return nil, fmt.Errorf("%s: %w", errUnmarshalError, err)
-	}
-
-	return response.Result, nil
-}
-
-// CreateCustomNameservers adds a custom nameserver.
-//
-// API documentation: https://developers.cloudflare.com/api/operations/account-level-custom-nameservers-add-account-custom-nameserver
-func (api *API) CreateCustomNameservers(ctx context.Context, rc *ResourceContainer, params CreateCustomNameserversParams) (CustomNameserverResult, error) {
-	if rc.Level != AccountRouteLevel {
-		return CustomNameserverResult{}, ErrRequiredAccountLevelResourceContainer
-	}
-
-	uri := fmt.Sprintf("/%s/%s/custom_ns", rc.Level, rc.Identifier)
-
-	res, err := api.makeRequestContext(ctx, http.MethodPost, uri, params)
-	if err != nil {
-		return CustomNameserverResult{}, err
-	}
-
-	response := &customNameserverCreateResponse{}
-	err = json.Unmarshal(res, &response)
-	if err != nil {
-		return CustomNameserverResult{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
-	}
-
-	return response.Result, nil
-}
-
-// DeleteCustomNameservers removes a custom nameserver.
-//
-// API documentation: https://developers.cloudflare.com/api/operations/account-level-custom-nameservers-delete-account-custom-nameserver
-func (api *API) DeleteCustomNameservers(ctx context.Context, rc *ResourceContainer, params DeleteCustomNameserversParams) error {
-	if rc.Level != AccountRouteLevel {
-		return ErrRequiredAccountLevelResourceContainer
-	}
-
-	if params.NSName == "" {
-		return errors.New("missing required NSName parameter")
-	}
-
-	uri := fmt.Sprintf("/%s/%s/custom_ns/%s", rc.Level, rc.Identifier, params.NSName)
-
-	_, err := api.makeRequestContext(ctx, http.MethodDelete, uri, nil)
-	if err != nil {
-		return err
-	}
-
-	return nil
-}
-
-// GetEligibleZonesAccountCustomNameservers lists zones eligible for custom nameservers.
-//
-// API documentation: https://developers.cloudflare.com/api/operations/account-level-custom-nameservers-get-eligible-zones-for-account-custom-nameservers
-func (api *API) GetEligibleZonesAccountCustomNameservers(ctx context.Context, rc *ResourceContainer, params GetEligibleZonesAccountCustomNameserversParams) ([]string, error) {
-	if rc.Level != AccountRouteLevel {
-		return []string{}, ErrRequiredAccountLevelResourceContainer
-	}
-
-	uri := fmt.Sprintf("/%s/%s/custom_ns/availability", rc.Level, rc.Identifier)
-
-	res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
-	if err != nil {
-		return nil, err
-	}
-
-	var response getEligibleZonesAccountCustomNameserversResponse
-	err = json.Unmarshal(res, &response)
-	if err != nil {
-		return nil, fmt.Errorf("%s: %w", errUnmarshalError, err)
-	}
-
-	return response.Result, nil
-}
-
-// GetCustomNameserverZoneMetadata get metadata for custom nameservers on a zone.
-//
-// API documentation: https://developers.cloudflare.com/api/operations/account-level-custom-nameservers-usage-for-a-zone-get-account-custom-nameserver-related-zone-metadata
-func (api *API) GetCustomNameserverZoneMetadata(ctx context.Context, rc *ResourceContainer, params GetCustomNameserverZoneMetadataParams) (CustomNameserverZoneMetadata, error) {
-	if rc.Level != ZoneRouteLevel {
-		return CustomNameserverZoneMetadata{}, ErrRequiredZoneLevelResourceContainer
-	}
-
-	uri := fmt.Sprintf("/%s/%s/custom_ns", rc.Level, rc.Identifier)
-
-	res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
-	if err != nil {
-		return CustomNameserverZoneMetadata{}, err
-	}
-
-	var response customNameserverZoneMetadata
-	err = json.Unmarshal(res, &response)
-	if err != nil {
-		return CustomNameserverZoneMetadata{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
-	}
-
-	return response.Result, nil
-}
-
-// UpdateCustomNameserverZoneMetadata set metadata for custom nameservers on a zone.
-//
-// API documentation: https://developers.cloudflare.com/api/operations/account-level-custom-nameservers-usage-for-a-zone-set-account-custom-nameserver-related-zone-metadata
-func (api *API) UpdateCustomNameserverZoneMetadata(ctx context.Context, rc *ResourceContainer, params UpdateCustomNameserverZoneMetadataParams) error {
-	if rc.Level != ZoneRouteLevel {
-		return ErrRequiredZoneLevelResourceContainer
-	}
-
-	uri := fmt.Sprintf("/%s/%s/custom_ns", rc.Level, rc.Identifier)
-
-	params.Type = ""
-	_, err := api.makeRequestContext(ctx, http.MethodPut, uri, params)
-	if err != nil {
-		return err
-	}
-
-	return nil
-}
diff --git a/device_posture_rule.go b/device_posture_rule.go
index 5d04e270b..56d143e8e 100644
--- a/device_posture_rule.go
+++ b/device_posture_rule.go
@@ -178,6 +178,7 @@ type DevicePostureRuleInput struct {
 	Os               string   `json:"os,omitempty"`
 	OsDistroName     string   `json:"os_distro_name,omitempty"`
 	OsDistroRevision string   `json:"os_distro_revision,omitempty"`
+	OSVersionExtra   string   `json:"os_version_extra,omitempty"`
 	Operator         string   `json:"operator,omitempty"`
 	Domain           string   `json:"domain,omitempty"`
 	ComplianceStatus string   `json:"compliance_status,omitempty"`
diff --git a/dns_firewall.go b/dns_firewall.go
index 064da15c7..a6282aea2 100644
--- a/dns_firewall.go
+++ b/dns_firewall.go
@@ -3,14 +3,13 @@ package cloudflare
 import (
 	"context"
 	"encoding/json"
-	"errors"
 	"fmt"
 	"net/http"
+	"net/url"
+	"strings"
 	"time"
 )
 
-var ErrMissingClusterID = errors.New("missing required cluster ID")
-
 // DNSFirewallCluster represents a DNS Firewall configuration.
 type DNSFirewallCluster struct {
 	ID                   string   `json:"id,omitempty"`
@@ -44,9 +43,9 @@ type DNSFirewallAnalytics struct {
 
 // DNSFirewallUserAnalyticsOptions represents range and dimension selection on analytics endpoint.
 type DNSFirewallUserAnalyticsOptions struct {
-	Metrics []string   `url:"metrics,omitempty" del:","`
-	Since   *time.Time `url:"since,omitempty"`
-	Until   *time.Time `url:"until,omitempty"`
+	Metrics []string
+	Since   *time.Time
+	Until   *time.Time
 }
 
 // dnsFirewallResponse represents a DNS Firewall response.
@@ -67,42 +66,12 @@ type dnsFirewallAnalyticsResponse struct {
 	Result DNSFirewallAnalytics `json:"result"`
 }
 
-type CreateDNSFirewallClusterParams struct {
-	Name                 string   `json:"name"`
-	UpstreamIPs          []string `json:"upstream_ips"`
-	DNSFirewallIPs       []string `json:"dns_firewall_ips,omitempty"`
-	MinimumCacheTTL      uint     `json:"minimum_cache_ttl,omitempty"`
-	MaximumCacheTTL      uint     `json:"maximum_cache_ttl,omitempty"`
-	DeprecateAnyRequests bool     `json:"deprecate_any_requests"`
-}
-
-type GetDNSFirewallClusterParams struct {
-	ClusterID string `json:"-"`
-}
-
-type UpdateDNSFirewallClusterParams struct {
-	ClusterID            string   `json:"-"`
-	Name                 string   `json:"name"`
-	UpstreamIPs          []string `json:"upstream_ips"`
-	DNSFirewallIPs       []string `json:"dns_firewall_ips,omitempty"`
-	MinimumCacheTTL      uint     `json:"minimum_cache_ttl,omitempty"`
-	MaximumCacheTTL      uint     `json:"maximum_cache_ttl,omitempty"`
-	DeprecateAnyRequests bool     `json:"deprecate_any_requests"`
-}
-
-type ListDNSFirewallClustersParams struct{}
-
-type GetDNSFirewallUserAnalyticsParams struct {
-	ClusterID string `json:"-"`
-	DNSFirewallUserAnalyticsOptions
-}
-
 // CreateDNSFirewallCluster creates a new DNS Firewall cluster.
 //
 // API reference: https://api.cloudflare.com/#dns-firewall-create-dns-firewall-cluster
-func (api *API) CreateDNSFirewallCluster(ctx context.Context, rc *ResourceContainer, params CreateDNSFirewallClusterParams) (*DNSFirewallCluster, error) {
-	uri := fmt.Sprintf("/%s/dns_firewall", rc.URLFragment())
-	res, err := api.makeRequestContext(ctx, http.MethodPost, uri, params)
+func (api *API) CreateDNSFirewallCluster(ctx context.Context, v DNSFirewallCluster) (*DNSFirewallCluster, error) {
+	uri := fmt.Sprintf("%s/dns_firewall", api.userBaseURL("/user"))
+	res, err := api.makeRequestContext(ctx, http.MethodPost, uri, v)
 	if err != nil {
 		return nil, err
 	}
@@ -116,15 +85,11 @@ func (api *API) CreateDNSFirewallCluster(ctx context.Context, rc *ResourceContai
 	return response.Result, nil
 }
 
-// GetDNSFirewallCluster fetches a single DNS Firewall cluster.
+// DNSFirewallCluster fetches a single DNS Firewall cluster.
 //
 // API reference: https://api.cloudflare.com/#dns-firewall-dns-firewall-cluster-details
-func (api *API) GetDNSFirewallCluster(ctx context.Context, rc *ResourceContainer, params GetDNSFirewallClusterParams) (*DNSFirewallCluster, error) {
-	if params.ClusterID == "" {
-		return &DNSFirewallCluster{}, ErrMissingClusterID
-	}
-
-	uri := fmt.Sprintf("/%s/dns_firewall/%s", rc.URLFragment(), params.ClusterID)
+func (api *API) DNSFirewallCluster(ctx context.Context, clusterID string) (*DNSFirewallCluster, error) {
+	uri := fmt.Sprintf("%s/dns_firewall/%s", api.userBaseURL("/user"), clusterID)
 	res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
 	if err != nil {
 		return nil, err
@@ -142,8 +107,8 @@ func (api *API) GetDNSFirewallCluster(ctx context.Context, rc *ResourceContainer
 // ListDNSFirewallClusters lists the DNS Firewall clusters associated with an account.
 //
 // API reference: https://api.cloudflare.com/#dns-firewall-list-dns-firewall-clusters
-func (api *API) ListDNSFirewallClusters(ctx context.Context, rc *ResourceContainer, params ListDNSFirewallClustersParams) ([]*DNSFirewallCluster, error) {
-	uri := fmt.Sprintf("/%s/dns_firewall", rc.URLFragment())
+func (api *API) ListDNSFirewallClusters(ctx context.Context) ([]*DNSFirewallCluster, error) {
+	uri := fmt.Sprintf("%s/dns_firewall", api.userBaseURL("/user"))
 	res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
 	if err != nil {
 		return nil, err
@@ -161,13 +126,9 @@ func (api *API) ListDNSFirewallClusters(ctx context.Context, rc *ResourceContain
 // UpdateDNSFirewallCluster updates a DNS Firewall cluster.
 //
 // API reference: https://api.cloudflare.com/#dns-firewall-update-dns-firewall-cluster
-func (api *API) UpdateDNSFirewallCluster(ctx context.Context, rc *ResourceContainer, params UpdateDNSFirewallClusterParams) error {
-	if params.ClusterID == "" {
-		return ErrMissingClusterID
-	}
-
-	uri := fmt.Sprintf("/%s/dns_firewall/%s", rc.URLFragment(), params.ClusterID)
-	res, err := api.makeRequestContext(ctx, http.MethodPatch, uri, params)
+func (api *API) UpdateDNSFirewallCluster(ctx context.Context, clusterID string, vv DNSFirewallCluster) error {
+	uri := fmt.Sprintf("%s/dns_firewall/%s", api.userBaseURL("/user"), clusterID)
+	res, err := api.makeRequestContext(ctx, http.MethodPatch, uri, vv)
 	if err != nil {
 		return err
 	}
@@ -185,8 +146,8 @@ func (api *API) UpdateDNSFirewallCluster(ctx context.Context, rc *ResourceContai
 // undone, and will stop all traffic to that cluster.
 //
 // API reference: https://api.cloudflare.com/#dns-firewall-delete-dns-firewall-cluster
-func (api *API) DeleteDNSFirewallCluster(ctx context.Context, rc *ResourceContainer, clusterID string) error {
-	uri := fmt.Sprintf("/%s/dns_firewall/%s", rc.URLFragment(), clusterID)
+func (api *API) DeleteDNSFirewallCluster(ctx context.Context, clusterID string) error {
+	uri := fmt.Sprintf("%s/dns_firewall/%s", api.userBaseURL("/user"), clusterID)
 	res, err := api.makeRequestContext(ctx, http.MethodDelete, uri, nil)
 	if err != nil {
 		return err
@@ -201,9 +162,24 @@ func (api *API) DeleteDNSFirewallCluster(ctx context.Context, rc *ResourceContai
 	return nil
 }
 
-// GetDNSFirewallUserAnalytics retrieves analytics report for a specified dimension and time range.
-func (api *API) GetDNSFirewallUserAnalytics(ctx context.Context, rc *ResourceContainer, params GetDNSFirewallUserAnalyticsParams) (DNSFirewallAnalytics, error) {
-	uri := buildURI(fmt.Sprintf("/%s/dns_firewall/%s/dns_analytics/report", rc.URLFragment(), params.ClusterID), params.DNSFirewallUserAnalyticsOptions)
+// encode encodes non-nil fields into URL encoded form.
+func (o DNSFirewallUserAnalyticsOptions) encode() string {
+	v := url.Values{}
+	if o.Since != nil {
+		v.Set("since", o.Since.UTC().Format(time.RFC3339))
+	}
+	if o.Until != nil {
+		v.Set("until", o.Until.UTC().Format(time.RFC3339))
+	}
+	if o.Metrics != nil {
+		v.Set("metrics", strings.Join(o.Metrics, ","))
+	}
+	return v.Encode()
+}
+
+// DNSFirewallUserAnalytics retrieves analytics report for a specified dimension and time range.
+func (api *API) DNSFirewallUserAnalytics(ctx context.Context, clusterID string, o DNSFirewallUserAnalyticsOptions) (DNSFirewallAnalytics, error) {
+	uri := fmt.Sprintf("%s/dns_firewall/%s/dns_analytics/report?%s", api.userBaseURL("/user"), clusterID, o.encode())
 	res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
 	if err != nil {
 		return DNSFirewallAnalytics{}, err
diff --git a/docs/conventions.md b/docs/conventions.md
deleted file mode 100644
index 7cd018949..000000000
--- a/docs/conventions.md
+++ /dev/null
@@ -1,40 +0,0 @@
-# Conventions
-
-This document aims to cover the conventions and guidance to consider when 
-making changes the Go SDK.
-
-## Methods
-
-- All methods should take a maxium of 3 parameter. See examples in [experimental](./experimental.md)
-- The first parameter is always `context.Context`.
-- The second is a `*ResourceContainer`.
-- The final is a struct of available parameters for the method. 
-  - The parameter naming convention should be `<MethodName>Params`. Example: 
-    method name of `GetDNSRecords` has a struct parameter name of 
-    `GetDNSRecordsParams`.
-  - Do not share parameter structs between methods. Each should have a dedicated
-    one.
-  - Even if you don't intend to have parameter configurations, you should add
-    the third parameter to your method signature for future flexibility.
-  
-## Types
-
-### Booleans
-
-- Should always be represented as pointers in structs with an `omitempty` 
-  marshaling tag (most commonly as JSON). This ensures you can determine unset, 
-  false and truthy values.
-
-### `time.Time`
-
-- Should always be represented as pointers in structs.
-
-### Ports (0-65535)
-
-- Should use `uint16` unless you have a reason to restrict the port range in 
-  which case, you should also provide a validator on the type.
-
-## Marshaling/unmarshaling
-
-- Avoid custom marshal/unmarshal handlers unless absolutely necessary. They can
-  be difficult to debug in a larger codebase.
diff --git a/email_routing_destination.go b/email_routing_destination.go
index 75e79ea50..cb9e30a94 100644
--- a/email_routing_destination.go
+++ b/email_routing_destination.go
@@ -92,9 +92,10 @@ func (api *API) CreateEmailRoutingDestinationAddress(ctx context.Context, rc *Re
 	}
 
 	uri := fmt.Sprintf("/accounts/%s/email/routing/addresses", rc.Identifier)
+
 	res, err := api.makeRequestContext(ctx, http.MethodPost, uri, params)
 	if err != nil {
-		return EmailRoutingDestinationAddress{}, err
+		return EmailRoutingDestinationAddress{}, ErrMissingAccountID
 	}
 
 	var r CreateEmailRoutingAddressResponse
@@ -118,7 +119,7 @@ func (api *API) GetEmailRoutingDestinationAddress(ctx context.Context, rc *Resou
 
 	res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
 	if err != nil {
-		return EmailRoutingDestinationAddress{}, err
+		return EmailRoutingDestinationAddress{}, ErrMissingAccountID
 	}
 
 	var r CreateEmailRoutingAddressResponse
@@ -142,7 +143,7 @@ func (api *API) DeleteEmailRoutingDestinationAddress(ctx context.Context, rc *Re
 
 	res, err := api.makeRequestContext(ctx, http.MethodDelete, uri, nil)
 	if err != nil {
-		return EmailRoutingDestinationAddress{}, err
+		return EmailRoutingDestinationAddress{}, ErrMissingAccountID
 	}
 
 	var r CreateEmailRoutingAddressResponse
diff --git a/go.mod b/go.mod
index 856fd9cb6..f434285b5 100644
--- a/go.mod
+++ b/go.mod
@@ -4,11 +4,11 @@ go 1.18
 
 require (
 	github.com/google/go-querystring v1.1.0
-	github.com/hashicorp/go-retryablehttp v0.7.4
+	github.com/hashicorp/go-retryablehttp v0.7.2
 	github.com/olekukonko/tablewriter v0.0.5
-	github.com/stretchr/testify v1.8.4
-	github.com/urfave/cli/v2 v2.25.7
-	golang.org/x/net v0.11.0
+	github.com/stretchr/testify v1.8.3
+	github.com/urfave/cli/v2 v2.25.3
+	golang.org/x/net v0.10.0
 	golang.org/x/time v0.3.0
 )
 
@@ -30,6 +30,6 @@ require (
 	github.com/rogpeppe/go-internal v1.8.1 // indirect
 	github.com/russross/blackfriday/v2 v2.1.0 // indirect
 	github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
-	golang.org/x/text v0.10.0 // indirect
+	golang.org/x/text v0.9.0 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 )
diff --git a/go.sum b/go.sum
index a528bd106..36dbcedcf 100644
--- a/go.sum
+++ b/go.sum
@@ -16,8 +16,8 @@ github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/S
 github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
 github.com/hashicorp/go-hclog v1.2.0 h1:La19f8d7WIlm4ogzNHB0JGqs5AUDAZ2UfCY4sJXcJdM=
 github.com/hashicorp/go-hclog v1.2.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ=
-github.com/hashicorp/go-retryablehttp v0.7.4 h1:ZQgVdpTdAL7WpMIwLzCfbalOcSUdkDZnpUv3/+BxzFA=
-github.com/hashicorp/go-retryablehttp v0.7.4/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8=
+github.com/hashicorp/go-retryablehttp v0.7.2 h1:AcYqCvkpalPnPF2pn0KamgwamS42TqUDDYFRKq/RAd0=
+github.com/hashicorp/go-retryablehttp v0.7.2/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8=
 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
 github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
 github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
@@ -51,23 +51,23 @@ github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4
 github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
 github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
-github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
-github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
-github.com/urfave/cli/v2 v2.25.7 h1:VAzn5oq403l5pHjc4OhD54+XGO9cdKVL/7lDjF+iKUs=
-github.com/urfave/cli/v2 v2.25.7/go.mod h1:8qnjx1vcq5s2/wpsqoZFndg2CE5tNFyrTvS6SinrnYQ=
+github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
+github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/urfave/cli/v2 v2.25.3 h1:VJkt6wvEBOoSjPFQvOkv6iWIrsJyCrKGtCtxXWwmGeY=
+github.com/urfave/cli/v2 v2.25.3/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc=
 github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
 github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
-golang.org/x/net v0.11.0 h1:Gi2tvZIJyBtO9SDr1q9h5hEQCp/4L2RQ+ar0qjx2oNU=
-golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
+golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
+golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
 golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.9.0 h1:KS/R3tvhPqvJvwcKfnBHJwwthS11LRhmM5D59eEXa0s=
-golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58=
-golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
+golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
+golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE=
+golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
 golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
 golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
diff --git a/load_balancing.go b/load_balancing.go
index 4f6422d07..5b01ff54c 100644
--- a/load_balancing.go
+++ b/load_balancing.go
@@ -33,31 +33,16 @@ type LoadBalancerPool struct {
 
 // LoadBalancerOrigin represents a Load Balancer origin's properties.
 type LoadBalancerOrigin struct {
-	Name    string `json:"name"`
-	Address string `json:"address"`
-	Enabled bool   `json:"enabled"`
-	// Weight of this origin relative to other origins in the pool.
-	// Based on the configured weight the total traffic is distributed
-	// among origins within the pool.
-	//
-	// When LoadBalancerOriginSteering.Policy="least_outstanding_requests", this
-	// weight is used to scale the origin's outstanding requests.
-	Weight float64             `json:"weight"`
-	Header map[string][]string `json:"header"`
+	Name    string              `json:"name"`
+	Address string              `json:"address"`
+	Enabled bool                `json:"enabled"`
+	Weight  float64             `json:"weight"`
+	Header  map[string][]string `json:"header"`
 }
 
 // LoadBalancerOriginSteering controls origin selection for new sessions and traffic without session affinity.
 type LoadBalancerOriginSteering struct {
-	// Policy determines the type of origin steering policy to use.
-	// It defaults to "random" (weighted) when empty or unspecified.
-	//
-	// "random": Select an origin randomly.
-	//
-	// "hash": Select an origin by computing a hash over the CF-Connecting-IP address.
-	//
-	// "least_outstanding_requests": Select an origin by taking into consideration origin weights,
-	// as well as each origin's number of outstanding requests. Origins with more pending requests
-	// are weighted proportionately less relative to others.
+	// Policy defaults to "random" (weighted) when empty or unspecified.
 	Policy string `json:"policy,omitempty"`
 }
 
@@ -120,10 +105,6 @@ type LoadBalancer struct {
 	// the Cloudflare PoP location for proxied requests or the location determined by
 	// LocationStrategy for non-proxied requests.
 	//
-	// "least_outstanding_requests": Select a pool by taking into consideration
-	// RandomSteering weights, as well as each pool's number of outstanding requests.
-	// Pools with more pending requests are weighted proportionately less relative to others.
-	//
 	// "": Maps to "geo" if RegionPools or PopPools or CountryPools have entries otherwise "off".
 	SteeringPolicy string `json:"steering_policy,omitempty"`
 }
@@ -197,13 +178,8 @@ type LoadBalancerRuleOverrides struct {
 	LocationStrategy *LocationStrategy `json:"location_strategy,omitempty"`
 }
 
-// RandomSteering configures pool weights.
-//
-// SteeringPolicy="random": A random pool is selected with probability
-// proportional to pool weights.
-//
-// SteeringPolicy="least_outstanding_requests": Use pool weights to
-// scale each pool's outstanding requests.
+// RandomSteering represents fields used to set pool weights on a load balancer
+// with "random" steering policy.
 type RandomSteering struct {
 	DefaultWeight float64            `json:"default_weight,omitempty"`
 	PoolWeights   map[string]float64 `json:"pool_weights,omitempty"`
diff --git a/options.go b/options.go
index 7754badbe..1ed891922 100644
--- a/options.go
+++ b/options.go
@@ -28,6 +28,17 @@ func Headers(headers http.Header) Option {
 	}
 }
 
+// UsingAccount allows you to apply account-level changes (Load Balancing,
+// Railguns) to an account instead.
+//
+// Deprecated: Resources should define the `AccountID` parameter explicitly.
+func UsingAccount(accountID string) Option {
+	return func(api *API) error {
+		api.AccountID = accountID
+		return nil
+	}
+}
+
 // UsingRateLimit applies a non-default rate limit to client API requests
 // If not specified the default of 4rps will be applied.
 func UsingRateLimit(rps float64) Option {
diff --git a/railgun.go b/railgun.go
new file mode 100644
index 000000000..e18912413
--- /dev/null
+++ b/railgun.go
@@ -0,0 +1,298 @@
+package cloudflare
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"net/url"
+	"time"
+)
+
+// Railgun represents a Railgun's properties.
+type Railgun struct {
+	ID             string    `json:"id"`
+	Name           string    `json:"name"`
+	Status         string    `json:"status"`
+	Enabled        bool      `json:"enabled"`
+	ZonesConnected int       `json:"zones_connected"`
+	Build          string    `json:"build"`
+	Version        string    `json:"version"`
+	Revision       string    `json:"revision"`
+	ActivationKey  string    `json:"activation_key"`
+	ActivatedOn    time.Time `json:"activated_on"`
+	CreatedOn      time.Time `json:"created_on"`
+	ModifiedOn     time.Time `json:"modified_on"`
+	UpgradeInfo    struct {
+		LatestVersion string `json:"latest_version"`
+		DownloadLink  string `json:"download_link"`
+	} `json:"upgrade_info"`
+}
+
+// RailgunListOptions represents the parameters used to list railguns.
+type RailgunListOptions struct {
+	Direction string
+}
+
+// railgunResponse represents the response from the Create Railgun and the Railgun Details endpoints.
+type railgunResponse struct {
+	Response
+	Result Railgun `json:"result"`
+}
+
+// railgunsResponse represents the response from the List Railguns endpoint.
+type railgunsResponse struct {
+	Response
+	Result []Railgun `json:"result"`
+}
+
+// CreateRailgun creates a new Railgun.
+//
+// API reference: https://api.cloudflare.com/#railgun-create-railgun
+func (api *API) CreateRailgun(ctx context.Context, name string) (Railgun, error) {
+	uri := fmt.Sprintf("%s/railguns", api.userBaseURL(""))
+	params := struct {
+		Name string `json:"name"`
+	}{
+		Name: name,
+	}
+	res, err := api.makeRequestContext(ctx, http.MethodPost, uri, params)
+	if err != nil {
+		return Railgun{}, err
+	}
+	var r railgunResponse
+	if err := json.Unmarshal(res, &r); err != nil {
+		return Railgun{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
+	}
+	return r.Result, nil
+}
+
+// ListRailguns lists Railguns connected to an account.
+//
+// API reference: https://api.cloudflare.com/#railgun-list-railguns
+func (api *API) ListRailguns(ctx context.Context, options RailgunListOptions) ([]Railgun, error) {
+	v := url.Values{}
+	if options.Direction != "" {
+		v.Set("direction", options.Direction)
+	}
+	uri := fmt.Sprintf("%s/railguns?%s", api.userBaseURL(""), v.Encode())
+	res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
+	if err != nil {
+		return nil, err
+	}
+	var r railgunsResponse
+	if err := json.Unmarshal(res, &r); err != nil {
+		return nil, fmt.Errorf("%s: %w", errUnmarshalError, err)
+	}
+	return r.Result, nil
+}
+
+// RailgunDetails returns the details for a Railgun.
+//
+// API reference: https://api.cloudflare.com/#railgun-railgun-details
+func (api *API) RailgunDetails(ctx context.Context, railgunID string) (Railgun, error) {
+	uri := fmt.Sprintf("%s/railguns/%s", api.userBaseURL(""), railgunID)
+	res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
+	if err != nil {
+		return Railgun{}, err
+	}
+	var r railgunResponse
+	if err := json.Unmarshal(res, &r); err != nil {
+		return Railgun{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
+	}
+	return r.Result, nil
+}
+
+// RailgunZones returns the zones that are currently using a Railgun.
+//
+// API reference: https://api.cloudflare.com/#railgun-get-zones-connected-to-a-railgun
+func (api *API) RailgunZones(ctx context.Context, railgunID string) ([]Zone, error) {
+	uri := fmt.Sprintf("%s/railguns/%s/zones", api.userBaseURL(""), railgunID)
+	res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
+	if err != nil {
+		return nil, err
+	}
+	var r ZonesResponse
+	if err := json.Unmarshal(res, &r); err != nil {
+		return nil, fmt.Errorf("%s: %w", errUnmarshalError, err)
+	}
+	return r.Result, nil
+}
+
+// enableRailgun enables (true) or disables (false) a Railgun for all zones connected to it.
+//
+// API reference: https://api.cloudflare.com/#railgun-enable-or-disable-a-railgun
+func (api *API) enableRailgun(ctx context.Context, railgunID string, enable bool) (Railgun, error) {
+	uri := fmt.Sprintf("%s/railguns/%s", api.userBaseURL(""), railgunID)
+	params := struct {
+		Enabled bool `json:"enabled"`
+	}{
+		Enabled: enable,
+	}
+	res, err := api.makeRequestContext(ctx, http.MethodPatch, uri, params)
+	if err != nil {
+		return Railgun{}, err
+	}
+	var r railgunResponse
+	if err := json.Unmarshal(res, &r); err != nil {
+		return Railgun{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
+	}
+	return r.Result, nil
+}
+
+// EnableRailgun enables a Railgun for all zones connected to it.
+//
+// API reference: https://api.cloudflare.com/#railgun-enable-or-disable-a-railgun
+func (api *API) EnableRailgun(ctx context.Context, railgunID string) (Railgun, error) {
+	return api.enableRailgun(ctx, railgunID, true)
+}
+
+// DisableRailgun enables a Railgun for all zones connected to it.
+//
+// API reference: https://api.cloudflare.com/#railgun-enable-or-disable-a-railgun
+func (api *API) DisableRailgun(ctx context.Context, railgunID string) (Railgun, error) {
+	return api.enableRailgun(ctx, railgunID, false)
+}
+
+// DeleteRailgun disables and deletes a Railgun.
+//
+// API reference: https://api.cloudflare.com/#railgun-delete-railgun
+func (api *API) DeleteRailgun(ctx context.Context, railgunID string) error {
+	uri := fmt.Sprintf("%s/railguns/%s", api.userBaseURL(""), railgunID)
+	if _, err := api.makeRequestContext(ctx, http.MethodDelete, uri, nil); err != nil {
+		return err
+	}
+	return nil
+}
+
+// ZoneRailgun represents the status of a Railgun on a zone.
+type ZoneRailgun struct {
+	ID        string `json:"id"`
+	Name      string `json:"name"`
+	Enabled   bool   `json:"enabled"`
+	Connected bool   `json:"connected"`
+}
+
+// zoneRailgunResponse represents the response from the Zone Railgun Details endpoint.
+type zoneRailgunResponse struct {
+	Response
+	Result ZoneRailgun `json:"result"`
+}
+
+// zoneRailgunsResponse represents the response from the Zone Railgun endpoint.
+type zoneRailgunsResponse struct {
+	Response
+	Result []ZoneRailgun `json:"result"`
+}
+
+// RailgunDiagnosis represents the test results from testing railgun connections
+// to a zone.
+type RailgunDiagnosis struct {
+	Method          string `json:"method"`
+	HostName        string `json:"host_name"`
+	HTTPStatus      int    `json:"http_status"`
+	Railgun         string `json:"railgun"`
+	URL             string `json:"url"`
+	ResponseStatus  string `json:"response_status"`
+	Protocol        string `json:"protocol"`
+	ElapsedTime     string `json:"elapsed_time"`
+	BodySize        string `json:"body_size"`
+	BodyHash        string `json:"body_hash"`
+	MissingHeaders  string `json:"missing_headers"`
+	ConnectionClose bool   `json:"connection_close"`
+	Cloudflare      string `json:"cloudflare"`
+	CFRay           string `json:"cf-ray"`
+	// NOTE: Cloudflare's online API documentation does not yet have definitions
+	// for the following fields. See: https://api.cloudflare.com/#railgun-connections-for-a-zone-test-railgun-connection/
+	CFWANError    string `json:"cf-wan-error"`
+	CFCacheStatus string `json:"cf-cache-status"`
+}
+
+// railgunDiagnosisResponse represents the response from the Test Railgun Connection endpoint.
+type railgunDiagnosisResponse struct {
+	Response
+	Result RailgunDiagnosis `json:"result"`
+}
+
+// ZoneRailguns returns the available Railguns for a zone.
+//
+// API reference: https://api.cloudflare.com/#railguns-for-a-zone-get-available-railguns
+func (api *API) ZoneRailguns(ctx context.Context, zoneID string) ([]ZoneRailgun, error) {
+	uri := fmt.Sprintf("/zones/%s/railguns", zoneID)
+	res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
+	if err != nil {
+		return nil, err
+	}
+	var r zoneRailgunsResponse
+	if err := json.Unmarshal(res, &r); err != nil {
+		return nil, fmt.Errorf("%s: %w", errUnmarshalError, err)
+	}
+	return r.Result, nil
+}
+
+// ZoneRailgunDetails returns the configuration for a given Railgun.
+//
+// API reference: https://api.cloudflare.com/#railguns-for-a-zone-get-railgun-details
+func (api *API) ZoneRailgunDetails(ctx context.Context, zoneID, railgunID string) (ZoneRailgun, error) {
+	uri := fmt.Sprintf("/zones/%s/railguns/%s", zoneID, railgunID)
+	res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
+	if err != nil {
+		return ZoneRailgun{}, err
+	}
+	var r zoneRailgunResponse
+	if err := json.Unmarshal(res, &r); err != nil {
+		return ZoneRailgun{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
+	}
+	return r.Result, nil
+}
+
+// TestRailgunConnection tests a Railgun connection for a given zone.
+//
+// API reference: https://api.cloudflare.com/#railgun-connections-for-a-zone-test-railgun-connection
+func (api *API) TestRailgunConnection(ctx context.Context, zoneID, railgunID string) (RailgunDiagnosis, error) {
+	uri := fmt.Sprintf("/zones/%s/railguns/%s/diagnose", zoneID, railgunID)
+	res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
+	if err != nil {
+		return RailgunDiagnosis{}, err
+	}
+	var r railgunDiagnosisResponse
+	if err := json.Unmarshal(res, &r); err != nil {
+		return RailgunDiagnosis{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
+	}
+	return r.Result, nil
+}
+
+// connectZoneRailgun connects (true) or disconnects (false) a Railgun for a given zone.
+//
+// API reference: https://api.cloudflare.com/#railguns-for-a-zone-connect-or-disconnect-a-railgun
+func (api *API) connectZoneRailgun(ctx context.Context, zoneID, railgunID string, connect bool) (ZoneRailgun, error) {
+	uri := fmt.Sprintf("/zones/%s/railguns/%s", zoneID, railgunID)
+	params := struct {
+		Connected bool `json:"connected"`
+	}{
+		Connected: connect,
+	}
+	res, err := api.makeRequestContext(ctx, http.MethodPatch, uri, params)
+	if err != nil {
+		return ZoneRailgun{}, err
+	}
+	var r zoneRailgunResponse
+	if err := json.Unmarshal(res, &r); err != nil {
+		return ZoneRailgun{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
+	}
+	return r.Result, nil
+}
+
+// ConnectZoneRailgun connects a Railgun for a given zone.
+//
+// API reference: https://api.cloudflare.com/#railguns-for-a-zone-connect-or-disconnect-a-railgun
+func (api *API) ConnectZoneRailgun(ctx context.Context, zoneID, railgunID string) (ZoneRailgun, error) {
+	return api.connectZoneRailgun(ctx, zoneID, railgunID, true)
+}
+
+// DisconnectZoneRailgun disconnects a Railgun for a given zone.
+//
+// API reference: https://api.cloudflare.com/#railguns-for-a-zone-connect-or-disconnect-a-railgun
+func (api *API) DisconnectZoneRailgun(ctx context.Context, zoneID, railgunID string) (ZoneRailgun, error) {
+	return api.connectZoneRailgun(ctx, zoneID, railgunID, false)
+}
diff --git a/resource.go b/resource.go
index 481eae25a..2113e5eba 100644
--- a/resource.go
+++ b/resource.go
@@ -27,11 +27,6 @@ func (rc *ResourceContainer) URLFragment() string {
 	if rc.Level == "" {
 		return rc.Identifier
 	}
-
-	if rc.Level == UserRouteLevel {
-		return "user"
-	}
-
 	return fmt.Sprintf("%s/%s", rc.Level, rc.Identifier)
 }
 
diff --git a/stream.go b/stream.go
index 9f81c61c3..95edd6b5e 100644
--- a/stream.go
+++ b/stream.go
@@ -28,7 +28,7 @@ var (
 type StreamVideo struct {
 	AllowedOrigins        []string                 `json:"allowedOrigins,omitempty"`
 	Created               *time.Time               `json:"created,omitempty"`
-	Duration              float64                  `json:"duration,omitempty"`
+	Duration              int                      `json:"duration,omitempty"`
 	Input                 StreamVideoInput         `json:"input,omitempty"`
 	MaxDurationSeconds    int                      `json:"maxDurationSeconds,omitempty"`
 	Meta                  map[string]interface{}   `json:"meta,omitempty"`
@@ -103,7 +103,6 @@ type StreamUploadFromURLParameters struct {
 	AllowedOrigins        []string                `json:"allowedOrigins,omitempty"`
 	RequireSignedURLs     bool                    `json:"requireSignedURLs,omitempty"`
 	Watermark             UploadVideoURLWatermark `json:"watermark,omitempty"`
-	Meta                  map[string]interface{}  `json:"meta,omitempty"`
 }
 
 // StreamCreateVideoParameters are parameters used when creating a video.
@@ -116,7 +115,6 @@ type StreamCreateVideoParameters struct {
 	AllowedOrigins        []string                `json:"allowedOrigins,omitempty"`
 	RequireSignedURLs     bool                    `json:"requireSignedURLs,omitempty"`
 	Watermark             UploadVideoURLWatermark `json:"watermark,omitempty"`
-	Meta                  map[string]interface{}  `json:"meta,omitempty"`
 }
 
 // UploadVideoURLWatermark represents UID of an existing watermark.
diff --git a/teams_devices.go b/teams_devices.go
index 8836cbe51..941a37670 100644
--- a/teams_devices.go
+++ b/teams_devices.go
@@ -31,6 +31,7 @@ type TeamsDeviceListItem struct {
 	OSVersion        string   `json:"os_version,omitempty"`
 	OSDistroName     string   `json:"os_distro_name,omitempty"`
 	OsDistroRevision string   `json:"os_distro_revision,omitempty"`
+	OSVersionExtra   string   `json:"os_version_extra,omitempty"`
 	MacAddress       string   `json:"mac_address,omitempty"`
 	IP               string   `json:"ip,omitempty"`
 	Created          string   `json:"created,omitempty"`
diff --git a/tunnel.go b/tunnel.go
index 92e91b991..30fa97896 100644
--- a/tunnel.go
+++ b/tunnel.go
@@ -6,34 +6,9 @@ import (
 	"errors"
 	"fmt"
 	"net/http"
-	"strconv"
 	"time"
 )
 
-// A TunnelDuration is a Duration that has custom serialization for JSON.
-// JSON in Javascript assumes that int fields are 32 bits and Duration fields
-// are deserialized assuming that numbers are in nanoseconds, which in 32bit
-// integers limits to just 2 seconds. This type assumes that when
-// serializing/deserializing from JSON, that the number is in seconds, while it
-// maintains the YAML serde assumptions.
-type TunnelDuration struct {
-	time.Duration
-}
-
-func (s TunnelDuration) MarshalJSON() ([]byte, error) {
-	return json.Marshal(s.Duration.Seconds())
-}
-
-func (s *TunnelDuration) UnmarshalJSON(data []byte) error {
-	seconds, err := strconv.ParseInt(string(data), 10, 64)
-	if err != nil {
-		return err
-	}
-
-	s.Duration = time.Duration(seconds * int64(time.Second))
-	return nil
-}
-
 // ErrMissingTunnelID is for when a required tunnel ID is missing from the
 // parameters.
 var ErrMissingTunnelID = errors.New("required missing tunnel ID")
@@ -143,17 +118,17 @@ type UnvalidatedIngressRule struct {
 // config.
 type OriginRequestConfig struct {
 	// HTTP proxy timeout for establishing a new connection
-	ConnectTimeout *TunnelDuration `json:"connectTimeout,omitempty"`
+	ConnectTimeout *time.Duration `json:"connectTimeout,omitempty"`
 	// HTTP proxy timeout for completing a TLS handshake
-	TLSTimeout *TunnelDuration `json:"tlsTimeout,omitempty"`
+	TLSTimeout *time.Duration `json:"tlsTimeout,omitempty"`
 	// HTTP proxy TCP keepalive duration
-	TCPKeepAlive *TunnelDuration `json:"tcpKeepAlive,omitempty"`
+	TCPKeepAlive *time.Duration `json:"tcpKeepAlive,omitempty"`
 	// HTTP proxy should disable "happy eyeballs" for IPv4/v6 fallback
 	NoHappyEyeballs *bool `json:"noHappyEyeballs,omitempty"`
 	// HTTP proxy maximum keepalive connection pool size
 	KeepAliveConnections *int `json:"keepAliveConnections,omitempty"`
 	// HTTP proxy timeout for closing an idle connection
-	KeepAliveTimeout *TunnelDuration `json:"keepAliveTimeout,omitempty"`
+	KeepAliveTimeout *time.Duration `json:"keepAliveTimeout,omitempty"`
 	// Sets the HTTP Host header for the local webserver.
 	HTTPHostHeader *string `json:"httpHostHeader,omitempty"`
 	// Hostname on the origin server certificate.
diff --git a/virtualdns.go b/virtualdns.go
new file mode 100644
index 000000000..d4bc91bd9
--- /dev/null
+++ b/virtualdns.go
@@ -0,0 +1,165 @@
+package cloudflare
+
+import (
+	"context"
+	"fmt"
+)
+
+// NOTE: Everything in this file is deprecated. See `dns_firewall.go` instead.
+
+// VirtualDNS represents a Virtual DNS configuration.
+//
+// Deprecated: Use DNSFirewallCluster instead.
+type VirtualDNS struct {
+	ID                   string   `json:"id"`
+	Name                 string   `json:"name"`
+	OriginIPs            []string `json:"origin_ips"`
+	VirtualDNSIPs        []string `json:"virtual_dns_ips"`
+	MinimumCacheTTL      uint     `json:"minimum_cache_ttl"`
+	MaximumCacheTTL      uint     `json:"maximum_cache_ttl"`
+	DeprecateAnyRequests bool     `json:"deprecate_any_requests"`
+	ModifiedOn           string   `json:"modified_on"`
+}
+
+// VirtualDNSAnalyticsMetrics represents a group of aggregated Virtual DNS metrics.
+//
+// Deprecated: Use DNSFirewallAnalyticsMetrics instead.
+type VirtualDNSAnalyticsMetrics DNSFirewallAnalyticsMetrics
+
+// VirtualDNSAnalytics represents a set of aggregated Virtual DNS metrics.
+//
+// Deprecated: Use DNSFirewallAnalytics instead.
+type VirtualDNSAnalytics struct {
+	Totals VirtualDNSAnalyticsMetrics `json:"totals"`
+	Min    VirtualDNSAnalyticsMetrics `json:"min"`
+	Max    VirtualDNSAnalyticsMetrics `json:"max"`
+}
+
+// VirtualDNSUserAnalyticsOptions represents range and dimension selection on analytics endpoint
+//
+// Deprecated: Use DNSFirewallUserAnalyticsOptions instead.
+type VirtualDNSUserAnalyticsOptions DNSFirewallUserAnalyticsOptions
+
+// VirtualDNSResponse represents a Virtual DNS response.
+//
+// Deprecated: This internal type will be removed in the future.
+type VirtualDNSResponse struct {
+	Response
+	Result *VirtualDNS `json:"result"`
+}
+
+// VirtualDNSListResponse represents an array of Virtual DNS responses.
+//
+// Deprecated: This internal type will be removed in the future.
+type VirtualDNSListResponse struct {
+	Response
+	Result []*VirtualDNS `json:"result"`
+}
+
+// VirtualDNSAnalyticsResponse represents a Virtual DNS analytics response.
+//
+// Deprecated: This internal type will be removed in the future.
+type VirtualDNSAnalyticsResponse struct {
+	Response
+	Result VirtualDNSAnalytics `json:"result"`
+}
+
+// CreateVirtualDNS creates a new Virtual DNS cluster.
+//
+// Deprecated: Use CreateDNSFirewallCluster instead.
+func (api *API) CreateVirtualDNS(ctx context.Context, v *VirtualDNS) (*VirtualDNS, error) {
+	if v == nil {
+		return nil, fmt.Errorf("cluster must not be nil")
+	}
+
+	res, err := api.CreateDNSFirewallCluster(ctx, v.vdnsUpgrade())
+	return res.vdnsDowngrade(), err
+}
+
+// VirtualDNS fetches a single virtual DNS cluster.
+//
+// Deprecated: Use DNSFirewallCluster instead.
+func (api *API) VirtualDNS(ctx context.Context, virtualDNSID string) (*VirtualDNS, error) {
+	res, err := api.DNSFirewallCluster(ctx, virtualDNSID)
+	return res.vdnsDowngrade(), err
+}
+
+// ListVirtualDNS lists the virtual DNS clusters associated with an account.
+//
+// Deprecated: Use ListDNSFirewallClusters instead.
+func (api *API) ListVirtualDNS(ctx context.Context) ([]*VirtualDNS, error) {
+	res, err := api.ListDNSFirewallClusters(ctx)
+	if res == nil {
+		return nil, err
+	}
+
+	clusters := make([]*VirtualDNS, 0, len(res))
+	for _, cluster := range res {
+		clusters = append(clusters, cluster.vdnsDowngrade())
+	}
+
+	return clusters, err
+}
+
+// UpdateVirtualDNS updates a Virtual DNS cluster.
+//
+// Deprecated: Use UpdateDNSFirewallCluster instead.
+func (api *API) UpdateVirtualDNS(ctx context.Context, virtualDNSID string, vv VirtualDNS) error {
+	return api.UpdateDNSFirewallCluster(ctx, virtualDNSID, vv.vdnsUpgrade())
+}
+
+// DeleteVirtualDNS deletes a Virtual DNS cluster. Note that this cannot be
+// undone, and will stop all traffic to that cluster.
+//
+// Deprecated: Use DeleteDNSFirewallCluster instead.
+func (api *API) DeleteVirtualDNS(ctx context.Context, virtualDNSID string) error {
+	return api.DeleteDNSFirewallCluster(ctx, virtualDNSID)
+}
+
+// VirtualDNSUserAnalytics retrieves analytics report for a specified dimension and time range
+//
+// Deprecated: Use DNSFirewallUserAnalytics instead.
+func (api *API) VirtualDNSUserAnalytics(ctx context.Context, virtualDNSID string, o VirtualDNSUserAnalyticsOptions) (VirtualDNSAnalytics, error) {
+	res, err := api.DNSFirewallUserAnalytics(ctx, virtualDNSID, DNSFirewallUserAnalyticsOptions(o))
+	return res.vdnsDowngrade(), err
+}
+
+// --- Compatibility helper functions ---
+
+func (v VirtualDNS) vdnsUpgrade() DNSFirewallCluster {
+	return DNSFirewallCluster{
+		ID:                   v.ID,
+		Name:                 v.Name,
+		UpstreamIPs:          v.OriginIPs,
+		DNSFirewallIPs:       v.VirtualDNSIPs,
+		MinimumCacheTTL:      v.MinimumCacheTTL,
+		MaximumCacheTTL:      v.MaximumCacheTTL,
+		DeprecateAnyRequests: v.DeprecateAnyRequests,
+		ModifiedOn:           v.ModifiedOn,
+	}
+}
+
+func (v *DNSFirewallCluster) vdnsDowngrade() *VirtualDNS {
+	if v == nil {
+		return nil
+	}
+
+	return &VirtualDNS{
+		ID:                   v.ID,
+		Name:                 v.Name,
+		OriginIPs:            v.UpstreamIPs,
+		VirtualDNSIPs:        v.DNSFirewallIPs,
+		MinimumCacheTTL:      v.MinimumCacheTTL,
+		MaximumCacheTTL:      v.MaximumCacheTTL,
+		DeprecateAnyRequests: v.DeprecateAnyRequests,
+		ModifiedOn:           v.ModifiedOn,
+	}
+}
+
+func (v DNSFirewallAnalytics) vdnsDowngrade() VirtualDNSAnalytics {
+	return VirtualDNSAnalytics{
+		Totals: VirtualDNSAnalyticsMetrics(v.Totals),
+		Min:    VirtualDNSAnalyticsMetrics(v.Min),
+		Max:    VirtualDNSAnalyticsMetrics(v.Max),
+	}
+}
diff --git a/waiting_room.go b/waiting_room.go
index fe0ed76f1..737e49dc4 100644
--- a/waiting_room.go
+++ b/waiting_room.go
@@ -16,27 +16,25 @@ var (
 
 // WaitingRoom describes a WaitingRoom object.
 type WaitingRoom struct {
-	CreatedOn                  time.Time           `json:"created_on,omitempty"`
-	ModifiedOn                 time.Time           `json:"modified_on,omitempty"`
-	Path                       string              `json:"path"`
-	Name                       string              `json:"name"`
-	Description                string              `json:"description,omitempty"`
-	QueueingMethod             string              `json:"queueing_method,omitempty"`
-	CustomPageHTML             string              `json:"custom_page_html,omitempty"`
-	DefaultTemplateLanguage    string              `json:"default_template_language,omitempty"`
-	Host                       string              `json:"host"`
-	ID                         string              `json:"id,omitempty"`
-	NewUsersPerMinute          int                 `json:"new_users_per_minute"`
-	TotalActiveUsers           int                 `json:"total_active_users"`
-	SessionDuration            int                 `json:"session_duration"`
-	QueueAll                   bool                `json:"queue_all"`
-	DisableSessionRenewal      bool                `json:"disable_session_renewal"`
-	Suspended                  bool                `json:"suspended"`
-	JsonResponseEnabled        bool                `json:"json_response_enabled"`
-	NextEventPrequeueStartTime *time.Time          `json:"next_event_prequeue_start_time,omitempty"`
-	NextEventStartTime         *time.Time          `json:"next_event_start_time,omitempty"`
-	CookieSuffix               string              `json:"cookie_suffix"`
-	AdditionalRoutes           []*WaitingRoomRoute `json:"additional_routes,omitempty"`
+	CreatedOn                  time.Time  `json:"created_on,omitempty"`
+	ModifiedOn                 time.Time  `json:"modified_on,omitempty"`
+	Path                       string     `json:"path"`
+	Name                       string     `json:"name"`
+	Description                string     `json:"description,omitempty"`
+	QueueingMethod             string     `json:"queueing_method,omitempty"`
+	CustomPageHTML             string     `json:"custom_page_html,omitempty"`
+	DefaultTemplateLanguage    string     `json:"default_template_language,omitempty"`
+	Host                       string     `json:"host"`
+	ID                         string     `json:"id,omitempty"`
+	NewUsersPerMinute          int        `json:"new_users_per_minute"`
+	TotalActiveUsers           int        `json:"total_active_users"`
+	SessionDuration            int        `json:"session_duration"`
+	QueueAll                   bool       `json:"queue_all"`
+	DisableSessionRenewal      bool       `json:"disable_session_renewal"`
+	Suspended                  bool       `json:"suspended"`
+	JsonResponseEnabled        bool       `json:"json_response_enabled"`
+	NextEventPrequeueStartTime *time.Time `json:"next_event_prequeue_start_time,omitempty"`
+	NextEventStartTime         *time.Time `json:"next_event_start_time,omitempty"`
 }
 
 // WaitingRoomStatus describes the status of a waiting room.
@@ -94,12 +92,6 @@ type WaitingRoomPagePreviewCustomHTML struct {
 	CustomHTML string `json:"custom_html"`
 }
 
-// WaitingRoomRoute describes a WaitingRoomRoute object.
-type WaitingRoomRoute struct {
-	Host string `json:"host"`
-	Path string `json:"path"`
-}
-
 // WaitingRoomDetailResponse is the API response, containing a single WaitingRoom.
 type WaitingRoomDetailResponse struct {
 	Response


Test patch:
diff --git a/access_application_test.go b/access_application_test.go
index 4275f4525..ce488347d 100644
--- a/access_application_test.go
+++ b/access_application_test.go
@@ -116,7 +116,6 @@ func TestAccessApplication(t *testing.T) {
 				"aud": "737646a56ab1df6ec9bddc7e5ca84eaf3b0768850f3ffb5d74f1534911fe3893",
 				"name": "Admin Site",
 				"domain": "test.example.com/admin",
-				"self_hosted_domains": ["test.example.com/admin", "test.example.com/admin2"],
 				"type": "self_hosted",
 				"session_duration": "24h",
 				"allowed_idps": ["f174e90a-fafe-4643-bbbc-4a0ed4fc8415"],
@@ -145,7 +144,6 @@ func TestAccessApplication(t *testing.T) {
 		AUD:                     "737646a56ab1df6ec9bddc7e5ca84eaf3b0768850f3ffb5d74f1534911fe3893",
 		Name:                    "Admin Site",
 		Domain:                  "test.example.com/admin",
-		SelfHostedDomains:       []string{"test.example.com/admin", "test.example.com/admin2"},
 		Type:                    "self_hosted",
 		SessionDuration:         "24h",
 		AllowedIdps:             []string{"f174e90a-fafe-4643-bbbc-4a0ed4fc8415"},
@@ -196,7 +194,6 @@ func TestCreateAccessApplications(t *testing.T) {
 				"aud": "737646a56ab1df6ec9bddc7e5ca84eaf3b0768850f3ffb5d74f1534911fe3893",
 				"name": "Admin Site",
 				"domain": "test.example.com/admin",
-				"self_hosted_domains": ["test.example.com/admin", "test.example.com/admin2"],
 				"type": "self_hosted",
 				"session_duration": "24h",
 				"allowed_idps": ["f174e90a-fafe-4643-bbbc-4a0ed4fc8415"],
@@ -219,7 +216,6 @@ func TestCreateAccessApplications(t *testing.T) {
 		ID:                     "480f4f69-1a28-4fdd-9240-1ed29f0ac1db",
 		Name:                   "Admin Site",
 		Domain:                 "test.example.com/admin",
-		SelfHostedDomains:      []string{"test.example.com/admin", "test.example.com/admin2"},
 		Type:                   "self_hosted",
 		SessionDuration:        "24h",
 		AUD:                    "737646a56ab1df6ec9bddc7e5ca84eaf3b0768850f3ffb5d74f1534911fe3893",
@@ -279,7 +275,6 @@ func TestUpdateAccessApplication(t *testing.T) {
 				"aud": "737646a56ab1df6ec9bddc7e5ca84eaf3b0768850f3ffb5d74f1534911fe3893",
 				"name": "Admin Site",
 				"domain": "test.example.com/admin",
-				"self_hosted_domains": ["test.example.com/admin", "test.example.com/admin2"],
 				"type": "self_hosted",
 				"session_duration": "24h",
 				"allowed_idps": ["f174e90a-fafe-4643-bbbc-4a0ed4fc8415"],
@@ -302,7 +297,6 @@ func TestUpdateAccessApplication(t *testing.T) {
 		ID:                     "480f4f69-1a28-4fdd-9240-1ed29f0ac1db",
 		Name:                   "Admin Site",
 		Domain:                 "test.example.com/admin",
-		SelfHostedDomains:      []string{"test.example.com/admin", "test.example.com/admin2"},
 		Type:                   "self_hosted",
 		SessionDuration:        "24h",
 		AUD:                    "737646a56ab1df6ec9bddc7e5ca84eaf3b0768850f3ffb5d74f1534911fe3893",
diff --git a/cloudflare_test.go b/cloudflare_test.go
index c84114421..831b1464d 100644
--- a/cloudflare_test.go
+++ b/cloudflare_test.go
@@ -352,7 +352,7 @@ func TestZoneIDByNameWithNonUniqueZonesWithoutOrgID(t *testing.T) {
 }
 
 func TestZoneIDByNameWithIDN(t *testing.T) {
-	setup()
+	setup(UsingAccount("01a7362d577a6c3019a474fd6f485823"))
 	defer teardown()
 
 	handler := func(w http.ResponseWriter, r *http.Request) {
diff --git a/custom_nameservers_test.go b/custom_nameservers_test.go
deleted file mode 100644
index e1b51fa16..000000000
--- a/custom_nameservers_test.go
+++ /dev/null
@@ -1,225 +0,0 @@
-package cloudflare
-
-import (
-	"context"
-	"fmt"
-	"net/http"
-	"testing"
-
-	"github.com/stretchr/testify/assert"
-)
-
-func TestAccountCustomNameserver_Get(t *testing.T) {
-	setup()
-	defer teardown()
-
-	handler := func(w http.ResponseWriter, r *http.Request) {
-		assert.Equal(t, http.MethodGet, r.Method, "Expected method 'GET', got %s", r.Method)
-		w.Header().Set("content-type", "application/json")
-		fmt.Fprintf(w, `{
-			"success": true,
-			"errors": [],
-			"messages": [],
-			"result": [
-				{
-					"ns_name": "ns1.example.com",
-					"ns_set": 1,
-					"dns_records": [
-						{
-							"type": "A",
-							"value": "192.0.2.1"
-						},
-						{
-							"type": "AAAA",
-							"value": "2400:cb00:2049:1::ffff:ffee"
-						}
-					]
-				},
-				{
-					"ns_name": "ns2.example.com",
-					"ns_set": 1,
-					"dns_records": [
-						{
-							"type": "A",
-							"value": "192.0.2.2"
-						},
-						{
-							"type": "AAAA",
-							"value": "2400:cb00:2049:1::ffff:fffe"
-						}
-					]
-				}
-			]
-		}`)
-	}
-
-	mux.HandleFunc("/accounts/"+testAccountID+"/custom_ns", handler)
-	want := []CustomNameserverResult{
-		{
-			DNSRecords: []CustomNameserverRecord{
-				{
-					Type:  "A",
-					Value: "192.0.2.1",
-				},
-				{
-					Type:  "AAAA",
-					Value: "2400:cb00:2049:1::ffff:ffee",
-				},
-			},
-			NSName: "ns1.example.com",
-			NSSet:  1,
-		},
-		{
-			DNSRecords: []CustomNameserverRecord{
-				{
-					Type:  "A",
-					Value: "192.0.2.2",
-				},
-				{
-					Type:  "AAAA",
-					Value: "2400:cb00:2049:1::ffff:fffe",
-				},
-			},
-			NSName: "ns2.example.com",
-			NSSet:  1,
-		},
-	}
-
-	actual, err := client.GetCustomNameservers(context.Background(), AccountIdentifier(testAccountID), GetCustomNameserversParams{})
-
-	if assert.NoError(t, err) {
-		assert.Equal(t, want, actual)
-	}
-}
-
-func TestAccountCustomNameserver_Create(t *testing.T) {
-	setup()
-	defer teardown()
-
-	handler := func(w http.ResponseWriter, r *http.Request) {
-		assert.Equal(t, http.MethodPost, r.Method, "Expected method 'GET', got %s", r.Method)
-		w.Header().Set("content-type", "application/json")
-		fmt.Fprintf(w, `{
-			"success": true,
-			"errors": [],
-			"messages": [],
-			"result": {
-				"ns_name": "ns1.example.com",
-				"ns_set": 1,
-				"dns_records": [
-					{
-						"type": "A",
-						"value": "192.0.2.1"
-					},
-					{
-						"type": "AAAA",
-						"value": "2400:cb00:2049:1::ffff:ffee"
-					}
-				]
-			}
-		}`)
-	}
-
-	mux.HandleFunc("/accounts/"+testAccountID+"/custom_ns", handler)
-	want := CustomNameserverResult{
-		DNSRecords: []CustomNameserverRecord{
-			{
-				Type:  "A",
-				Value: "192.0.2.1",
-			},
-			{
-				Type:  "AAAA",
-				Value: "2400:cb00:2049:1::ffff:ffee",
-			},
-		},
-		NSName: "ns1.example.com",
-		NSSet:  1,
-	}
-
-	actual, err := client.CreateCustomNameservers(
-		context.Background(),
-		AccountIdentifier(testAccountID),
-		CreateCustomNameserversParams{
-			NSName: "ns1.example.com",
-			NSSet:  1,
-		},
-	)
-
-	if assert.NoError(t, err) {
-		assert.Equal(t, want, actual)
-	}
-}
-
-func TestAccountCustomNameserver_GetEligibleZones(t *testing.T) {
-	setup()
-	defer teardown()
-
-	handler := func(w http.ResponseWriter, r *http.Request) {
-		assert.Equal(t, http.MethodGet, r.Method, "Expected method 'GET', got %s", r.Method)
-		w.Header().Set("content-type", "application/json")
-		fmt.Fprintf(w, `{
-    "result": [
-        "example.com",
-        "example2.com",
-        "example3.com"
-    ],
-    "success": true,
-    "errors": [],
-    "messages": []
-}`)
-	}
-
-	mux.HandleFunc("/accounts/"+testAccountID+"/custom_ns/availability", handler)
-	want := []string{
-		"example.com",
-		"example2.com",
-		"example3.com",
-	}
-
-	actual, err := client.GetEligibleZonesAccountCustomNameservers(
-		context.Background(),
-		AccountIdentifier(testAccountID),
-		GetEligibleZonesAccountCustomNameserversParams{},
-	)
-
-	if assert.NoError(t, err) {
-		assert.Equal(t, want, actual)
-	}
-}
-
-func TestAccountCustomNameserver_GetAccountCustomNameserverZoneMetadata(t *testing.T) {
-	setup()
-	defer teardown()
-
-	handler := func(w http.ResponseWriter, r *http.Request) {
-		assert.Equal(t, http.MethodGet, r.Method, "Expected method 'GET', got %s", r.Method)
-		w.Header().Set("content-type", "application/json")
-		fmt.Fprintf(w, `{
-			"result": {
-				"type": "account",
-				"ns_set": "1",
-				"enabled": true
-			},
-			"success": true,
-			"errors": [],
-			"messages": []
-		}`)
-	}
-
-	mux.HandleFunc("/zones/"+testZoneID+"/custom_ns", handler)
-	want := CustomNameserverZoneMetadata{
-		Type:    "account",
-		NSSet:   "1",
-		Enabled: true,
-	}
-
-	actual, err := client.GetCustomNameserverZoneMetadata(
-		context.Background(),
-		ZoneIdentifier(testZoneID),
-		GetCustomNameserverZoneMetadataParams{},
-	)
-
-	if assert.NoError(t, err) {
-		assert.Equal(t, want, actual)
-	}
-}
diff --git a/dns_firewall_test.go b/dns_firewall_test.go
index 5c85320cb..1002b8d9d 100644
--- a/dns_firewall_test.go
+++ b/dns_firewall_test.go
@@ -10,74 +10,15 @@ import (
 	"github.com/stretchr/testify/assert"
 )
 
-func TestDNSFirewallUserAnalytics_UserLevel(t *testing.T) {
-	setup()
-	defer teardown()
-
-	now := time.Now().UTC()
-	since := now.Add(-1 * time.Hour)
-	until := now
-
-	handler := func(w http.ResponseWriter, r *http.Request) {
-		expectedMetrics := "queryCount,uncachedCount,staleCount,responseTimeAvg,responseTimeMedia,responseTime90th,responseTime99th"
-
-		assert.Equal(t, http.MethodGet, r.Method, "Expected method 'GET'")
-		assert.Equal(t, expectedMetrics, r.URL.Query().Get("metrics"), "Expected many metrics in URL parameter")
-		assert.Equal(t, since.Format(time.RFC3339), r.URL.Query().Get("since"), "Expected since parameter in URL")
-		assert.Equal(t, until.Format(time.RFC3339), r.URL.Query().Get("until"), "Expected until parameter in URL")
-
-		w.Header().Set("content-type", "application/json")
-		fmt.Fprint(w, `{
-		  "result": {
-			"totals":{
-				"queryCount": 5,
-				"uncachedCount":6,
-				"staleCount":7,
-				"responseTimeAvg":1.0,
-				"responseTimeMedian":2.0,
-				"responseTime90th":3.0,
-				"responseTime99th":4.0
-			  }
-		  },
-		  "success": true,
-		  "errors": null,
-		  "messages": null
-		}`)
-	}
-
-	mux.HandleFunc("/user/dns_firewall/12345/dns_analytics/report", handler)
-	want := DNSFirewallAnalytics{
-		Totals: DNSFirewallAnalyticsMetrics{
-			QueryCount:         Int64Ptr(5),
-			UncachedCount:      Int64Ptr(6),
-			StaleCount:         Int64Ptr(7),
-			ResponseTimeAvg:    Float64Ptr(1.0),
-			ResponseTimeMedian: Float64Ptr(2.0),
-			ResponseTime90th:   Float64Ptr(3.0),
-			ResponseTime99th:   Float64Ptr(4.0),
-		},
-	}
-
-	actual, err := client.GetDNSFirewallUserAnalytics(context.Background(), UserIdentifier("foo"), GetDNSFirewallUserAnalyticsParams{ClusterID: "12345", DNSFirewallUserAnalyticsOptions: DNSFirewallUserAnalyticsOptions{
-		Metrics: []string{
-			"queryCount",
-			"uncachedCount",
-			"staleCount",
-			"responseTimeAvg",
-			"responseTimeMedia",
-			"responseTime90th",
-			"responseTime99th",
-		},
-		Since: &since,
-		Until: &until,
-	}})
+func float64Ptr(v float64) *float64 {
+	return &v
+}
 
-	if assert.NoError(t, err) {
-		assert.Equal(t, want, actual)
-	}
+func int64Ptr(v int64) *int64 {
+	return &v
 }
 
-func TestDNSFirewallUserAnalytics_AccountLevel(t *testing.T) {
+func TestDNSFirewallUserAnalytics(t *testing.T) {
 	setup()
 	defer teardown()
 
@@ -112,20 +53,20 @@ func TestDNSFirewallUserAnalytics_AccountLevel(t *testing.T) {
 		}`)
 	}
 
-	mux.HandleFunc("/accounts/"+testAccountID+"/dns_firewall/12345/dns_analytics/report", handler)
+	mux.HandleFunc("/user/dns_firewall/12345/dns_analytics/report", handler)
 	want := DNSFirewallAnalytics{
 		Totals: DNSFirewallAnalyticsMetrics{
-			QueryCount:         Int64Ptr(5),
-			UncachedCount:      Int64Ptr(6),
-			StaleCount:         Int64Ptr(7),
-			ResponseTimeAvg:    Float64Ptr(1.0),
-			ResponseTimeMedian: Float64Ptr(2.0),
-			ResponseTime90th:   Float64Ptr(3.0),
-			ResponseTime99th:   Float64Ptr(4.0),
+			QueryCount:         int64Ptr(5),
+			UncachedCount:      int64Ptr(6),
+			StaleCount:         int64Ptr(7),
+			ResponseTimeAvg:    float64Ptr(1.0),
+			ResponseTimeMedian: float64Ptr(2.0),
+			ResponseTime90th:   float64Ptr(3.0),
+			ResponseTime99th:   float64Ptr(4.0),
 		},
 	}
 
-	actual, err := client.GetDNSFirewallUserAnalytics(context.Background(), AccountIdentifier(testAccountID), GetDNSFirewallUserAnalyticsParams{ClusterID: "12345", DNSFirewallUserAnalyticsOptions: DNSFirewallUserAnalyticsOptions{
+	params := DNSFirewallUserAnalyticsOptions{
 		Metrics: []string{
 			"queryCount",
 			"uncachedCount",
@@ -137,8 +78,8 @@ func TestDNSFirewallUserAnalytics_AccountLevel(t *testing.T) {
 		},
 		Since: &since,
 		Until: &until,
-	}})
-
+	}
+	actual, err := client.DNSFirewallUserAnalytics(context.Background(), "12345", params)
 	if assert.NoError(t, err) {
 		assert.Equal(t, want, actual)
 	}
diff --git a/railgun_test.go b/railgun_test.go
new file mode 100644
index 000000000..e80c146c8
--- /dev/null
+++ b/railgun_test.go
@@ -0,0 +1,622 @@
+package cloudflare
+
+import (
+	"context"
+	"fmt"
+	"io"
+	"net/http"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestCreateRailgun(t *testing.T) {
+	setup()
+	defer teardown()
+
+	handler := func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, http.MethodPost, r.Method, "Expected method 'POST', got %s", r.Method)
+		w.Header().Set("content-type", "application/json")
+		b, err := io.ReadAll(r.Body)
+		defer r.Body.Close()
+		if assert.NoError(t, err) {
+			assert.JSONEq(t, `{"name":"My Railgun"}`, string(b))
+		}
+		fmt.Fprint(w, `{
+            "success": true,
+            "errors": [],
+            "messages": [],
+            "result": {
+                "id": "e928d310693a83094309acf9ead50448",
+                "name": "My Railgun",
+                "status": "active",
+                "enabled": true,
+                "zones_connected": 2,
+                "build": "b1234",
+                "version": "2.1",
+                "revision": "123",
+                "activation_key": "e4edc00281cb56ebac22c81be9bac8f3",
+                "activated_on": "2014-01-02T02:20:00Z",
+                "created_on": "2014-01-01T05:20:00Z",
+                "modified_on": "2014-01-01T05:20:00Z"
+            }
+        }`)
+	}
+
+	mux.HandleFunc("/railguns", handler)
+	activatedOn, _ := time.Parse(time.RFC3339, "2014-01-02T02:20:00Z")
+	createdOn, _ := time.Parse(time.RFC3339, "2014-01-01T05:20:00Z")
+	modifiedOn, _ := time.Parse(time.RFC3339, "2014-01-01T05:20:00Z")
+	want := Railgun{
+		ID:             "e928d310693a83094309acf9ead50448",
+		Name:           "My Railgun",
+		Status:         "active",
+		Enabled:        true,
+		ZonesConnected: 2,
+		Build:          "b1234",
+		Version:        "2.1",
+		Revision:       "123",
+		ActivationKey:  "e4edc00281cb56ebac22c81be9bac8f3",
+		ActivatedOn:    activatedOn,
+		CreatedOn:      createdOn,
+		ModifiedOn:     modifiedOn,
+	}
+
+	actual, err := client.CreateRailgun(context.Background(), "My Railgun")
+	if assert.NoError(t, err) {
+		assert.Equal(t, want, actual)
+	}
+}
+
+func TestListRailguns(t *testing.T) {
+	setup()
+	defer teardown()
+
+	handler := func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, http.MethodGet, r.Method, "Expected method 'GET', got %s", r.Method)
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprint(w, `{
+            "success": true,
+            "errors": [],
+            "messages": [],
+            "result": [
+                {
+                    "id": "e928d310693a83094309acf9ead50448",
+                    "name": "My Railgun",
+                    "status": "active",
+                    "enabled": true,
+                    "zones_connected": 2,
+                    "build": "b1234",
+                    "version": "2.1",
+                    "revision": "123",
+                    "activation_key": "e4edc00281cb56ebac22c81be9bac8f3",
+                    "activated_on": "2014-01-02T02:20:00Z",
+                    "created_on": "2014-01-01T05:20:00Z",
+                    "modified_on": "2014-01-01T05:20:00Z"
+                }
+            ],
+            "result_info": {
+                "page": 1,
+                "per_page": 20,
+                "count": 1,
+                "total_count": 2000
+            }
+        }`)
+	}
+
+	mux.HandleFunc("/railguns", handler)
+	activatedOn, _ := time.Parse(time.RFC3339, "2014-01-02T02:20:00Z")
+	createdOn, _ := time.Parse(time.RFC3339, "2014-01-01T05:20:00Z")
+	modifiedOn, _ := time.Parse(time.RFC3339, "2014-01-01T05:20:00Z")
+	want := []Railgun{
+		{
+			ID:             "e928d310693a83094309acf9ead50448",
+			Name:           "My Railgun",
+			Status:         "active",
+			Enabled:        true,
+			ZonesConnected: 2,
+			Build:          "b1234",
+			Version:        "2.1",
+			Revision:       "123",
+			ActivationKey:  "e4edc00281cb56ebac22c81be9bac8f3",
+			ActivatedOn:    activatedOn,
+			CreatedOn:      createdOn,
+			ModifiedOn:     modifiedOn,
+		},
+	}
+
+	actual, err := client.ListRailguns(context.Background(), RailgunListOptions{Direction: "desc"})
+	if assert.NoError(t, err) {
+		assert.Equal(t, want, actual)
+	}
+}
+
+func TestRailgunDetails(t *testing.T) {
+	setup()
+	defer teardown()
+
+	handler := func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, http.MethodGet, r.Method, "Expected method 'GET', got %s", r.Method)
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprint(w, `{
+            "success": true,
+            "errors": [],
+            "messages": [],
+            "result": {
+                "id": "e928d310693a83094309acf9ead50448",
+                "name": "My Railgun",
+                "status": "active",
+                "enabled": true,
+                "zones_connected": 2,
+                "build": "b1234",
+                "version": "2.1",
+                "revision": "123",
+                "activation_key": "e4edc00281cb56ebac22c81be9bac8f3",
+                "activated_on": "2014-01-02T02:20:00Z",
+                "created_on": "2014-01-01T05:20:00Z",
+                "modified_on": "2014-01-01T05:20:00Z"
+            }
+        }`)
+	}
+
+	mux.HandleFunc("/railguns/e928d310693a83094309acf9ead50448", handler)
+	activatedOn, _ := time.Parse(time.RFC3339, "2014-01-02T02:20:00Z")
+	createdOn, _ := time.Parse(time.RFC3339, "2014-01-01T05:20:00Z")
+	modifiedOn, _ := time.Parse(time.RFC3339, "2014-01-01T05:20:00Z")
+	want := Railgun{
+		ID:             "e928d310693a83094309acf9ead50448",
+		Name:           "My Railgun",
+		Status:         "active",
+		Enabled:        true,
+		ZonesConnected: 2,
+		Build:          "b1234",
+		Version:        "2.1",
+		Revision:       "123",
+		ActivationKey:  "e4edc00281cb56ebac22c81be9bac8f3",
+		ActivatedOn:    activatedOn,
+		CreatedOn:      createdOn,
+		ModifiedOn:     modifiedOn,
+	}
+
+	actual, err := client.RailgunDetails(context.Background(), "e928d310693a83094309acf9ead50448")
+	if assert.NoError(t, err) {
+		assert.Equal(t, want, actual)
+	}
+
+	_, err = client.RailgunDetails(context.Background(), "bar")
+	assert.Error(t, err)
+}
+
+func TestRailgunZones(t *testing.T) {
+	setup()
+	defer teardown()
+
+	handler := func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, http.MethodGet, r.Method, "Expected method 'GET', got %s", r.Method)
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprint(w, `{
+            "success": true,
+            "errors": [],
+            "messages": [],
+            "result": [
+                {
+                    "id": "023e105f4ecef8ad9ca31a8372d0c353",
+                    "name": "example.com",
+                    "development_mode": 7200,
+                    "original_name_servers": [
+                        "ns1.originaldnshost.com",
+                        "ns2.originaldnshost.com"
+                    ],
+                    "original_registrar": "GoDaddy",
+                    "original_dnshost": "NameCheap",
+                    "created_on": "2014-01-01T05:20:00.12345Z",
+                    "modified_on": "2014-01-01T05:20:00.12345Z"
+                }
+            ],
+            "result_info": {
+                "page": 1,
+                "per_page": 20,
+                "count": 1,
+                "total_count": 2000
+            }
+        }`)
+	}
+
+	mux.HandleFunc("/railguns/e928d310693a83094309acf9ead50448/zones", handler)
+	createdOn, _ := time.Parse(time.RFC3339, "2014-01-01T05:20:00.12345Z")
+	modifiedOn, _ := time.Parse(time.RFC3339, "2014-01-01T05:20:00.12345Z")
+	want := []Zone{
+		{
+			ID:                "023e105f4ecef8ad9ca31a8372d0c353",
+			Name:              "example.com",
+			DevMode:           7200,
+			OriginalNS:        []string{"ns1.originaldnshost.com", "ns2.originaldnshost.com"},
+			OriginalRegistrar: "GoDaddy",
+			OriginalDNSHost:   "NameCheap",
+			CreatedOn:         createdOn,
+			ModifiedOn:        modifiedOn,
+		},
+	}
+
+	actual, err := client.RailgunZones(context.Background(), "e928d310693a83094309acf9ead50448")
+	if assert.NoError(t, err) {
+		assert.Equal(t, want, actual)
+	}
+
+	_, err = client.RailgunZones(context.Background(), "bar")
+	assert.Error(t, err)
+}
+
+func TestEnableRailgun(t *testing.T) {
+	setup()
+	defer teardown()
+
+	handler := func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, http.MethodPatch, r.Method, "Expected method 'PATCH', got %s", r.Method)
+		b, err := io.ReadAll(r.Body)
+		defer r.Body.Close()
+		if assert.NoError(t, err) {
+			assert.JSONEq(t, `{"enabled":true}`, string(b))
+		}
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprint(w, `{
+            "success": true,
+            "errors": [],
+            "messages": [],
+            "result": {
+                "id": "e928d310693a83094309acf9ead50448",
+                "name": "My Railgun",
+                "status": "active",
+                "enabled": true,
+                "zones_connected": 2,
+                "build": "b1234",
+                "version": "2.1",
+                "revision": "123",
+                "activation_key": "e4edc00281cb56ebac22c81be9bac8f3",
+                "activated_on": "2014-01-02T02:20:00Z",
+                "created_on": "2014-01-01T05:20:00Z",
+                "modified_on": "2014-01-01T05:20:00Z"
+            }
+        }`)
+	}
+
+	mux.HandleFunc("/railguns/e928d310693a83094309acf9ead50448", handler)
+	activatedOn, _ := time.Parse(time.RFC3339, "2014-01-02T02:20:00Z")
+	createdOn, _ := time.Parse(time.RFC3339, "2014-01-01T05:20:00Z")
+	modifiedOn, _ := time.Parse(time.RFC3339, "2014-01-01T05:20:00Z")
+	want := Railgun{
+		ID:             "e928d310693a83094309acf9ead50448",
+		Name:           "My Railgun",
+		Status:         "active",
+		Enabled:        true,
+		ZonesConnected: 2,
+		Build:          "b1234",
+		Version:        "2.1",
+		Revision:       "123",
+		ActivationKey:  "e4edc00281cb56ebac22c81be9bac8f3",
+		ActivatedOn:    activatedOn,
+		CreatedOn:      createdOn,
+		ModifiedOn:     modifiedOn,
+	}
+
+	actual, err := client.EnableRailgun(context.Background(), "e928d310693a83094309acf9ead50448")
+	if assert.NoError(t, err) {
+		assert.Equal(t, want, actual)
+	}
+
+	_, err = client.EnableRailgun(context.Background(), "bar")
+	assert.Error(t, err)
+}
+
+func TestDisableRailgun(t *testing.T) {
+	setup()
+	defer teardown()
+
+	handler := func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, http.MethodPatch, r.Method, "Expected method 'PATCH', got %s", r.Method)
+		b, err := io.ReadAll(r.Body)
+		defer r.Body.Close()
+		if assert.NoError(t, err) {
+			assert.JSONEq(t, `{"enabled":false}`, string(b))
+		}
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprint(w, `{
+            "success": true,
+            "errors": [],
+            "messages": [],
+            "result": {
+                "id": "e928d310693a83094309acf9ead50448",
+                "name": "My Railgun",
+                "status": "active",
+                "enabled": false,
+                "zones_connected": 2,
+                "build": "b1234",
+                "version": "2.1",
+                "revision": "123",
+                "activation_key": "e4edc00281cb56ebac22c81be9bac8f3",
+                "activated_on": "2014-01-02T02:20:00Z",
+                "created_on": "2014-01-01T05:20:00Z",
+                "modified_on": "2014-01-01T05:20:00Z"
+            }
+        }`)
+	}
+
+	mux.HandleFunc("/railguns/e928d310693a83094309acf9ead50448", handler)
+	activatedOn, _ := time.Parse(time.RFC3339, "2014-01-02T02:20:00Z")
+	createdOn, _ := time.Parse(time.RFC3339, "2014-01-01T05:20:00Z")
+	modifiedOn, _ := time.Parse(time.RFC3339, "2014-01-01T05:20:00Z")
+	want := Railgun{
+		ID:             "e928d310693a83094309acf9ead50448",
+		Name:           "My Railgun",
+		Status:         "active",
+		Enabled:        false,
+		ZonesConnected: 2,
+		Build:          "b1234",
+		Version:        "2.1",
+		Revision:       "123",
+		ActivationKey:  "e4edc00281cb56ebac22c81be9bac8f3",
+		ActivatedOn:    activatedOn,
+		CreatedOn:      createdOn,
+		ModifiedOn:     modifiedOn,
+	}
+
+	actual, err := client.DisableRailgun(context.Background(), "e928d310693a83094309acf9ead50448")
+	if assert.NoError(t, err) {
+		assert.Equal(t, want, actual)
+	}
+
+	_, err = client.DisableRailgun(context.Background(), "bar")
+	assert.Error(t, err)
+}
+
+func TestDeleteRailgun(t *testing.T) {
+	setup()
+	defer teardown()
+
+	handler := func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, http.MethodDelete, r.Method, "Expected method 'DELETE', got %s", r.Method)
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprint(w, `{
+            "success": true,
+            "errors": [],
+            "messages": [],
+            "result": {
+                "id": "e928d310693a83094309acf9ead50448"
+            }
+        }`)
+	}
+
+	mux.HandleFunc("/railguns/e928d310693a83094309acf9ead50448", handler)
+	assert.NoError(t, client.DeleteRailgun(context.Background(), "e928d310693a83094309acf9ead50448"))
+	assert.Error(t, client.DeleteRailgun(context.Background(), "bar"))
+}
+
+func TestZoneRailguns(t *testing.T) {
+	setup()
+	defer teardown()
+
+	handler := func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, http.MethodGet, r.Method, "Expected method 'GET', got %s", r.Method)
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprint(w, `{
+            "success": true,
+            "errors": [],
+            "messages": [],
+            "result": [
+                {
+                    "id": "e928d310693a83094309acf9ead50448",
+                    "name": "My Railgun",
+                    "enabled": true,
+                    "connected": true
+                }
+            ],
+            "result_info": {
+                "page": 1,
+                "per_page": 20,
+                "count": 1,
+                "total_count": 2000
+            }
+        }`)
+	}
+
+	mux.HandleFunc("/zones/023e105f4ecef8ad9ca31a8372d0c353/railguns", handler)
+	want := []ZoneRailgun{
+		{
+			ID:        "e928d310693a83094309acf9ead50448",
+			Name:      "My Railgun",
+			Enabled:   true,
+			Connected: true,
+		},
+	}
+
+	actual, err := client.ZoneRailguns(context.Background(), "023e105f4ecef8ad9ca31a8372d0c353")
+	if assert.NoError(t, err) {
+		assert.Equal(t, want, actual)
+	}
+
+	_, err = client.ZoneRailguns(context.Background(), "bar")
+	assert.Error(t, err)
+}
+
+func TestZoneRailgunDetails(t *testing.T) {
+	setup()
+	defer teardown()
+
+	handler := func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, http.MethodGet, r.Method, "Expected method 'GET', got %s", r.Method)
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprint(w, `{
+            "success": true,
+            "errors": [],
+            "messages": [],
+            "result": {
+                "id": "e928d310693a83094309acf9ead50448",
+                "name": "My Railgun",
+                "enabled": true,
+                "connected": true
+            }
+        }`)
+	}
+
+	mux.HandleFunc("/zones/023e105f4ecef8ad9ca31a8372d0c353/railguns/e928d310693a83094309acf9ead50448", handler)
+	want := ZoneRailgun{
+		ID:        "e928d310693a83094309acf9ead50448",
+		Name:      "My Railgun",
+		Enabled:   true,
+		Connected: true,
+	}
+
+	actual, err := client.ZoneRailgunDetails(context.Background(), "023e105f4ecef8ad9ca31a8372d0c353", "e928d310693a83094309acf9ead50448")
+	if assert.NoError(t, err) {
+		assert.Equal(t, want, actual)
+	}
+
+	_, err = client.ZoneRailgunDetails(context.Background(), "bar", "baz")
+	assert.Error(t, err)
+}
+
+func TestTestRailgunConnection(t *testing.T) {
+	setup()
+	defer teardown()
+
+	handler := func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, http.MethodGet, r.Method, "Expected method 'GET', got %s", r.Method)
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprint(w, `{
+            "success": true,
+            "errors": [],
+            "messages": [],
+            "result": {
+                "method": "GET",
+                "host_name": "www.example.com",
+                "http_status": 200,
+                "railgun": "on",
+                "url": "https://www.cloudflare.com",
+                "response_status": "200 OK",
+                "protocol": "HTTP/1.1",
+                "elapsed_time": "0.239013s",
+                "body_size": "63910 bytes",
+                "body_hash": "be27f2429421e12f200cab1da43ba301bdc70e1d",
+                "missing_headers": "No Content-Length or Transfer-Encoding",
+                "connection_close": false,
+                "cloudflare": "on",
+                "cf-ray": "1ddd7570575207d9-LAX",
+                "cf-wan-error": null,
+                "cf-cache-status": null
+            }
+        }`)
+	}
+
+	mux.HandleFunc("/zones/023e105f4ecef8ad9ca31a8372d0c353/railguns/e928d310693a83094309acf9ead50448/diagnose", handler)
+	want := RailgunDiagnosis{
+		Method:          http.MethodGet,
+		HostName:        "www.example.com",
+		HTTPStatus:      200,
+		Railgun:         "on",
+		URL:             "https://www.cloudflare.com",
+		ResponseStatus:  "200 OK",
+		Protocol:        "HTTP/1.1",
+		ElapsedTime:     "0.239013s",
+		BodySize:        "63910 bytes",
+		BodyHash:        "be27f2429421e12f200cab1da43ba301bdc70e1d",
+		MissingHeaders:  "No Content-Length or Transfer-Encoding",
+		ConnectionClose: false,
+		Cloudflare:      "on",
+		CFRay:           "1ddd7570575207d9-LAX",
+		CFWANError:      "",
+		CFCacheStatus:   "",
+	}
+
+	actual, err := client.TestRailgunConnection(context.Background(), "023e105f4ecef8ad9ca31a8372d0c353", "e928d310693a83094309acf9ead50448")
+	if assert.NoError(t, err) {
+		assert.Equal(t, want, actual)
+	}
+
+	_, err = client.TestRailgunConnection(context.Background(), "bar", "baz")
+	assert.Error(t, err)
+}
+
+func TestConnectRailgun(t *testing.T) {
+	setup()
+	defer teardown()
+
+	handler := func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, http.MethodPatch, r.Method, "Expected method 'PATCH', got %s", r.Method)
+		b, err := io.ReadAll(r.Body)
+		defer r.Body.Close()
+		if assert.NoError(t, err) {
+			assert.JSONEq(t, `{"connected":true}`, string(b))
+		}
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprint(w, `{
+            "success": true,
+            "errors": [],
+            "messages": [],
+            "result": {
+                "id": "e928d310693a83094309acf9ead50448",
+                "name": "My Railgun",
+                "enabled": true,
+                "connected": true
+            }
+        }`)
+	}
+
+	mux.HandleFunc("/zones/023e105f4ecef8ad9ca31a8372d0c353/railguns/e928d310693a83094309acf9ead50448", handler)
+	want := ZoneRailgun{
+		ID:        "e928d310693a83094309acf9ead50448",
+		Name:      "My Railgun",
+		Enabled:   true,
+		Connected: true,
+	}
+
+	actual, err := client.ConnectZoneRailgun(context.Background(), "023e105f4ecef8ad9ca31a8372d0c353", "e928d310693a83094309acf9ead50448")
+	if assert.NoError(t, err) {
+		assert.Equal(t, want, actual)
+	}
+
+	_, err = client.ConnectZoneRailgun(context.Background(), "bar", "baz")
+	assert.Error(t, err)
+}
+
+func TestDisconnectRailgun(t *testing.T) {
+	setup()
+	defer teardown()
+
+	handler := func(w http.ResponseWriter, r *http.Request) {
+		assert.Equal(t, http.MethodPatch, r.Method, "Expected method 'PATCH', got %s", r.Method)
+		b, err := io.ReadAll(r.Body)
+		defer r.Body.Close()
+		if assert.NoError(t, err) {
+			assert.JSONEq(t, `{"connected":false}`, string(b))
+		}
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprint(w, `{
+            "success": true,
+            "errors": [],
+            "messages": [],
+            "result": {
+                "id": "e928d310693a83094309acf9ead50448",
+                "name": "My Railgun",
+                "enabled": true,
+                "connected": false
+            }
+        }`)
+	}
+
+	mux.HandleFunc("/zones/023e105f4ecef8ad9ca31a8372d0c353/railguns/e928d310693a83094309acf9ead50448", handler)
+	want := ZoneRailgun{
+		ID:        "e928d310693a83094309acf9ead50448",
+		Name:      "My Railgun",
+		Enabled:   true,
+		Connected: false,
+	}
+
+	actual, err := client.DisconnectZoneRailgun(context.Background(), "023e105f4ecef8ad9ca31a8372d0c353", "e928d310693a83094309acf9ead50448")
+	if assert.NoError(t, err) {
+		assert.Equal(t, want, actual)
+	}
+
+	_, err = client.DisconnectZoneRailgun(context.Background(), "bar", "baz")
+	assert.Error(t, err)
+}
diff --git a/resource_test.go b/resource_test.go
index 0a9e82caa..ef0c96d31 100644
--- a/resource_test.go
+++ b/resource_test.go
@@ -11,11 +11,8 @@ func TestResourcURLFragment(t *testing.T) {
 		container *ResourceContainer
 		want      string
 	}{
-		"account resource": {container: AccountIdentifier("foo"), want: "accounts/foo"},
-		"zone resource":    {container: ZoneIdentifier("foo"), want: "zones/foo"},
-		// this is pretty well deprecated in favour of `AccountIdentifier` but
-		// here for completeness.
-		"user level resource":    {container: UserIdentifier("foo"), want: "user"},
+		"account resource":       {container: AccountIdentifier("foo"), want: "accounts/foo"},
+		"zone resource":          {container: ZoneIdentifier("foo"), want: "zones/foo"},
 		"missing level resource": {container: &ResourceContainer{Level: "", Identifier: "foo"}, want: "foo"},
 	}
 
diff --git a/stream_test.go b/stream_test.go
index a95cc6e45..eb821cec6 100644
--- a/stream_test.go
+++ b/stream_test.go
@@ -22,15 +22,13 @@ const (
       "example.com"
     ],
     "created": "2014-01-02T02:20:00Z",
-    "duration": 300.5,
+    "duration": 300,
     "input": {
       "height": 1080,
       "width": 1920
     },
     "maxDurationSeconds": 300,
-    "meta": {
-	  "name": "My First Stream Video"
-	},
+    "meta": {},
     "modified": "2014-01-02T02:20:00Z",
     "uploadExpiry": "2014-01-02T02:20:00Z",
     "playback": {
@@ -88,7 +86,7 @@ func createTestVideo() StreamVideo {
 	return StreamVideo{
 		AllowedOrigins:     []string{"example.com"},
 		Created:            &created,
-		Duration:           300.5,
+		Duration:           300,
 		Input:              StreamVideoInput{Height: 1080, Width: 1920},
 		MaxDurationSeconds: 300,
 		Modified:           &modified,
@@ -123,9 +121,7 @@ func createTestVideo() StreamVideo {
 			Scale:          0.1,
 			Position:       "center",
 		},
-		Meta: map[string]interface{}{
-			"name": "My First Stream Video",
-		},
+		Meta: map[string]interface{}{},
 		NFT: StreamVideoNFTParameters{
 			Token:    5,
 			Contract: "0x57f1887a8bf19b14fc0d912b9b2acc9af147ea85",
@@ -159,9 +155,6 @@ func TestStream_StreamUploadFromURL(t *testing.T) {
 	input := StreamUploadFromURLParameters{
 		AccountID: testAccountID,
 		URL:       "https://example.com/myvideo.mp4",
-		Meta: map[string]interface{}{
-			"name": "My First Stream Video",
-		},
 	}
 
 	out, err := client.StreamUploadFromURL(context.Background(), input)
@@ -248,9 +241,7 @@ func TestStream_CreateVideoDirectURL(t *testing.T) {
 		assert.Equal(t, ErrMissingMaxDuration, err)
 	}
 
-	input := StreamCreateVideoParameters{AccountID: testAccountID, MaxDurationSeconds: 300, Meta: map[string]interface{}{
-		"name": "My First Stream Video",
-	}}
+	input := StreamCreateVideoParameters{AccountID: testAccountID, MaxDurationSeconds: 300}
 	out, err := client.StreamCreateVideoDirectURL(context.Background(), input)
 
 	created, _ := time.Parse(time.RFC3339, "2014-01-02T02:20:00Z")
@@ -294,15 +285,13 @@ func TestStream_ListVideos(t *testing.T) {
       "example.com"
     ],
     "created": "2014-01-02T02:20:00Z",
-    "duration": 300.5,
+    "duration": 300,
     "input": {
       "height": 1080,
       "width": 1920
     },
     "maxDurationSeconds": 300,
-    "meta": {
-	  "name": "My First Stream Video"
-	},
+    "meta": {},
     "modified": "2014-01-02T02:20:00Z",
     "uploadExpiry": "2014-01-02T02:20:00Z",
     "playback": {
diff --git a/teams_devices_test.go b/teams_devices_test.go
index b639c164c..3c78bb38d 100644
--- a/teams_devices_test.go
+++ b/teams_devices_test.go
@@ -41,6 +41,7 @@ func TestTeamsDevicesList(t *testing.T) {
               "os_version": "10.0.0",
               "os_distro_name": "ubuntu",
               "os_distro_revision": "1.0.0",
+			  "os_version_extra": "(a)",
               "mac_address": "00-00-5E-00-53-00",
               "ip": "192.0.2.1",
               "created": "2017-06-14T00:00:00Z",
@@ -71,6 +72,7 @@ func TestTeamsDevicesList(t *testing.T) {
 		OSVersion:        "10.0.0",
 		OSDistroName:     "ubuntu",
 		OsDistroRevision: "1.0.0",
+		OSVersionExtra:   "(a)",
 		MacAddress:       "00-00-5E-00-53-00",
 		IP:               "192.0.2.1",
 		Created:          "2017-06-14T00:00:00Z",
diff --git a/tunnel_test.go b/tunnel_test.go
index da912ccf4..cacc12fb6 100644
--- a/tunnel_test.go
+++ b/tunnel_test.go
@@ -189,7 +189,6 @@ func TestUpdateTunnelConfiguration(t *testing.T) {
 		fmt.Fprint(w, loadFixture("tunnel", "configuration"))
 	}
 
-	timeout, _ := time.ParseDuration("10s")
 	mux.HandleFunc(fmt.Sprintf("/accounts/%s/cfd_tunnel/%s/configurations", testAccountID, testTunnelID), handler)
 	want := TunnelConfigurationResult{
 		TunnelID: testTunnelID,
@@ -211,7 +210,7 @@ func TestUpdateTunnelConfiguration(t *testing.T) {
 				Enabled: true,
 			},
 			OriginRequest: OriginRequestConfig{
-				ConnectTimeout: &TunnelDuration{timeout},
+				ConnectTimeout: DurationPtr(10),
 			},
 		}}
 
@@ -234,7 +233,7 @@ func TestUpdateTunnelConfiguration(t *testing.T) {
 				Enabled: true,
 			},
 			OriginRequest: OriginRequestConfig{
-				ConnectTimeout: &TunnelDuration{10},
+				ConnectTimeout: DurationPtr(10 * time.Second),
 			},
 		},
 	})
@@ -255,7 +254,6 @@ func TestGetTunnelConfiguration(t *testing.T) {
 		fmt.Fprint(w, loadFixture("tunnel", "configuration"))
 	}
 
-	timeout, _ := time.ParseDuration("10s")
 	mux.HandleFunc(fmt.Sprintf("/accounts/%s/cfd_tunnel/%s/configurations", testAccountID, testTunnelID), handler)
 	want := TunnelConfigurationResult{
 		TunnelID: testTunnelID,
@@ -277,7 +275,7 @@ func TestGetTunnelConfiguration(t *testing.T) {
 				Enabled: true,
 			},
 			OriginRequest: OriginRequestConfig{
-				ConnectTimeout: &TunnelDuration{timeout},
+				ConnectTimeout: DurationPtr(10),
 			},
 		}}
 
diff --git a/virtualdns_test.go b/virtualdns_test.go
new file mode 100644
index 000000000..9adcce6d9
--- /dev/null
+++ b/virtualdns_test.go
@@ -0,0 +1,78 @@
+package cloudflare
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestVirtualDNSUserAnalytics(t *testing.T) {
+	setup()
+	defer teardown()
+
+	now := time.Now().UTC()
+	since := now.Add(-1 * time.Hour)
+	until := now
+
+	handler := func(w http.ResponseWriter, r *http.Request) {
+		expectedMetrics := "queryCount,uncachedCount,staleCount,responseTimeAvg,responseTimeMedia,responseTime90th,responseTime99th"
+
+		assert.Equal(t, http.MethodGet, r.Method, "Expected method 'GET'")
+		assert.Equal(t, expectedMetrics, r.URL.Query().Get("metrics"), "Expected many metrics in URL parameter")
+		assert.Equal(t, since.Format(time.RFC3339), r.URL.Query().Get("since"), "Expected since parameter in URL")
+		assert.Equal(t, until.Format(time.RFC3339), r.URL.Query().Get("until"), "Expected until parameter in URL")
+
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprint(w, `{
+		  "result": {
+			"totals":{
+				"queryCount": 5,
+				"uncachedCount":6,
+				"staleCount":7,
+				"responseTimeAvg":1.0,
+				"responseTimeMedian":2.0,
+				"responseTime90th":3.0,
+				"responseTime99th":4.0
+			  }
+		  },
+		  "success": true,
+		  "errors": null,
+		  "messages": null
+		}`)
+	}
+
+	mux.HandleFunc("/user/dns_firewall/12345/dns_analytics/report", handler)
+	want := VirtualDNSAnalytics{
+		Totals: VirtualDNSAnalyticsMetrics{
+			QueryCount:         int64Ptr(5),
+			UncachedCount:      int64Ptr(6),
+			StaleCount:         int64Ptr(7),
+			ResponseTimeAvg:    float64Ptr(1.0),
+			ResponseTimeMedian: float64Ptr(2.0),
+			ResponseTime90th:   float64Ptr(3.0),
+			ResponseTime99th:   float64Ptr(4.0),
+		},
+	}
+
+	params := VirtualDNSUserAnalyticsOptions{
+		Metrics: []string{
+			"queryCount",
+			"uncachedCount",
+			"staleCount",
+			"responseTimeAvg",
+			"responseTimeMedia",
+			"responseTime90th",
+			"responseTime99th",
+		},
+		Since: &since,
+		Until: &until,
+	}
+	actual, err := client.VirtualDNSUserAnalytics(context.Background(), "12345", params)
+	if assert.NoError(t, err) {
+		assert.Equal(t, want, actual)
+	}
+}
diff --git a/waiting_room_test.go b/waiting_room_test.go
index df7e0da6b..057789906 100644
--- a/waiting_room_test.go
+++ b/waiting_room_test.go
@@ -39,9 +39,7 @@ var waitingRoomJSON = fmt.Sprintf(`
       "custom_page_html": "{{#waitTimeKnown}} {{waitTime}} mins {{/waitTimeKnown}} {{^waitTimeKnown}} Queue all enabled {{/waitTimeKnown}}",
       "default_template_language": "en-US",
       "next_event_prequeue_start_time": null,
-      "next_event_start_time": "%s",
-			"cookie_suffix": "example_shop",
-			"additional_routes": [{"host": "shop2.example.com", "path": "/shop/checkout"}]
+      "next_event_start_time": "%s"
     }
    `, waitingRoomID, testTimestampWaitingRoom.Format(time.RFC3339Nano), testTimestampWaitingRoom.Format(time.RFC3339Nano),
 	testTimestampWaitingRoomEventStart.Format(time.RFC3339Nano))
@@ -125,8 +123,6 @@ var waitingRoom = WaitingRoom{
 	DefaultTemplateLanguage:    "en-US",
 	NextEventStartTime:         &testTimestampWaitingRoomEventStart,
 	NextEventPrequeueStartTime: nil,
-	CookieSuffix:               "example_shop",
-	AdditionalRoutes:           []*WaitingRoomRoute{{Host: "shop2.example.com", Path: "/shop/checkout"}},
 }
 
 var waitingRoomEvent = WaitingRoomEvent{