Repository: honeycombio/terraform-provider-honeycombio (language: python)

Issue:
Resource: api key
A resource to manage API keys.

More information: _no documentation available_
API: _there is no API yet for this_

Possible Terraform config:

```hcl
resource "honeycombio_api_key" "example" {
  name   = "Backend"
  enable = true

  permissions = ["send_events"]
}

# honeycombio_api_key exports the field `key`
```


Discussion / hints:
Adding the new APIs to support this is on the team's roadmap in the near-ish future.
I think the API has now been released.

/subscribe
@DavidS-ovm it has indeed! I'm just wrapping something else up and then am going to pickup this work.

Fix patch:
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 8aaeff7f..9df1cb3e 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -61,7 +61,10 @@ jobs:
         timeout-minutes: 10
         env:
           HONEYCOMB_API_KEY: ${{ secrets.HONEYCOMB_API_KEY }}
+          HONEYCOMB_KEY_ID: ${{ secrets.HONEYCOMB_KEY_ID }}
+          HONEYCOMB_KEY_SECRET: ${{ secrets.HONEYCOMB_KEY_SECRET }}
           HONEYCOMB_DATASET: testacc
+          HONEYCOMB_ENVIRONMENT_ID: hcaen_01j1d7t02zf7wgw7q89z3t60vf # TODO: remove and do a lookup or create in tests
         run: go test -v -coverprofile=client-coverage.txt -covermode=atomic ./client/...
 
       - uses: hashicorp/setup-terraform@v3
@@ -73,7 +76,10 @@ jobs:
         timeout-minutes: 10
         env:
           HONEYCOMB_API_KEY: ${{ secrets.HONEYCOMB_API_KEY }}
+          HONEYCOMB_KEY_ID: ${{ secrets.HONEYCOMB_KEY_ID }}
+          HONEYCOMB_KEY_SECRET: ${{ secrets.HONEYCOMB_KEY_SECRET }}
           HONEYCOMB_DATASET: testacc
+          HONEYCOMB_ENVIRONMENT_ID: hcaen_01j1d7t02zf7wgw7q89z3t60vf # TODO: remove and do a lookup or create in tests
           TF_ACC: 1
           TF_ACC_TERRAFORM_VERSION: ${{ env.TERRAFORM_VERSION }}
         run: go test -v -coverprofile=tf-coverage.txt -covermode=atomic ./internal/... ./honeycombio/...
@@ -112,7 +118,10 @@ jobs:
         env:
           HONEYCOMB_API_ENDPOINT: https://api.eu1.honeycomb.io
           HONEYCOMB_API_KEY: ${{ secrets.HONEYCOMB_API_KEY_EU }}
+          HONEYCOMB_KEY_ID: ${{ secrets.HONEYCOMB_KEY_ID_EU }}
+          HONEYCOMB_KEY_SECRET: ${{ secrets.HONEYCOMB_KEY_SECRET_EU }}
           HONEYCOMB_DATASET: testacc
+          HONEYCOMB_ENVIRONMENT_ID: hcben_01hvp28qbgzaeebbz29qvbb7gt # TODO: remove and do a lookup or create in tests
         run: go test -v -coverprofile=client-coverage.txt -covermode=atomic ./client/...
 
       - uses: hashicorp/setup-terraform@v3
@@ -125,7 +134,10 @@ jobs:
         env:
           HONEYCOMB_API_ENDPOINT: https://api.eu1.honeycomb.io
           HONEYCOMB_API_KEY: ${{ secrets.HONEYCOMB_API_KEY_EU }}
+          HONEYCOMB_KEY_ID: ${{ secrets.HONEYCOMB_KEY_ID_EU }}
+          HONEYCOMB_KEY_SECRET: ${{ secrets.HONEYCOMB_KEY_SECRET_EU }}
           HONEYCOMB_DATASET: testacc
+          HONEYCOMB_ENVIRONMENT_ID: hcben_01hvp28qbgzaeebbz29qvbb7gt # TODO: remove and do a lookup or create in tests
           TF_ACC: 1
           TF_ACC_TERRAFORM_VERSION: ${{ env.TERRAFORM_VERSION }}
         run: go test -v -coverprofile=tf-coverage.txt -covermode=atomic ./internal/... ./honeycombio/...
diff --git a/client/client.go b/client/client.go
index 89970901..021b1b60 100644
--- a/client/client.go
+++ b/client/client.go
@@ -221,7 +221,7 @@ func (c *Client) Do(ctx context.Context, method, path string, requestBody, respo
 	defer resp.Body.Close()
 
 	if !(resp.StatusCode >= 200 && resp.StatusCode <= 299) {
-		return errorFromResponse(resp)
+		return ErrorFromResponse(resp)
 	}
 	if responseBody != nil {
 		err = json.NewDecoder(resp.Body).Decode(responseBody)
diff --git a/client/errors.go b/client/errors.go
index 76280311..cb77a9d4 100644
--- a/client/errors.go
+++ b/client/errors.go
@@ -2,9 +2,10 @@ package client
 
 import (
 	"encoding/json"
-	"fmt"
-	"io"
+	"errors"
 	"net/http"
+
+	"github.com/hashicorp/jsonapi"
 )
 
 // DetailedError is an RFC7807 'Problem Detail' formatted error message.
@@ -52,6 +53,14 @@ func (td ErrorTypeDetail) String() string {
 	return response
 }
 
+// IsNotFound returns true if the error is an HTTP 404
+func (e *DetailedError) IsNotFound() bool {
+	if e == nil {
+		return false
+	}
+	return e.Status == http.StatusNotFound
+}
+
 // Error returns a pretty-printed representation of the error
 func (e DetailedError) Error() string {
 	if len(e.Details) > 0 {
@@ -60,7 +69,8 @@ func (e DetailedError) Error() string {
 		for index, details := range e.Details {
 			response += details.String()
 
-			// If we haven't reached the end of the list of error details, add a newline separator between each error
+			// If we haven't reached the end of the list of error details,
+			// add a newline separator between each error
 			if index < len(e.Details)-1 {
 				response += "\n"
 			}
@@ -72,34 +82,58 @@ func (e DetailedError) Error() string {
 	return e.Message
 }
 
-// IsNotFound returns true if the error is an HTTP 404
-func (e *DetailedError) IsNotFound() bool {
-	if e == nil {
-		return false
+func ErrorFromResponse(r *http.Response) error {
+	if r == nil {
+		return errors.New("invalid response")
 	}
-	return e.Status == http.StatusNotFound
-}
 
-func errorFromResponse(resp *http.Response) error {
-	e, err := io.ReadAll(resp.Body)
-	if err != nil {
-		return fmt.Errorf("unable to read response body: %w", err)
-	}
+	switch r.Header.Get("Content-Type") {
+	case jsonapi.MediaType:
+		var detailedError DetailedError
 
-	var detailedErr DetailedError
-	err = json.Unmarshal(e, &detailedErr)
-	if err != nil {
-		// we failed to parse the body as a DetailedError, so build one from what we know
-		return DetailedError{
-			Status:  resp.StatusCode,
-			Message: resp.Status,
+		errPayload := new(jsonapi.ErrorsPayload)
+		err := json.NewDecoder(r.Body).Decode(errPayload)
+		if err != nil || len(errPayload.Errors) == 0 {
+			return DetailedError{
+				Status:  r.StatusCode,
+				Message: r.Status,
+			}
 		}
-	}
 
-	// quick sanity check to make sure we got a StatusCode
-	if detailedErr.Status == 0 {
-		detailedErr.Status = resp.StatusCode
-	}
+		detailedError = DetailedError{
+			Status: r.StatusCode,
+			Title:  errPayload.Errors[0].Title,
+		}
+		if len(errPayload.Errors) == 1 {
+			// If there's only one error we don't need to build up details
+			detailedError.Message = errPayload.Errors[0].Detail
+			detailedError.Type = errPayload.Errors[0].Code
+		} else {
+			details := make([]ErrorTypeDetail, len(errPayload.Errors))
+			for i, e := range errPayload.Errors {
+				details[i] = ErrorTypeDetail{
+					Code:        e.Code,
+					Description: e.Detail,
+					// TODO: field when we have it via pointer
+				}
+			}
+			detailedError.Details = details
+		}
+		return detailedError
+	default:
+		var detailedError DetailedError
+		if err := json.NewDecoder(r.Body).Decode(&detailedError); err != nil {
+			// If we can't decode the error, return a generic error
+			return DetailedError{
+				Status:  r.StatusCode,
+				Message: r.Status,
+			}
+		}
 
-	return detailedErr
+		// sanity check: ensure we have the status code set
+		if detailedError.Status == 0 {
+			detailedError.Status = r.StatusCode
+		}
+		return detailedError
+	}
 }
diff --git a/client/v2/api_keys.go b/client/v2/api_keys.go
new file mode 100644
index 00000000..58d4712c
--- /dev/null
+++ b/client/v2/api_keys.go
@@ -0,0 +1,115 @@
+package v2
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+
+	"github.com/hashicorp/jsonapi"
+
+	hnyclient "github.com/honeycombio/terraform-provider-honeycombio/client"
+)
+
+// Compile-time proof of interface implementation.
+var _ APIKeys = (*apiKeys)(nil)
+
+type APIKeys interface {
+	Create(ctx context.Context, key *APIKey) (*APIKey, error)
+	Get(ctx context.Context, id string) (*APIKey, error)
+	Update(ctx context.Context, key *APIKey) (*APIKey, error)
+	Delete(ctx context.Context, id string) error
+	List(ctx context.Context, opts ...ListOption) (*Pager[APIKey], error)
+}
+
+const (
+	apiKeysPath     = "/2/teams/%s/api-keys"
+	apiKeysByIDPath = "/2/teams/%s/api-keys/%s"
+)
+
+type apiKeys struct {
+	client   *Client
+	authinfo *AuthMetadata
+}
+
+func (a *apiKeys) Create(ctx context.Context, k *APIKey) (*APIKey, error) {
+	r, err := a.client.Do(ctx,
+		http.MethodPost,
+		fmt.Sprintf(apiKeysPath, a.authinfo.Team.Slug),
+		k,
+	)
+	if err != nil {
+		return nil, err
+	}
+	if r.StatusCode != http.StatusCreated {
+		return nil, hnyclient.ErrorFromResponse(r)
+	}
+
+	key := new(APIKey)
+	if err := jsonapi.UnmarshalPayload(r.Body, key); err != nil {
+		return nil, err
+	}
+	return key, nil
+}
+
+func (a *apiKeys) Get(ctx context.Context, id string) (*APIKey, error) {
+	r, err := a.client.Do(ctx,
+		http.MethodGet,
+		fmt.Sprintf(apiKeysByIDPath, a.authinfo.Team.Slug, id),
+		nil,
+	)
+	if err != nil {
+		return nil, err
+	}
+	if r.StatusCode != http.StatusOK {
+		return nil, hnyclient.ErrorFromResponse(r)
+	}
+
+	key := new(APIKey)
+	if err := jsonapi.UnmarshalPayload(r.Body, key); err != nil {
+		return nil, err
+	}
+	return key, nil
+}
+
+func (a *apiKeys) Update(ctx context.Context, k *APIKey) (*APIKey, error) {
+	r, err := a.client.Do(ctx,
+		http.MethodPatch,
+		fmt.Sprintf(apiKeysByIDPath, a.authinfo.Team.Slug, k.ID),
+		k,
+	)
+	if err != nil {
+		return nil, err
+	}
+	if r.StatusCode != http.StatusOK {
+		return nil, hnyclient.ErrorFromResponse(r)
+	}
+
+	key := new(APIKey)
+	if err := jsonapi.UnmarshalPayload(r.Body, key); err != nil {
+		return nil, err
+	}
+	return key, nil
+}
+
+func (a *apiKeys) Delete(ctx context.Context, id string) error {
+	r, err := a.client.Do(ctx,
+		http.MethodDelete,
+		fmt.Sprintf(apiKeysByIDPath, a.authinfo.Team.Slug, id),
+		nil,
+	)
+	if err != nil {
+		return err
+	}
+	if r.StatusCode != http.StatusNoContent {
+		return hnyclient.ErrorFromResponse(r)
+	}
+	return nil
+}
+
+func (a *apiKeys) List(ctx context.Context, os ...ListOption) (*Pager[APIKey], error) {
+	return NewPager[APIKey](
+		a.client,
+		fmt.Sprintf(apiKeysPath, a.authinfo.Team.Slug),
+		os...,
+	)
+}
diff --git a/client/v2/client.go b/client/v2/client.go
new file mode 100644
index 00000000..a2c0116c
--- /dev/null
+++ b/client/v2/client.go
@@ -0,0 +1,214 @@
+package v2
+
+import (
+	"bytes"
+	"context"
+	"errors"
+	"fmt"
+	"io"
+	"log"
+	"net/http"
+	"net/url"
+	"os"
+	"time"
+
+	"github.com/hashicorp/go-retryablehttp"
+	"github.com/hashicorp/jsonapi"
+
+	hnyclient "github.com/honeycombio/terraform-provider-honeycombio/client"
+)
+
+const (
+	DefaultAPIHost         = "https://api.honeycomb.io"
+	DefaultAPIEndpointEnv  = "HONEYCOMB_API_ENDPOINT"
+	DefaultAPIKeyIDEnv     = "HONEYCOMB_KEY_ID"
+	DefaultAPIKeySecretEnv = "HONEYCOMB_KEY_SECRET"
+
+	defaultUserAgent = "go-honeycomb"
+)
+
+type Config struct {
+	APIKeyID           string
+	APIKeySecret       string
+	BaseURL            string
+	Debug              bool
+	HTTPClient         *http.Client
+	UserAgent          string
+	skipInitialization bool
+}
+
+type Client struct {
+	BaseURL   *url.URL
+	Headers   http.Header
+	UserAgent string
+
+	http *retryablehttp.Client
+
+	// API handlers here
+	APIKeys APIKeys
+}
+
+func NewClient() (*Client, error) {
+	return NewClientWithConfig(nil)
+}
+
+func NewClientWithConfig(config *Config) (*Client, error) {
+	if config == nil {
+		config = &Config{}
+	}
+	if config.BaseURL == "" {
+		host := os.Getenv(DefaultAPIEndpointEnv)
+		if host == "" {
+			config.BaseURL = DefaultAPIHost
+		} else {
+			config.BaseURL = host
+		}
+	}
+	baseURL, err := url.Parse(config.BaseURL)
+	if err != nil {
+		return nil, fmt.Errorf("invalid BaseURL: %w", err)
+	}
+	if config.UserAgent == "" {
+		config.UserAgent = defaultUserAgent
+	}
+
+	if config.APIKeyID == "" && config.APIKeySecret == "" {
+		config.APIKeyID = os.Getenv(DefaultAPIKeyIDEnv)
+		config.APIKeySecret = os.Getenv(DefaultAPIKeySecretEnv)
+
+		// if we still don't have the API key, we'll need to error out
+		if config.APIKeyID == "" || config.APIKeySecret == "" {
+			return nil, errors.New("missing API Key ID and Secret pair")
+		}
+	}
+	token := config.APIKeyID + ":" + config.APIKeySecret
+
+	client := &Client{
+		UserAgent: config.UserAgent,
+		BaseURL:   baseURL,
+		Headers: http.Header{
+			"Authorization": {"Bearer " + token},
+			"Content-Type":  {jsonapi.MediaType},
+			"User-Agent":    {config.UserAgent},
+		},
+	}
+	client.http = &retryablehttp.Client{
+		Backoff:      retryablehttp.DefaultBackoff,
+		CheckRetry:   client.retryHTTPCheck,
+		ErrorHandler: retryablehttp.PassthroughErrorHandler,
+		HTTPClient:   config.HTTPClient,
+		RetryWaitMin: 200 * time.Millisecond,
+		RetryWaitMax: 10 * time.Second,
+		RetryMax:     15,
+	}
+
+	if config.Debug {
+		// if enabled we log all requests and responses to sterr
+		client.http.Logger = log.New(os.Stderr, "", log.LstdFlags)
+		client.http.ResponseLogHook = func(l retryablehttp.Logger, resp *http.Response) {
+			l.Printf("[DEBUG] Request: %s %s", resp.Request.Method, resp.Request.URL.String())
+			// TODO: Log request body
+		}
+	}
+
+	// early out if we're just creating the client for testing
+	if config.skipInitialization {
+		return client, nil
+	}
+
+	var authinfo *AuthMetadata
+	authinfo, err = client.AuthInfo(context.Background())
+	if err != nil {
+		return nil, err
+	}
+
+	// bind API handlers here
+	client.APIKeys = &apiKeys{client: client, authinfo: authinfo}
+
+	return client, nil
+}
+
+func (c *Client) Do(
+	ctx context.Context,
+	method,
+	path string,
+	body any,
+) (*http.Response, error) {
+	url, err := c.BaseURL.Parse(path)
+	if err != nil {
+		return nil, err
+	}
+
+	req, err := c.newRequest(
+		ctx,
+		method,
+		url.String(),
+		body,
+	)
+	if err != nil {
+		return nil, err
+	}
+
+	return c.http.Do(req)
+}
+
+func (c *Client) AuthInfo(ctx context.Context) (*AuthMetadata, error) {
+	r, err := c.Do(ctx, http.MethodGet, "/2/auth", nil)
+	if err != nil {
+		return nil, err
+	}
+	if r.StatusCode != http.StatusOK {
+		return nil, hnyclient.ErrorFromResponse(r)
+	}
+
+	auth := new(AuthMetadata)
+	if err := jsonapi.UnmarshalPayload(r.Body, auth); err != nil {
+		return nil, err
+	}
+	return auth, err
+}
+
+func (c *Client) newRequest(
+	ctx context.Context,
+	method,
+	url string,
+	body any,
+) (*retryablehttp.Request, error) {
+	var bodyReader io.Reader
+	if body != nil {
+		buf := bytes.NewBuffer(nil)
+		if err := jsonapi.MarshalPayloadWithoutIncluded(buf, body); err != nil {
+			return nil, err
+		}
+		bodyReader = buf
+	}
+	req, err := retryablehttp.NewRequestWithContext(ctx, method, url, bodyReader)
+	if err != nil {
+		return nil, err
+	}
+	for k, h := range c.Headers {
+		req.Header[k] = append(req.Header[k], h...)
+	}
+
+	return req, err
+}
+
+func (c *Client) retryHTTPCheck(
+	ctx context.Context,
+	r *http.Response,
+	_ error,
+) (bool, error) {
+	if r == nil || ctx.Err() != nil {
+		return false, ctx.Err()
+	}
+
+	switch r.StatusCode {
+	case http.StatusTooManyRequests:
+		// TODO: use new retry header timestamps to determine when to retry
+		return true, nil
+	case http.StatusBadGateway, http.StatusGatewayTimeout:
+		return true, nil
+	default:
+		return false, nil
+	}
+}
diff --git a/client/v2/models.go b/client/v2/models.go
new file mode 100644
index 00000000..6df9decb
--- /dev/null
+++ b/client/v2/models.go
@@ -0,0 +1,47 @@
+package v2
+
+import (
+	"time"
+)
+
+type Environment struct {
+	ID   string `jsonapi:"primary,environments"`
+	Name string `jsonapi:"attr,name"`
+	Slug string `jsonapi:"attr,slug"`
+}
+
+type Team struct {
+	ID   string `jsonapi:"primary,teams"`
+	Name string `jsonapi:"attr,name"`
+	Slug string `jsonapi:"attr,slug"`
+}
+
+type Timestamps struct {
+	CreatedAt time.Time `jsonapi:"attr,created,rfc3339,omitempty"`
+	UpdatedAt time.Time `jsonapi:"attr,updated,rfc3339,omitempty"`
+}
+
+type AuthMetadata struct {
+	ID         string      `jsonapi:"primary,api-keys"`
+	Name       string      `jsonapi:"attr,name"`
+	KeyType    string      `jsonapi:"attr,key_type"`
+	Disabled   bool        `jsonapi:"attr,disabled"`
+	Scopes     []string    `jsonapi:"attr,scopes"`
+	Timestamps *Timestamps `jsonapi:"attr,timestamps"`
+	Team       *Team       `jsonapi:"relation,team"`
+}
+
+type APIKey struct {
+	ID          string             `jsonapi:"primary,api-keys,omitempty"`
+	Name        *string            `jsonapi:"attr,name,omitempty"`
+	KeyType     string             `jsonapi:"attr,key_type,omitempty"`
+	Disabled    *bool              `jsonapi:"attr,disabled,omitempty"`
+	Secret      string             `jsonapi:"attr,secret,omitempty"`
+	Permissions *APIKeyPermissions `jsonapi:"attr,permissions,omitempty"`
+	Timestamps  *Timestamps        `jsonapi:"attr,timestamps,omitempty"`
+	Environment *Environment       `jsonapi:"relation,environment"`
+}
+
+type APIKeyPermissions struct {
+	CreateDatasets bool `json:"create_datasets" jsonapi:"attr,create_datasets"`
+}
diff --git a/client/v2/pagination.go b/client/v2/pagination.go
new file mode 100644
index 00000000..312cde47
--- /dev/null
+++ b/client/v2/pagination.go
@@ -0,0 +1,136 @@
+package v2
+
+import (
+	"bytes"
+	"context"
+	"encoding/json"
+	"io"
+	"net/http"
+	"reflect"
+
+	"github.com/google/go-querystring/query"
+	"github.com/hashicorp/jsonapi"
+
+	hnyclient "github.com/honeycombio/terraform-provider-honeycombio/client"
+)
+
+type PaginationLinks struct {
+	Next *string `json:"next,omitempty"`
+}
+
+type ListOptions struct {
+	// PageSize is the number of results to return per page.
+	// Default is 20, max is 100.
+	PageSize int `url:"page[size],omitempty"`
+}
+
+type ListOption func(*ListOptions)
+
+const defaultPageSize = 20
+
+func PageSize(size int) ListOption {
+	return func(po *ListOptions) { po.PageSize = size }
+}
+
+type Pager[T any] struct {
+	client *Client
+	next   *string
+	opts   ListOptions
+}
+
+func NewPager[T any](
+	c *Client,
+	url string,
+	os ...ListOption,
+) (*Pager[T], error) {
+	var opts ListOptions
+	for _, o := range os {
+		o(&opts)
+	}
+	if opts.PageSize == 0 {
+		opts.PageSize = defaultPageSize
+	}
+
+	u, err := c.BaseURL.Parse(url)
+	if err != nil {
+		return nil, err
+	}
+	// add any options to the URL
+	v, err := query.Values(opts)
+	if err != nil {
+		return nil, err
+	}
+	u.RawQuery = v.Encode()
+	nextUrl := u.RequestURI()
+
+	return &Pager[T]{
+		client: c,
+		next:   &nextUrl,
+		opts:   opts,
+	}, nil
+}
+
+// HasNext returns true if there are more results to fetch.
+func (p *Pager[T]) HasNext() bool { return p.next != nil }
+
+// Next fetches the next page of results.
+func (p *Pager[T]) Next(ctx context.Context) ([]*T, error) {
+	if p.next == nil {
+		return nil, nil
+	}
+	r, err := p.client.Do(
+		ctx,
+		http.MethodGet,
+		*p.next,
+		nil,
+	)
+	if err != nil {
+		return nil, err
+	}
+
+	if r.StatusCode != http.StatusOK {
+		return nil, hnyclient.ErrorFromResponse(r)
+	}
+	pagination, err := parsePagination(r)
+	if err != nil {
+		return nil, err
+	}
+
+	payload, err := jsonapi.UnmarshalManyPayload(r.Body, reflect.TypeOf(new(T)))
+	if err != nil {
+		return nil, err
+	}
+	items := make([]*T, len(payload))
+	for i, obj := range payload {
+		if item, ok := obj.(*T); ok {
+			items[i] = item
+		}
+	}
+
+	// update 'next' for the next fetch
+	p.next = pagination.Next
+
+	return items, nil
+}
+
+func parsePagination(r *http.Response) (*PaginationLinks, error) {
+	body, err := io.ReadAll(r.Body)
+	if err != nil {
+		return nil, err
+	}
+	if err = r.Body.Close(); err != nil {
+		return nil, err
+	}
+
+	var raw struct {
+		PaginationLinks `json:"links"`
+	}
+	if err := json.NewDecoder(bytes.NewBuffer(body)).Decode(&raw); err != nil {
+		return nil, err
+	}
+
+	// put body back to be used properly downstream
+	r.Body = io.NopCloser(bytes.NewBuffer(body))
+
+	return &raw.PaginationLinks, nil
+}
diff --git a/docs/index.md b/docs/index.md
index 8f48ee41..acee900d 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -21,7 +21,8 @@ terraform {
 
 # Configure the Honeycomb provider
 provider "honeycombio" {
-  # You can set the API key with the environment variable HONEYCOMB_API_KEY
+  # You can set the API key with the environment variable HONEYCOMB_API_KEY,
+  # or the HONEYCOMB_KEY_ID+HONEYCOMB_KEY_SECRET environment variable pair
 }
 
 variable "dataset" {
@@ -51,18 +52,41 @@ provider "honeycombio" {
 
 ## Authentication
 
-The Honeycomb provider requires an API key to communicate with the Honeycomb API. API keys and their permissions can be managed in _Team settings_.
+The Honeycomb provider requires an API key to communicate with the Honeycomb APIs.
+The provider can make calls to v1 and v2 APIs and requires specific key configurations for each.
+For more information about API Keys, check out [Best Practices for API Keys](https://docs.honeycomb.io/get-started/best-practices/api-keys/).
+
+A single instance of the provider can be configured with both key types.
+At least one of the v1 or v2 API key configuration is required.
+
+### v1 APIs
+
+v1 APIs require Configuration Keys.
+Their permissions can be managed in _Environment settings_.
+Most resources and data sources call v1 APIs today.
 
 The key can be set with the `api_key` argument or via the `HONEYCOMB_API_KEY` or `HONEYCOMBIO_APIKEY` environment variable.
 
 `HONEYCOMB_API_KEY` environment variable will take priority over the `HONEYCOMBIO_APIKEY` environment variable.
 
+### v2 APIs
+
+v2 APIs require a Mangement Key.
+Their permissions can be managed in _Team settings_.
+Resources and data sources that call v2 APIs will be noted along with the scope required to use the resource or data source.
+
+The key pair can be set with the `api_key_id` and `api_key_secret` arguments, or via the `HONEYCOMB_KEY_ID` and `HONEYCOMB_KEY_SECRET` environment variables.
+
 ~> **Note** Hard-coding API keys in any Terraform configuration is not recommended. Consider using the one of the environment variable options.
 
 ## Argument Reference
 
 Arguments accepted by this provider include:
 
-* `api_key` - (Required) The Honeycomb API key to use. It can also be set using `HONEYCOMB_API_KEY` or `HONEYCOMBIO_APIKEY` environment variables.
+* `api_key` - (Optional) The Honeycomb API key to use. It can also be set using `HONEYCOMB_API_KEY` or `HONEYCOMBIO_APIKEY` environment variables.
+* `api_key_id` - (Optional) The ID portion of the Honeycomb Management API key to use. It can also be set via the `HONEYCOMB_KEY_ID` environment variable.
+* `api_key_secret` - (Optional) The secret portion of the Honeycomb Management API key to use. It can also be set via the `HONEYCOMB_KEY_SECRET` environment variable.
 * `api_url` - (Optional) Override the URL of the Honeycomb.io API. It can also be set using `HONEYCOMB_API_ENDPOINT`. Defaults to `https://api.honeycomb.io`.
 * `debug` - (Optional) Enable to log additional debug information. To view the logs, set `TF_LOG` to at least debug.
+
+At least one of `api_key`, or the `api_key_id` and `api_key_secret` pair must be configured.
diff --git a/docs/resources/api_key.md b/docs/resources/api_key.md
new file mode 100644
index 00000000..d1933f5f
--- /dev/null
+++ b/docs/resources/api_key.md
@@ -0,0 +1,50 @@
+# Resource: honeycombio_api_key
+
+Creates a Honeycomb API Key.
+For more information about API Keys, check out [Best Practices for API Keys](https://docs.honeycomb.io/get-started/best-practices/api-keys/).
+
+-> **NOTE** This resource requires the provider be configured with a Management Key with `api-keys:write` in the configured scopes.
+
+## Example Usage
+
+```hcl
+resource "honeycombio_api_key" "prod_ingest" {
+  name = "Production Ingest"
+  type = "ingest"
+
+  environment_id = var.environment_id
+
+  permissions {
+    create_datasets = true
+  }
+}
+
+output "ingest_key" {
+  value = "${honeycomb_api_key.prod_ingest.id}${honeycomb_api_key.prod_ingest.secret}"
+}
+```
+
+## Argument Reference
+
+The following arguments are supported:
+
+* `name` - (Required) The name of the API key.
+* `type` - (Required) The type of API key. Currently only `ingest` is supported.
+* `environment_id` - (Required) The Environment ID the API key is scoped to.
+* `disabled` - (Optional) Whether the API key is disabled. Defaults to `false`.
+* `permissions` - (Optional) A configuration block (described below) setting what actions the API key can perform.
+
+Each API key configuration may contain a single `permissions` block, which accepts the following arguments:
+
+* `create_datasets` - (Optional) Allow this key to create missing datasets when sending telemetry. Defaults to `false`.
+
+## Attribute Reference
+
+In addition to all arguments above, the following attributes are exported:
+
+* `id` - The ID of the API Key.
+* `secret` - The secret portion of the API Key.
+
+## Import
+
+API Keys cannot be imported.
diff --git a/go.mod b/go.mod
index 25254d29..4a8639b9 100644
--- a/go.mod
+++ b/go.mod
@@ -3,8 +3,10 @@ module github.com/honeycombio/terraform-provider-honeycombio
 go 1.21
 
 require (
+	github.com/google/go-querystring v1.1.0
 	github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320
 	github.com/hashicorp/go-retryablehttp v0.7.7
+	github.com/hashicorp/jsonapi v1.3.1
 	github.com/hashicorp/terraform-plugin-framework v1.10.0
 	github.com/hashicorp/terraform-plugin-go v0.23.0
 	github.com/hashicorp/terraform-plugin-mux v0.16.0
diff --git a/go.sum b/go.sum
index 79b4e7f2..1a726492 100644
--- a/go.sum
+++ b/go.sum
@@ -39,9 +39,12 @@ github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiu
 github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
 github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
 github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
 github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
 github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
+github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
 github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
 github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
 github.com/hashicorp/go-checkpoint v0.5.0 h1:MFYpPZCnQqQTE18jFwSII6eUQrD/oxMFp3mlgcqk5mU=
@@ -68,6 +71,8 @@ github.com/hashicorp/hc-install v0.7.0 h1:Uu9edVqjKQxxuD28mR5TikkKDd/p55S8vzPC16
 github.com/hashicorp/hc-install v0.7.0/go.mod h1:ELmmzZlGnEcqoUMKUuykHaPCIR1sYLYX+KSggWSKZuA=
 github.com/hashicorp/hcl/v2 v2.21.0 h1:lve4q/o/2rqwYOgUg3y3V2YPyD1/zkCLGjIV74Jit14=
 github.com/hashicorp/hcl/v2 v2.21.0/go.mod h1:62ZYHrXgPoX8xBnzl8QzbWq4dyDsDtfCRgIq1rbJEvA=
+github.com/hashicorp/jsonapi v1.3.1 h1:GtPvnmcWgYwCuDGvYT5VZBHcUyFdq9lSyCzDjn1DdPo=
+github.com/hashicorp/jsonapi v1.3.1/go.mod h1:kWfdn49yCjQvbpnvY1dxxAuAFzISwrrMDQOcu6NsFoM=
 github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y=
 github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
 github.com/hashicorp/terraform-exec v0.21.0 h1:uNkLAe95ey5Uux6KJdua6+cv8asgILFVWkd/RG0D2XQ=
diff --git a/honeycombio/data_source_column.go b/honeycombio/data_source_column.go
index 4f8be430..17f90505 100644
--- a/honeycombio/data_source_column.go
+++ b/honeycombio/data_source_column.go
@@ -6,8 +6,6 @@ import (
 
 	"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
 	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
-
-	honeycombio "github.com/honeycombio/terraform-provider-honeycombio/client"
 )
 
 func dataSourceHoneycombioColumn() *schema.Resource {
@@ -79,7 +77,10 @@ func dataSourceHoneycombioColumn() *schema.Resource {
 }
 
 func dataSourceHoneycombioColumnRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
-	client := meta.(*honeycombio.Client)
+	client, err := getConfiguredClient(meta)
+	if err != nil {
+		return diagFromErr(err)
+	}
 
 	dataset := d.Get("dataset").(string)
 	matchName := d.Get("name").(string)
diff --git a/honeycombio/data_source_columns.go b/honeycombio/data_source_columns.go
index 43af2688..43a4a8bc 100644
--- a/honeycombio/data_source_columns.go
+++ b/honeycombio/data_source_columns.go
@@ -8,7 +8,6 @@ import (
 	"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
 	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
 
-	honeycombio "github.com/honeycombio/terraform-provider-honeycombio/client"
 	"github.com/honeycombio/terraform-provider-honeycombio/internal/helper/hashcode"
 )
 
@@ -40,7 +39,10 @@ func dataSourceHoneycombioColumns() *schema.Resource {
 }
 
 func dataSourceHoneycombioColumnsRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
-	client := meta.(*honeycombio.Client)
+	client, err := getConfiguredClient(meta)
+	if err != nil {
+		return diagFromErr(err)
+	}
 
 	dataset := d.Get("dataset").(string)
 
diff --git a/honeycombio/data_source_datasets.go b/honeycombio/data_source_datasets.go
index 8a08dca4..56688596 100644
--- a/honeycombio/data_source_datasets.go
+++ b/honeycombio/data_source_datasets.go
@@ -8,7 +8,6 @@ import (
 	"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
 	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
 
-	honeycombio "github.com/honeycombio/terraform-provider-honeycombio/client"
 	"github.com/honeycombio/terraform-provider-honeycombio/internal/helper/hashcode"
 )
 
@@ -40,7 +39,10 @@ func dataSourceHoneycombioDatasets() *schema.Resource {
 }
 
 func dataSourceHoneycombioDatasetsRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
-	client := meta.(*honeycombio.Client)
+	client, err := getConfiguredClient(meta)
+	if err != nil {
+		return diagFromErr(err)
+	}
 
 	datasets, err := client.Datasets.List(ctx)
 	if err != nil {
diff --git a/honeycombio/data_source_query_result.go b/honeycombio/data_source_query_result.go
index 7d2b4244..93937dc9 100644
--- a/honeycombio/data_source_query_result.go
+++ b/honeycombio/data_source_query_result.go
@@ -53,10 +53,13 @@ func dataSourceHoneycombioQueryResult() *schema.Resource {
 
 func dataSourceHoneycombioQueryResultRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
 	var querySpec honeycombio.QuerySpec
-	client := meta.(*honeycombio.Client)
+	client, err := getConfiguredClient(meta)
+	if err != nil {
+		return diagFromErr(err)
+	}
 	dataset := d.Get("dataset").(string)
 
-	err := json.Unmarshal([]byte(d.Get("query_json").(string)), &querySpec)
+	err = json.Unmarshal([]byte(d.Get("query_json").(string)), &querySpec)
 	if err != nil {
 		return diag.FromErr(err)
 	}
diff --git a/honeycombio/data_source_recipient.go b/honeycombio/data_source_recipient.go
index a6bd0f42..cf6a8c30 100644
--- a/honeycombio/data_source_recipient.go
+++ b/honeycombio/data_source_recipient.go
@@ -134,7 +134,10 @@ If you want to match multiple recipients, use the 'honeycombio_recipients' data
 }
 
 func dataSourceHoneycombioRecipientRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
-	client := meta.(*honeycombio.Client)
+	client, err := getConfiguredClient(meta)
+	if err != nil {
+		return diagFromErr(err)
+	}
 
 	rcpts, err := client.Recipients.List(ctx)
 	if err != nil {
diff --git a/honeycombio/data_source_recipients.go b/honeycombio/data_source_recipients.go
index 47b79983..e475e610 100644
--- a/honeycombio/data_source_recipients.go
+++ b/honeycombio/data_source_recipients.go
@@ -73,7 +73,10 @@ func dataSourceHoneycombioRecipients() *schema.Resource {
 }
 
 func dataSourceHoneycombioRecipientsRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
-	client := meta.(*honeycombio.Client)
+	client, err := getConfiguredClient(meta)
+	if err != nil {
+		return diagFromErr(err)
+	}
 
 	rcpts, err := client.Recipients.List(ctx)
 	if err != nil {
diff --git a/honeycombio/data_source_trigger_recipient.go b/honeycombio/data_source_trigger_recipient.go
index e06eeecb..4661d793 100644
--- a/honeycombio/data_source_trigger_recipient.go
+++ b/honeycombio/data_source_trigger_recipient.go
@@ -35,7 +35,10 @@ func dataSourceHoneycombioSlackRecipient() *schema.Resource {
 }
 
 func dataSourceHoneycombioSlackRecipientRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
-	client := meta.(*honeycombio.Client)
+	client, err := getConfiguredClient(meta)
+	if err != nil {
+		return diagFromErr(err)
+	}
 	dataset := d.Get("dataset").(string)
 
 	triggers, err := client.Triggers.List(ctx, dataset)
diff --git a/honeycombio/provider.go b/honeycombio/provider.go
index 5fe5c5d5..bfa89d51 100644
--- a/honeycombio/provider.go
+++ b/honeycombio/provider.go
@@ -2,6 +2,7 @@ package honeycombio
 
 import (
 	"context"
+	"errors"
 	"os"
 
 	"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
@@ -26,6 +27,18 @@ func Provider(version string) *schema.Provider {
 				Optional:    true,
 				Sensitive:   true,
 			},
+			"api_key_id": { // unused in the provider but required to be set for the MuxServer
+				Type:        schema.TypeString,
+				Description: "The ID portion of the Honeycomb Management API key to use. It can also be set via the `HONEYCOMB_KEY_ID` environment variable.",
+				Optional:    true,
+				Sensitive:   false,
+			},
+			"api_key_secret": { // unused in the provider but required to be set for the MuxServer
+				Type:        schema.TypeString,
+				Description: "The secret portion of the Honeycomb Management API key to use. It can also be set via the `HONEYCOMB_KEY_SECRET` environment variable.",
+				Optional:    true,
+				Sensitive:   true,
+			},
 			"api_url": {
 				Type:        schema.TypeString,
 				Description: "Override the URL of the Honeycomb API. Defaults to `https://api.honeycomb.io`. It can also be set via the `HONEYCOMB_API_ENDPOINT` environment variable.",
@@ -79,28 +92,34 @@ func Provider(version string) *schema.Provider {
 			debug = v.(bool)
 		}
 
-		// API Key cannot be determined
-		if apiKey == "" {
-			return nil, diag.Errorf(
-				"Unknown Honeycomb API Key.\n\n" +
-					"The provider cannot create the Honeycomb client as there is an unknown configuration value for the Honeycomb API Key. " +
-					"Either target apply the source of the value first, set the value statically in the configuration, or use the HONEYCOMB_API_KEY environment variable.",
-			)
-		}
-
-		config := &honeycombio.Config{
-			APIKey:    apiKey,
-			APIUrl:    d.Get("api_url").(string),
-			UserAgent: provider.UserAgent("terraform-provider-honeycombio", version),
-			Debug:     debug,
-		}
-		c, err := honeycombio.NewClientWithConfig(config)
-		if err != nil {
-			return nil, diag.FromErr(err)
+		// if the API key is set, use it to create the client
+		// we now rely on the Framework version of the provider to validate the configuration
+		if apiKey != "" {
+			config := &honeycombio.Config{
+				APIKey:    apiKey,
+				APIUrl:    d.Get("api_url").(string),
+				UserAgent: provider.UserAgent("terraform-provider-honeycombio", version),
+				Debug:     debug,
+			}
+			c, err := honeycombio.NewClientWithConfig(config)
+			if err != nil {
+				return nil, diag.FromErr(err)
+			}
+			return c, nil
 		}
 
-		return c, nil
+		return nil, nil
 	}
 
 	return provider
 }
+
+func getConfiguredClient(meta any) (*honeycombio.Client, error) {
+	client, ok := meta.(*honeycombio.Client)
+	if !ok || client == nil {
+		return nil, errors.New("No v1 API client configured for this provider. " +
+			"Set the `api_key` attribute in the provider's configuration, " +
+			"or set the HONEYCOMB_API_KEY environment variable.")
+	}
+	return client, nil
+}
diff --git a/honeycombio/resource_board.go b/honeycombio/resource_board.go
index 0619a60d..8677ce25 100644
--- a/honeycombio/resource_board.go
+++ b/honeycombio/resource_board.go
@@ -164,7 +164,10 @@ See [Graph Settings](https://docs.honeycomb.io/working-with-your-data/graph-sett
 }
 
 func resourceBoardCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
-	client := meta.(*honeycombio.Client)
+	client, err := getConfiguredClient(meta)
+	if err != nil {
+		return diagFromErr(err)
+	}
 
 	b, err := expandBoard(d)
 	if err != nil {
@@ -181,7 +184,10 @@ func resourceBoardCreate(ctx context.Context, d *schema.ResourceData, meta inter
 }
 
 func resourceBoardRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
-	client := meta.(*honeycombio.Client)
+	client, err := getConfiguredClient(meta)
+	if err != nil {
+		return diagFromErr(err)
+	}
 
 	var detailedErr honeycombio.DetailedError
 	b, err := client.Boards.Get(ctx, d.Id())
@@ -229,7 +235,10 @@ func resourceBoardRead(ctx context.Context, d *schema.ResourceData, meta interfa
 }
 
 func resourceBoardUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
-	client := meta.(*honeycombio.Client)
+	client, err := getConfiguredClient(meta)
+	if err != nil {
+		return diagFromErr(err)
+	}
 
 	b, err := expandBoard(d)
 	if err != nil {
@@ -246,9 +255,12 @@ func resourceBoardUpdate(ctx context.Context, d *schema.ResourceData, meta inter
 }
 
 func resourceBoardDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
-	client := meta.(*honeycombio.Client)
+	client, err := getConfiguredClient(meta)
+	if err != nil {
+		return diagFromErr(err)
+	}
 
-	err := client.Boards.Delete(ctx, d.Id())
+	err = client.Boards.Delete(ctx, d.Id())
 	if err != nil {
 		return diagFromErr(err)
 	}
diff --git a/honeycombio/resource_column.go b/honeycombio/resource_column.go
index ac37ba4b..03672cf0 100644
--- a/honeycombio/resource_column.go
+++ b/honeycombio/resource_column.go
@@ -98,7 +98,10 @@ func resourceColumnImport(ctx context.Context, d *schema.ResourceData, i interfa
 }
 
 func resourceColumnCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
-	client := meta.(*honeycombio.Client)
+	client, err := getConfiguredClient(meta)
+	if err != nil {
+		return diagFromErr(err)
+	}
 
 	dataset := d.Get("dataset").(string)
 
@@ -112,7 +115,10 @@ func resourceColumnCreate(ctx context.Context, d *schema.ResourceData, meta inte
 }
 
 func resourceColumnRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
-	client := meta.(*honeycombio.Client)
+	client, err := getConfiguredClient(meta)
+	if err != nil {
+		return diagFromErr(err)
+	}
 
 	dataset := d.Get("dataset").(string)
 	// if name is not set, try to get key_name.
@@ -149,7 +155,10 @@ func resourceColumnRead(ctx context.Context, d *schema.ResourceData, meta interf
 }
 
 func resourceColumnUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
-	client := meta.(*honeycombio.Client)
+	client, err := getConfiguredClient(meta)
+	if err != nil {
+		return diagFromErr(err)
+	}
 
 	dataset := d.Get("dataset").(string)
 
@@ -163,11 +172,14 @@ func resourceColumnUpdate(ctx context.Context, d *schema.ResourceData, meta inte
 }
 
 func resourceColumnDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
-	client := meta.(*honeycombio.Client)
+	client, err := getConfiguredClient(meta)
+	if err != nil {
+		return diagFromErr(err)
+	}
 
 	dataset := d.Get("dataset").(string)
 
-	err := client.Columns.Delete(ctx, dataset, d.Id())
+	err = client.Columns.Delete(ctx, dataset, d.Id())
 	if err != nil {
 		return diagFromErr(err)
 	}
diff --git a/honeycombio/resource_dataset.go b/honeycombio/resource_dataset.go
index 59a467cd..c3d85f8e 100644
--- a/honeycombio/resource_dataset.go
+++ b/honeycombio/resource_dataset.go
@@ -60,7 +60,10 @@ func newDataset() *schema.Resource {
 }
 
 func resourceDatasetCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
-	client := meta.(*honeycombio.Client)
+	client, err := getConfiguredClient(meta)
+	if err != nil {
+		return diagFromErr(err)
+	}
 
 	data := &honeycombio.Dataset{
 		Name:            d.Get("name").(string),
@@ -77,7 +80,10 @@ func resourceDatasetCreate(ctx context.Context, d *schema.ResourceData, meta int
 }
 
 func resourceDatasetRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
-	client := meta.(*honeycombio.Client)
+	client, err := getConfiguredClient(meta)
+	if err != nil {
+		return diagFromErr(err)
+	}
 
 	var detailedErr honeycombio.DetailedError
 	dataset, err := client.Datasets.Get(ctx, d.Id())
@@ -103,7 +109,10 @@ func resourceDatasetRead(ctx context.Context, d *schema.ResourceData, meta inter
 }
 
 func resourceDatasetUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
-	client := meta.(*honeycombio.Client)
+	client, err := getConfiguredClient(meta)
+	if err != nil {
+		return diagFromErr(err)
+	}
 
 	data := &honeycombio.Dataset{
 		Name:            d.Get("name").(string),
diff --git a/honeycombio/resource_dataset_definition.go b/honeycombio/resource_dataset_definition.go
index 2988ebb5..c2f0c5fa 100644
--- a/honeycombio/resource_dataset_definition.go
+++ b/honeycombio/resource_dataset_definition.go
@@ -53,7 +53,10 @@ func newDatasetDefinition() *schema.Resource {
 }
 
 func resourceDatasetDefinitionRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
-	client := meta.(*honeycombio.Client)
+	client, err := getConfiguredClient(meta)
+	if err != nil {
+		return diagFromErr(err)
+	}
 
 	dataset := d.Get("dataset").(string)
 
@@ -82,14 +85,17 @@ func resourceDatasetDefinitionRead(ctx context.Context, d *schema.ResourceData,
 }
 
 func resourceDatasetDefinitionUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
-	client := meta.(*honeycombio.Client)
+	client, err := getConfiguredClient(meta)
+	if err != nil {
+		return diagFromErr(err)
+	}
 
 	dataset := d.Get("dataset").(string)
 	name := d.Get("name").(string)
 	value := d.Get("column").(string)
 
 	dd := expandDatasetDefinition(name, value)
-	_, err := client.DatasetDefinitions.Update(ctx, dataset, dd)
+	_, err = client.DatasetDefinitions.Update(ctx, dataset, dd)
 	if err != nil {
 		return diagFromErr(err)
 	}
@@ -98,14 +104,17 @@ func resourceDatasetDefinitionUpdate(ctx context.Context, d *schema.ResourceData
 }
 
 func resourceDatasetDefinitionDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
-	client := meta.(*honeycombio.Client)
+	client, err := getConfiguredClient(meta)
+	if err != nil {
+		return diagFromErr(err)
+	}
 
 	dataset := d.Get("dataset").(string)
 	name := d.Get("name").(string)
 
 	// 'deleting' a definition is really resetting it
 	dd := expandDatasetDefinition(name, "")
-	_, err := client.DatasetDefinitions.Update(ctx, dataset, dd)
+	_, err = client.DatasetDefinitions.Update(ctx, dataset, dd)
 	if err != nil {
 		return diagFromErr(err)
 	}
diff --git a/honeycombio/resource_derived_column.go b/honeycombio/resource_derived_column.go
index e9449206..f857dbed 100644
--- a/honeycombio/resource_derived_column.go
+++ b/honeycombio/resource_derived_column.go
@@ -61,12 +61,15 @@ func resourceDerivedColumnImport(ctx context.Context, d *schema.ResourceData, i
 }
 
 func resourceDerivedColumnCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
-	client := meta.(*honeycombio.Client)
+	client, err := getConfiguredClient(meta)
+	if err != nil {
+		return diagFromErr(err)
+	}
 
 	dataset := d.Get("dataset").(string)
 	derivedColumn := readDerivedColumn(d)
 
-	derivedColumn, err := client.DerivedColumns.Create(ctx, dataset, derivedColumn)
+	derivedColumn, err = client.DerivedColumns.Create(ctx, dataset, derivedColumn)
 	if err != nil {
 		return diagFromErr(err)
 	}
@@ -76,7 +79,10 @@ func resourceDerivedColumnCreate(ctx context.Context, d *schema.ResourceData, me
 }
 
 func resourceDerivedColumnRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
-	client := meta.(*honeycombio.Client)
+	client, err := getConfiguredClient(meta)
+	if err != nil {
+		return diagFromErr(err)
+	}
 
 	dataset := d.Get("dataset").(string)
 
@@ -101,12 +107,15 @@ func resourceDerivedColumnRead(ctx context.Context, d *schema.ResourceData, meta
 }
 
 func resourceDerivedColumnUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
-	client := meta.(*honeycombio.Client)
+	client, err := getConfiguredClient(meta)
+	if err != nil {
+		return diagFromErr(err)
+	}
 
 	dataset := d.Get("dataset").(string)
 	derivedColumn := readDerivedColumn(d)
 
-	derivedColumn, err := client.DerivedColumns.Update(ctx, dataset, derivedColumn)
+	derivedColumn, err = client.DerivedColumns.Update(ctx, dataset, derivedColumn)
 	if err != nil {
 		return diagFromErr(err)
 	}
@@ -116,11 +125,14 @@ func resourceDerivedColumnUpdate(ctx context.Context, d *schema.ResourceData, me
 }
 
 func resourceDerivedColumnDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
-	client := meta.(*honeycombio.Client)
+	client, err := getConfiguredClient(meta)
+	if err != nil {
+		return diagFromErr(err)
+	}
 
 	dataset := d.Get("dataset").(string)
 
-	err := client.DerivedColumns.Delete(ctx, dataset, d.Id())
+	err = client.DerivedColumns.Delete(ctx, dataset, d.Id())
 	if err != nil {
 		return diagFromErr(err)
 	}
diff --git a/honeycombio/resource_marker.go b/honeycombio/resource_marker.go
index a4ff4c25..7760c8fc 100644
--- a/honeycombio/resource_marker.go
+++ b/honeycombio/resource_marker.go
@@ -43,7 +43,10 @@ func newMarker() *schema.Resource {
 }
 
 func resourceMarkerCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
-	client := meta.(*honeycombio.Client)
+	client, err := getConfiguredClient(meta)
+	if err != nil {
+		return diagFromErr(err)
+	}
 
 	dataset := d.Get("dataset").(string)
 
@@ -62,7 +65,10 @@ func resourceMarkerCreate(ctx context.Context, d *schema.ResourceData, meta inte
 }
 
 func resourceMarkerRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
-	client := meta.(*honeycombio.Client)
+	client, err := getConfiguredClient(meta)
+	if err != nil {
+		return diagFromErr(err)
+	}
 
 	var detailedErr honeycombio.DetailedError
 	marker, err := client.Markers.Get(ctx, d.Get("dataset").(string), d.Id())
diff --git a/honeycombio/resource_marker_setting.go b/honeycombio/resource_marker_setting.go
index 3530cce8..21539f8e 100644
--- a/honeycombio/resource_marker_setting.go
+++ b/honeycombio/resource_marker_setting.go
@@ -52,7 +52,10 @@ func newMarkerSetting() *schema.Resource {
 }
 
 func resourceMarkerSettingCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
-	client := meta.(*honeycombio.Client)
+	client, err := getConfiguredClient(meta)
+	if err != nil {
+		return diagFromErr(err)
+	}
 
 	dataset := d.Get("dataset").(string)
 
@@ -72,7 +75,10 @@ func resourceMarkerSettingCreate(ctx context.Context, d *schema.ResourceData, me
 }
 
 func resourceMarkerSettingRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
-	client := meta.(*honeycombio.Client)
+	client, err := getConfiguredClient(meta)
+	if err != nil {
+		return diagFromErr(err)
+	}
 
 	var detailedErr honeycombio.DetailedError
 	markerSetting, err := client.MarkerSettings.Get(ctx, d.Get("dataset").(string), d.Id())
@@ -96,7 +102,10 @@ func resourceMarkerSettingRead(ctx context.Context, d *schema.ResourceData, meta
 }
 
 func resourceMarkerSettingUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
-	client := meta.(*honeycombio.Client)
+	client, err := getConfiguredClient(meta)
+	if err != nil {
+		return diagFromErr(err)
+	}
 
 	dataset := d.Get("dataset").(string)
 	markerType := d.Get("type").(string)
@@ -116,11 +125,14 @@ func resourceMarkerSettingUpdate(ctx context.Context, d *schema.ResourceData, me
 }
 
 func resourceMarkerSettingDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
-	client := meta.(*honeycombio.Client)
+	client, err := getConfiguredClient(meta)
+	if err != nil {
+		return diagFromErr(err)
+	}
 
 	dataset := d.Get("dataset").(string)
 
-	err := client.MarkerSettings.Delete(ctx, dataset, d.Id())
+	err = client.MarkerSettings.Delete(ctx, dataset, d.Id())
 	if err != nil {
 		return diagFromErr(err)
 	}
diff --git a/honeycombio/resource_query_annotation.go b/honeycombio/resource_query_annotation.go
index c819a06f..d391ab68 100644
--- a/honeycombio/resource_query_annotation.go
+++ b/honeycombio/resource_query_annotation.go
@@ -54,11 +54,14 @@ func buildQueryAnnotation(d *schema.ResourceData) *honeycombio.QueryAnnotation {
 }
 
 func resourceQueryAnnotationCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
-	client := meta.(*honeycombio.Client)
+	client, err := getConfiguredClient(meta)
+	if err != nil {
+		return diagFromErr(err)
+	}
 	dataset := d.Get("dataset").(string)
 	queryAnnotation := buildQueryAnnotation(d)
 
-	queryAnnotation, err := client.QueryAnnotations.Create(ctx, dataset, queryAnnotation)
+	queryAnnotation, err = client.QueryAnnotations.Create(ctx, dataset, queryAnnotation)
 	if err != nil {
 		return diagFromErr(err)
 	}
@@ -68,11 +71,14 @@ func resourceQueryAnnotationCreate(ctx context.Context, d *schema.ResourceData,
 }
 
 func resourceQueryAnnotationUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
-	client := meta.(*honeycombio.Client)
+	client, err := getConfiguredClient(meta)
+	if err != nil {
+		return diagFromErr(err)
+	}
 	dataset := d.Get("dataset").(string)
 	queryAnnotation := buildQueryAnnotation(d)
 
-	_, err := client.QueryAnnotations.Update(ctx, dataset, queryAnnotation)
+	_, err = client.QueryAnnotations.Update(ctx, dataset, queryAnnotation)
 	if err != nil {
 		return diagFromErr(err)
 	}
@@ -81,10 +87,13 @@ func resourceQueryAnnotationUpdate(ctx context.Context, d *schema.ResourceData,
 }
 
 func resourceQueryAnnotationDestroy(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
-	client := meta.(*honeycombio.Client)
+	client, err := getConfiguredClient(meta)
+	if err != nil {
+		return diagFromErr(err)
+	}
 	dataset := d.Get("dataset").(string)
 
-	err := client.QueryAnnotations.Delete(ctx, dataset, d.Id())
+	err = client.QueryAnnotations.Delete(ctx, dataset, d.Id())
 	if err != nil {
 		return diagFromErr(err)
 	}
@@ -92,7 +101,10 @@ func resourceQueryAnnotationDestroy(ctx context.Context, d *schema.ResourceData,
 }
 
 func resourceQueryAnnotationRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
-	client := meta.(*honeycombio.Client)
+	client, err := getConfiguredClient(meta)
+	if err != nil {
+		return diagFromErr(err)
+	}
 	dataset := d.Get("dataset").(string)
 
 	var detailedErr honeycombio.DetailedError
diff --git a/honeycombio/resource_slo.go b/honeycombio/resource_slo.go
index a037990e..eb5563a4 100644
--- a/honeycombio/resource_slo.go
+++ b/honeycombio/resource_slo.go
@@ -80,7 +80,10 @@ func resourceSLOImport(ctx context.Context, d *schema.ResourceData, i interface{
 }
 
 func resourceSLOCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
-	client := meta.(*honeycombio.Client)
+	client, err := getConfiguredClient(meta)
+	if err != nil {
+		return diagFromErr(err)
+	}
 
 	dataset := d.Get("dataset").(string)
 	s, err := expandSLO(d)
@@ -98,7 +101,10 @@ func resourceSLOCreate(ctx context.Context, d *schema.ResourceData, meta interfa
 }
 
 func resourceSLORead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
-	client := meta.(*honeycombio.Client)
+	client, err := getConfiguredClient(meta)
+	if err != nil {
+		return diagFromErr(err)
+	}
 
 	dataset := d.Get("dataset").(string)
 
@@ -126,7 +132,10 @@ func resourceSLORead(ctx context.Context, d *schema.ResourceData, meta interface
 }
 
 func resourceSLOUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
-	client := meta.(*honeycombio.Client)
+	client, err := getConfiguredClient(meta)
+	if err != nil {
+		return diagFromErr(err)
+	}
 
 	dataset := d.Get("dataset").(string)
 	s, err := expandSLO(d)
@@ -144,11 +153,14 @@ func resourceSLOUpdate(ctx context.Context, d *schema.ResourceData, meta interfa
 }
 
 func resourceSLODelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
-	client := meta.(*honeycombio.Client)
+	client, err := getConfiguredClient(meta)
+	if err != nil {
+		return diagFromErr(err)
+	}
 
 	dataset := d.Get("dataset").(string)
 
-	err := client.SLOs.Delete(ctx, dataset, d.Id())
+	err = client.SLOs.Delete(ctx, dataset, d.Id())
 	if err != nil {
 		return diag.FromErr(err)
 	}
diff --git a/honeycombio/type_helpers.go b/honeycombio/type_helpers.go
index 5fdf2a31..821f87da 100644
--- a/honeycombio/type_helpers.go
+++ b/honeycombio/type_helpers.go
@@ -57,7 +57,10 @@ func expandRecipient(t honeycombio.RecipientType, d *schema.ResourceData) (*hone
 }
 
 func createRecipient(ctx context.Context, d *schema.ResourceData, meta interface{}, t honeycombio.RecipientType) diag.Diagnostics {
-	client := meta.(*honeycombio.Client)
+	client, err := getConfiguredClient(meta)
+	if err != nil {
+		return diagFromErr(err)
+	}
 
 	r, err := expandRecipient(t, d)
 	if err != nil {
@@ -74,7 +77,10 @@ func createRecipient(ctx context.Context, d *schema.ResourceData, meta interface
 }
 
 func readRecipient(ctx context.Context, d *schema.ResourceData, meta interface{}, t honeycombio.RecipientType) diag.Diagnostics {
-	client := meta.(*honeycombio.Client)
+	client, err := getConfiguredClient(meta)
+	if err != nil {
+		return diagFromErr(err)
+	}
 
 	var detailedErr honeycombio.DetailedError
 	r, err := client.Recipients.Get(ctx, d.Id())
@@ -113,7 +119,10 @@ func readRecipient(ctx context.Context, d *schema.ResourceData, meta interface{}
 }
 
 func updateRecipient(ctx context.Context, d *schema.ResourceData, meta interface{}, t honeycombio.RecipientType) diag.Diagnostics {
-	client := meta.(*honeycombio.Client)
+	client, err := getConfiguredClient(meta)
+	if err != nil {
+		return diagFromErr(err)
+	}
 
 	r, err := expandRecipient(t, d)
 	if err != nil {
@@ -130,9 +139,12 @@ func updateRecipient(ctx context.Context, d *schema.ResourceData, meta interface
 }
 
 func deleteRecipient(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
-	client := meta.(*honeycombio.Client)
+	client, err := getConfiguredClient(meta)
+	if err != nil {
+		return diagFromErr(err)
+	}
 
-	err := client.Recipients.Delete(ctx, d.Id())
+	err = client.Recipients.Delete(ctx, d.Id())
 	if err != nil {
 		return diagFromErr(err)
 	}
diff --git a/internal/helper/diag.go b/internal/helper/diag.go
index 65d9c311..55666b62 100644
--- a/internal/helper/diag.go
+++ b/internal/helper/diag.go
@@ -7,13 +7,13 @@ import (
 
 	"github.com/hashicorp/terraform-plugin-framework/diag"
 
-	"github.com/honeycombio/terraform-provider-honeycombio/client"
+	hnyclient "github.com/honeycombio/terraform-provider-honeycombio/client"
 )
 
 // DetailedErrorDiagnostic is a Diagnostic which nicely wraps a client.DetailedError
 type DetailedErrorDiagnostic struct {
 	summary string
-	e       *client.DetailedError
+	e       *hnyclient.DetailedError
 }
 
 // compile-time check that DetailedErrorDiagnostic implements diag.Diagnostic
@@ -21,7 +21,7 @@ var _ diag.Diagnostic = DetailedErrorDiagnostic{}
 
 // NewDetailedErrorDiagnostic creates a new DetailedErrorDiagnostic
 // taking a context-specific summary of the action and a DetailedError.
-func NewDetailedErrorDiagnostic(summary string, e *client.DetailedError) DetailedErrorDiagnostic {
+func NewDetailedErrorDiagnostic(summary string, e *hnyclient.DetailedError) DetailedErrorDiagnostic {
 	return DetailedErrorDiagnostic{
 		e:       e,
 		summary: summary,
@@ -40,7 +40,7 @@ func AddDiagnosticOnError(diag *diag.Diagnostics, summary string, err error) boo
 		return false
 	}
 
-	var detailedErr *client.DetailedError
+	var detailedErr *hnyclient.DetailedError
 	if errors.As(err, &detailedErr) {
 		diag.Append(DetailedErrorDiagnostic{
 			summary: "Error " + summary,
diff --git a/internal/models/api_keys.go b/internal/models/api_keys.go
new file mode 100644
index 00000000..0ca64fc2
--- /dev/null
+++ b/internal/models/api_keys.go
@@ -0,0 +1,24 @@
+package models
+
+import (
+	"github.com/hashicorp/terraform-plugin-framework/attr"
+	"github.com/hashicorp/terraform-plugin-framework/types"
+)
+
+type APIKeyResourceModel struct {
+	ID            types.String `tfsdk:"id"`
+	Name          types.String `tfsdk:"name"`
+	Type          types.String `tfsdk:"type"`
+	EnvironmentID types.String `tfsdk:"environment_id"`
+	Disabled      types.Bool   `tfsdk:"disabled"`
+	Permissions   types.List   `tfsdk:"permissions"` // APIKeyPermissionModel
+	Secret        types.String `tfsdk:"secret"`
+}
+
+type APIKeyPermissionModel struct {
+	CreateDatasets types.Bool `tfsdk:"create_datasets"`
+}
+
+var APIKeyPermissionsAttrType = map[string]attr.Type{
+	"create_datasets": types.BoolType,
+}
diff --git a/internal/provider/api_key_resource.go b/internal/provider/api_key_resource.go
new file mode 100644
index 00000000..cb04eb74
--- /dev/null
+++ b/internal/provider/api_key_resource.go
@@ -0,0 +1,316 @@
+package provider
+
+import (
+	"context"
+	"errors"
+
+	"github.com/hashicorp/terraform-plugin-framework-validators/listvalidator"
+	"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
+	"github.com/hashicorp/terraform-plugin-framework/attr"
+	"github.com/hashicorp/terraform-plugin-framework/diag"
+	"github.com/hashicorp/terraform-plugin-framework/resource"
+	"github.com/hashicorp/terraform-plugin-framework/resource/schema"
+	"github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault"
+	"github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier"
+	"github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier"
+	"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
+	"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
+	"github.com/hashicorp/terraform-plugin-framework/schema/validator"
+	"github.com/hashicorp/terraform-plugin-framework/types"
+
+	"github.com/honeycombio/terraform-provider-honeycombio/client"
+	hny "github.com/honeycombio/terraform-provider-honeycombio/client"
+	v2client "github.com/honeycombio/terraform-provider-honeycombio/client/v2"
+	"github.com/honeycombio/terraform-provider-honeycombio/internal/helper"
+	"github.com/honeycombio/terraform-provider-honeycombio/internal/models"
+)
+
+// Ensure the implementation satisfies the expected interfaces.
+//
+// This resource is not implementing ResourceWithImportState because importing keys
+// won't give us the secret portion of the key which is arguably the whole reason
+// for the resource.
+var (
+	_ resource.Resource              = &apiKeyResource{}
+	_ resource.ResourceWithConfigure = &apiKeyResource{}
+)
+
+type apiKeyResource struct {
+	client *v2client.Client
+}
+
+func NewAPIKeyResource() resource.Resource {
+	return &apiKeyResource{}
+}
+
+func (*apiKeyResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
+	resp.TypeName = req.ProviderTypeName + "_api_key"
+}
+
+func (r *apiKeyResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
+	w := getClientFromResourceRequest(&req)
+	if w == nil {
+		return
+	}
+
+	c, err := w.V2Client()
+	if err != nil || c == nil {
+		resp.Diagnostics.AddError("Failed to configure client", err.Error())
+		return
+	}
+	r.client = c
+}
+
+func (*apiKeyResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
+	resp.Schema = schema.Schema{
+		Description: "API keys are used to authenticate the Honeycomb API.",
+		Attributes: map[string]schema.Attribute{
+			"id": schema.StringAttribute{
+				Description: "The unique identifier for this API key.",
+				Computed:    true,
+				Required:    false,
+				Optional:    false,
+				PlanModifiers: []planmodifier.String{
+					stringplanmodifier.UseStateForUnknown(),
+				},
+			},
+			"name": schema.StringAttribute{
+				Required:    true,
+				Description: "The name of the API Key.",
+				Validators: []validator.String{
+					stringvalidator.LengthBetween(1, 100),
+				},
+			},
+			"type": schema.StringAttribute{
+				Required:    true,
+				Description: "The type of the API key.",
+				Validators: []validator.String{
+					stringvalidator.OneOf("ingest"),
+				},
+				PlanModifiers: []planmodifier.String{
+					stringplanmodifier.RequiresReplace(),
+				},
+			},
+			"environment_id": schema.StringAttribute{
+				Required:    true,
+				Description: "The Environment ID the API key is scoped to.",
+				PlanModifiers: []planmodifier.String{
+					stringplanmodifier.RequiresReplace(),
+				},
+			},
+			"disabled": schema.BoolAttribute{
+				Optional:    true,
+				Computed:    true,
+				Description: "Whether the API key is disabled.",
+				Default:     booldefault.StaticBool(false),
+			},
+			"secret": schema.StringAttribute{
+				Computed:    true,
+				Required:    false,
+				Optional:    false,
+				Sensitive:   true,
+				Description: "The secret portion of the API key. This is only available when creating a new key.",
+			},
+		},
+		Blocks: map[string]schema.Block{
+			"permissions": schema.ListNestedBlock{
+				Description: "Permissions control what actions the API key can perform.",
+				Validators: []validator.List{
+					listvalidator.SizeAtMost(1),
+				},
+				PlanModifiers: []planmodifier.List{
+					listplanmodifier.RequiresReplace(),
+				},
+				NestedObject: schema.NestedBlockObject{
+					Attributes: map[string]schema.Attribute{
+						"create_datasets": schema.BoolAttribute{
+							Optional:    true,
+							Computed:    true,
+							Default:     booldefault.StaticBool(false),
+							Description: "Allow this key to create missing datasets when sending telemetry.",
+							PlanModifiers: []planmodifier.Bool{
+								boolplanmodifier.RequiresReplace(),
+							},
+						},
+					},
+				},
+			},
+		},
+	}
+}
+
+func (r *apiKeyResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
+	var plan models.APIKeyResourceModel
+	resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	newKey := &v2client.APIKey{
+		Name:        plan.Name.ValueStringPointer(),
+		KeyType:     plan.Type.ValueString(),
+		Environment: &v2client.Environment{ID: plan.EnvironmentID.ValueString()},
+		Disabled:    plan.Disabled.ValueBoolPointer(),
+		Permissions: expandAPIKeyPermissions(ctx, plan.Permissions, &resp.Diagnostics),
+	}
+
+	key, err := r.client.APIKeys.Create(ctx, newKey)
+	if helper.AddDiagnosticOnError(&resp.Diagnostics, "Creating Honeycomb API Key", err) {
+		return
+	}
+
+	var state models.APIKeyResourceModel
+	state.ID = types.StringValue(key.ID)
+	state.Name = types.StringValue(*key.Name)
+	state.Type = types.StringValue(key.KeyType)
+	state.EnvironmentID = types.StringValue(key.Environment.ID)
+	state.Disabled = types.BoolValue(*key.Disabled)
+	state.Secret = types.StringValue(key.Secret)
+
+	if !plan.Permissions.IsNull() {
+		state.Permissions = flattenAPIKeyPermissions(ctx, key.Permissions, &resp.Diagnostics)
+	} else {
+		state.Permissions = types.ListNull(types.ObjectType{AttrTypes: models.APIKeyPermissionsAttrType})
+	}
+
+	resp.Diagnostics.Append(resp.State.Set(ctx, state)...)
+}
+
+func (r *apiKeyResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
+	var state models.APIKeyResourceModel
+	resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	var detailedErr hny.DetailedError
+	key, err := r.client.APIKeys.Get(ctx, state.ID.ValueString())
+	if errors.As(err, &detailedErr) {
+		if detailedErr.IsNotFound() {
+			// if not found consider it deleted -- so just remove it from state
+			resp.State.RemoveResource(ctx)
+			return
+		} else {
+			resp.Diagnostics.Append(helper.NewDetailedErrorDiagnostic(
+				"Error Reading Honeycomb API Key",
+				&detailedErr,
+			))
+		}
+	} else if err != nil {
+		resp.Diagnostics.AddError(
+			"Error Reading Honeycomb API Key",
+			"Unexpected error reading API Key ID "+state.ID.ValueString()+": "+err.Error(),
+		)
+	}
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	state.ID = types.StringValue(key.ID)
+	state.Name = types.StringValue(*key.Name)
+	state.Type = types.StringValue(key.KeyType)
+	state.Disabled = types.BoolValue(*key.Disabled)
+	state.EnvironmentID = types.StringValue(key.Environment.ID)
+
+	if !state.Permissions.IsNull() {
+		state.Permissions = flattenAPIKeyPermissions(ctx, key.Permissions, &resp.Diagnostics)
+	} else {
+		state.Permissions = types.ListNull(types.ObjectType{AttrTypes: models.APIKeyPermissionsAttrType})
+	}
+
+	resp.Diagnostics.Append(resp.State.Set(ctx, state)...)
+}
+
+func (r *apiKeyResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
+	var plan, state models.APIKeyResourceModel
+	resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
+	resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	updateRequest := &v2client.APIKey{
+		ID:       plan.ID.ValueString(),
+		Name:     plan.Name.ValueStringPointer(),
+		Disabled: plan.Disabled.ValueBoolPointer(),
+	}
+
+	_, err := r.client.APIKeys.Update(ctx, updateRequest)
+	if helper.AddDiagnosticOnError(&resp.Diagnostics, "Updating Honeycomb API Key", err) {
+		return
+	}
+
+	key, err := r.client.APIKeys.Get(ctx, plan.ID.ValueString())
+	if helper.AddDiagnosticOnError(&resp.Diagnostics, "Updating Honeycomb API Key", err) {
+		return
+	}
+
+	state.ID = types.StringValue(key.ID)
+	state.Name = types.StringValue(*key.Name)
+	state.Type = types.StringValue(key.KeyType)
+	state.Disabled = types.BoolValue(*key.Disabled)
+	state.EnvironmentID = types.StringValue(key.Environment.ID)
+	if !state.Permissions.IsNull() {
+		state.Permissions = flattenAPIKeyPermissions(ctx, key.Permissions, &resp.Diagnostics)
+	} else {
+		state.Permissions = types.ListNull(types.ObjectType{AttrTypes: models.APIKeyPermissionsAttrType})
+	}
+
+	resp.Diagnostics.Append(resp.State.Set(ctx, state)...)
+}
+
+func (r *apiKeyResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) {
+	var state models.APIKeyResourceModel
+	resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
+	if resp.Diagnostics.HasError() {
+		return
+	}
+
+	err := r.client.APIKeys.Delete(ctx, state.ID.ValueString())
+	var detailedErr client.DetailedError
+	if err != nil {
+		if errors.As(err, &detailedErr) {
+			resp.Diagnostics.Append(helper.NewDetailedErrorDiagnostic(
+				"Error Reading Honeycomb API Key",
+				&detailedErr,
+			))
+		} else {
+			resp.Diagnostics.AddError(
+				"Error Deleting Honeycomb API Key",
+				"Could not delete API Key ID "+state.ID.ValueString()+": "+err.Error(),
+			)
+		}
+	}
+}
+
+func expandAPIKeyPermissions(ctx context.Context, list types.List, diags *diag.Diagnostics) *v2client.APIKeyPermissions {
+	var permissions []models.APIKeyPermissionModel
+	diags.Append(list.ElementsAs(ctx, &permissions, false)...)
+	if diags.HasError() {
+		return nil
+	}
+
+	if len(permissions) == 0 {
+		return nil
+	}
+
+	return &v2client.APIKeyPermissions{
+		CreateDatasets: permissions[0].CreateDatasets.ValueBool(),
+	}
+}
+
+func flattenAPIKeyPermissions(ctx context.Context, p *v2client.APIKeyPermissions, diags *diag.Diagnostics) types.List {
+	if p == nil {
+		return types.ListNull(types.ObjectType{AttrTypes: models.APIKeyPermissionsAttrType})
+	}
+
+	obj, d := types.ObjectValue(models.APIKeyPermissionsAttrType, map[string]attr.Value{
+		"create_datasets": types.BoolValue(p.CreateDatasets),
+	})
+	diags.Append(d...)
+
+	result, d := types.ListValueFrom(ctx, types.ObjectType{AttrTypes: models.APIKeyPermissionsAttrType}, []attr.Value{obj})
+	diags.Append(d...)
+
+	return result
+}
diff --git a/internal/provider/auth_metadata_data_source.go b/internal/provider/auth_metadata_data_source.go
index 56315f8a..28663a87 100644
--- a/internal/provider/auth_metadata_data_source.go
+++ b/internal/provider/auth_metadata_data_source.go
@@ -160,8 +160,18 @@ func (d *authMetadataDataSource) Schema(_ context.Context, _ datasource.SchemaRe
 	}
 }
 
-func (d *authMetadataDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, _ *datasource.ConfigureResponse) {
-	d.client = getClientFromDatasourceRequest(&req)
+func (d *authMetadataDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
+	w := getClientFromDatasourceRequest(&req)
+	if w == nil {
+		return
+	}
+
+	c, err := w.V1Client()
+	if err != nil || c == nil {
+		resp.Diagnostics.AddError("Failed to configure client", err.Error())
+		return
+	}
+	d.client = c
 }
 
 func (d *authMetadataDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
diff --git a/internal/provider/burn_alert_resource.go b/internal/provider/burn_alert_resource.go
index 5bfd4dae..661d218e 100644
--- a/internal/provider/burn_alert_resource.go
+++ b/internal/provider/burn_alert_resource.go
@@ -45,7 +45,17 @@ func (*burnAlertResource) Metadata(_ context.Context, req resource.MetadataReque
 }
 
 func (r *burnAlertResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
-	r.client = getClientFromResourceRequest(&req)
+	w := getClientFromResourceRequest(&req)
+	if w == nil {
+		return
+	}
+
+	c, err := w.V1Client()
+	if err != nil || c == nil {
+		resp.Diagnostics.AddError("Failed to configure client", err.Error())
+		return
+	}
+	r.client = c
 }
 
 func (*burnAlertResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
diff --git a/internal/provider/derived_column_data_source.go b/internal/provider/derived_column_data_source.go
index 0619c9d4..eee84b77 100644
--- a/internal/provider/derived_column_data_source.go
+++ b/internal/provider/derived_column_data_source.go
@@ -70,8 +70,18 @@ func (d *derivedColumnDataSource) Schema(_ context.Context, _ datasource.SchemaR
 	}
 }
 
-func (d *derivedColumnDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, _ *datasource.ConfigureResponse) {
-	d.client = getClientFromDatasourceRequest(&req)
+func (d *derivedColumnDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
+	w := getClientFromDatasourceRequest(&req)
+	if w == nil {
+		return
+	}
+
+	c, err := w.V1Client()
+	if err != nil || c == nil {
+		resp.Diagnostics.AddError("Failed to configure client", err.Error())
+		return
+	}
+	d.client = c
 }
 
 func (d *derivedColumnDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
diff --git a/internal/provider/derived_columns_data_source.go b/internal/provider/derived_columns_data_source.go
index a68d50b3..5d81ad86 100644
--- a/internal/provider/derived_columns_data_source.go
+++ b/internal/provider/derived_columns_data_source.go
@@ -65,8 +65,18 @@ func (d *derivedColumnsDataSource) Schema(_ context.Context, _ datasource.Schema
 	}
 }
 
-func (d *derivedColumnsDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, _ *datasource.ConfigureResponse) {
-	d.client = getClientFromDatasourceRequest(&req)
+func (d *derivedColumnsDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
+	w := getClientFromDatasourceRequest(&req)
+	if w == nil {
+		return
+	}
+
+	c, err := w.V1Client()
+	if err != nil || c == nil {
+		resp.Diagnostics.AddError("Failed to configure client", err.Error())
+		return
+	}
+	d.client = c
 }
 
 func (d *derivedColumnsDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
diff --git a/internal/provider/provider.go b/internal/provider/provider.go
index 4b029650..417d0c0a 100644
--- a/internal/provider/provider.go
+++ b/internal/provider/provider.go
@@ -2,6 +2,7 @@ package provider
 
 import (
 	"context"
+	"errors"
 	"fmt"
 	"os"
 
@@ -13,6 +14,7 @@ import (
 	"github.com/hashicorp/terraform-plugin-framework/types"
 
 	"github.com/honeycombio/terraform-provider-honeycombio/client"
+	v2client "github.com/honeycombio/terraform-provider-honeycombio/client/v2"
 	"github.com/honeycombio/terraform-provider-honeycombio/internal/helper/log"
 )
 
@@ -28,9 +30,11 @@ type HoneycombioProvider struct {
 
 // HoneycombioProviderModel describes the provider data model.
 type HoneycombioProviderModel struct {
-	APIKey types.String `tfsdk:"api_key"`
-	APIUrl types.String `tfsdk:"api_url"`
-	Debug  types.Bool   `tfsdk:"debug"`
+	APIKey    types.String `tfsdk:"api_key"`
+	KeyID     types.String `tfsdk:"api_key_id"`
+	KeySecret types.String `tfsdk:"api_key_secret"`
+	APIUrl    types.String `tfsdk:"api_url"`
+	Debug     types.Bool   `tfsdk:"debug"`
 }
 
 func New(version string) provider.Provider {
@@ -47,6 +51,16 @@ func (p *HoneycombioProvider) Schema(_ context.Context, _ provider.SchemaRequest
 				Optional:            true,
 				Sensitive:           true,
 			},
+			"api_key_id": schema.StringAttribute{
+				MarkdownDescription: "The ID portion of the Honeycomb Management API key to use. It can also be set via the `HONEYCOMB_KEY_ID` environment variable.",
+				Optional:            true,
+				Sensitive:           false,
+			},
+			"api_key_secret": schema.StringAttribute{
+				MarkdownDescription: "The secret portion of the Honeycomb Management API key to use. It can also be set via the `HONEYCOMB_KEY_SECRET` environment variable.",
+				Optional:            true,
+				Sensitive:           true,
+			},
 			"api_url": schema.StringAttribute{
 				MarkdownDescription: "Override the URL of the Honeycomb API. Defaults to `https://api.honeycomb.io`. It can also be set via the `HONEYCOMB_API_ENDPOINT` environment variable.",
 				Optional:            true,
@@ -64,6 +78,7 @@ func (p *HoneycombioProvider) Resources(ctx context.Context) []func() resource.R
 		NewBurnAlertResource,
 		NewTriggerResource,
 		NewQueryResource,
+		NewAPIKeyResource,
 	}
 }
 
@@ -98,6 +113,25 @@ func (p *HoneycombioProvider) Configure(ctx context.Context, req provider.Config
 				"Either target apply the source of the value first, set the value statically in the configuration, or use the HONEYCOMB_API_KEY environment variable.",
 		)
 	}
+	if config.KeyID.IsUnknown() {
+		resp.Diagnostics.AddAttributeError(
+			path.Root("api_key_id"),
+			"Unknown Honeycomb API Key ID",
+			"The provider cannot create the Honeycomb client as there is an unknown configuration value for the Honeycomb API Key ID. "+
+				"Either target apply the source of the value first, set the value statically in the configuration, or use the HONEYCOMB_KEY_ID environment variable.",
+		)
+	}
+	if config.KeySecret.IsUnknown() {
+		resp.Diagnostics.AddAttributeError(
+			path.Root("api_key_secret"),
+			"Unknown Honeycomb API Key Secret",
+			"The provider cannot create the Honeycomb client as there is an unknown configuration value for the Honeycomb API Key Secret. "+
+				"Either target apply the source of the value first, set the value statically in the configuration, or use the HONEYCOMB_KEY_SECRET environment variable.",
+		)
+	}
+	if resp.Diagnostics.HasError() {
+		return
+	}
 
 	apiKey := os.Getenv(client.DefaultAPIKeyEnv)
 	if apiKey == "" {
@@ -105,18 +139,45 @@ func (p *HoneycombioProvider) Configure(ctx context.Context, req provider.Config
 		//nolint:staticcheck
 		apiKey = os.Getenv(client.LegacyAPIKeyEnv)
 	}
+	keyID := os.Getenv(v2client.DefaultAPIKeyIDEnv)
+	keySecret := os.Getenv(v2client.DefaultAPIKeySecretEnv)
+
 	if !config.APIKey.IsNull() {
 		apiKey = config.APIKey.ValueString()
 	}
-	if apiKey == "" {
-		resp.Diagnostics.AddAttributeError(
-			path.Root("api_key"),
-			"Missing Honeycomb API Key",
-			"The provider cannot create the Honeycomb API client as there is a missing or empty value for the Honeycomb API Key. "+
-				"Set the value in the configuration or use the HONEYCOMB_API_KEY environment variable. "+
-				"If either is already set, ensure the value is not empty.",
+	if !config.KeyID.IsNull() {
+		keyID = config.KeyID.ValueString()
+	}
+	if !config.KeySecret.IsNull() {
+		keySecret = config.KeySecret.ValueString()
+	}
+
+	initv1Client, initv2Client := false, false
+	if apiKey != "" {
+		initv1Client = true
+	}
+	if keyID != "" && keySecret != "" {
+		initv2Client = true
+	} else if (keyID != "" && keySecret == "") || (keyID == "" && keySecret != "") {
+		resp.Diagnostics.AddError(
+			"Unable to initialize Honeycomb provider",
+			"The provider requires both a Honeycomb API Key ID and Secret. "+
+				"Set them both in the configuration or via the HONEYCOMB_KEY_ID and HONEYCOMB_KEY_SECRET"+
+				"environment variables. "+
+				"If you believe both are already set, ensure the values are not empty.",
 		)
+		return
 	}
+
+	if !initv1Client && !initv2Client {
+		resp.Diagnostics.AddError(
+			"Unable to initialize Honeycomb provider",
+			"The provider requires at least one of a Honeycomb API Key, or the Honeycomb API Key ID and Secret pair. "+
+				"Set either HONEYCOMB_API_KEY for v1 APIs, or HONEYCOMB_KEY_ID and HONEYCOMB_KEY_SECRET for v2 APIs. "+
+				"If you believe either is already set, ensure the values are not empty.",
+		)
+	}
+
 	if resp.Diagnostics.HasError() {
 		return
 	}
@@ -125,38 +186,96 @@ func (p *HoneycombioProvider) Configure(ctx context.Context, req provider.Config
 	if !config.Debug.IsNull() {
 		debug = config.Debug.ValueBool()
 	}
+	userAgent := fmt.Sprintf(
+		"Terraform/%s terraform-provider-honeycombio/%s",
+		req.TerraformVersion,
+		p.version,
+	)
 
-	client, err := client.NewClientWithConfig(&client.Config{
-		APIKey:    apiKey,
-		APIUrl:    config.APIUrl.ValueString(),
-		Debug:     debug,
-		UserAgent: fmt.Sprintf("Terraform/%s terraform-provider-honeycombio/%s", req.TerraformVersion, p.version),
-	})
-	if err != nil {
-		resp.Diagnostics.AddError(
-			"Unable to create Honeycomb API Client",
-			"An unexpected error occurred when creating the Honeycomb API client.\n\n "+
-				"Honeycomb Client Error: "+err.Error(),
+	cc := &ConfiguredClient{}
+	if initv1Client {
+		client, err := client.NewClientWithConfig(&client.Config{
+			APIKey:    apiKey,
+			APIUrl:    config.APIUrl.ValueString(),
+			Debug:     debug,
+			UserAgent: userAgent,
+		})
+		if err != nil {
+			resp.Diagnostics.AddError(
+				"Unable to create Honeycomb API Client",
+				"An unexpected error occurred when creating the Honeycomb API client.\n\n "+
+					"Honeycomb Client Error: "+err.Error(),
+			)
+			return
+		}
+		cc.v1client = client
+	}
+
+	if initv2Client {
+		v2client, err := v2client.NewClientWithConfig(&v2client.Config{
+			APIKeyID:     keyID,
+			APIKeySecret: keySecret,
+			BaseURL:      config.APIUrl.ValueString(),
+			Debug:        debug,
+			UserAgent:    userAgent,
+		})
+		if err != nil {
+			resp.Diagnostics.AddError(
+				"Unable to create Honeycomb API V2 Client",
+				"An unexpected error occurred when creating the Honeycomb API V2 client.\n\n "+
+					"Honeycomb V2 Client Error: "+err.Error(),
+			)
+			return
+		}
+		cc.v2client = v2client
+	}
+
+	resp.DataSourceData = cc
+	resp.ResourceData = cc
+}
+
+// ConfiguredClient is a wrapper around the configured Honeycomb API clients.
+type ConfiguredClient struct {
+	v1client *client.Client
+	v2client *v2client.Client
+}
+
+func (c *ConfiguredClient) V1Client() (*client.Client, error) {
+	if c.v1client == nil {
+		return nil, errors.New("No v1 API client configured for this provider. " +
+			"Set the `api_key` attribute in the provider's configuration, " +
+			"or set the HONEYCOMB_API_KEY environment variable.",
 		)
-		return
 	}
+	return c.v1client, nil
+}
 
-	resp.DataSourceData = client
-	resp.ResourceData = client
+func (c *ConfiguredClient) V2Client() (*v2client.Client, error) {
+	if c.v2client == nil {
+		return nil, errors.New("No v2 API client configured for this provider. " +
+			"Set the Key ID and Secret pair in the provider's configuration, " +
+			"or via the HONEYCOMB_KEY_ID and HONEYCOMB_KEY_SECRET environment variables.",
+		)
+	}
+	return c.v2client, nil
 }
 
-func getClientFromDatasourceRequest(req *datasource.ConfigureRequest) *client.Client {
-	if req.ProviderData == nil {
-		return nil
+func getClientFromDatasourceRequest(req *datasource.ConfigureRequest) *ConfiguredClient {
+	if req.ProviderData != nil {
+		if c, ok := req.ProviderData.(*ConfiguredClient); ok {
+			return c
+		}
 	}
-	//nolint:forcetypeassert
-	return req.ProviderData.(*client.Client)
+	// ProviderData hasn't been initialized yet -- so fail gracefully
+	return nil
 }
 
-func getClientFromResourceRequest(req *resource.ConfigureRequest) *client.Client {
-	if req.ProviderData == nil {
-		return nil
+func getClientFromResourceRequest(req *resource.ConfigureRequest) *ConfiguredClient {
+	if req.ProviderData != nil {
+		if c, ok := req.ProviderData.(*ConfiguredClient); ok {
+			return c
+		}
 	}
-	//nolint:forcetypeassert
-	return req.ProviderData.(*client.Client)
+	// ProviderData hasn't been initialized yet -- so fail gracefully
+	return nil
 }
diff --git a/internal/provider/query_resource.go b/internal/provider/query_resource.go
index d861f3bc..23c5dc6f 100644
--- a/internal/provider/query_resource.go
+++ b/internal/provider/query_resource.go
@@ -41,7 +41,17 @@ func (*queryResource) Metadata(_ context.Context, req resource.MetadataRequest,
 }
 
 func (r *queryResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
-	r.client = getClientFromResourceRequest(&req)
+	w := getClientFromResourceRequest(&req)
+	if w == nil {
+		return
+	}
+
+	c, err := w.V1Client()
+	if err != nil || c == nil {
+		resp.Diagnostics.AddError("Failed to configure client", err.Error())
+		return
+	}
+	r.client = c
 }
 
 func (*queryResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
diff --git a/internal/provider/query_specification_data_source.go b/internal/provider/query_specification_data_source.go
index 31bfb495..46eddb0a 100644
--- a/internal/provider/query_specification_data_source.go
+++ b/internal/provider/query_specification_data_source.go
@@ -200,7 +200,17 @@ func (d *querySpecDataSource) Schema(_ context.Context, _ datasource.SchemaReque
 }
 
 func (d *querySpecDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
-	d.client = getClientFromDatasourceRequest(&req)
+	w := getClientFromDatasourceRequest(&req)
+	if w == nil {
+		return
+	}
+
+	c, err := w.V1Client()
+	if err != nil || c == nil {
+		resp.Diagnostics.AddError("Failed to configure client", err.Error())
+		return
+	}
+	d.client = c
 }
 
 func (d *querySpecDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
diff --git a/internal/provider/slo_data_source.go b/internal/provider/slo_data_source.go
index e49f692e..94587fb0 100644
--- a/internal/provider/slo_data_source.go
+++ b/internal/provider/slo_data_source.go
@@ -87,8 +87,18 @@ func (d *sloDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, re
 	}
 }
 
-func (d *sloDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, _ *datasource.ConfigureResponse) {
-	d.client = getClientFromDatasourceRequest(&req)
+func (d *sloDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
+	w := getClientFromDatasourceRequest(&req)
+	if w == nil {
+		return
+	}
+
+	c, err := w.V1Client()
+	if err != nil || c == nil {
+		resp.Diagnostics.AddError("Failed to configure client", err.Error())
+		return
+	}
+	d.client = c
 }
 
 func (d *sloDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
diff --git a/internal/provider/slos_data_source.go b/internal/provider/slos_data_source.go
index 7987907b..056c569c 100644
--- a/internal/provider/slos_data_source.go
+++ b/internal/provider/slos_data_source.go
@@ -109,8 +109,18 @@ func (d *slosDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, r
 	}
 }
 
-func (d *slosDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, _ *datasource.ConfigureResponse) {
-	d.client = getClientFromDatasourceRequest(&req)
+func (d *slosDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
+	w := getClientFromDatasourceRequest(&req)
+	if w == nil {
+		return
+	}
+
+	c, err := w.V1Client()
+	if err != nil || c == nil {
+		resp.Diagnostics.AddError("Failed to configure client", err.Error())
+		return
+	}
+	d.client = c
 }
 
 func (d *slosDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) {
diff --git a/internal/provider/trigger_resource.go b/internal/provider/trigger_resource.go
index 2ad4d6cf..124ec645 100644
--- a/internal/provider/trigger_resource.go
+++ b/internal/provider/trigger_resource.go
@@ -214,8 +214,18 @@ func (r *triggerResource) Schema(_ context.Context, _ resource.SchemaRequest, re
 	}
 }
 
-func (r *triggerResource) Configure(_ context.Context, req resource.ConfigureRequest, _ *resource.ConfigureResponse) {
-	r.client = getClientFromResourceRequest(&req)
+func (r *triggerResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
+	w := getClientFromResourceRequest(&req)
+	if w == nil {
+		return
+	}
+
+	c, err := w.V1Client()
+	if err != nil || c == nil {
+		resp.Diagnostics.AddError("Failed to configure client", err.Error())
+		return
+	}
+	r.client = c
 }
 
 func (r *triggerResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {


Test patch:
diff --git a/client/errors_test.go b/client/errors_test.go
index bf3240ce..f5557dbf 100644
--- a/client/errors_test.go
+++ b/client/errors_test.go
@@ -1,11 +1,14 @@
 package client_test
 
 import (
+	"bytes"
 	"context"
 	"fmt"
 	"net/http"
+	"net/http/httptest"
 	"testing"
 
+	"github.com/hashicorp/jsonapi"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 
@@ -191,3 +194,83 @@ func TestErrors_ErrorTypeDetail_String(t *testing.T) {
 		})
 	}
 }
+
+func TestErrors_JSONAPI(t *testing.T) {
+	t.Parallel()
+
+	testCases := []struct {
+		name           string
+		code           int
+		body           jsonapi.ErrorsPayload
+		expectedOutput client.DetailedError
+	}{
+		{
+			name: "single error",
+			code: http.StatusConflict,
+			body: jsonapi.ErrorsPayload{
+				Errors: []*jsonapi.ErrorObject{
+					{
+						Status: "409",
+						Title:  "Conflict",
+						Detail: "The resource already exists.",
+						Code:   "/errors/conflict",
+					},
+				},
+			},
+			expectedOutput: client.DetailedError{
+				Status:  409,
+				Type:    "/errors/conflict",
+				Message: "The resource already exists.",
+				Title:   "Conflict",
+			},
+		},
+		{
+			name: "multi error",
+			code: http.StatusUnprocessableEntity,
+			body: jsonapi.ErrorsPayload{
+				Errors: []*jsonapi.ErrorObject{
+					{
+						Status: "422",
+						Code:   "/errors/validation-failed",
+						Title:  "The provided input is invalid.",
+					},
+					{
+						Status: "422",
+						Code:   "/errors/validation-failed",
+						Title:  "The provided input is invalid.",
+					},
+				},
+			},
+			expectedOutput: client.DetailedError{
+				Status: 422,
+				Title:  "The provided input is invalid.",
+				Details: []client.ErrorTypeDetail{
+					{
+						Code: "/errors/validation-failed",
+					},
+					{
+						Code: "/errors/validation-failed",
+					},
+				},
+			},
+		},
+	}
+
+	for _, testCase := range testCases {
+		t.Run(testCase.name, func(t *testing.T) {
+			resp := &httptest.ResponseRecorder{
+				Code: testCase.code,
+				HeaderMap: http.Header{
+					"Content-Type": []string{jsonapi.MediaType},
+				},
+			}
+
+			buf := bytes.NewBuffer(nil)
+			jsonapi.MarshalErrors(buf, testCase.body.Errors)
+			resp.Body = bytes.NewBuffer(buf.Bytes())
+
+			actualOutput := client.ErrorFromResponse(resp.Result())
+			assert.Equal(t, testCase.expectedOutput, actualOutput)
+		})
+	}
+}
diff --git a/client/v2/api_keys_test.go b/client/v2/api_keys_test.go
new file mode 100644
index 00000000..57710344
--- /dev/null
+++ b/client/v2/api_keys_test.go
@@ -0,0 +1,132 @@
+package v2
+
+import (
+	"context"
+	"fmt"
+	"math"
+	"os"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	hnyclient "github.com/honeycombio/terraform-provider-honeycombio/client"
+	"github.com/honeycombio/terraform-provider-honeycombio/internal/helper"
+)
+
+func TestClient_APIKeys(t *testing.T) {
+	ctx := context.Background()
+	c := newTestClient(t)
+	// TODO: use the environments API
+	testEnvironmentID := os.Getenv("HONEYCOMB_ENVIRONMENT_ID")
+
+	// create a new key
+	k, err := c.APIKeys.Create(ctx, &APIKey{
+		Name:    helper.ToPtr("test key"),
+		KeyType: "ingest",
+		Environment: &Environment{
+			ID: testEnvironmentID,
+		},
+		Permissions: &APIKeyPermissions{
+			CreateDatasets: true,
+		},
+	})
+	require.NoError(t, err)
+	assert.NotEmpty(t, k.ID)
+	assert.NotEmpty(t, k.Secret)
+	assert.Equal(t, "test key", *k.Name)
+	assert.False(t, *k.Disabled)
+	assert.True(t, k.Permissions.CreateDatasets)
+
+	// read the key back and compare
+	key, err := c.APIKeys.Get(ctx, k.ID)
+	require.NoError(t, err)
+	assert.Equal(t, k.ID, key.ID)
+	assert.Equal(t, k.Name, key.Name)
+	assert.Equal(t, k.KeyType, key.KeyType)
+	assert.Equal(t, k.Disabled, key.Disabled)
+	assert.Equal(t, k.Environment.ID, key.Environment.ID)
+	assert.Equal(t, k.Permissions.CreateDatasets, key.Permissions.CreateDatasets)
+	assert.WithinDuration(t, k.Timestamps.CreatedAt, key.Timestamps.CreatedAt, 5*time.Second)
+	assert.WithinDuration(t, k.Timestamps.UpdatedAt, key.Timestamps.UpdatedAt, 5*time.Second)
+
+	// update the key's name and disable it
+	key, err = c.APIKeys.Update(ctx, &APIKey{
+		ID:       k.ID,
+		Name:     helper.ToPtr("new name"),
+		Disabled: helper.ToPtr(true),
+	})
+	require.NoError(t, err)
+	assert.Equal(t, k.ID, key.ID)
+	assert.Equal(t, "new name", *key.Name)
+	assert.True(t, *key.Disabled)
+	assert.WithinDuration(t, time.Now(), key.Timestamps.UpdatedAt, time.Second)
+
+	// delete the key
+	err = c.APIKeys.Delete(ctx, k.ID)
+	require.NoError(t, err)
+
+	// verify that the key was deleted
+	_, err = c.APIKeys.Get(ctx, k.ID)
+	var de hnyclient.DetailedError
+	require.ErrorAs(t, err, &de)
+	assert.True(t, de.IsNotFound())
+}
+
+func TestClient_APIKeys_Pagination(t *testing.T) {
+	ctx := context.Background()
+	c := newTestClient(t)
+	// TODO: use the environments API
+	testEnvironmentID := os.Getenv("HONEYCOMB_ENVIRONMENT_ID")
+
+	// create a bunch of keys
+	numKeys := int(math.Floor(1.5 * float64(defaultPageSize)))
+	testKeys := make([]*APIKey, numKeys)
+	for i := 0; i < numKeys; i++ {
+		k, err := c.APIKeys.Create(ctx, &APIKey{
+			Name:    helper.ToPtr(fmt.Sprintf("testkey-%d", i)),
+			KeyType: "ingest",
+			Environment: &Environment{
+				ID: testEnvironmentID,
+			},
+		})
+		testKeys[i] = k
+		require.NoError(t, err)
+	}
+	t.Cleanup(func() {
+		for _, k := range testKeys {
+			c.APIKeys.Delete(ctx, k.ID)
+		}
+	})
+
+	t.Run("happy path", func(t *testing.T) {
+		keys := make([]*APIKey, 0)
+		pager, err := c.APIKeys.List(ctx)
+		require.NoError(t, err)
+
+		items, err := pager.Next(ctx)
+		require.NoError(t, err)
+		assert.Len(t, items, defaultPageSize, "incorrect number of items")
+		assert.True(t, pager.HasNext(), "should have more pages")
+		keys = append(keys, items...)
+
+		for pager.HasNext() {
+			items, err = pager.Next(ctx)
+			require.NoError(t, err)
+			keys = append(keys, items...)
+		}
+		assert.Len(t, keys, numKeys)
+	})
+
+	t.Run("works with custom page size", func(t *testing.T) {
+		pageSize := 5
+		pager, err := c.APIKeys.List(ctx, PageSize(pageSize))
+		require.NoError(t, err)
+
+		items, err := pager.Next(ctx)
+		require.NoError(t, err)
+		assert.Len(t, items, pageSize, "incorrect number of items")
+		assert.True(t, pager.HasNext(), "should have more pages")
+	})
+}
diff --git a/client/v2/client_test.go b/client/v2/client_test.go
new file mode 100644
index 00000000..6ccfb75d
--- /dev/null
+++ b/client/v2/client_test.go
@@ -0,0 +1,138 @@
+package v2
+
+import (
+	"context"
+	"net/http"
+	"testing"
+
+	"github.com/joho/godotenv"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+
+	hnyclient "github.com/honeycombio/terraform-provider-honeycombio/client"
+)
+
+const (
+	testEnvFilePath = "../../.env"
+	testUserAgent   = "go-honeycombio/test"
+)
+
+func TestClient_Config(t *testing.T) {
+	// load environment values from a .env, if available
+	_ = godotenv.Load(testEnvFilePath)
+
+	t.Run("constructs a default client", func(t *testing.T) {
+		c, err := NewClient()
+		require.NoError(t, err)
+		assert.Equal(t, defaultUserAgent, c.UserAgent)
+		assert.NotEmpty(t, c.BaseURL.String())
+	})
+
+	t.Run("constructs a client with config overrides", func(t *testing.T) {
+		c, err := NewClientWithConfig(&Config{
+			APIKeyID:           "123",
+			APIKeySecret:       "456",
+			BaseURL:            "https://api.example.com",
+			UserAgent:          testUserAgent,
+			skipInitialization: true,
+		})
+		require.NoError(t, err)
+		assert.Equal(t, "Bearer 123:456", c.Headers.Get("Authorization"))
+		assert.Equal(t, "https://api.example.com", c.BaseURL.String())
+		assert.Equal(t, testUserAgent, c.UserAgent)
+	})
+
+	t.Run("fails to construct a client with an invalid API URL", func(t *testing.T) {
+		_, err := NewClientWithConfig(&Config{
+			BaseURL:            "cache_object:foo/bar",
+			skipInitialization: true,
+		})
+		require.Error(t, err)
+		assert.Contains(t, err.Error(), "invalid BaseURL")
+	})
+
+	t.Run("fails to construct a client without key id and secret pair", func(t *testing.T) {
+		t.Run("with both missing", func(t *testing.T) {
+			// load environment values from a .env, if available
+			_ = godotenv.Load(testEnvFilePath)
+			t.Setenv(DefaultAPIKeyIDEnv, "")
+			t.Setenv(DefaultAPIKeySecretEnv, "")
+
+			_, err := NewClient()
+			require.Error(t, err)
+			assert.Contains(t, err.Error(), "missing API Key ID and Secret pair")
+		})
+
+		t.Run("with key ID missing", func(t *testing.T) {
+			// load environment values from a .env, if available
+			_ = godotenv.Load(testEnvFilePath)
+			t.Setenv(DefaultAPIKeyIDEnv, "")
+
+			_, err := NewClient()
+			require.Error(t, err)
+			assert.Contains(t, err.Error(), "missing API Key ID and Secret pair")
+		})
+
+		t.Run("with key secret missing", func(t *testing.T) {
+			// load environment values from a .env, if available
+			_ = godotenv.Load(testEnvFilePath)
+			t.Setenv(DefaultAPIKeyIDEnv, "")
+
+			_, err := NewClient()
+			require.Error(t, err)
+			assert.Contains(t, err.Error(), "missing API Key ID and Secret pair")
+		})
+	})
+}
+
+func TestClient_AuthInfo(t *testing.T) {
+	t.Parallel()
+
+	ctx := context.Background()
+
+	t.Run("happy path", func(t *testing.T) {
+		c := newTestClient(t)
+		metadata, err := c.AuthInfo(ctx)
+		require.NoError(t, err)
+		assert.NotEmpty(t, metadata.ID)
+		assert.NotEmpty(t, metadata.Name)
+		assert.Equal(t, "management", metadata.KeyType)
+		assert.False(t, metadata.Disabled)
+		assert.NotEmpty(t, metadata.Scopes)
+		if assert.NotNil(t, metadata.Timestamps) {
+			assert.NotEmpty(t, metadata.Timestamps.CreatedAt)
+			assert.NotEmpty(t, metadata.Timestamps.UpdatedAt)
+		}
+		if assert.NotNil(t, metadata.Team) {
+			assert.NotEmpty(t, metadata.Team.ID)
+			assert.NotEmpty(t, metadata.Team.Name)
+			assert.NotEmpty(t, metadata.Team.Slug)
+		}
+	})
+
+	t.Run("handles unauthorized gracefully", func(t *testing.T) {
+		_, err := NewClientWithConfig(&Config{
+			APIKeyID:     "foo",
+			APIKeySecret: "bar",
+			UserAgent:    testUserAgent,
+		})
+
+		var de hnyclient.DetailedError
+		require.ErrorAs(t, err, &de)
+		assert.Equal(t, http.StatusUnauthorized, de.Status)
+	})
+}
+
+func newTestClient(t *testing.T) *Client {
+	t.Helper()
+
+	// load environment values from a .env, if available
+	_ = godotenv.Load(testEnvFilePath)
+
+	c, err := NewClientWithConfig(&Config{
+		UserAgent: testUserAgent,
+	})
+	require.NoError(t, err, "failed to create test client")
+
+	return c
+}
diff --git a/client/v2/pagination_test.go b/client/v2/pagination_test.go
new file mode 100644
index 00000000..fbc86688
--- /dev/null
+++ b/client/v2/pagination_test.go
@@ -0,0 +1,37 @@
+package v2
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestClient_Pagination(t *testing.T) {
+	t.Parallel()
+
+	c := newTestClient(t)
+	type TestType struct{}
+	pagerUrl := "/2/thing/myteam/resource"
+
+	t.Run("returns a Pager with the correct URL and options", func(t *testing.T) {
+		p, err := NewPager[TestType](c, pagerUrl)
+		require.NoError(t, err)
+
+		assert.NotNil(t, p.client)
+		assert.Equal(t, defaultPageSize, p.opts.PageSize)
+		assert.Equal(t, fmt.Sprintf("%s?page%%5Bsize%%5D=%d", pagerUrl, defaultPageSize), *p.next)
+		assert.True(t, p.HasNext())
+	})
+
+	t.Run("returns a Pager with the correct URL and options when PageSize is set", func(t *testing.T) {
+		p, err := NewPager[TestType](c, pagerUrl, PageSize(50))
+		require.NoError(t, err)
+
+		assert.NotNil(t, p.client)
+		assert.Equal(t, 50, p.opts.PageSize)
+		assert.Equal(t, fmt.Sprintf("%s?page%%5Bsize%%5D=%d", pagerUrl, 50), *p.next)
+		assert.True(t, p.HasNext())
+	})
+}
diff --git a/internal/helper/validation/divisible_by_test.go b/internal/helper/validation/divisible_by_test.go
index 9b6c44e4..b646b64b 100644
--- a/internal/helper/validation/divisible_by_test.go
+++ b/internal/helper/validation/divisible_by_test.go
@@ -53,7 +53,7 @@ func Test_BetweenValidator(t *testing.T) {
 				ConfigValue:    test.val,
 			}
 			response := validator.Int64Response{}
-			validation.Int64DivisibleBy(test.divisor).ValidateInt64(context.TODO(), request, &response)
+			validation.Int64DivisibleBy(test.divisor).ValidateInt64(context.Background(), request, &response)
 
 			if !response.Diagnostics.HasError() && test.expectError {
 				t.Fatal("expected error, got no error")
diff --git a/internal/helper/validation/precision_at_most_test.go b/internal/helper/validation/precision_at_most_test.go
index db87f56d..60e7fb91 100644
--- a/internal/helper/validation/precision_at_most_test.go
+++ b/internal/helper/validation/precision_at_most_test.go
@@ -63,7 +63,7 @@ func TestValidation_PrecisionAtMost(t *testing.T) {
 				ConfigValue:    test.value,
 			}
 			response := validator.Float64Response{}
-			validation.Float64PrecisionAtMost(test.maxPrecision).ValidateFloat64(context.TODO(), request, &response)
+			validation.Float64PrecisionAtMost(test.maxPrecision).ValidateFloat64(context.Background(), request, &response)
 
 			if !response.Diagnostics.HasError() && test.expectError {
 				t.Fatal("expected error, got no error")
diff --git a/internal/helper/validation/query_spec_test.go b/internal/helper/validation/query_spec_test.go
index 3720b42b..50916824 100644
--- a/internal/helper/validation/query_spec_test.go
+++ b/internal/helper/validation/query_spec_test.go
@@ -47,7 +47,7 @@ func Test_QuerySpecValidator(t *testing.T) {
 				ConfigValue:    test.val,
 			}
 			response := validator.StringResponse{}
-			validation.ValidQuerySpec().ValidateString(context.TODO(), request, &response)
+			validation.ValidQuerySpec().ValidateString(context.Background(), request, &response)
 
 			if !response.Diagnostics.HasError() && test.expectError {
 				t.Fatal("expected error, got no error")
diff --git a/internal/helper/validation/valid_regex_test.go b/internal/helper/validation/valid_regex_test.go
index e0b49b92..39bf36e0 100644
--- a/internal/helper/validation/valid_regex_test.go
+++ b/internal/helper/validation/valid_regex_test.go
@@ -44,7 +44,7 @@ func Test_IsValidRegexValidator(t *testing.T) {
 				ConfigValue:    test.val,
 			}
 			response := validator.StringResponse{}
-			validation.IsValidRegExp().ValidateString(context.TODO(), request, &response)
+			validation.IsValidRegExp().ValidateString(context.Background(), request, &response)
 
 			if !response.Diagnostics.HasError() && test.expectError {
 				t.Fatal("expected error, got no error")
diff --git a/internal/provider/api_key_resource_test.go b/internal/provider/api_key_resource_test.go
new file mode 100644
index 00000000..13f676e5
--- /dev/null
+++ b/internal/provider/api_key_resource_test.go
@@ -0,0 +1,110 @@
+package provider
+
+import (
+	"context"
+	"fmt"
+	"testing"
+
+	"github.com/hashicorp/terraform-plugin-testing/helper/resource"
+	"github.com/hashicorp/terraform-plugin-testing/terraform"
+)
+
+func TestAcc_APIKeyResource(t *testing.T) {
+	t.Parallel()
+
+	envID := testAccEnvironment()
+
+	t.Run("happy path", func(t *testing.T) {
+		resource.Test(t, resource.TestCase{
+			PreCheck:                 testAccPreCheckV2API(t),
+			ProtoV5ProviderFactories: testAccProtoV5MuxServerFactory,
+			Steps: []resource.TestStep{
+				{
+					Config: testAccConfigBasicAPIKeyTest("test key", "false", envID),
+					Check: resource.ComposeAggregateTestCheckFunc(
+						testAccEnsureAPIKeyExists(t, "honeycombio_api_key.test"),
+						resource.TestCheckResourceAttrSet("honeycombio_api_key.test", "id"),
+						resource.TestCheckResourceAttrSet("honeycombio_api_key.test", "secret"),
+						resource.TestCheckResourceAttr("honeycombio_api_key.test", "name", "test key"),
+						resource.TestCheckResourceAttr("honeycombio_api_key.test", "type", "ingest"),
+						resource.TestCheckResourceAttr("honeycombio_api_key.test", "environment_id", envID),
+						resource.TestCheckResourceAttr("honeycombio_api_key.test", "disabled", "false"),
+						resource.TestCheckResourceAttr("honeycombio_api_key.test", "permissions.0.create_datasets", "true"),
+					),
+				},
+				{ // now update the name and disabled state
+					Config: testAccConfigBasicAPIKeyTest("updated test key", "true", envID),
+					Check: resource.ComposeAggregateTestCheckFunc(
+						testAccEnsureAPIKeyExists(t, "honeycombio_api_key.test"),
+						resource.TestCheckResourceAttrSet("honeycombio_api_key.test", "id"),
+						resource.TestCheckResourceAttrSet("honeycombio_api_key.test", "secret"),
+						resource.TestCheckResourceAttr("honeycombio_api_key.test", "name", "updated test key"),
+						resource.TestCheckResourceAttr("honeycombio_api_key.test", "type", "ingest"),
+						resource.TestCheckResourceAttr("honeycombio_api_key.test", "environment_id", envID),
+						resource.TestCheckResourceAttr("honeycombio_api_key.test", "disabled", "true"),
+						resource.TestCheckResourceAttr("honeycombio_api_key.test", "permissions.0.create_datasets", "true"),
+					),
+				},
+			},
+		})
+	})
+
+	t.Run("default values", func(t *testing.T) {
+		resource.Test(t, resource.TestCase{
+			PreCheck:                 testAccPreCheckV2API(t),
+			ProtoV5ProviderFactories: testAccProtoV5MuxServerFactory,
+			Steps: []resource.TestStep{
+				{
+					Config: fmt.Sprintf(`
+resource "honeycombio_api_key" "test" {
+  name = "test key"
+  type = "ingest"
+
+  environment_id = "%s"
+}`, envID),
+					Check: resource.ComposeAggregateTestCheckFunc(
+						testAccEnsureAPIKeyExists(t, "honeycombio_api_key.test"),
+						resource.TestCheckResourceAttrSet("honeycombio_api_key.test", "id"),
+						resource.TestCheckResourceAttrSet("honeycombio_api_key.test", "secret"),
+						resource.TestCheckResourceAttr("honeycombio_api_key.test", "name", "test key"),
+						resource.TestCheckResourceAttr("honeycombio_api_key.test", "type", "ingest"),
+						resource.TestCheckResourceAttr("honeycombio_api_key.test", "environment_id", envID),
+						resource.TestCheckResourceAttr("honeycombio_api_key.test", "disabled", "false"),
+					),
+				},
+			},
+		})
+	})
+}
+
+func testAccConfigBasicAPIKeyTest(name, disabled, envID string) string {
+	return fmt.Sprintf(`
+resource "honeycombio_api_key" "test" {
+  name     = "%s"
+  type     = "ingest"
+  disabled = %s
+
+  environment_id = "%s"
+
+  permissions {
+    create_datasets = true
+  }
+}`, name, disabled, envID)
+}
+
+func testAccEnsureAPIKeyExists(t *testing.T, name string) resource.TestCheckFunc {
+	return func(s *terraform.State) error {
+		rs, ok := s.RootModule().Resources[name]
+		if !ok {
+			return fmt.Errorf("resource not found: %s", name)
+		}
+
+		client := testAccV2Client(t)
+		_, err := client.APIKeys.Get(context.Background(), rs.Primary.ID)
+		if err != nil {
+			return fmt.Errorf("failed to fetch created API key: %s", err)
+		}
+
+		return nil
+	}
+}
diff --git a/internal/provider/provider_test.go b/internal/provider/provider_test.go
index d91ab3c2..6b62a468 100644
--- a/internal/provider/provider_test.go
+++ b/internal/provider/provider_test.go
@@ -3,6 +3,7 @@ package provider
 import (
 	"context"
 	"os"
+	"regexp"
 	"testing"
 
 	"github.com/hashicorp/terraform-plugin-framework/providerserver"
@@ -12,6 +13,7 @@ import (
 	"github.com/joho/godotenv"
 
 	"github.com/honeycombio/terraform-provider-honeycombio/client"
+	v2client "github.com/honeycombio/terraform-provider-honeycombio/client/v2"
 	"github.com/honeycombio/terraform-provider-honeycombio/honeycombio"
 )
 
@@ -58,10 +60,29 @@ func testAccPreCheck(t *testing.T) func() {
 	}
 }
 
+func testAccPreCheckV2API(t *testing.T) func() {
+	return func() {
+		if _, ok := os.LookupEnv("HONEYCOMB_KEY_ID"); !ok {
+			t.Fatalf("environment variable HONEYCOMB_KEY_ID must be set to run acceptance tests")
+		}
+		if _, ok := os.LookupEnv("HONEYCOMB_KEY_SECRET"); !ok {
+			t.Fatalf("environment variable HONEYCOMB_KEY_SECRET must be set to run acceptance tests")
+		}
+		if _, ok := os.LookupEnv("HONEYCOMB_ENVIRONMENT_ID"); !ok {
+			t.Fatalf("environment variable HONEYCOMB_ENVIRONMENT_ID must be set to run acceptance tests")
+		}
+	}
+}
+
 func testAccDataset() string {
 	return os.Getenv("HONEYCOMB_DATASET")
 }
 
+func testAccEnvironment() string {
+	// TODO: replace with looking up the environment by name or doing a create at test start
+	return os.Getenv("HONEYCOMB_ENVIRONMENT_ID")
+}
+
 func testAccClient(t *testing.T) *client.Client {
 	c, err := client.NewClient()
 	if err != nil {
@@ -70,16 +91,166 @@ func testAccClient(t *testing.T) *client.Client {
 	return c
 }
 
-// TestMuxServer verifies that a V5 Mux Server can be properly created while
-// the Plugin SDK and the Plugin Framework are both in use in the provider
-func TestAcc_MuxServer(t *testing.T) {
-	resource.Test(t, resource.TestCase{
-		ProtoV5ProviderFactories: testAccProtoV5MuxServerFactory,
-		Steps: []resource.TestStep{
-			{
-				// simple smoketest by accessing a datasource
-				Config: `data "honeycombio_datasets" "all" {}`,
+func testAccV2Client(t *testing.T) *v2client.Client {
+	c, err := v2client.NewClient()
+	if err != nil {
+		t.Fatalf("could not initialize Honeycomb client: %v", err)
+	}
+	return c
+}
+
+// TestAcc_Configuration verifies that the provider is correctly
+// configured with the supported configuration permutations.
+func TestAcc_Configuration(t *testing.T) {
+	// t.Parallel() - not parallelized due to environment variable manipulation
+
+	t.Run("v1 and v2 clients", func(t *testing.T) {
+		resource.Test(t, resource.TestCase{
+			ProtoV5ProviderFactories: testAccProtoV5MuxServerFactory,
+			Steps: []resource.TestStep{
+				{
+					Config: `
+data "honeycombio_datasets" "all" {}
+
+# TODO: replace with v2-client data source
+resource "honeycombio_api_key" "test" {
+  name = "test"
+  type = "ingest"
+
+  environment_id = "1"
+}`,
+					PlanOnly:           true,
+					ExpectNonEmptyPlan: true,
+				},
+			},
+		})
+	})
+
+	t.Run("v1 client only", func(t *testing.T) {
+		resource.Test(t, resource.TestCase{
+			ProtoV5ProviderFactories: testAccProtoV5MuxServerFactory,
+			Steps: []resource.TestStep{
+				{
+					PreConfig: func() {
+						t.Setenv("HONEYCOMB_KEY_ID", "")
+						t.Setenv("HONEYCOMB_KEY_SECRET", "")
+					},
+					Config:   `data "honeycombio_datasets" "all" {}`,
+					PlanOnly: true,
+				},
+			},
+		})
+	})
+
+	t.Run("v2 client only", func(t *testing.T) {
+		resource.Test(t, resource.TestCase{
+			ProtoV5ProviderFactories: testAccProtoV5MuxServerFactory,
+			Steps: []resource.TestStep{
+				{
+					PreConfig: func() {
+						t.Setenv("HONEYCOMB_API_KEY", "")
+					},
+					Config: `
+# TODO: replace with v2-client data source
+resource "honeycombio_api_key" "test" {
+  name = "test"
+  type = "ingest"
+
+  environment_id = "1"
+}`,
+					PlanOnly:           true,
+					ExpectNonEmptyPlan: true,
+				},
+			},
+		})
+	})
+
+	t.Run("fails when only half of v2 configuration is set", func(t *testing.T) {
+		resource.Test(t, resource.TestCase{
+			ProtoV5ProviderFactories: testAccProtoV5MuxServerFactory,
+			Steps: []resource.TestStep{
+				{
+					PreConfig: func() {
+						t.Setenv("HONEYCOMB_API_KEY", "")
+						t.Setenv("HONEYCOMB_KEY_SECRET", "")
+					},
+					Config: `
+# TODO: replace with v2-client data source
+resource "honeycombio_api_key" "test" {
+  name = "test"
+  type = "ingest"
+
+  environment_id = "1"
+}`,
+					PlanOnly:    true,
+					ExpectError: regexp.MustCompile(`provider requires both a Honeycomb API Key ID and Secret`),
+				},
+			},
+		})
+	})
+
+	t.Run("fails when no clients configured", func(t *testing.T) {
+		resource.Test(t, resource.TestCase{
+			ProtoV5ProviderFactories: testAccProtoV5MuxServerFactory,
+			Steps: []resource.TestStep{
+				{
+					PreConfig: func() {
+						t.Setenv("HONEYCOMB_API_KEY", "")
+						t.Setenv("HONEYCOMB_KEY_ID", "")
+						t.Setenv("HONEYCOMB_KEY_SECRET", "")
+					},
+					Config:      `data "honeycombio_datasets" "all" {}`,
+					PlanOnly:    true,
+					ExpectError: regexp.MustCompile(`requires at least one of a Honeycomb API Key`),
+				},
+			},
+		})
+	})
+
+	t.Run("fails v1-only config using v2 resource", func(t *testing.T) {
+		resource.Test(t, resource.TestCase{
+			ProtoV5ProviderFactories: testAccProtoV5MuxServerFactory,
+			Steps: []resource.TestStep{
+				{
+					PreConfig: func() {
+						t.Setenv("HONEYCOMB_KEY_ID", "")
+						t.Setenv("HONEYCOMB_KEY_SECRET", "")
+					},
+					Config: `
+# TODO: replace with v2-client data source
+resource "honeycombio_api_key" "test" {
+  name = "test"
+  type = "ingest"
+
+  environment_id = "1"
+}`,
+					ExpectError: regexp.MustCompile(`No v2 API client configured`),
+				},
+			},
+		})
+	})
+
+	t.Run("fails v2-only config using v1 resource", func(t *testing.T) {
+		resource.Test(t, resource.TestCase{
+			ProtoV5ProviderFactories: testAccProtoV5MuxServerFactory,
+			Steps: []resource.TestStep{
+				{
+					PreConfig: func() {
+						t.Setenv("HONEYCOMB_API_KEY", "")
+					},
+					// plugin SDK-based
+					Config:      `data "honeycombio_datasets" "all" {}`,
+					ExpectError: regexp.MustCompile(`No v1 API client configured`),
+				},
+				// framework-based
+				{
+					PreConfig: func() {
+						t.Setenv("HONEYCOMB_API_KEY", "")
+					},
+					Config:      `data "honeycombio_auth_metadata" "current" {}`,
+					ExpectError: regexp.MustCompile(`No v1 API client configured`),
+				},
 			},
-		},
+		})
 	})
 }