Repository: honeycombio/terraform-provider-honeycombio
Pull request #88 (2021-12-09 22:09:57)

Integrate go-honeycombio
This imports the current [go-honeycombio](https://github.com/kvrhdn/go-honeycombio) library (with history) directly into this project with the aim to remove the external dependency until such a time that we are ready to maintain a separate Golang API client.

Commits done today (Dec 9) are the changes required to wire the client directly into the repository: I attempted to do it with as few code changes as possible.

CI is still broken from the transfer, but this builds and non-integration tests look good.


Patch:
diff --git a/client/LICENSE b/client/LICENSE
new file mode 100644
index 0000000..aee3a22
--- /dev/null
+++ b/client/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2020 Koenraad Verheyden
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
diff --git a/client/board.go b/client/board.go
new file mode 100644
index 0000000..ae83f3f
--- /dev/null
+++ b/client/board.go
@@ -0,0 +1,124 @@
+package client
+
+import (
+	"context"
+	"fmt"
+)
+
+// Boards describes all the board-related methods that the Honeycomb API
+// supports.
+//
+// API docs: https://docs.honeycomb.io/api/boards-api/
+type Boards interface {
+	// List all boards.
+	List(ctx context.Context) ([]Board, error)
+
+	// Get a board by its ID. Returns ErrNotFound if there is no board with the
+	// given ID.
+	Get(ctx context.Context, id string) (*Board, error)
+
+	// Create a new board. When creating a new board ID may not be set.
+	Create(ctx context.Context, b *Board) (*Board, error)
+
+	// Update an existing board.
+	Update(ctx context.Context, b *Board) (*Board, error)
+
+	// Delete a board.
+	Delete(ctx context.Context, id string) error
+}
+
+// boards implements Boards.
+type boards struct {
+	client *Client
+}
+
+// Compile-time proof of interface implementation by type boards.
+var _ Boards = (*boards)(nil)
+
+// Board represents a Honeycomb board.
+//
+// API docs: https://docs.honeycomb.io/api/boards-api/#fields-on-a-board
+type Board struct {
+	ID string `json:"id,omitempty"`
+
+	// Name of the board, this is displayed in the Honeycomb UI. This field is
+	// required.
+	Name string `json:"name"`
+	// Description of the board.
+	Description string `json:"description,omitempty"`
+	// How the board should be displayed in the UI, defaults to "list".
+	Style BoardStyle `json:"style,omitempty"`
+	// A list of queries displayed on the board, in order of appearance.
+	Queries []BoardQuery `json:"queries"`
+}
+
+// BoardStyle determines how a Board should be displayed within the Honeycomb UI.
+type BoardStyle string
+
+// Declaration of board styles.
+const (
+	BoardStyleList   BoardStyle = "list"
+	BoardStyleVisual BoardStyle = "visual"
+)
+
+// BoardStyles returns an exhaustive list of board styles.
+func BoardStyles() []BoardStyle {
+	return []BoardStyle{BoardStyleList, BoardStyleVisual}
+}
+
+// BoardQuery represents a query that is part of a board.
+type BoardQuery struct {
+	Caption string `json:"caption,omitempty"`
+	// Defaults to graph.
+	QueryStyle BoardQueryStyle `json:"query_style,omitempty"`
+	// This field is required.
+	Dataset string `json:"dataset"`
+	// Either QueryID or Query is required.
+	QueryID string     `json:"query_id,omitempty"`
+	Query   *QuerySpec `json:"query,omitempty"`
+	// Optional
+	QueryAnnotationID string `json:"query_annotation_id,omitempty"`
+}
+
+// BoardQueryStyle determines how a query should be displayed on the board.
+type BoardQueryStyle string
+
+// Declaration of board query styles.
+const (
+	BoardQueryStyleGraph BoardQueryStyle = "graph"
+	BoardQueryStyleTable BoardQueryStyle = "table"
+	BoardQueryStyleCombo BoardQueryStyle = "combo"
+)
+
+// BoardQueryStyles returns an exhaustive list of board query styles.
+func BoardQueryStyles() []BoardQueryStyle {
+	return []BoardQueryStyle{BoardQueryStyleGraph, BoardQueryStyleTable, BoardQueryStyleCombo}
+}
+
+func (s *boards) List(ctx context.Context) ([]Board, error) {
+	var b []Board
+	err := s.client.performRequest(ctx, "GET", "/1/boards", nil, &b)
+	return b, err
+}
+
+func (s *boards) Get(ctx context.Context, ID string) (*Board, error) {
+	var b Board
+	err := s.client.performRequest(ctx, "GET", fmt.Sprintf("/1/boards/%s", ID), nil, &b)
+	return &b, err
+}
+
+func (s *boards) Create(ctx context.Context, data *Board) (*Board, error) {
+	var b Board
+	err := s.client.performRequest(ctx, "POST", "/1/boards", data, &b)
+	return &b, err
+}
+
+func (s *boards) Update(ctx context.Context, data *Board) (*Board, error) {
+	var b Board
+	err := s.client.performRequest(ctx, "PUT", fmt.Sprintf("/1/boards/%s", data.ID), data, &b)
+	return &b, err
+}
+
+func (s *boards) Delete(ctx context.Context, id string) error {
+	return s.client.performRequest(ctx, "DELETE", fmt.Sprintf("/1/boards/%s", id), nil, nil)
+}
diff --git a/client/client.go b/client/client.go
new file mode 100644
index 0000000..e4e285f
--- /dev/null
+++ b/client/client.go
@@ -0,0 +1,197 @@
+// Package client provides a client to interact with the Honeycomb API.
+//
+// Documentation of the API can be found here: https://docs.honeycomb.io/api/
+package client
+
+import (
+	"bytes"
+	"context"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"net/http"
+	"net/url"
+	"strings"
+
+	"github.com/honeycombio/terraform-provider-honeycombio/client/internal/httputil"
+)
+
+// Config holds all configuration options for the client.
+type Config struct {
+	// Required - the API key to use when sending request to Honeycomb.
+	APIKey string
+	// URL of the Honeycomb API, defaults to "https://api.honeycomb.io".
+	APIUrl string
+	// User agent to send with all requests, defaults to "go-honeycombio".
+	UserAgent string
+	// With debug enabled the client will log all requests and responses.
+	Debug bool
+}
+
+func defaultConfig() *Config {
+	return &Config{
+		APIKey:    "",
+		APIUrl:    "https://api.honeycomb.io",
+		UserAgent: "go-honeycombio",
+		Debug:     false,
+	}
+}
+
+// Merge the given config by copying all non-blank values.
+func (c *Config) merge(other *Config) {
+	if other.APIKey != "" {
+		c.APIKey = other.APIKey
+	}
+	if other.APIUrl != "" {
+		c.APIUrl = other.APIUrl
+	}
+	if other.UserAgent != "" {
+		c.UserAgent = other.UserAgent
+	}
+	if c.Debug || other.Debug {
+		c.Debug = true
+	}
+}
+
+// Client to interact with Honeycomb.
+type Client struct {
+	apiKey     string
+	apiURL     *url.URL
+	userAgent  string
+	httpClient *http.Client
+
+	Boards           Boards
+	Columns          Columns
+	Datasets         Datasets
+	DerivedColumns   DerivedColumns
+	Markers          Markers
+	Queries          Queries
+	QueryAnnotations QueryAnnotations
+	Triggers         Triggers
+}
+
+// NewClient creates a new Honeycomb API client.
+func NewClient(config *Config) (*Client, error) {
+	cfg := defaultConfig()
+	cfg.merge(config)
+
+	if cfg.APIKey == "" {
+		return nil, errors.New("APIKey must be configured")
+	}
+	apiURL, err := url.Parse(cfg.APIUrl)
+	if err != nil {
+		return nil, fmt.Errorf("could not parse APIUrl: %w", err)
+	}
+
+	httpClient := &http.Client{}
+	if cfg.Debug {
+		httpClient = httputil.WrapWithLogging(httpClient)
+	}
+
+	client := &Client{
+		apiKey:     cfg.APIKey,
+		apiURL:     apiURL,
+		userAgent:  cfg.UserAgent,
+		httpClient: httpClient,
+	}
+	client.Boards = &boards{client: client}
+	client.Columns = &columns{client: client}
+	client.Datasets = &datasets{client: client}
+	client.DerivedColumns = &derivedColumns{client: client}
+	client.Markers = &markers{client: client}
+	client.Queries = &queries{client: client}
+	client.QueryAnnotations = &queryAnnotations{client: client}
+	client.Triggers = &triggers{client: client}
+
+	return client, nil
+}
+
+// ErrNotFound is returned when the requested item could not be found.
+var ErrNotFound = errors.New("404 Not Found")
+
+// performRequest against the Honeycomb API with the necessary headers and, if
+// requestBody is not nil, a JSON body. The response is parsed in responseBody,
+// if responseBody is not nil.
+// Returns an error if the request failed, if the response contained a non-2xx
+// status code or if parsing the response in responseBody failed. ErrNotFound
+// is returned on a 404 response.
+func (c *Client) performRequest(ctx context.Context, method, path string, requestBody, responseBody interface{}) error {
+	var body io.Reader
+
+	if requestBody != nil {
+		buf := new(bytes.Buffer)
+		err := json.NewEncoder(buf).Encode(requestBody)
+		if err != nil {
+			return err
+		}
+		body = buf
+	}
+
+	requestURL, err := c.apiURL.Parse(path)
+	if err != nil {
+		return err
+	}
+
+	req, err := http.NewRequestWithContext(ctx, method, requestURL.String(), body)
+	if err != nil {
+		return err
+	}
+
+	req.Header.Add("X-Honeycomb-Team", c.apiKey)
+	req.Header.Add("Content-Type", "application/json")
+	req.Header.Add("User-Agent", c.userAgent)
+
+	resp, err := c.httpClient.Do(req)
+	if err != nil {
+		return err
+	}
+	defer resp.Body.Close()
+
+	if !httputil.Is2xx(resp.StatusCode) {
+		if resp.StatusCode == 404 {
+			return ErrNotFound
+		}
+		return errorFromResponse(resp)
+	}
+
+	if responseBody != nil {
+		err = json.NewDecoder(resp.Body).Decode(responseBody)
+	}
+	return err
+}
+
+func errorFromResponse(resp *http.Response) error {
+	errorMsg := attemptToExtractHoneycombioError(resp.Body)
+	if errorMsg == "" {
+		return fmt.Errorf("%s", resp.Status)
+	}
+	return fmt.Errorf("%s: %s", resp.Status, errorMsg)
+}
+
+type honeycombioError struct {
+	ErrorMessage string `json:"error"`
+}
+
+func attemptToExtractHoneycombioError(bodyReader io.Reader) string {
+	body, err := ioutil.ReadAll(bodyReader)
+	if err != nil {
+		return ""
+	}
+
+	var honeycombioErr honeycombioError
+
+	err = json.Unmarshal(body, &honeycombioErr)
+	if err != nil || honeycombioErr.ErrorMessage == "" {
+		return string(body)
+	}
+
+	return honeycombioErr.ErrorMessage
+}
+
+// urlEncodeDataset sanitizes the dataset name for when it is used as part of
+// the URL.
+func urlEncodeDataset(dataset string) string {
+	return strings.Replace(dataset, "/", "-", -1)
+}
diff --git a/client/column.go b/client/column.go
new file mode 100644
index 0000000..1a5e78f
--- /dev/null
+++ b/client/column.go
@@ -0,0 +1,109 @@
+package client
+
+import (
+	"context"
+	"fmt"
+)
+
+// Columns describe all the columns-related methods that the Honeycomb API
+// supports.
+//
+// API docs: https://docs.honeycomb.io/api/columns/
+type Columns interface {
+	// List all columns in this dataset.
+	List(ctx context.Context, dataset string) ([]Column, error)
+
+	// Get a column by its ID. Returns ErrNotFound if there is no column with
+	// the given ID in this dataset.
+	Get(ctx context.Context, dataset string, id string) (*Column, error)
+
+	// GetByKeyName searches a column by its key name. Returns ErrNotFound if
+	// there is no column with the given key name in this dataset.
+	GetByKeyName(ctx context.Context, dataset string, keyName string) (*Column, error)
+
+	// Create a new column in this dataset. When creating a new column ID may
+	// not be set. The KeyName must be unique for this dataset.
+	Create(ctx context.Context, dataset string, c *Column) (*Column, error)
+
+	// Update an existing column.
+	Update(ctx context.Context, dataset string, c *Column) (*Column, error)
+
+	// Delete a column.
+	Delete(ctx context.Context, dataset string, id string) error
+}
+
+// columns implements Columns.
+type columns struct {
+	client *Client
+}
+
+// Compile-time proof of interface implementation by type columns.
+var _ Columns = (*columns)(nil)
+
+// Column represents a Honeycomb column in a dataset.
+//
+// API docs: https://docs.honeycomb.io/api/columns/#fields-on-a-column
+type Column struct {
+	ID string `json:"id,omitempty"`
+
+	// Name of the column, this field is required.
+	KeyName string `json:"key_name"`
+	// Deprecated, optional.
+	Alias string `json:"alias,omitempty"`
+	// Optional, defaults to false.
+	Hidden *bool `json:"hidden,omitempty"`
+	// Optional.
+	Description string `json:"description,omitempty"`
+	// Optional, defaults to string.
+	Type *ColumnType `json:"type,omitempty"`
+}
+
+// ColumnType determines the type of column.
+type ColumnType string
+
+// Declaration of column types.
+const (
+	ColumnTypeString  ColumnType = "string"
+	ColumnTypeFloat   ColumnType = "float"
+	ColumnTypeInteger ColumnType = "integer"
+	ColumnTypeBoolean ColumnType = "boolean"
+)
+
+// ColumnTypes returns an exhaustive list of column types.
+func ColumnTypes() []ColumnType {
+	return []ColumnType{ColumnTypeString, ColumnTypeFloat, ColumnTypeInteger, ColumnTypeBoolean}
+}
+
+func (s *columns) List(ctx context.Context, dataset string) ([]Column, error) {
+	var c []Column
+	err := s.client.performRequest(ctx, "GET", "/1/columns/"+urlEncodeDataset(dataset), nil, &c)
+	return c, err
+}
+
+func (s *columns) Get(ctx context.Context, dataset string, id string) (*Column, error) {
+	var c Column
+	err := s.client.performRequest(ctx, "GET", fmt.Sprintf("/1/columns/%s/%s", urlEncodeDataset(dataset), id), nil, &c)
+	return &c, err
+}
+
+func (s *columns) GetByKeyName(ctx context.Context, dataset string, keyName string) (*Column, error) {
+	var c Column
+	err := s.client.performRequest(ctx, "GET", fmt.Sprintf("/1/columns/%s?key_name=%s", urlEncodeDataset(dataset), keyName), nil, &c)
+	return &c, err
+}
+
+func (s *columns) Create(ctx context.Context, dataset string, data *Column) (*Column, error) {
+	var c Column
+	err := s.client.performRequest(ctx, "POST", "/1/columns/"+urlEncodeDataset(dataset), data, &c)
+	return &c, err
+}
+
+func (s *columns) Update(ctx context.Context, dataset string, data *Column) (*Column, error) {
+	var c Column
+	err := s.client.performRequest(ctx, "PUT", fmt.Sprintf("/1/columns/%s/%s", urlEncodeDataset(dataset), data.ID), data, &c)
+	return &c, err
+}
+
+func (s *columns) Delete(ctx context.Context, dataset string, id string) error {
+	return s.client.performRequest(ctx, "DELETE", fmt.Sprintf("/1/columns/%s/%s", urlEncodeDataset(dataset), id), nil, nil)
+}
diff --git a/client/dataset.go b/client/dataset.go
new file mode 100644
index 0000000..0e14f05
--- /dev/null
+++ b/client/dataset.go
@@ -0,0 +1,56 @@
+package client
+
+import (
+	"context"
+)
+
+// Datasets describes all the dataset-related methods that the Honeycomb API
+// supports.
+//
+// API docs: https://docs.honeycomb.io/api/datasets/
+type Datasets interface {
+	// List all datasets.
+	List(ctx context.Context) ([]Dataset, error)
+
+	// Get a dataset by its slug. Returns ErrNotFound if there is no dataset
+	// with the given slug.
+	Get(ctx context.Context, slug string) (*Dataset, error)
+
+	// Create a new dataset. Only name should be set when creating a dataset,
+	// all other fields are ignored.
+	Create(ctx context.Context, dataset *Dataset) (*Dataset, error)
+}
+
+// datasets implements Datasets.
+type datasets struct {
+	client *Client
+}
+
+// Compile-time proof of interface implementation by type datasets.
+var _ Datasets = (*datasets)(nil)
+
+// Dataset represents a Honeycomb dataset.
+//
+// API docs: https://docs.honeycomb.io/api/dataset
+type Dataset struct {
+	Name string `json:"name"`
+	Slug string `json:"slug,omitempty"`
+}
+
+func (s datasets) List(ctx context.Context) ([]Dataset, error) {
+	var datasets []Dataset
+	err := s.client.performRequest(ctx, "GET", "/1/datasets", nil, &datasets)
+	return datasets, err
+}
+
+func (s datasets) Get(ctx context.Context, slug string) (*Dataset, error) {
+	var dataset Dataset
+	err := s.client.performRequest(ctx, "GET", "/1/datasets/"+slug, nil, &dataset)
+	return &dataset, err
+}
+
+func (s datasets) Create(ctx context.Context, data *Dataset) (*Dataset, error) {
+	var dataset Dataset
+	err := s.client.performRequest(ctx, "POST", "/1/datasets", data, &dataset)
+	return &dataset, err
+}
diff --git a/client/derived_column.go b/client/derived_column.go
new file mode 100644
index 0000000..ad14418
--- /dev/null
+++ b/client/derived_column.go
@@ -0,0 +1,92 @@
+package client
+
+import (
+	"context"
+	"fmt"
+)
+
+// DerivedColumns describe all the derived columns-related methods that the
+// Honeycomb API supports.
+//
+// API docs: https://docs.honeycomb.io/api/derived_columns/
+type DerivedColumns interface {
+	// List all derived columns in this dataset.
+	List(ctx context.Context, dataset string) ([]DerivedColumn, error)
+
+	// Get a derived column by its ID. Returns ErrNotFound if there is no
+	// derived column with the given ID in this dataset.
+	Get(ctx context.Context, dataset string, id string) (*DerivedColumn, error)
+
+	// GetByAlias searches a derived column by its alias. Returns ErrNotFound if
+	// there is no derived column with the given alias in this dataset.
+	GetByAlias(ctx context.Context, dataset string, alias string) (*DerivedColumn, error)
+
+	// Create a new derived column in this dataset. When creating a new derived
+	// column ID may not be set. The Alias must be unique for this dataset.
+	Create(ctx context.Context, dataset string, d *DerivedColumn) (*DerivedColumn, error)
+
+	// Update an existing derived column.
+	Update(ctx context.Context, dataset string, d *DerivedColumn) (*DerivedColumn, error)
+
+	// Delete a derived column.
+	Delete(ctx context.Context, dataset string, id string) error
+}
+
+// derivedColumns implements DerivedColumns.
+type derivedColumns struct {
+	client *Client
+}
+
+// Compile-time proof of interface implementation by type derivedColumns.
+var _ DerivedColumns = (*derivedColumns)(nil)
+
+// Column represents a Honeycomb derived column in a dataset.
+//
+// API docs: https://docs.honeycomb.io/api/derived_columns/#fields-on-a-derivedcolumn
+type DerivedColumn struct {
+	ID string `json:"id,omitempty"`
+	// Alias of the derived column, this field is required and can not be
+	// updated.
+	Alias string `json:"alias"`
+	// Expression of the derived column, this field is required and can not be
+	// updated.
+	// This should be an expression following the Derived Column syntax, as
+	// described on https://docs.honeycomb.io/working-with-your-data/customizing-your-query/derived-columns/#derived-column-syntax
+	Expression string `json:"expression"`
+	// Optional.
+	Description string `json:"description,omitempty"`
+}
+
+func (s *derivedColumns) List(ctx context.Context, dataset string) ([]DerivedColumn, error) {
+	var c []DerivedColumn
+	err := s.client.performRequest(ctx, "GET", "/1/derived_columns/"+urlEncodeDataset(dataset), nil, &c)
+	return c, err
+}
+
+func (s *derivedColumns) Get(ctx context.Context, dataset string, id string) (*DerivedColumn, error) {
+	var c DerivedColumn
+	err := s.client.performRequest(ctx, "GET", fmt.Sprintf("/1/derived_columns/%s/%s", urlEncodeDataset(dataset), id), nil, &c)
+	return &c, err
+}
+
+func (s *derivedColumns) GetByAlias(ctx context.Context, dataset string, alias string) (*DerivedColumn, error) {
+	var c DerivedColumn
+	err := s.client.performRequest(ctx, "GET", fmt.Sprintf("/1/derived_columns/%s?alias=%s", urlEncodeDataset(dataset), alias), nil, &c)
+	return &c, err
+}
+
+func (s *derivedColumns) Create(ctx context.Context, dataset string, data *DerivedColumn) (*DerivedColumn, error) {
+	var d DerivedColumn
+	err := s.client.performRequest(ctx, "POST", "/1/derived_columns/"+urlEncodeDataset(dataset), data, &d)
+	return &d, err
+}
+
+func (s *derivedColumns) Update(ctx context.Context, dataset string, data *DerivedColumn) (*DerivedColumn, error) {
+	var d DerivedColumn
+	err := s.client.performRequest(ctx, "PUT", fmt.Sprintf("/1/derived_columns/%s/%s", urlEncodeDataset(dataset), data.ID), data, &d)
+	return &d, err
+}
+
+func (s *derivedColumns) Delete(ctx context.Context, dataset string, id string) error {
+	return s.client.performRequest(ctx, "DELETE", fmt.Sprintf("/1/derived_columns/%s/%s", urlEncodeDataset(dataset), id), nil, nil)
+}
diff --git a/client/internal/httputil/httputil.go b/client/internal/httputil/httputil.go
new file mode 100644
index 0000000..c64f894
--- /dev/null
+++ b/client/internal/httputil/httputil.go
@@ -0,0 +1,44 @@
+package httputil
+
+import (
+	"log"
+	"net/http"
+	"net/http/httputil"
+)
+
+// Is2xx checks whether the HTTP status code is successful.
+func Is2xx(status int) bool {
+	return status >= 200 && status < 300
+}
+
+// roundTripFunc implements http.RoundTripper and can be used as utility
+// function to simplify creating a new http.RoundTripper.
+type roundTripFunc func(*http.Request) (*http.Response, error)
+
+func (rf roundTripFunc) RoundTrip(req *http.Request) (*http.Response, error) {
+	return rf(req)
+}
+
+// Compile-time proof of interface implementation by type roundTripFunc.
+var _ http.RoundTripper = (*roundTripFunc)(nil)
+
+// WrapWithLogging wraps the HTTP client to log the entire request and
+// response.
+func WrapWithLogging(c *http.Client) *http.Client {
+	return &http.Client{
+		Transport: roundTripFunc(func(req *http.Request) (*http.Response, error) {
+			b, _ := httputil.DumpRequestOut(req, true)
+			log.Printf("Sending request\n\n%s\n", b)
+
+			res, err := c.Do(req)
+			if err != nil {
+				return res, err
+			}
+
+			b, _ = httputil.DumpResponse(res, true)
+			log.Printf("Received response\n\n%s\n", b)
+
+			return res, nil
+		}),
+	}
+}
diff --git a/client/marker.go b/client/marker.go
new file mode 100644
index 0000000..4d7c70e
--- /dev/null
+++ b/client/marker.go
@@ -0,0 +1,108 @@
+package client
+
+import (
+	"context"
+	"fmt"
+	"time"
+)
+
+// Markers describes all the marker-related methods that the Honeycomb API
+// supports.
+//
+// API docs: https://docs.honeycomb.io/api/markers/
+type Markers interface {
+	// List all markers present in this dataset.
+	List(ctx context.Context, dataset string) ([]Marker, error)
+
+	// Get a marker by its ID. Returns ErrNotFound if there is no marker with
+	// the given ID in this dataset.
+	//
+	// This method calls List internally since there is no API available to
+	// directly get a single marker.
+	Get(ctx context.Context, dataset string, id string) (*Marker, error)
+
+	// Create a new marker in this dataset. When creating a marker ID may not
+	// be set.
+	Create(ctx context.Context, dataset string, m *Marker) (*Marker, error)
+
+	// Update an existing marker.
+	Update(ctx context.Context, dataset string, m *Marker) (*Marker, error)
+
+	// Delete a marker from the dataset.
+	Delete(ctx context.Context, dataset string, id string) error
+}
+
+// markers implements Markers.
+type markers struct {
+	client *Client
+}
+
+// Compile-time proof of interface implementation by type markers.
+var _ Markers = (*markers)(nil)
+
+// Marker represents a Honeycomb marker.
+//
+// API docs: https://docs.honeycomb.io/api/markers/#fields-on-a-marker
+type Marker struct {
+	ID string `json:"id,omitempty"`
+
+	// The time the marker should be placed at, in Unix Time (= seconds since
+	// epoch). If not set this will be set to when the request was received by
+	// the API.
+	StartTime int64 `json:"start_time,omitempty"`
+	// The end time of the marker, in Unix Time (= seconds since epoch). This
+	// can be used to indicate a time range. This field is optional.
+	EndTime int64 `json:"end_time,omitempty"`
+	// Message appears above the marker and can be used to desribe the marker.
+	// This field is optional.
+	Message string `json:"message,omitempty"`
+	// Type is an optional marker identifier, eg 'deploy' or 'chef-run'. This
+	// field is optional.
+	Type string `json:"type,omitempty"`
+	// URL is an optional url associated with the marker. This field is optional.
+	URL string `json:"url,omitempty"`
+
+	// Time the marker was created. This field is set by the API.
+	CreatedAt *time.Time `json:"created_at,omitempty"`
+	// Time the marker was last modified. This field is set by the API.
+	UpdatedAt *time.Time `json:"updated_at,omitempty"`
+	// Color of the marker. Colors are configured per dataset and can be set
+	// per type of marker. This field is set by the API.
+	Color string `json:"color,omitempty"`
+}
+
+func (s *markers) List(ctx context.Context, dataset string) ([]Marker, error) {
+	var m []Marker
+	err := s.client.performRequest(ctx, "GET", fmt.Sprintf("/1/markers/%s", urlEncodeDataset(dataset)), nil, &m)
+	return m, err
+}
+
+func (s *markers) Get(ctx context.Context, dataset string, id string) (*Marker, error) {
+	markers, err := s.List(ctx, dataset)
+	if err != nil {
+		return nil, err
+	}
+
+	for _, m := range markers {
+		if m.ID == id {
+			return &m, nil
+		}
+	}
+	return nil, ErrNotFound
+}
+
+func (s *markers) Create(ctx context.Context, dataset string, data *Marker) (*Marker, error) {
+	var m Marker
+	err := s.client.performRequest(ctx, "POST", fmt.Sprintf("/1/markers/%s", urlEncodeDataset(dataset)), data, &m)
+	return &m, err
+}
+
+func (s *markers) Update(ctx context.Context, dataset string, data *Marker) (*Marker, error) {
+	var m Marker
+	err := s.client.performRequest(ctx, "PUT", fmt.Sprintf("/1/markers/%s/%s", urlEncodeDataset(dataset), data.ID), data, &m)
+	return &m, err
+}
+
+func (s *markers) Delete(ctx context.Context, dataset string, id string) error {
+	return s.client.performRequest(ctx, "DELETE", fmt.Sprintf("/1/markers/%s/%s", urlEncodeDataset(dataset), id), nil, nil)
+}
diff --git a/client/query.go b/client/query.go
new file mode 100644
index 0000000..513feb5
--- /dev/null
+++ b/client/query.go
@@ -0,0 +1,39 @@
+package client
+
+import (
+	"context"
+	"fmt"
+)
+
+// Queries describe all the query-related methods that the Honeycomb API
+// supports.
+//
+// API docs: https://docs.honeycomb.io/api/queries/
+type Queries interface {
+	// Get a query by its ID.
+	Get(ctx context.Context, dataset string, id string) (*QuerySpec, error)
+
+	// Create a new query in this dataset. When creating a new query ID may
+	// not be set.
+	Create(ctx context.Context, dataset string, c *QuerySpec) (*QuerySpec, error)
+}
+
+// queries implements Queries.
+type queries struct {
+	client *Client
+}
+
+// Compile-time proof of interface implementation by type queries.
+var _ Queries = (*queries)(nil)
+
+func (s *queries) Get(ctx context.Context, dataset string, id string) (*QuerySpec, error) {
+	var q QuerySpec
+	err := s.client.performRequest(ctx, "GET", fmt.Sprintf("/1/queries/%s/%s", urlEncodeDataset(dataset), id), nil, &q)
+	return &q, err
+}
+
+func (s *queries) Create(ctx context.Context, dataset string, data *QuerySpec) (*QuerySpec, error) {
+	var q QuerySpec
+	err := s.client.performRequest(ctx, "POST", "/1/queries/"+urlEncodeDataset(dataset), data, &q)
+	return &q, err
+}
diff --git a/client/query_annotation.go b/client/query_annotation.go
new file mode 100644
index 0000000..b2a7d38
--- /dev/null
+++ b/client/query_annotation.go
@@ -0,0 +1,80 @@
+package client
+
+import (
+	"context"
+	"fmt"
+	"time"
+)
+
+// QueryAnnotations describes all the query annotation-related methods that the
+// Honeycomb API supports.
+//
+// API docs: https://docs.honeycomb.io/api/query-annotations/
+type QueryAnnotations interface {
+	// List all query annotations.
+	List(ctx context.Context, dataset string) ([]QueryAnnotation, error)
+
+	// Get a query annotation by its ID. Returns ErrNotFound if there is no
+	// query annotation with the given ID.
+	Get(ctx context.Context, dataset string, id string) (*QueryAnnotation, error)
+
+	// Create a new query annotation. When creating a new query annotation ID
+	// may not be set.
+	Create(ctx context.Context, dataset string, b *QueryAnnotation) (*QueryAnnotation, error)
+
+	// Update an existing query annotation.
+	Update(ctx context.Context, dataset string, b *QueryAnnotation) (*QueryAnnotation, error)
+
+	// Delete a query annotation.
+	Delete(ctx context.Context, dataset string, id string) error
+}
+
+// queryAnnotations implements QueryAnnotations.
+type queryAnnotations struct {
+	client *Client
+}
+
+// Compile-time proof of interface implementation by type queryAnnotations.
+var _ QueryAnnotations = (*queryAnnotations)(nil)
+
+// QueryAnnotation represents a Honeycomb query annotation.
+//
+// API docs: https://docs.honeycomb.io/api/query-annotations/#fields-on-a-query-annotation
+type QueryAnnotation struct {
+	ID string `json:"id,omitempty"`
+
+	Name        string `json:"name"`
+	Description string `json:"description"`
+	QueryID     string `json:"query_id"`
+
+	CreatedAt *time.Time `json:"created-at,omitempty"`
+	UpdatedAt *time.Time `json:"updated-at,omitempty"`
+}
+
+func (s *queryAnnotations) List(ctx context.Context, dataset string) ([]QueryAnnotation, error) {
+	var q []QueryAnnotation
+	err := s.client.performRequest(ctx, "GET", fmt.Sprintf("/1/query_annotations/%s", urlEncodeDataset(dataset)), nil, &q)
+	return q, err
+}
+
+func (s *queryAnnotations) Get(ctx context.Context, dataset string, ID string) (*QueryAnnotation, error) {
+	var q QueryAnnotation
+	err := s.client.performRequest(ctx, "GET", fmt.Sprintf("/1/query_annotations/%s/%s", urlEncodeDataset(dataset), ID), nil, &q)
+	return &q, err
+}
+
+func (s *queryAnnotations) Create(ctx context.Context, dataset string, data *QueryAnnotation) (*QueryAnnotation, error) {
+	var q QueryAnnotation
+	err := s.client.performRequest(ctx, "POST", fmt.Sprintf("/1/query_annotations/%s", urlEncodeDataset(dataset)), data, &q)
+	return &q, err
+}
+
+func (s *queryAnnotations) Update(ctx context.Context, dataset string, data *QueryAnnotation) (*QueryAnnotation, error) {
+	var q QueryAnnotation
+	err := s.client.performRequest(ctx, "PUT", fmt.Sprintf("/1/query_annotations/%s/%s", urlEncodeDataset(dataset), data.ID), data, &q)
+	return &q, err
+}
+
+func (s *queryAnnotations) Delete(ctx context.Context, dataset string, id string) error {
+	return s.client.performRequest(ctx, "DELETE", fmt.Sprintf("/1/query_annotations/%s/%s", urlEncodeDataset(dataset), id), nil, nil)
+}
diff --git a/client/query_spec.go b/client/query_spec.go
new file mode 100644
index 0000000..019c81f
--- /dev/null
+++ b/client/query_spec.go
@@ -0,0 +1,188 @@
+package client
+
+// QuerySpec represents a Honeycomb query.
+//
+// API docs: https://docs.honeycomb.io/api/query-specification/
+type QuerySpec struct {
+	// ID of a query is only set when QuerySpec is returned from the Queries
+	// API. This value should not be set when creating or updating queries.
+	ID *string `json:"id,omitempty"`
+
+	// The calculations to return as a time series and summary table. If no
+	// calculations are provided, COUNT is applied.
+	Calculations []CalculationSpec `json:"calculations,omitempty"`
+	// The filters with which to restrict the considered events.
+	Filters []FilterSpec `json:"filters,omitempty"`
+	// If multiple filters are specified, filter_combination determines how
+	// they are applied. Defaults to AND.
+	//
+	// From experience it seems the API will never answer with AND, instead
+	// always omitting the filter combination field entirely.
+	FilterCombination FilterCombination `json:"filter_combination,omitempty"`
+	// A list of strings describing the columns by which to break events down
+	// into groups.
+	Breakdowns []string `json:"breakdowns,omitempty"`
+	// A list of objects describing the terms on which to order the query
+	// results. Each term must appear in either the breakdowns field or the
+	// calculations field.
+	Orders []OrderSpec `json:"orders,omitempty"`
+	// The maximum number of query results, must be between 1 and 1000.
+	Limit *int `json:"limit,omitempty"`
+	// The time range of query in seconds. Defaults to two hours. If combined
+	// with start time or end time, this time range is added after start time
+	// or before end time. Cannot be combined with both start time and end time.
+	//
+	// For more details, check https://docs.honeycomb.io/api/query-specification/#a-caveat-on-time
+	TimeRange *int `json:"time_range,omitempty"`
+	// The absolute start time of the query, in Unix Time (= seconds since epoch).
+	StartTime *int64 `json:"start_time,omitempty"`
+	// The absolute end time of the query, in Unix Time (= seconds since epoch).
+	EndTime *int64 `json:"end_time,omitempty"`
+	// The time resolution of the query’s graph, in seconds. Valid values are
+	// the query’s time range /10 at maximum, and /1000 at minimum.
+	Granularity *int `json:"granularity,omitempty"`
+}
+
+// CalculationSpec represents a calculation within a query.
+type CalculationSpec struct {
+	Op CalculationOp `json:"op"`
+	// Column to perform the operation on. Not needed with COUNT.
+	Column *string `json:"column,omitempty"`
+}
+
+// CalculationOp represents the operator of a calculation.
+type CalculationOp string
+
+// Declaration of calculation operators.
+const (
+	CalculationOpCount         CalculationOp = "COUNT"
+	CalculationOpSum           CalculationOp = "SUM"
+	CalculationOpAvg           CalculationOp = "AVG"
+	CalculationOpCountDistinct CalculationOp = "COUNT_DISTINCT"
+	CalculationOpMax           CalculationOp = "MAX"
+	CalculationOpMin           CalculationOp = "MIN"
+	CalculationOpP001          CalculationOp = "P001"
+	CalculationOpP01           CalculationOp = "P01"
+	CalculationOpP05           CalculationOp = "P05"
+	CalculationOpP10           CalculationOp = "P10"
+	CalculationOpP25           CalculationOp = "P25"
+	CalculationOpP50           CalculationOp = "P50"
+	CalculationOpP75           CalculationOp = "P75"
+	CalculationOpP90           CalculationOp = "P90"
+	CalculationOpP95           CalculationOp = "P95"
+	CalculationOpP99           CalculationOp = "P99"
+	CalculationOpP999          CalculationOp = "P999"
+	CalculationOpHeatmap       CalculationOp = "HEATMAP"
+)
+
+// CalculationOps returns an exhaustive list of calculation operators.
+func CalculationOps() []CalculationOp {
+	return []CalculationOp{
+		CalculationOpCount,
+		CalculationOpSum,
+		CalculationOpAvg,
+		CalculationOpCountDistinct,
+		CalculationOpMax,
+		CalculationOpMin,
+		CalculationOpP001,
+		CalculationOpP01,
+		CalculationOpP05,
+		CalculationOpP10,
+		CalculationOpP25,
+		CalculationOpP50,
+		CalculationOpP75,
+		CalculationOpP90,
+		CalculationOpP95,
+		CalculationOpP99,
+		CalculationOpP999,
+		CalculationOpHeatmap,
+	}
+}
+
+// FilterSpec represents a filter within a query.
+type FilterSpec struct {
+	Column string   `json:"column"`
+	Op     FilterOp `json:"op"`
+	// Value to use with the filter operation. The type of the filter value
+	// depends on the operator:
+	//  - 'exists' and 'does-not-exist': value should be nil
+	//  - 'in' and 'not-in': value should be a []string
+	//  - all other ops: value should be a string
+	Value interface{} `json:"value,omitempty"`
+}
+
+// FilterOp represents the operator of a filter.
+type FilterOp string
+
+// Declaration of filter operators.
+const (
+	FilterOpEquals             FilterOp = "="
+	FilterOpNotEquals          FilterOp = "!="
+	FilterOpGreaterThan        FilterOp = ">"
+	FilterOpGreaterThanOrEqual FilterOp = ">="
+	FilterOpSmallerThan        FilterOp = "<"
+	FilterOpSmallerThanOrEqual FilterOp = "<="
+	FilterOpStartsWith         FilterOp = "starts-with"
+	FilterOpDoesNotStartWith   FilterOp = "does-not-start-with"
+	FilterOpExists             FilterOp = "exists"
+	FilterOpDoesNotExist       FilterOp = "does-not-exist"
+	FilterOpContains           FilterOp = "contains"
+	FilterOpDoesNotContain     FilterOp = "does-not-contain"
+	FilterOpIn                 FilterOp = "in"
+	FilterOpNotIn              FilterOp = "not-in"
+)
+
+// FilterOps returns an exhaustive list of available filter operators.
+func FilterOps() []FilterOp {
+	return []FilterOp{
+		FilterOpEquals,
+		FilterOpNotEquals,
+		FilterOpGreaterThan,
+		FilterOpGreaterThanOrEqual,
+		FilterOpSmallerThan,
+		FilterOpSmallerThanOrEqual,
+		FilterOpStartsWith,
+		FilterOpDoesNotStartWith,
+		FilterOpExists,
+		FilterOpDoesNotExist,
+		FilterOpContains,
+		FilterOpDoesNotContain,
+		FilterOpIn,
+		FilterOpNotIn,
+	}
+}
+
+// FilterCombination describes how the filters of a query should be combined.
+type FilterCombination string
+
+// Declaration of filter combinations.
+const (
+	FilterCombinationOr  FilterCombination = "OR"
+	FilterCombinationAnd FilterCombination = "AND"
+)
+
+// FilterCombinations returns an exhaustive list of filter combinations.
+func FilterCombinations() []FilterCombination {
+	return []FilterCombination{FilterCombinationOr, FilterCombinationAnd}
+}
+
+// OrderSpec describes how to order the results of a query.
+type OrderSpec struct {
+	Op     *CalculationOp `json:"op,omitempty"`
+	Column *string        `json:"column,omitempty"`
+	Order  *SortOrder     `json:"order,omitempty"`
+}
+
+// SortOrder describes in which order the results should be sorted.
+type SortOrder string
+
+// Declaration of sort orders.
+const (
+	SortOrderAsc  SortOrder = "ascending"
+	SortOrderDesc SortOrder = "descending"
+)
+
+// SortOrders returns an exhaustive list of all sort orders.
+func SortOrders() []SortOrder {
+	return []SortOrder{SortOrderAsc, SortOrderDesc}
+}
diff --git a/client/trigger.go b/client/trigger.go
new file mode 100644
index 0000000..0b874be
--- /dev/null
+++ b/client/trigger.go
@@ -0,0 +1,203 @@
+package client
+
+import (
+	"context"
+	"errors"
+	"fmt"
+)
+
+// Triggers describes all the trigger-related methods that the Honeycomb API
+// supports.
+//
+// API docs: https://docs.honeycomb.io/api/triggers/
+type Triggers interface {
+	// List all triggers present in this dataset.
+	List(ctx context.Context, dataset string) ([]Trigger, error)
+
+	// Get a trigger by its ID. Returns ErrNotFound if there is no trigger with
+	// the given ID in this dataset.
+	Get(ctx context.Context, dataset string, id string) (*Trigger, error)
+
+	// Create a new trigger in this dataset. When creating a new trigger ID
+	// may not be set.
+	Create(ctx context.Context, dataset string, t *Trigger) (*Trigger, error)
+
+	// Update an existing trigger. Missing (optional) fields will set to their
+	// respective defaults and not the currently existing values. Except for
+	// the disabled flag, which will retain its existing value when omitted.
+	Update(ctx context.Context, dataset string, t *Trigger) (*Trigger, error)
+
+	// Delete a trigger from the dataset.
+	Delete(ctx context.Context, dataset string, id string) error
+}
+
+// triggers implements Triggers.
+type triggers struct {
+	client *Client
+}
+
+// Compile-time proof of interface implementation by type triggers.
+var _ Triggers = (*triggers)(nil)
+
+// Trigger represents a Honeycomb trigger.
+//
+// API docs: https://docs.honeycomb.io/api/triggers/#fields-on-a-trigger
+type Trigger struct {
+	ID string `json:"id,omitempty"`
+
+	// Name of the trigger. This field is required.
+	Name string `json:"name"`
+	// Description is displayed on the triggers page.
+	Description string `json:"description,omitempty"`
+	// State of the trigger, if disabled is true the trigger will not run.
+	Disabled bool `json:"disabled,omitempty"`
+	// Query of the trigger. This field is required. The query must respect the
+	// properties described with and validated by MatchesTriggerSubset.
+	// Additionally, time_range of the query can be at most 1 day and may not
+	// be greater than 4 times the frequency.
+	Query *QuerySpec `json:"query"`
+	// Threshold. This fild is required.
+	Threshold *TriggerThreshold `json:"threshold"`
+	// Frequency describes how often the trigger should run. Frequency is an
+	// interval in seconds, defaulting to 900 (15 minutes). Its value must be
+	// divisible by 60 and between 60 and 86400 (between 1 minute and 1 day).
+	Frequency int `json:"frequency,omitempty"`
+	// Recipients are notified when the trigger fires.
+	Recipients []TriggerRecipient `json:"recipients,omitempty"`
+}
+
+// TriggerThreshold represents the threshold of a trigger.
+type TriggerThreshold struct {
+	Op    TriggerThresholdOp `json:"op"`
+	Value float64            `json:"value"`
+}
+
+// TriggerThresholdOp the operator of the trigger threshold.
+type TriggerThresholdOp string
+
+// Declaration of trigger threshold ops.
+const (
+	TriggerThresholdOpGreaterThan        TriggerThresholdOp = ">"
+	TriggerThresholdOpGreaterThanOrEqual TriggerThresholdOp = ">="
+	TriggerThresholdOpLessThan           TriggerThresholdOp = "<"
+	TriggerThresholdOpLessThanOrEqual    TriggerThresholdOp = "<="
+)
+
+// TriggerThresholdOps returns an exhaustive list of trigger threshold ops.
+func TriggerThresholdOps() []TriggerThresholdOp {
+	return []TriggerThresholdOp{
+		TriggerThresholdOpGreaterThan,
+		TriggerThresholdOpGreaterThanOrEqual,
+		TriggerThresholdOpLessThan,
+		TriggerThresholdOpLessThanOrEqual,
+	}
+}
+
+// TriggerRecipient represents a recipient that will receive a notification
+// when the trigger fires.
+//
+// API docs: https://docs.honeycomb.io/api/triggers/#specifying-recipients
+//
+// Notes
+//
+// Recipients of type Slack should be specified by their ID. It is not possible
+// to create a new recipient of type Slack using the API. Instead use the ID of
+// a recipient of type Slack that was manually added to another trigger.
+//
+// Recipients of type webhook can be added by their name. If a webhook with
+// this name does not exist yet (or if the name contains a typo), the Honeycomb
+// API will not complain about this but the webhook will not be valid.
+type TriggerRecipient struct {
+	// ID of the recipient.
+	ID string `json:"id,omitempty"`
+	// Type of the recipient.
+	Type TriggerRecipientType `json:"type"`
+	// Target of the trigger, this has another meaning depending on type:
+	// - email: an email address
+	// - marker: name of the marker
+	// - PagerDuty: N/A
+	// - Slack: name of a channel
+	// - Webhook: name of the webhook
+	Target string `json:"target,omitempty"`
+}
+
+// TriggerRecipientType holds all the possible trigger recipient types.
+type TriggerRecipientType string
+
+// Declaration of trigger recipient types
+const (
+	TriggerRecipientTypeEmail     TriggerRecipientType = "email"
+	TriggerRecipientTypeMarker    TriggerRecipientType = "marker"
+	TriggerRecipientTypePagerDuty TriggerRecipientType = "pagerduty"
+	TriggerRecipientTypeSlack     TriggerRecipientType = "slack"
+	TriggerRecipientTypeWebhook   TriggerRecipientType = "webhook"
+)
+
+// TriggerRecipientTypes returns an exhaustive list of trigger recipient types.
+func TriggerRecipientTypes() []TriggerRecipientType {
+	return []TriggerRecipientType{
+		TriggerRecipientTypeEmail,
+		TriggerRecipientTypeMarker,
+		TriggerRecipientTypePagerDuty,
+		TriggerRecipientTypeSlack,
+		TriggerRecipientTypeWebhook,
+	}
+}
+
+func (s *triggers) List(ctx context.Context, dataset string) ([]Trigger, error) {
+	var t []Trigger
+	err := s.client.performRequest(ctx, "GET", "/1/triggers/"+urlEncodeDataset(dataset), nil, &t)
+	return t, err
+}
+
+func (s *triggers) Get(ctx context.Context, dataset string, id string) (*Trigger, error) {
+	var t Trigger
+	err := s.client.performRequest(ctx, "GET", fmt.Sprintf("/1/triggers/%s/%s", urlEncodeDataset(dataset), id), nil, &t)
+	return &t, err
+}
+
+func (s *triggers) Create(ctx context.Context, dataset string, data *Trigger) (*Trigger, error) {
+	var t Trigger
+	err := s.client.performRequest(ctx, "POST", fmt.Sprintf("/1/triggers/%s", urlEncodeDataset(dataset)), data, &t)
+	return &t, err
+}
+
+func (s *triggers) Update(ctx context.Context, dataset string, data *Trigger) (*Trigger, error) {
+	var t Trigger
+	err := s.client.performRequest(ctx, "PUT", fmt.Sprintf("/1/triggers/%s/%s", urlEncodeDataset(dataset), data.ID), data, &t)
+	return &t, err
+}
+
+func (s *triggers) Delete(ctx context.Context, dataset string, id string) error {
+	return s.client.performRequest(ctx, "DELETE", fmt.Sprintf("/1/triggers/%s/%s", urlEncodeDataset(dataset), id), nil, nil)
+}
+
+// MatchesTriggerSubset checks that the given QuerySpec matches the strict
+// subset required to be used in a trigger.
+//
+// The following properties must be valid:
+//
+//  - the query must contain exactly one calculation
+//  - the HEATMAP calculation may not be used
+//  - only the following fields may be set: calculations, breakdown, filters, filter_combination and time_range
+//
+// For more information, refer to https://docs.honeycomb.io/api/triggers/#fields-on-a-trigger
+func MatchesTriggerSubset(query *QuerySpec) error {
+	if len(query.Calculations) != 1 {
+		return errors.New("a trigger query should contain exactly one calculation")
+	}
+
+	if query.Calculations[0].Op == CalculationOpHeatmap {
+		return errors.New("a trigger query may not contain a HEATMAP calculation")
+	}
+
+	if query.Orders != nil {
+		return errors.New("orders is not allowed in a trigger query")
+	}
+
+	if query.Limit != nil {
+		return errors.New("limit is not allowed in a trigger query")
+	}
+
+	return nil
+}
diff --git a/client/type_helpers.go b/client/type_helpers.go
new file mode 100644
index 0000000..a02dcc6
--- /dev/null
+++ b/client/type_helpers.go
@@ -0,0 +1,36 @@
+package client
+
+// BoolPtr returns a pointer to the given bool
+func BoolPtr(v bool) *bool {
+	return &v
+}
+
+// CalculationOpPtr returns a pointer to the given CalculationOp.
+func CalculationOpPtr(v CalculationOp) *CalculationOp {
+	return &v
+}
+
+// ColumnTypePtr returns a pointer to the given ColumnType.
+func ColumnTypePtr(v ColumnType) *ColumnType {
+	return &v
+}
+
+// IntPtr returns a pointer to the given int.
+func IntPtr(v int) *int {
+	return &v
+}
+
+// Int64Ptr returns a pointer to the given int64.
+func Int64Ptr(v int64) *int64 {
+	return &v
+}
+
+// SortOrderPtr returns a pointer to the given SortOrder.
+func SortOrderPtr(v SortOrder) *SortOrder {
+	return &v
+}
+
+// StringPtr returns a pointer to the given string.
+func StringPtr(v string) *string {
+	return &v
+}
diff --git a/go.mod b/go.mod
index 55c68b6..c28bf07 100644
--- a/go.mod
+++ b/go.mod
@@ -1,4 +1,4 @@
-module github.com/kvrhdn/terraform-provider-honeycombio
+module github.com/honeycombio/terraform-provider-honeycombio
 
 go 1.16
 
@@ -6,6 +6,5 @@ require (
 	github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320
 	github.com/hashicorp/terraform-plugin-sdk/v2 v2.8.0
 	github.com/joho/godotenv v1.4.0
-	github.com/kvrhdn/go-honeycombio v0.2.2
 	github.com/stretchr/testify v1.7.0
 )
diff --git a/go.sum b/go.sum
index 8cf71f7..ed19581 100644
--- a/go.sum
+++ b/go.sum
@@ -212,7 +212,6 @@ github.com/jhump/protoreflect v1.6.0/go.mod h1:eaTn3RZAmMBcV0fifFvlm6VHNz3wSkYyX
 github.com/jmespath/go-jmespath v0.0.0-20160202185014-0b12d6b521d8/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
 github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af h1:pmfjZENx5imkbgOkpRUYLnmbU7UEFbjtDA2hxJ1ichM=
 github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
-github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
 github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg=
 github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
 github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
@@ -231,8 +230,6 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
 github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
 github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
-github.com/kvrhdn/go-honeycombio v0.2.2 h1:0iXkxEiYVEOdMyGB0tcS8AdgfmcuP0alf8+IXeBsRDY=
-github.com/kvrhdn/go-honeycombio v0.2.2/go.mod h1:ZvOwJXuMuBxdtYYVvjwZDJu/UNpGez7pQ4aGBuuHpFE=
 github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k=
 github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
diff --git a/honeycombio/data_source_datasets.go b/honeycombio/data_source_datasets.go
index 046d5c3..ae89801 100644
--- a/honeycombio/data_source_datasets.go
+++ b/honeycombio/data_source_datasets.go
@@ -7,8 +7,8 @@ import (
 
 	"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
 	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
-	"github.com/kvrhdn/go-honeycombio"
-	"github.com/kvrhdn/terraform-provider-honeycombio/honeycombio/internal/hashcode"
+	honeycombio "github.com/honeycombio/terraform-provider-honeycombio/client"
+	"github.com/honeycombio/terraform-provider-honeycombio/honeycombio/internal/hashcode"
 )
 
 func dataSourceHoneycombioDatasets() *schema.Resource {
diff --git a/honeycombio/data_source_query.go b/honeycombio/data_source_query.go
index b256f93..9f29e18 100644
--- a/honeycombio/data_source_query.go
+++ b/honeycombio/data_source_query.go
@@ -9,8 +9,8 @@ import (
 	"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
 	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
 	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
-	"github.com/kvrhdn/go-honeycombio"
-	"github.com/kvrhdn/terraform-provider-honeycombio/honeycombio/internal/hashcode"
+	honeycombio "github.com/honeycombio/terraform-provider-honeycombio/client"
+	"github.com/honeycombio/terraform-provider-honeycombio/honeycombio/internal/hashcode"
 )
 
 func dataSourceHoneycombioQuery() *schema.Resource {
diff --git a/honeycombio/data_source_trigger_recipient.go b/honeycombio/data_source_trigger_recipient.go
index 32cd9ef..ffdff01 100644
--- a/honeycombio/data_source_trigger_recipient.go
+++ b/honeycombio/data_source_trigger_recipient.go
@@ -6,7 +6,7 @@ import (
 	"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
 	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
 	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
-	"github.com/kvrhdn/go-honeycombio"
+	honeycombio "github.com/honeycombio/terraform-provider-honeycombio/client"
 )
 
 func dataSourceHoneycombioSlackRecipient() *schema.Resource {
diff --git a/honeycombio/provider.go b/honeycombio/provider.go
index f5fbbd7..90d7fc6 100644
--- a/honeycombio/provider.go
+++ b/honeycombio/provider.go
@@ -8,7 +8,7 @@ import (
 
 	"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
 	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
-	"github.com/kvrhdn/go-honeycombio"
+	honeycombio "github.com/honeycombio/terraform-provider-honeycombio/client"
 )
 
 func init() {
diff --git a/honeycombio/query_spec.go b/honeycombio/query_spec.go
index dd773fd..5509c0d 100644
--- a/honeycombio/query_spec.go
+++ b/honeycombio/query_spec.go
@@ -6,7 +6,7 @@ import (
 	"github.com/hashicorp/go-cty/cty"
 	"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
 	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
-	"github.com/kvrhdn/go-honeycombio"
+	honeycombio "github.com/honeycombio/terraform-provider-honeycombio/client"
 )
 
 // encodeQuery in a JSON string.
diff --git a/honeycombio/resource_board.go b/honeycombio/resource_board.go
index 76b2c62..820a599 100644
--- a/honeycombio/resource_board.go
+++ b/honeycombio/resource_board.go
@@ -7,7 +7,7 @@ import (
 	"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
 	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
 	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
-	"github.com/kvrhdn/go-honeycombio"
+	honeycombio "github.com/honeycombio/terraform-provider-honeycombio/client"
 )
 
 func newBoard() *schema.Resource {
@@ -112,7 +112,7 @@ func resourceBoardRead(ctx context.Context, d *schema.ResourceData, meta interfa
 	queries := make([]map[string]interface{}, len(b.Queries))
 
 	for i, q := range b.Queries {
-		queryJSON, err := encodeQuery(&q.Query)
+		queryJSON, err := encodeQuery(q.Query)
 		if err != nil {
 			return diag.FromErr(err)
 		}
@@ -174,7 +174,7 @@ func expandBoard(d *schema.ResourceData) (*honeycombio.Board, error) {
 			Caption:    m["caption"].(string),
 			QueryStyle: honeycombio.BoardQueryStyle(m["query_style"].(string)),
 			Dataset:    m["dataset"].(string),
-			Query:      query,
+			Query:      &query,
 		})
 	}
 
diff --git a/honeycombio/resource_column.go b/honeycombio/resource_column.go
index 9987dbc..1dd181a 100644
--- a/honeycombio/resource_column.go
+++ b/honeycombio/resource_column.go
@@ -8,7 +8,7 @@ import (
 	"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
 	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
 	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
-	"github.com/kvrhdn/go-honeycombio"
+	honeycombio "github.com/honeycombio/terraform-provider-honeycombio/client"
 )
 
 func newColumn() *schema.Resource {
diff --git a/honeycombio/resource_dataset.go b/honeycombio/resource_dataset.go
index dcccfb2..0cd49a3 100644
--- a/honeycombio/resource_dataset.go
+++ b/honeycombio/resource_dataset.go
@@ -5,7 +5,7 @@ import (
 
 	"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
 	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
-	"github.com/kvrhdn/go-honeycombio"
+	honeycombio "github.com/honeycombio/terraform-provider-honeycombio/client"
 )
 
 func newDataset() *schema.Resource {
diff --git a/honeycombio/resource_derived_column.go b/honeycombio/resource_derived_column.go
index 742cae7..d668546 100644
--- a/honeycombio/resource_derived_column.go
+++ b/honeycombio/resource_derived_column.go
@@ -7,7 +7,7 @@ import (
 
 	"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
 	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
-	"github.com/kvrhdn/go-honeycombio"
+	honeycombio "github.com/honeycombio/terraform-provider-honeycombio/client"
 )
 
 func newDerivedColumn() *schema.Resource {
diff --git a/honeycombio/resource_marker.go b/honeycombio/resource_marker.go
index 5289274..61aed41 100644
--- a/honeycombio/resource_marker.go
+++ b/honeycombio/resource_marker.go
@@ -5,7 +5,7 @@ import (
 
 	"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
 	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
-	"github.com/kvrhdn/go-honeycombio"
+	honeycombio "github.com/honeycombio/terraform-provider-honeycombio/client"
 )
 
 func newMarker() *schema.Resource {
diff --git a/honeycombio/resource_query.go b/honeycombio/resource_query.go
index e2eb435..5b79a06 100644
--- a/honeycombio/resource_query.go
+++ b/honeycombio/resource_query.go
@@ -6,7 +6,7 @@ import (
 
 	"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
 	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
-	"github.com/kvrhdn/go-honeycombio"
+	honeycombio "github.com/honeycombio/terraform-provider-honeycombio/client"
 )
 
 func newQuery() *schema.Resource {
diff --git a/honeycombio/resource_query_annotation.go b/honeycombio/resource_query_annotation.go
index 5eedc55..9a1c8c1 100644
--- a/honeycombio/resource_query_annotation.go
+++ b/honeycombio/resource_query_annotation.go
@@ -5,7 +5,7 @@ import (
 
 	"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
 	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
-	"github.com/kvrhdn/go-honeycombio"
+	honeycombio "github.com/honeycombio/terraform-provider-honeycombio/client"
 )
 
 func newQueryAnnotation() *schema.Resource {
diff --git a/honeycombio/resource_trigger.go b/honeycombio/resource_trigger.go
index a6ede2a..7710d1c 100644
--- a/honeycombio/resource_trigger.go
+++ b/honeycombio/resource_trigger.go
@@ -9,7 +9,7 @@ import (
 	"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
 	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
 	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
-	"github.com/kvrhdn/go-honeycombio"
+	honeycombio "github.com/honeycombio/terraform-provider-honeycombio/client"
 )
 
 func newTrigger() *schema.Resource {
diff --git a/honeycombio/type_helpers.go b/honeycombio/type_helpers.go
index 977dead..6019ec1 100644
--- a/honeycombio/type_helpers.go
+++ b/honeycombio/type_helpers.go
@@ -1,6 +1,6 @@
 package honeycombio
 
-import "github.com/kvrhdn/go-honeycombio"
+import honeycombio "github.com/honeycombio/terraform-provider-honeycombio/client"
 
 func boardStyleStrings() []string {
 	in := honeycombio.BoardStyles()
diff --git a/main.go b/main.go
index 0ef3ca5..0981f86 100644
--- a/main.go
+++ b/main.go
@@ -3,7 +3,7 @@ package main
 import (
 	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
 	"github.com/hashicorp/terraform-plugin-sdk/v2/plugin"
-	"github.com/kvrhdn/terraform-provider-honeycombio/honeycombio"
+	"github.com/honeycombio/terraform-provider-honeycombio/honeycombio"
 )
 
 func main() {


Test patch:
diff --git a/client/board_test.go b/client/board_test.go
new file mode 100644
index 0000000..1b0e07d
--- /dev/null
+++ b/client/board_test.go
@@ -0,0 +1,108 @@
+package client
+
+import (
+	"context"
+	"fmt"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestBoards(t *testing.T) {
+	ctx := context.Background()
+
+	var b *Board
+	var err error
+
+	c := newTestClient(t)
+	dataset := testDataset(t)
+
+	t.Run("Create", func(t *testing.T) {
+		data := &Board{
+			Name:        fmt.Sprintf("Test Board, created at %v", time.Now()),
+			Description: "A board with some queries",
+			Style:       BoardStyleVisual,
+			Queries: []BoardQuery{
+				{
+					Caption:    "A sample query",
+					QueryStyle: BoardQueryStyleCombo,
+					Dataset:    dataset,
+					Query: &QuerySpec{
+						Calculations: []CalculationSpec{
+							{
+								Op:     CalculationOpAvg,
+								Column: StringPtr("duration_ms"),
+							},
+						},
+						TimeRange: IntPtr(3600), // 1 hour
+					},
+				},
+			},
+		}
+		b, err = c.Boards.Create(ctx, data)
+
+		if err != nil {
+			t.Fatal(err)
+		}
+		assert.NotNil(t, b.ID)
+
+		// copy ID before asserting equality
+		data.ID = b.ID
+		data.Queries[0].QueryID = b.Queries[0].QueryID
+
+		assert.Equal(t, data, b)
+	})
+
+	t.Run("List", func(t *testing.T) {
+		result, err := c.Boards.List(ctx)
+
+		assert.NoError(t, err)
+		assert.Contains(t, result, *b, "could not find newly created board with List")
+	})
+
+	t.Run("Get", func(t *testing.T) {
+		board, err := c.Boards.Get(ctx, b.ID)
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		assert.Equal(t, *b, *board)
+	})
+
+	t.Run("Update", func(t *testing.T) {
+		b.Queries[0].QueryID = ""
+		b.Queries = append(b.Queries, BoardQuery{
+			Caption:    "A second query",
+			QueryStyle: BoardQueryStyleGraph,
+			Dataset:    dataset,
+			Query: &QuerySpec{
+				Calculations: []CalculationSpec{
+					{
+						Op: CalculationOpCount,
+					},
+				},
+				TimeRange: IntPtr(7200), // 2 hours
+			},
+		})
+
+		result, err := c.Boards.Update(ctx, b)
+		b.Queries[0].QueryID = result.Queries[0].QueryID
+		b.Queries[1].QueryID = result.Queries[1].QueryID
+
+		assert.NoError(t, err)
+		assert.Equal(t, b, result)
+	})
+
+	t.Run("Delete", func(t *testing.T) {
+		err := c.Boards.Delete(ctx, b.ID)
+
+		assert.NoError(t, err)
+	})
+
+	t.Run("Get_deletedBoard", func(t *testing.T) {
+		_, err := c.Boards.Get(ctx, b.ID)
+
+		assert.Equal(t, ErrNotFound, err)
+	})
+}
diff --git a/client/client_test.go b/client/client_test.go
new file mode 100644
index 0000000..192a205
--- /dev/null
+++ b/client/client_test.go
@@ -0,0 +1,91 @@
+package client
+
+import (
+	"context"
+	"os"
+	"testing"
+
+	"github.com/joho/godotenv"
+	"github.com/stretchr/testify/assert"
+)
+
+func init() {
+	// load environment values from a .env, if available
+	_ = godotenv.Load()
+}
+
+func newTestClient(t *testing.T) *Client {
+	apiKey, ok := os.LookupEnv("HONEYCOMBIO_APIKEY")
+	if !ok {
+		t.Fatal("expected environment variable HONEYCOMBIO_APIKEY")
+	}
+	_, debug := os.LookupEnv("DEBUG")
+
+	cfg := &Config{
+		APIKey: apiKey,
+		Debug:  debug,
+	}
+	c, err := NewClient(cfg)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	return c
+}
+
+func testDataset(t *testing.T) string {
+	dataset, ok := os.LookupEnv("HONEYCOMBIO_DATASET")
+	if !ok {
+		t.Fatalf("expected environment variable HONEYCOMBIO_DATASET")
+	}
+
+	return dataset
+}
+
+func TestConfig_merge(t *testing.T) {
+	config1 := &Config{
+		APIKey:    "",
+		APIUrl:    "",
+		UserAgent: "go-honeycombio",
+	}
+	config2 := &Config{
+		APIKey:    "",
+		APIUrl:    "http://localhost",
+		UserAgent: "",
+	}
+
+	expected := &Config{
+		APIKey:    "",
+		APIUrl:    "http://localhost",
+		UserAgent: "go-honeycombio",
+	}
+
+	config1.merge(config2)
+
+	assert.Equal(t, expected, config1)
+}
+
+func TestClient_invalidConfig(t *testing.T) {
+	cfg := &Config{
+		APIKey: "",
+	}
+	_, err := NewClient(cfg)
+	assert.Error(t, err, "APIKey must be configured")
+
+	cfg = &Config{
+		APIKey: "123",
+		APIUrl: "cache_object:foo/bar",
+	}
+	_, err = NewClient(cfg)
+	assert.Error(t, err)
+	assert.Contains(t, err.Error(), "could not parse APIUrl")
+}
+
+func TestClient_parseHoneycombioError(t *testing.T) {
+	ctx := context.Background()
+	c := newTestClient(t)
+
+	err := c.performRequest(ctx, "POST", "/1/boards/", nil, nil)
+
+	assert.Error(t, err, "400 Bad Request: request body should not be empty")
+}
diff --git a/client/column_test.go b/client/column_test.go
new file mode 100644
index 0000000..14d8496
--- /dev/null
+++ b/client/column_test.go
@@ -0,0 +1,83 @@
+package client
+
+import (
+	"context"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestColumns(t *testing.T) {
+	ctx := context.Background()
+
+	c := newTestClient(t)
+	dataset := testDataset(t)
+
+	var column *Column
+	var err error
+
+	t.Run("Create", func(t *testing.T) {
+		data := &Column{
+			KeyName:     "column_test",
+			Hidden:      BoolPtr(false),
+			Description: "This column is created by a test",
+			Type:        ColumnTypePtr(ColumnTypeFloat),
+		}
+		column, err = c.Columns.Create(ctx, dataset, data)
+
+		assert.NoError(t, err)
+
+		data.ID = column.ID
+		assert.Equal(t, data, column)
+	})
+
+	t.Run("List", func(t *testing.T) {
+		columns, err := c.Columns.List(ctx, dataset)
+
+		assert.NoError(t, err)
+		assert.Contains(t, columns, *column, "could not find column with List")
+	})
+
+	t.Run("Get", func(t *testing.T) {
+		result, err := c.Columns.Get(ctx, dataset, column.ID)
+
+		assert.NoError(t, err)
+		assert.Equal(t, *column, *result)
+	})
+
+	t.Run("GetByKeyName", func(t *testing.T) {
+		result, err := c.Columns.GetByKeyName(ctx, dataset, "column_test")
+
+		assert.NoError(t, err)
+		assert.Equal(t, *column, *result)
+	})
+
+	t.Run("Update", func(t *testing.T) {
+		// change all the fields to test
+		data := &Column{
+			ID:          column.ID,
+			KeyName:     "a-new-keyname",
+			Hidden:      BoolPtr(true),
+			Description: "This is a new description",
+			Type:        ColumnTypePtr(ColumnTypeBoolean),
+		}
+		column, err = c.Columns.Update(ctx, dataset, data)
+
+		assert.NoError(t, err)
+
+		data.ID = column.ID
+		assert.Equal(t, data, column)
+	})
+
+	t.Run("Delete", func(t *testing.T) {
+		err = c.Columns.Delete(ctx, dataset, column.ID)
+
+		assert.NoError(t, err)
+	})
+
+	t.Run("Get_notFound", func(t *testing.T) {
+		_, err := c.Columns.Get(ctx, dataset, column.ID)
+
+		assert.Equal(t, ErrNotFound, err)
+	})
+}
diff --git a/client/dataset_test.go b/client/dataset_test.go
new file mode 100644
index 0000000..710bfca
--- /dev/null
+++ b/client/dataset_test.go
@@ -0,0 +1,50 @@
+package client
+
+import (
+	"context"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestDatasets(t *testing.T) {
+	ctx := context.Background()
+
+	c := newTestClient(t)
+	datasetName := testDataset(t)
+
+	currentDataset := &Dataset{
+		Name: datasetName,
+		Slug: urlEncodeDataset(datasetName),
+	}
+
+	t.Run("List", func(t *testing.T) {
+		d, err := c.Datasets.List(ctx)
+
+		assert.NoError(t, err)
+		assert.Contains(t, d, *currentDataset, "could not find current dataset with List")
+	})
+
+	t.Run("Get", func(t *testing.T) {
+		d, err := c.Datasets.Get(ctx, currentDataset.Slug)
+
+		assert.NoError(t, err)
+		assert.Equal(t, *currentDataset, *d)
+	})
+
+	t.Run("Get_notFound", func(t *testing.T) {
+		_, err := c.Datasets.Get(ctx, "does-not-exist")
+
+		assert.Equal(t, ErrNotFound, err)
+	})
+
+	t.Run("Create", func(t *testing.T) {
+		createDataset := &Dataset{
+			Name: "kvrhdn/go-honeycombio",
+		}
+		d, err := c.Datasets.Create(ctx, createDataset)
+
+		assert.NoError(t, err)
+		assert.Equal(t, currentDataset, d)
+	})
+}
diff --git a/client/derived_column_test.go b/client/derived_column_test.go
new file mode 100644
index 0000000..c660df4
--- /dev/null
+++ b/client/derived_column_test.go
@@ -0,0 +1,87 @@
+package client
+
+import (
+	"context"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestDerivedColumns(t *testing.T) {
+	ctx := context.Background()
+
+	c := newTestClient(t)
+	dataset := testDataset(t)
+
+	var derivedColumn *DerivedColumn
+	var err error
+
+	t.Run("Create", func(t *testing.T) {
+		data := &DerivedColumn{
+			Alias:       "derived_column_test",
+			Expression:  "LOG10($duration_ms)",
+			Description: "This derived column is created by a test",
+		}
+		derivedColumn, err = c.DerivedColumns.Create(ctx, dataset, data)
+
+		assert.NoError(t, err)
+
+		data.ID = derivedColumn.ID
+		assert.Equal(t, data, derivedColumn)
+	})
+
+	t.Run("List", func(t *testing.T) {
+		derivedColumns, err := c.DerivedColumns.List(ctx, dataset)
+
+		assert.NoError(t, err)
+		assert.Contains(t, derivedColumns, *derivedColumn, "could not find DerivedColumn with List")
+	})
+
+	t.Run("Get", func(t *testing.T) {
+		result, err := c.DerivedColumns.Get(ctx, dataset, derivedColumn.ID)
+
+		assert.NoError(t, err)
+		assert.Equal(t, *derivedColumn, *result)
+	})
+
+	t.Run("GetByAlias", func(t *testing.T) {
+		result, err := c.DerivedColumns.GetByAlias(ctx, dataset, derivedColumn.Alias)
+
+		assert.NoError(t, err)
+		assert.Equal(t, *derivedColumn, *result)
+	})
+
+	t.Run("Update", func(t *testing.T) {
+		// change all the fields to test
+		data := &DerivedColumn{
+			ID:          derivedColumn.ID,
+			Alias:       "derived_column_test_new_alias",
+			Expression:  "DIV($duration_ms, 2)",
+			Description: "This is a new description",
+		}
+		derivedColumn, err = c.DerivedColumns.Update(ctx, dataset, data)
+
+		assert.NoError(t, err)
+
+		data.ID = derivedColumn.ID
+		assert.Equal(t, data, derivedColumn)
+	})
+
+	t.Run("Delete", func(t *testing.T) {
+		err = c.DerivedColumns.Delete(ctx, dataset, derivedColumn.ID)
+
+		assert.NoError(t, err)
+	})
+
+	t.Run("Get_notFound", func(t *testing.T) {
+		_, err := c.DerivedColumns.Get(ctx, dataset, derivedColumn.ID)
+
+		assert.Equal(t, ErrNotFound, err)
+	})
+
+	t.Run("GetByAlias_notFound", func(t *testing.T) {
+		_, err := c.DerivedColumns.GetByAlias(ctx, dataset, derivedColumn.Alias)
+
+		assert.Equal(t, ErrNotFound, err)
+	})
+}
diff --git a/client/example_test.go b/client/example_test.go
new file mode 100644
index 0000000..a228335
--- /dev/null
+++ b/client/example_test.go
@@ -0,0 +1,33 @@
+package client
+
+import (
+	"context"
+	"fmt"
+	"os"
+)
+
+// This example shows how to configure and use a client to list all boards.
+func Example() {
+	apiKey, _ := os.LookupEnv("HONEYCOMBIO_APIKEY")
+
+	config := &Config{
+		APIKey: apiKey,
+	}
+	client, err := NewClient(config)
+	if err != nil {
+		panic(err)
+	}
+
+	ctx := context.Background()
+
+	boards, err := client.Boards.List(ctx)
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Printf("Found %d boards\n", len(boards))
+
+	for i, board := range boards {
+		fmt.Printf("%d| %s (%d queries)\n", i, board.Name, len(board.Queries))
+	}
+}
diff --git a/client/marker_test.go b/client/marker_test.go
new file mode 100644
index 0000000..d854c34
--- /dev/null
+++ b/client/marker_test.go
@@ -0,0 +1,75 @@
+package client
+
+import (
+	"context"
+	"fmt"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestMarkers(t *testing.T) {
+	ctx := context.Background()
+
+	var m *Marker
+	var err error
+
+	c := newTestClient(t)
+	dataset := testDataset(t)
+
+	t.Run("Create", func(t *testing.T) {
+		data := &Marker{
+			Message:   fmt.Sprintf("Test run at %v", time.Now()),
+			Type:      "deploys",
+			URL:       "http://example.com",
+			StartTime: time.Now().Unix(),
+		}
+		m, err = c.Markers.Create(ctx, dataset, data)
+
+		if err != nil {
+			t.Fatal(err)
+		}
+		assert.NotNil(t, m.ID)
+		assert.Equal(t, data.Message, m.Message)
+		assert.Equal(t, data.Type, m.Type)
+		assert.Equal(t, data.URL, m.URL)
+		assert.Equal(t, data.StartTime, m.StartTime)
+		assert.Equal(t, data.EndTime, m.EndTime)
+	})
+
+	t.Run("List", func(t *testing.T) {
+		markers, err := c.Markers.List(ctx, dataset)
+
+		assert.NoError(t, err)
+		assert.Contains(t, markers, *m, "could not find newly created marker with List")
+	})
+
+	t.Run("Get", func(t *testing.T) {
+		result, err := c.Markers.Get(ctx, dataset, m.ID)
+
+		assert.NoError(t, err)
+		assert.Equal(t, *m, *result)
+	})
+
+	t.Run("Update", func(t *testing.T) {
+		m.EndTime = time.Now().Unix()
+
+		result, err := c.Markers.Update(ctx, dataset, m)
+
+		assert.NoError(t, err)
+		assert.Equal(t, m, result)
+	})
+
+	t.Run("Delete", func(t *testing.T) {
+		err := c.Markers.Delete(ctx, dataset, m.ID)
+
+		assert.NoError(t, err)
+	})
+
+	t.Run("Get_deletedMarker", func(t *testing.T) {
+		_, err := c.Markers.Get(ctx, dataset, m.ID)
+
+		assert.Equal(t, ErrNotFound, err)
+	})
+}
diff --git a/client/query_annotation_test.go b/client/query_annotation_test.go
new file mode 100644
index 0000000..57ff26f
--- /dev/null
+++ b/client/query_annotation_test.go
@@ -0,0 +1,98 @@
+package client
+
+import (
+	"context"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestQueryAnnotations(t *testing.T) {
+	ctx := context.Background()
+
+	c := newTestClient(t)
+	dataset := testDataset(t)
+
+	var queryAnnotation *QueryAnnotation
+	var err error
+
+	query, err := c.Queries.Create(ctx, dataset, &QuerySpec{
+		Calculations: []CalculationSpec{
+			{
+				Op: "COUNT",
+			},
+		},
+	})
+
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	t.Run("Create", func(t *testing.T) {
+		data := &QueryAnnotation{
+			Name:        "Query created by a test",
+			Description: "This derived column is created by a test",
+			QueryID:     *query.ID,
+		}
+		queryAnnotation, err = c.QueryAnnotations.Create(ctx, dataset, data)
+
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		data.ID = queryAnnotation.ID
+		assert.Equal(t, data, queryAnnotation)
+	})
+
+	t.Run("List", func(t *testing.T) {
+		queryAnnotations, err := c.QueryAnnotations.List(ctx, dataset)
+
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		assert.Contains(t, queryAnnotations, *queryAnnotation, "could not find QueryAnnotation with List")
+	})
+
+	t.Run("Get", func(t *testing.T) {
+		result, err := c.QueryAnnotations.Get(ctx, dataset, queryAnnotation.ID)
+
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		assert.Equal(t, *queryAnnotation, *result)
+	})
+
+	t.Run("Update", func(t *testing.T) {
+		// change all the fields to test
+		data := &QueryAnnotation{
+			ID:          queryAnnotation.ID,
+			Name:        "This is a new name for the query created by a test",
+			Description: "This is a new description",
+			QueryID:     *query.ID,
+		}
+		queryAnnotation, err = c.QueryAnnotations.Update(ctx, dataset, data)
+
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		data.ID = queryAnnotation.ID
+		assert.Equal(t, data, queryAnnotation)
+	})
+
+	t.Run("Delete", func(t *testing.T) {
+		err = c.QueryAnnotations.Delete(ctx, dataset, queryAnnotation.ID)
+
+		if err != nil {
+			t.Fatal(err)
+		}
+	})
+
+	t.Run("Get_notFound", func(t *testing.T) {
+		_, err := c.QueryAnnotations.Get(ctx, dataset, queryAnnotation.ID)
+
+		assert.Equal(t, ErrNotFound, err)
+	})
+}
diff --git a/client/query_spec_test.go b/client/query_spec_test.go
new file mode 100644
index 0000000..71eeb69
--- /dev/null
+++ b/client/query_spec_test.go
@@ -0,0 +1,155 @@
+package client
+
+import (
+	"context"
+	"fmt"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+)
+
+// create a board with an elaborate QuerySpec as smoke test
+func TestQuerySpec(t *testing.T) {
+	ctx := context.Background()
+
+	c := newTestClient(t)
+	dataset := testDataset(t)
+
+	query := QuerySpec{
+		Calculations: []CalculationSpec{
+			{
+				Op: CalculationOpCount,
+			},
+			{
+				Op:     CalculationOpHeatmap,
+				Column: StringPtr("duration_ms"),
+			},
+		},
+		Filters: []FilterSpec{
+			{
+				Column: "column_1",
+				Op:     FilterOpExists,
+			},
+			{
+				Column: "duration_ms",
+				Op:     FilterOpSmallerThan,
+				Value:  10000.0,
+			},
+		},
+		FilterCombination: FilterCombinationOr,
+		Breakdowns:        []string{"column_1", "column_2"},
+		Orders: []OrderSpec{
+			{
+				Column: StringPtr("column_1"),
+			},
+			{
+				Op:    CalculationOpPtr(CalculationOpCount),
+				Order: SortOrderPtr(SortOrderDesc),
+			},
+		},
+		Limit:       IntPtr(100),
+		TimeRange:   IntPtr(3600), // 1 hour
+		Granularity: IntPtr(60),   // 1 minute
+	}
+
+	b := &Board{
+		Name: fmt.Sprintf("Test QuerySpec, created at %v", time.Now()),
+
+		Queries: []BoardQuery{
+			{
+				Dataset: dataset,
+				Query:   &query,
+			},
+		},
+	}
+
+	b, err := c.Boards.Create(ctx, b)
+	if err != nil {
+		t.Fatal(err)
+	}
+	defer func() {
+		err := c.Boards.Delete(ctx, b.ID)
+		if err != nil {
+			t.Fatal(err)
+		}
+	}()
+
+	assert.Len(t, b.Queries, 1)
+	assert.Equal(t, &query, b.Queries[0].Query)
+}
+
+func TestCalcuationOps(t *testing.T) {
+	ctx := context.Background()
+	c := newTestClient(t)
+	dataset := testDataset(t)
+
+	b := &Board{
+		Name: "go-honeycombio: TestCalcula§tionOps",
+	}
+	b, err := c.Boards.Create(ctx, b)
+	assert.NoError(t, err)
+
+	defer c.Boards.Delete(ctx, b.ID)
+
+	for _, calculationOp := range CalculationOps() {
+		column := StringPtr("duration_ms")
+		if calculationOp == CalculationOpCount {
+			column = nil
+		}
+
+		q := QuerySpec{
+			Calculations: []CalculationSpec{
+				{
+					Op:     calculationOp,
+					Column: column,
+				},
+			},
+		}
+		b.Queries = []BoardQuery{{Dataset: dataset, Query: &q}}
+
+		_, err = c.Boards.Update(ctx, b)
+		assert.NoError(t, err, fmt.Sprintf("Failed to create board that contains calcuation with op \"%v\"", calculationOp))
+	}
+}
+
+func TestFilterOps(t *testing.T) {
+	ctx := context.Background()
+	c := newTestClient(t)
+	dataset := testDataset(t)
+
+	b := &Board{
+		Name: "go-honeycombio: TestFilterOps",
+	}
+	b, err := c.Boards.Create(ctx, b)
+	assert.NoError(t, err)
+
+	defer c.Boards.Delete(ctx, b.ID)
+
+	for _, filterOp := range FilterOps() {
+		var value interface{}
+
+		switch filterOp {
+		case FilterOpExists, FilterOpDoesNotExist:
+			value = nil
+		case FilterOpIn, FilterOpNotIn:
+			value = []string{"foo", "bar"}
+		default:
+			value = "foo"
+		}
+
+		q := QuerySpec{
+			Filters: []FilterSpec{
+				{
+					Column: "column_1",
+					Op:     filterOp,
+					Value:  value,
+				},
+			},
+		}
+		b.Queries = []BoardQuery{{Dataset: dataset, Query: &q}}
+
+		_, err = c.Boards.Update(ctx, b)
+		assert.NoError(t, err, fmt.Sprintf("Failed to create board that contains filter with op \"%v\"", filterOp))
+	}
+}
diff --git a/client/query_test.go b/client/query_test.go
new file mode 100644
index 0000000..e7a6a17
--- /dev/null
+++ b/client/query_test.go
@@ -0,0 +1,76 @@
+package client
+
+import (
+	"context"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestQueries(t *testing.T) {
+	ctx := context.Background()
+
+	c := newTestClient(t)
+	dataset := testDataset(t)
+
+	var query *QuerySpec
+	var err error
+
+	t.Run("Create", func(t *testing.T) {
+		data := &QuerySpec{
+			Calculations: []CalculationSpec{
+				{
+					Op: CalculationOpCount,
+				},
+				{
+					Op:     CalculationOpHeatmap,
+					Column: StringPtr("duration_ms"),
+				},
+			},
+			Filters: []FilterSpec{
+				{
+					Column: "column_1",
+					Op:     FilterOpExists,
+				},
+				{
+					Column: "duration_ms",
+					Op:     FilterOpSmallerThan,
+					Value:  10000.0,
+				},
+			},
+			FilterCombination: FilterCombinationOr,
+			Breakdowns:        []string{"column_1", "column_2"},
+			Orders: []OrderSpec{
+				{
+					Column: StringPtr("column_1"),
+				},
+				{
+					Op:    CalculationOpPtr(CalculationOpCount),
+					Order: SortOrderPtr(SortOrderDesc),
+				},
+			},
+			Limit:       IntPtr(100),
+			TimeRange:   IntPtr(3600), // 1 hour
+			Granularity: IntPtr(60),   // 1 minute
+		}
+
+		query, err = c.Queries.Create(ctx, dataset, data)
+
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		data.ID = query.ID
+		assert.Equal(t, data, query)
+	})
+
+	t.Run("Get", func(t *testing.T) {
+		q, err := c.Queries.Get(ctx, dataset, *query.ID)
+
+		if err != nil {
+			t.Fatal(err)
+		}
+
+		assert.Equal(t, query, q)
+	})
+}
diff --git a/client/trigger_test.go b/client/trigger_test.go
new file mode 100644
index 0000000..295f5f3
--- /dev/null
+++ b/client/trigger_test.go
@@ -0,0 +1,182 @@
+package client
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestTriggers(t *testing.T) {
+	ctx := context.Background()
+
+	var trigger *Trigger
+	var err error
+
+	c := newTestClient(t)
+	dataset := testDataset(t)
+
+	t.Run("Create", func(t *testing.T) {
+		data := &Trigger{
+			Name:        fmt.Sprintf("Test trigger created at %v", time.Now()),
+			Description: "Some description",
+			Disabled:    true,
+			Query: &QuerySpec{
+				Breakdowns: nil,
+				Calculations: []CalculationSpec{
+					{
+						Op:     CalculationOpP99,
+						Column: StringPtr("duration_ms"),
+					},
+				},
+				Filters: []FilterSpec{
+					{
+						Column: "column_1",
+						Op:     FilterOpExists,
+					},
+					{
+						Column: "column_2",
+						Op:     FilterOpContains,
+						Value:  "foobar",
+					},
+				},
+				FilterCombination: FilterCombinationOr,
+			},
+			Frequency: 300,
+			Threshold: &TriggerThreshold{
+				Op:    TriggerThresholdOpGreaterThan,
+				Value: 10000,
+			},
+			Recipients: []TriggerRecipient{
+				{
+					Type:   TriggerRecipientTypeEmail,
+					Target: "hello@example.com",
+				},
+				{
+					Type:   TriggerRecipientTypeMarker,
+					Target: "This marker is created by a trigger",
+				},
+			},
+		}
+		trigger, err = c.Triggers.Create(ctx, dataset, data)
+
+		if err != nil {
+			t.Fatal(err)
+		}
+		assert.NotNil(t, trigger.ID)
+
+		// copy IDs before asserting equality
+		data.ID = trigger.ID
+		for i := range trigger.Recipients {
+			data.Recipients[i].ID = trigger.Recipients[i].ID
+		}
+		// set default time range
+		data.Query.TimeRange = IntPtr(300)
+
+		assert.Equal(t, data, trigger)
+	})
+
+	t.Run("List", func(t *testing.T) {
+		result, err := c.Triggers.List(ctx, dataset)
+
+		assert.NoError(t, err)
+		assert.Contains(t, result, *trigger, "could not find newly created trigger with List")
+	})
+
+	t.Run("Get", func(t *testing.T) {
+		getTrigger, err := c.Triggers.Get(ctx, dataset, trigger.ID)
+
+		assert.NoError(t, err)
+		assert.Equal(t, *trigger, *getTrigger)
+	})
+
+	t.Run("Update", func(t *testing.T) {
+		trigger.Description = "A new description"
+
+		result, err := c.Triggers.Update(ctx, dataset, trigger)
+
+		assert.NoError(t, err)
+		assert.Equal(t, trigger, result)
+	})
+
+	t.Run("Delete", func(t *testing.T) {
+		err = c.Triggers.Delete(ctx, dataset, trigger.ID)
+
+		assert.NoError(t, err)
+	})
+
+	t.Run("Get_deletedTrigger", func(t *testing.T) {
+		_, err := c.Triggers.Get(ctx, dataset, trigger.ID)
+
+		assert.Equal(t, ErrNotFound, err)
+	})
+}
+
+func TestMatchesTriggerSubset(t *testing.T) {
+	cases := []struct {
+		in          QuerySpec
+		expectedErr error
+	}{
+		{
+			in: QuerySpec{
+				Calculations: []CalculationSpec{
+					{
+						Op: CalculationOpCount,
+					},
+				},
+			},
+			expectedErr: nil,
+		},
+		{
+			in: QuerySpec{
+				Calculations: nil,
+			},
+			expectedErr: errors.New("a trigger query should contain exactly one calculation"),
+		},
+		{
+			in: QuerySpec{
+				Calculations: []CalculationSpec{
+					{
+						Op: CalculationOpHeatmap,
+					},
+				},
+			},
+			expectedErr: errors.New("a trigger query may not contain a HEATMAP calculation"),
+		},
+		{
+			in: QuerySpec{
+				Calculations: []CalculationSpec{
+					{
+						Op: CalculationOpCount,
+					},
+				},
+				Limit: IntPtr(100),
+			},
+			expectedErr: errors.New("limit is not allowed in a trigger query"),
+		},
+		{
+			in: QuerySpec{
+				Calculations: []CalculationSpec{
+					{
+						Op: CalculationOpCount,
+					},
+				},
+				Orders: []OrderSpec{
+					{
+						Column: StringPtr("duration_ms"),
+					},
+				},
+			},
+			expectedErr: errors.New("orders is not allowed in a trigger query"),
+		},
+	}
+
+	for i, c := range cases {
+		err := MatchesTriggerSubset(&c.in)
+
+		assert.Equal(t, c.expectedErr, err, "Test case %d, QuerySpec: %v", i, c.in)
+	}
+}
diff --git a/honeycombio/data_source_trigger_recipient_test.go b/honeycombio/data_source_trigger_recipient_test.go
index 4aec699..77d173e 100644
--- a/honeycombio/data_source_trigger_recipient_test.go
+++ b/honeycombio/data_source_trigger_recipient_test.go
@@ -6,7 +6,7 @@ import (
 	"testing"
 
 	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
-	"github.com/kvrhdn/go-honeycombio"
+	honeycombio "github.com/honeycombio/terraform-provider-honeycombio/client"
 )
 
 func TestAccDataSourceHoneycombioTriggerRecipient_basic(t *testing.T) {
diff --git a/honeycombio/helpers_test.go b/honeycombio/helpers_test.go
index bd978a5..8996a6a 100644
--- a/honeycombio/helpers_test.go
+++ b/honeycombio/helpers_test.go
@@ -7,7 +7,7 @@ import (
 
 	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
 	"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
-	"github.com/kvrhdn/go-honeycombio"
+	honeycombio "github.com/honeycombio/terraform-provider-honeycombio/client"
 )
 
 // testCheckOutputContains checks an output in the Terraform configuration
diff --git a/honeycombio/provider_test.go b/honeycombio/provider_test.go
index 96ae2bd..c43be40 100644
--- a/honeycombio/provider_test.go
+++ b/honeycombio/provider_test.go
@@ -5,8 +5,8 @@ import (
 	"testing"
 
 	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
+	honeycombio "github.com/honeycombio/terraform-provider-honeycombio/client"
 	"github.com/joho/godotenv"
-	"github.com/kvrhdn/go-honeycombio"
 )
 
 func init() {
diff --git a/honeycombio/resource_board_test.go b/honeycombio/resource_board_test.go
index 7f4e921..e40cc8d 100644
--- a/honeycombio/resource_board_test.go
+++ b/honeycombio/resource_board_test.go
@@ -8,7 +8,7 @@ import (
 
 	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
 	"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
-	"github.com/kvrhdn/go-honeycombio"
+	honeycombio "github.com/honeycombio/terraform-provider-honeycombio/client"
 	"github.com/stretchr/testify/assert"
 )
 
@@ -92,7 +92,7 @@ func testAccCheckBoardExists(t *testing.T, name string) resource.TestCheckFunc {
 					Caption:    "test query 0",
 					QueryStyle: honeycombio.BoardQueryStyleGraph,
 					Dataset:    testAccDataset(),
-					Query: honeycombio.QuerySpec{
+					Query: &honeycombio.QuerySpec{
 						Calculations: []honeycombio.CalculationSpec{
 							{
 								Op:     honeycombio.CalculationOpAvg,
@@ -113,7 +113,7 @@ func testAccCheckBoardExists(t *testing.T, name string) resource.TestCheckFunc {
 					Caption:    "test query 1",
 					QueryStyle: honeycombio.BoardQueryStyleCombo,
 					Dataset:    testAccDataset(),
-					Query: honeycombio.QuerySpec{
+					Query: &honeycombio.QuerySpec{
 						Calculations: []honeycombio.CalculationSpec{
 							{
 								Op:     honeycombio.CalculationOpAvg,
diff --git a/honeycombio/resource_query_test.go b/honeycombio/resource_query_test.go
index 6547aa6..afa0a24 100644
--- a/honeycombio/resource_query_test.go
+++ b/honeycombio/resource_query_test.go
@@ -9,7 +9,7 @@ import (
 
 	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
 	"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
-	"github.com/kvrhdn/go-honeycombio"
+	honeycombio "github.com/honeycombio/terraform-provider-honeycombio/client"
 	"github.com/stretchr/testify/assert"
 )
 
diff --git a/honeycombio/resource_trigger_test.go b/honeycombio/resource_trigger_test.go
index 4e9d8ab..2187455 100644
--- a/honeycombio/resource_trigger_test.go
+++ b/honeycombio/resource_trigger_test.go
@@ -8,7 +8,7 @@ import (
 
 	"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
 	"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
-	"github.com/kvrhdn/go-honeycombio"
+	honeycombio "github.com/honeycombio/terraform-provider-honeycombio/client"
 )
 
 func TestAccHoneycombioTrigger_basic(t *testing.T) {