diff --git a/.travis.yml b/.travis.yml
index 0112c2173a0f131d6d65d80ea0c428ebcf3698ab..28d41082267ceefa2ab179666d9389d6640015a3 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,6 +1,6 @@
 language: go
 go:
-  - 1.6.2
+  - 1.x
 before_install:
   - go get github.com/golang/lint/golint
 before_script:
diff --git a/README.md b/README.md
index e80e3a6f242d2281692cd94392113cb212d13039..93027bfd4be180eb8fbfb0bef38a02baa5cdcb4e 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,7 @@
 netbox [![GoDoc](http://godoc.org/github.com/digitalocean/go-netbox?status.svg)](http://godoc.org/github.com/digitalocean/go-netbox) [![Build Status](https://travis-ci.org/digitalocean/go-netbox.svg?branch=master)](https://travis-ci.org/digitalocean/go-netbox) [![Report Card](https://goreportcard.com/badge/github.com/digitalocean/go-netbox)](https://goreportcard.com/report/github.com/digitalocean/go-netbox)
 ======
 
-**Note**: the existing code is "frozen" and will be updated when NetBox API
-2.0 is released.
-
-Package `netbox` provides an API client for [DigitalOcean's NetBox](https://github.com/digitalocean/netbox)
+Package `netbox` provides an API 2.0 client for [DigitalOcean's NetBox](https://github.com/digitalocean/netbox)
 IPAM and DCIM service.
+
+This package assumes you are using NetBox 2.0, as the NetBox 1.0 API no longer exists.
diff --git a/asn.go b/asn.go
deleted file mode 100644
index 7f84641a35ab744604f2ef3432d84004ce79856b..0000000000000000000000000000000000000000
--- a/asn.go
+++ /dev/null
@@ -1,97 +0,0 @@
-// Copyright 2016 The go-netbox Authors.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//   http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package netbox
-
-// An ASN is an Autonomous System Number, used to designate that a collection of
-// Internet Protocol routing prefixes are under the control of one or more
-// network operators.
-type ASN int
-
-// Reserved and private ASN ranges, used in filters below.
-// Reference: http://www.iana.org/assignments/as-numbers/as-numbers.xhtml.
-const (
-	reservedStart16Bit  = 0
-	reservedMin16Bit    = 64297
-	reservedMax16Bit    = 64495
-	reservedDocMin16Bit = 64496
-	reservedDocMax16Bit = 64511
-	reservedEnd16Bit    = 65535
-
-	reservedDocMin32Bit = 65536
-	reservedDocMax32Bit = 65551
-	reservedMin32Bit    = 65552
-	reservedMax32Bit    = 131071
-	reservedEnd32Bit    = 4294967295
-
-	privateMin16Bit = 64512
-	privateMax16Bit = 65534
-
-	privateMin32Bit = 4200000000
-	privateMax32Bit = 4294967294
-
-	// RFC 6793, Section 9, AS_TRANS, reserved
-	asTrans = 23456
-)
-
-// Private determines if an ASN resides within a private range.
-func (a ASN) Private() bool {
-	switch {
-	case a >= privateMin16Bit && a <= privateMax16Bit:
-		return true
-	case a >= privateMin32Bit && a <= privateMax32Bit:
-		return true
-	default:
-		return false
-	}
-}
-
-// Public determines if an ASN does not reside within a private or reserved
-// range.
-func (a ASN) Public() bool {
-	if !a.Valid() {
-		return false
-	}
-
-	return !a.Private() && !a.Reserved()
-}
-
-// Reserved determines if an ASN resides within a reserved range.
-func (a ASN) Reserved() bool {
-	switch {
-	case a == reservedStart16Bit:
-		return true
-	case a == asTrans:
-		return true
-	case a >= reservedMin16Bit && a <= reservedMax16Bit:
-		return true
-	case a >= reservedDocMin16Bit && a <= reservedDocMax16Bit:
-		return true
-	case a == reservedEnd16Bit:
-		return true
-	case a >= reservedDocMin32Bit && a <= reservedDocMax32Bit:
-		return true
-	case a >= reservedMin32Bit && a <= reservedMax32Bit:
-		return true
-	case a == reservedEnd32Bit:
-		return true
-	default:
-		return false
-	}
-}
-
-// Valid determines if an ASN is valid.
-func (a ASN) Valid() bool {
-	return a >= reservedStart16Bit && a <= reservedEnd32Bit
-}
diff --git a/asn_test.go b/asn_test.go
deleted file mode 100644
index 118406f514128021140355f21483d292a6b852e9..0000000000000000000000000000000000000000
--- a/asn_test.go
+++ /dev/null
@@ -1,284 +0,0 @@
-// Copyright 2016 The go-netbox Authors.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//   http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package netbox
-
-import (
-	"math"
-	"testing"
-)
-
-func TestASNPrivate(t *testing.T) {
-	var tests = []struct {
-		a  ASN
-		ok bool
-	}{
-		{
-			a: 0,
-		},
-		{
-			a: math.MinInt64,
-		},
-		{
-			a: math.MaxInt64,
-		},
-		{
-			a:  privateMin16Bit,
-			ok: true,
-		},
-		{
-			a:  privateMin16Bit + 1,
-			ok: true,
-		},
-		{
-			a:  privateMax16Bit - 1,
-			ok: true,
-		},
-		{
-			a:  privateMax16Bit,
-			ok: true,
-		},
-		{
-			a:  privateMin32Bit,
-			ok: true,
-		},
-		{
-			a:  privateMax32Bit,
-			ok: true,
-		},
-	}
-
-	for _, tt := range tests {
-		if want, got := tt.ok, tt.a.Private(); want != got {
-			t.Fatalf("unexpected ASN(%d).Private():\n- want: %v\n-  got: %v",
-				tt.a, want, got)
-		}
-	}
-}
-
-func TestASNPublic(t *testing.T) {
-	var tests = []struct {
-		a  ASN
-		ok bool
-	}{
-		{
-			a: math.MinInt64,
-		},
-		{
-			a: math.MaxInt64,
-		},
-		{
-			a: privateMin16Bit,
-		},
-		{
-			a: privateMin16Bit + 1,
-		},
-		{
-			a: privateMax16Bit - 1,
-		},
-		{
-			a: privateMax16Bit,
-		},
-		{
-			a: privateMin32Bit,
-		},
-		{
-			a: privateMax32Bit,
-		},
-		{
-			a: reservedStart16Bit,
-		},
-		{
-			a: reservedMin16Bit,
-		},
-		{
-			a: reservedMin16Bit + 1,
-		},
-		{
-			a: reservedMax16Bit - 1,
-		},
-		{
-			a: reservedMax16Bit,
-		},
-		{
-			a: reservedDocMin16Bit,
-		},
-		{
-			a: reservedDocMin16Bit + 1,
-		},
-		{
-			a: reservedDocMax16Bit - 1,
-		},
-		{
-			a: reservedDocMax16Bit,
-		},
-		{
-			a: reservedEnd16Bit,
-		},
-		{
-			a: reservedDocMin32Bit,
-		},
-		{
-			a: reservedDocMin32Bit + 1,
-		},
-		{
-			a: reservedDocMax32Bit - 1,
-		},
-		{
-			a: reservedDocMax32Bit,
-		},
-		{
-			a: reservedMin32Bit,
-		},
-		{
-			a: reservedMin32Bit + 1,
-		},
-		{
-			a: reservedMax32Bit - 1,
-		},
-		{
-			a: reservedMax32Bit,
-		},
-		{
-			a: reservedEnd32Bit,
-		},
-		{
-			a: asTrans,
-		},
-		{
-			a:  reservedStart16Bit + 1,
-			ok: true,
-		},
-		{
-			a:  reservedMin16Bit - 1,
-			ok: true,
-		},
-		{
-			a:  reservedMax32Bit + 1,
-			ok: true,
-		},
-		{
-			a:  privateMin32Bit - 1,
-			ok: true,
-		},
-	}
-
-	for _, tt := range tests {
-		if want, got := tt.ok, tt.a.Public(); want != got {
-			t.Fatalf("unexpected ASN(%d).Public():\n- want: %v\n-  got: %v",
-				tt.a, want, got)
-		}
-	}
-}
-
-func TestASNReserved(t *testing.T) {
-	var tests = []struct {
-		a  ASN
-		ok bool
-	}{
-		{
-			a: math.MinInt64,
-		},
-		{
-			a: math.MaxInt64,
-		},
-		{
-			a:  reservedStart16Bit,
-			ok: true,
-		},
-		{
-			a:  reservedMin16Bit,
-			ok: true,
-		},
-		{
-			a:  reservedMin16Bit + 1,
-			ok: true,
-		},
-		{
-			a:  reservedMax16Bit - 1,
-			ok: true,
-		},
-		{
-			a:  reservedMax16Bit,
-			ok: true,
-		},
-		{
-			a:  reservedDocMin16Bit,
-			ok: true,
-		},
-		{
-			a:  reservedDocMin16Bit + 1,
-			ok: true,
-		},
-		{
-			a:  reservedDocMax16Bit - 1,
-			ok: true,
-		},
-		{
-			a:  reservedDocMax16Bit,
-			ok: true,
-		},
-		{
-			a:  reservedEnd16Bit,
-			ok: true,
-		},
-		{
-			a:  reservedDocMin32Bit,
-			ok: true,
-		},
-		{
-			a:  reservedDocMin32Bit + 1,
-			ok: true,
-		},
-		{
-			a:  reservedDocMax32Bit - 1,
-			ok: true,
-		},
-		{
-			a:  reservedDocMax32Bit,
-			ok: true,
-		},
-		{
-			a:  reservedMin32Bit,
-			ok: true,
-		},
-		{
-			a:  reservedMin32Bit + 1,
-			ok: true,
-		},
-		{
-			a:  reservedMax32Bit - 1,
-			ok: true,
-		},
-		{
-			a:  reservedMax32Bit,
-			ok: true,
-		},
-		{
-			a:  reservedEnd32Bit,
-			ok: true,
-		},
-		{
-			a:  asTrans,
-			ok: true,
-		},
-	}
-
-	for _, tt := range tests {
-		if want, got := tt.ok, tt.a.Reserved(); want != got {
-			t.Fatalf("unexpected ASN(%d).Reserved():\n- want: %v\n-  got: %v",
-				tt.a, want, got)
-		}
-	}
-}
diff --git a/client.go b/client.go
deleted file mode 100644
index 6e1f36f2ee20086103803e55401f8fe89b5bad18..0000000000000000000000000000000000000000
--- a/client.go
+++ /dev/null
@@ -1,115 +0,0 @@
-// Copyright 2016 The go-netbox Authors.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//   http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package netbox
-
-import (
-	"encoding/json"
-	"net/http"
-	"net/url"
-	"path"
-)
-
-// A Client is a NetBox client.  It can be used to retrieve network and
-// datacenter infrastructure information from a NetBox server.
-type Client struct {
-	// DCIM provides access to methods in NetBox's DCIM API.
-	DCIM *DCIMService
-
-	// IPAM provides access to methods in NetBox's IPAM API.
-	IPAM *IPAMService
-
-	u      *url.URL
-	client *http.Client
-}
-
-// NewClient returns a new instance of a NetBox client.  addr specifies the address
-// of the NetBox server, and client specifies an optional HTTP client to use
-// for requests.
-//
-// If client is nil, a default HTTP client will be used.
-func NewClient(addr string, client *http.Client) (*Client, error) {
-	if client == nil {
-		client = &http.Client{}
-	}
-
-	u, err := url.Parse(addr)
-	if err != nil {
-		return nil, err
-	}
-
-	c := &Client{
-		u:      u,
-		client: client,
-	}
-
-	c.DCIM = &DCIMService{c: c}
-	c.IPAM = &IPAMService{c: c}
-
-	return c, nil
-}
-
-// NewRequest creates a HTTP request using the input HTTP method, URL
-// endpoint, and a Valuer which creates URL parameters for the request.
-//
-// If a nil Valuer is specified, no query parameters will be sent with the
-// request.
-func (c *Client) NewRequest(method string, endpoint string, options Valuer) (*http.Request, error) {
-	rel, err := url.Parse(endpoint)
-	if err != nil {
-		return nil, err
-	}
-
-	// Allow specifying a base path for API requests, so if a NetBox server
-	// resides at a path like http://example.com/netbox/, API requests will
-	// be sent to http://example.com/netbox/api/...
-	//
-	// Enables support of: https://github.com/digitalocean/netbox/issues/212.
-	if c.u.Path != "" {
-		rel.Path = path.Join(c.u.Path, rel.Path)
-	}
-
-	u := c.u.ResolveReference(rel)
-
-	// If no valuer specified, create a request with no query parameters
-	if options == nil {
-		return http.NewRequest(method, u.String(), nil)
-	}
-
-	values, err := options.Values()
-	if err != nil {
-		return nil, err
-	}
-	u.RawQuery = values.Encode()
-
-	return http.NewRequest(method, u.String(), nil)
-}
-
-// Do executes an HTTP request and if v is not nil, Do unmarshals result
-// JSON onto v.
-func (c *Client) Do(req *http.Request, v interface{}) error {
-	res, err := c.client.Do(req)
-	if err != nil {
-		return err
-	}
-	defer func() {
-		_ = res.Body.Close()
-	}()
-
-	if v == nil {
-		return nil
-	}
-
-	return json.NewDecoder(res.Body).Decode(v)
-}
diff --git a/client_test.go b/client_test.go
deleted file mode 100644
index d94fb981d643904563666e700786182222cd1cb6..0000000000000000000000000000000000000000
--- a/client_test.go
+++ /dev/null
@@ -1,486 +0,0 @@
-// Copyright 2016 The go-netbox Authors.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//   http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package netbox
-
-import (
-	"encoding/json"
-	"fmt"
-	"net"
-	"net/http"
-	"net/http/httptest"
-	"net/url"
-	"strconv"
-	"testing"
-	"time"
-)
-
-func TestClientBadJSON(t *testing.T) {
-	c, done := testClient(t, func(w http.ResponseWriter, r *http.Request) {
-		w.Write([]byte("foo"))
-	})
-	defer done()
-
-	req, err := c.NewRequest(http.MethodGet, "/", nil)
-	if err != nil {
-		t.Fatal("expected an error, but no error returned")
-	}
-
-	// Pass empty struct to trigger JSON unmarshaling path
-	var v struct{}
-
-	err = c.Do(req, &v)
-	if _, ok := err.(*json.SyntaxError); !ok {
-		t.Fatalf("unexpected error type: %T", err)
-	}
-}
-
-func TestClientQueryParameters(t *testing.T) {
-	c := &Client{
-		u:      &url.URL{},
-		client: &http.Client{},
-	}
-
-	const (
-		wantFoo = "foo"
-		wantBar = 1
-	)
-
-	req, err := c.NewRequest(http.MethodGet, "/", testValuer{
-		Foo: wantFoo,
-		Bar: wantBar,
-	})
-	if err != nil {
-		t.Fatal("expected an error, but no error returned")
-	}
-
-	q := req.URL.Query()
-	if want, got := 2, len(q); want != got {
-		t.Fatalf("unexpected number of query parameters:\n- want: %v\n-  got: %v",
-			want, got)
-	}
-
-	if want, got := wantFoo, q.Get("foo"); want != got {
-		t.Fatalf("unexpected foo:\n- want: %v\n-  got: %v", want, got)
-	}
-
-	if want, got := strconv.Itoa(wantBar), q.Get("bar"); want != got {
-		t.Fatalf("unexpected bar:\n- want: %v\n-  got: %v", want, got)
-	}
-}
-
-func TestClientPrependBaseURLPath(t *testing.T) {
-	u, err := url.Parse("http://example.com/netbox/")
-	if err != nil {
-		t.Fatalf("unexpected error: %v", err)
-	}
-
-	c := &Client{
-		u:      u,
-		client: &http.Client{},
-	}
-
-	req, err := c.NewRequest(http.MethodGet, "/api/ipam/vlans", nil)
-	if err != nil {
-		t.Fatal("expected an error, but no error returned")
-	}
-
-	if want, got := "/netbox/api/ipam/vlans", req.URL.Path; want != got {
-		t.Fatalf("unexpected URL path:\n- want: %q\n-  got: %q",
-			want, got)
-	}
-}
-
-type testValuer struct {
-	Foo string
-	Bar int
-}
-
-func (q testValuer) Values() (url.Values, error) {
-	v := url.Values{}
-
-	if q.Foo != "" {
-		v.Set("foo", q.Foo)
-	}
-
-	if q.Bar != 0 {
-		v.Set("bar", strconv.Itoa(q.Bar))
-	}
-
-	return v, nil
-}
-
-func testClient(t *testing.T, fn func(w http.ResponseWriter, r *http.Request)) (*Client, func()) {
-	s := httptest.NewServer(http.HandlerFunc(fn))
-
-	c, err := NewClient(s.URL, nil)
-	if err != nil {
-		t.Fatalf("error creating Client: %v", err)
-	}
-
-	return c, func() { s.Close() }
-}
-
-func testHandler(t *testing.T, method string, path string, v interface{}) http.HandlerFunc {
-	return func(w http.ResponseWriter, r *http.Request) {
-		if want, got := method, r.Method; want != got {
-			t.Fatalf("unexpected HTTP method:\n- want: %v\n-  got: %v", want, got)
-		}
-
-		if want, got := path, r.URL.Path; want != got {
-			t.Fatalf("unexpected URL path:\n- want: %v\n-  got: %v", want, got)
-		}
-
-		if err := json.NewEncoder(w).Encode(v); err != nil {
-			t.Fatalf("error while encoding JSON: %v", err)
-		}
-	}
-}
-
-// Test helpers for generating mock data
-
-func testAggregate(family Family, n int) *Aggregate {
-	prefix := &net.IPNet{
-		IP:   net.IPv4(8, 0, 0, 0),
-		Mask: net.CIDRMask(8, 32),
-	}
-	if family == FamilyIPv6 {
-		prefix = &net.IPNet{
-			IP:   net.ParseIP("2001::"),
-			Mask: net.CIDRMask(16, 128),
-		}
-	}
-
-	return &Aggregate{
-		ID:          n,
-		Family:      family,
-		Prefix:      prefix,
-		RIR:         testRIRIdentifier(n),
-		DateAdded:   time.Date(2016, time.January, 1, 0, 0, 0, 0, time.UTC),
-		Description: fmt.Sprintf("description %d", n),
-	}
-}
-
-func testConsolePort(n int) *ConsolePort {
-	return &ConsolePort{
-		ID:               n,
-		Device:           testDeviceIdentifier(n),
-		Name:             fmt.Sprintf("deviceport %d", n),
-		CSPort:           testConsoleServerPort(n),
-		ConnectionStatus: true,
-	}
-}
-
-func testConsolePortIdentifier(n int) *ConsolePortIdentifier {
-	return &ConsolePortIdentifier{
-		Device: fmt.Sprintf("device %d", n),
-		Name:   fmt.Sprintf("rc console port %d", n),
-		Port:   fmt.Sprintf("port %d", n),
-	}
-}
-
-func testConsoleServerPort(n int) *ConsoleServerPort {
-	return &ConsoleServerPort{
-		ID:     n,
-		Device: testDeviceIdentifier(n),
-		Name:   fmt.Sprintf("consoleserverport %d", n),
-	}
-}
-
-func testDevice(n int) *Device {
-	return &Device{
-		ID:           n,
-		Name:         fmt.Sprintf("device %d", n),
-		DisplayName:  fmt.Sprintf("Device %d", n),
-		DeviceType:   testDeviceTypeIdentifier(n),
-		DeviceRole:   testSimpleIdentifier(n),
-		Platform:     testSimpleIdentifier(n),
-		Serial:       fmt.Sprintf("relatedconnection%d", n),
-		Rack:         testRackIdentifier(n),
-		Position:     n,
-		Face:         n,
-		ParentDevice: testDeviceIdentifier(n),
-		Status:       true,
-		PrimaryIP:    testIPAddressIdentifier(FamilyIPv4, n),
-		PrimaryIP4:   testIPAddressIdentifier(FamilyIPv4, n),
-		PrimaryIP6:   testIPAddressIdentifier(FamilyIPv6, n),
-		Comments:     "",
-	}
-}
-
-func testDeviceIdentifier(n int) *DeviceIdentifier {
-	return &DeviceIdentifier{
-		ID:   n,
-		Name: fmt.Sprintf("DeviceIdentifier %d", n),
-	}
-}
-
-func testDeviceTypeIdentifier(n int) *DeviceTypeIdentifier {
-	return &DeviceTypeIdentifier{
-		ID:           n,
-		Manufacturer: testSimpleIdentifier(n),
-		Model:        fmt.Sprintf("device model %d", n),
-		Slug:         fmt.Sprintf("devicetype%d", n),
-	}
-}
-
-func testInterface(n int) *Interface {
-	return &Interface{
-		ID:                 n,
-		Name:               fmt.Sprintf("interface %d", n),
-		FormFactor:         fmt.Sprintf("form factor %d", n),
-		MacAddress:         fmt.Sprintf("f4:9d:82:9e:34:c%d", n),
-		MgmtOnly:           true,
-		Description:        fmt.Sprintf("Description %d", n),
-		IsConnected:        true,
-		ConnectedInterface: testInterfaceDetail(n),
-	}
-}
-
-func testInterfaceDetail(n int) *InterfaceDetail {
-	return &InterfaceDetail{
-		ID:          n,
-		Device:      testDeviceIdentifier(n),
-		Name:        fmt.Sprintf("interfacedetail %d", n),
-		FormFactor:  fmt.Sprintf("form factor %d", n),
-		MacAddress:  fmt.Sprintf("f4:9d:82:9e:34:c%d", n),
-		MgmtOnly:    true,
-		Description: fmt.Sprintf("Description %d", n),
-		IsConnected: true,
-	}
-}
-
-func testInterfaceIdentifier(n int) *InterfaceIdentifier {
-	return &InterfaceIdentifier{
-		ID:     n,
-		Device: testDeviceIdentifier(n),
-		Name:   fmt.Sprintf("InterfaceIdentifier %d", n),
-	}
-}
-
-func testIPAddress(family Family, n int) *IPAddress {
-	address := &net.IPNet{
-		IP:   net.IPv4(8, 8, 8, 0),
-		Mask: net.CIDRMask(24, 32),
-	}
-	if family == FamilyIPv6 {
-		address = &net.IPNet{
-			IP:   net.ParseIP("2001:4860:4860::"),
-			Mask: net.CIDRMask(48, 128),
-		}
-	}
-
-	return &IPAddress{
-		ID:          n,
-		Family:      family,
-		Address:     address,
-		VRF:         testVRFIdentifier(n),
-		Interface:   testInterfaceIdentifier(n),
-		Description: fmt.Sprintf("description %d", n),
-		NATInside:   testIPAddressIdentifier(family, n),
-		NATOutside:  testIPAddressIdentifier(family, n),
-	}
-}
-
-func testIPAddressIdentifier(family Family, n int) *IPAddressIdentifier {
-	address := &net.IPNet{
-		IP:   net.IPv4(8, 8, 8, 0),
-		Mask: net.CIDRMask(24, 32),
-	}
-	if family == FamilyIPv6 {
-		address = &net.IPNet{
-			IP:   net.ParseIP("2001:4860:4860::"),
-			Mask: net.CIDRMask(48, 128),
-		}
-	}
-
-	return &IPAddressIdentifier{
-		ID:      n,
-		Family:  family,
-		Address: address,
-	}
-}
-
-func testPrefix(family Family, n int) *Prefix {
-	prefix := &net.IPNet{
-		IP:   net.IPv4(8, 8, 0, 0),
-		Mask: net.CIDRMask(16, 32),
-	}
-	if family == FamilyIPv6 {
-		prefix = &net.IPNet{
-			IP:   net.ParseIP("2001:4860::"),
-			Mask: net.CIDRMask(32, 128),
-		}
-	}
-
-	return &Prefix{
-		ID:          n,
-		Family:      family,
-		Prefix:      prefix,
-		Site:        testSiteIdentifier(n),
-		VRF:         testVRFIdentifier(n),
-		VLAN:        testVLANIdentifier(n),
-		Status:      StatusActive,
-		Role:        testRoleIdentifier(n),
-		Description: fmt.Sprintf("description %d", n),
-	}
-}
-
-func testRackIdentifier(n int) *RackIdentifier {
-	return &RackIdentifier{
-		ID:          n,
-		Name:        fmt.Sprintf("rack %d", n),
-		FacilityID:  fmt.Sprintf("facility%d", n),
-		DisplayName: fmt.Sprintf("Rack %d", n),
-	}
-}
-
-func testPowerOutlet(n int) *PowerOutletIdentifier {
-	return &PowerOutletIdentifier{
-		ID:     n,
-		Device: testDeviceIdentifier(n),
-		Name:   fmt.Sprintf("poweroutlet %d", n),
-	}
-}
-
-func testPowerPort(n int) *PowerPort {
-	return &PowerPort{
-		ID:               n,
-		Name:             fmt.Sprintf("powerport %d", n),
-		PowerOutlet:      testPowerOutlet(n),
-		ConnectionStatus: true,
-	}
-}
-
-func testPowerPortIdentifier(n int) *PowerPortIdentifier {
-	return &PowerPortIdentifier{
-		Device: fmt.Sprintf("device %d", n),
-		Name:   fmt.Sprintf("rc power port %d", n),
-		Outlet: fmt.Sprintf("outlet %d", n),
-	}
-}
-
-func testRelatedConnection(n int) *RelatedConnection {
-	return &RelatedConnection{
-		Device:       testDevice(n),
-		ConsolePorts: []*ConsolePort{testConsolePort(n)},
-		Interfaces:   []*Interface{testInterface(n)},
-		PowerPorts:   []*PowerPort{testPowerPort(n)},
-	}
-}
-
-func testRIR(n int) *RIR {
-	return &RIR{
-		ID:   n,
-		Name: fmt.Sprintf("RIR %d", n),
-		Slug: fmt.Sprintf("rir%d", n),
-	}
-}
-
-func testRIRIdentifier(n int) *RIRIdentifier {
-	return &RIRIdentifier{
-		ID:   n,
-		Name: fmt.Sprintf("RIRIdentifier %d", n),
-		Slug: fmt.Sprintf("riridentifier%d", n),
-	}
-}
-
-func testRole(n int) *Role {
-	return &Role{
-		ID:     n,
-		Name:   fmt.Sprintf("Role %d", n),
-		Slug:   fmt.Sprintf("role%d", n),
-		Weight: n,
-	}
-}
-
-func testRoleIdentifier(n int) *RoleIdentifier {
-	return &RoleIdentifier{
-		ID:   n,
-		Name: fmt.Sprintf("RoleIdentifier %d", n),
-		Slug: fmt.Sprintf("roleidentifier%d", n),
-	}
-}
-
-func testSimpleIdentifier(n int) *SimpleIdentifier {
-	return &SimpleIdentifier{
-		ID:   n,
-		Name: fmt.Sprintf("simple %d", n),
-		Slug: fmt.Sprintf("simple%d", n),
-	}
-}
-
-func testSite(n int) *Site {
-	return &Site{
-		ID:              n,
-		Name:            fmt.Sprintf("Site %d", n),
-		Slug:            fmt.Sprintf("site%d", n),
-		Facility:        fmt.Sprintf("Facility %d", n),
-		ASN:             ASN(n),
-		PhysicalAddress: fmt.Sprintf("%d Facility Street, City, State 12345", n),
-		ShippingAddress: fmt.Sprintf("%d Facility Street, ATTN: Shipping & Receiving, City, State 12345", n),
-		Comments:        fmt.Sprintf("comment %d", n),
-		CountPrefixes:   n,
-		CountVLANs:      n,
-		CountRacks:      n,
-		CountDevices:    n,
-		CountCircuits:   n,
-	}
-}
-
-func testSiteIdentifier(n int) *SiteIdentifier {
-	return &SiteIdentifier{
-		ID:   n,
-		Name: fmt.Sprintf("SiteIdentifier %d", n),
-		Slug: fmt.Sprintf("siteidentifier%d", n),
-	}
-}
-
-func testVLAN(n int) *VLAN {
-	return &VLAN{
-		ID:          n,
-		Site:        testSiteIdentifier(n),
-		VID:         VLANID(n),
-		Name:        fmt.Sprintf("vlan %d", n),
-		Status:      StatusActive,
-		Role:        testRoleIdentifier(n),
-		DisplayName: fmt.Sprintf("VLAN %d", n),
-	}
-}
-
-func testVLANIdentifier(n int) *VLANIdentifier {
-	return &VLANIdentifier{
-		ID:          n,
-		VID:         VLANID(n),
-		Name:        fmt.Sprintf("vlanidentifier %d", n),
-		DisplayName: fmt.Sprintf("VLANIdentifier %d", n),
-	}
-}
-
-func testVRF(n int) *VRF {
-	return &VRF{
-		ID:          n,
-		Name:        fmt.Sprintf("VRF %d", n),
-		RD:          fmt.Sprintf("rd %d", n),
-		Description: fmt.Sprintf("description %d", n),
-	}
-}
-
-func testVRFIdentifier(n int) *VRFIdentifier {
-	return &VRFIdentifier{
-		ID:   n,
-		Name: fmt.Sprintf("VRFIdentifier %d", n),
-		RD:   fmt.Sprintf("rd %d", n),
-	}
-}
diff --git a/dcim_consoleports.go b/dcim_consoleports.go
deleted file mode 100644
index 63791c2bce9cd1a0ffc5841bd8e3cb80ff43fe36..0000000000000000000000000000000000000000
--- a/dcim_consoleports.go
+++ /dev/null
@@ -1,31 +0,0 @@
-// Copyright 2016 The go-netbox Authors.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//   http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package netbox
-
-// ConsolePort represents a console port object.
-type ConsolePort struct {
-	ID               int                `json:"id"`
-	Device           *DeviceIdentifier  `json:"device"`
-	Name             string             `json:"name"`
-	CSPort           *ConsoleServerPort `json:"cs_port"`
-	ConnectionStatus bool               `json:"connection_status"`
-}
-
-// ConsoleServerPort represents a console server port object.
-type ConsoleServerPort struct {
-	ID     int               `json:"id"`
-	Device *DeviceIdentifier `json:"device"`
-	Name   string            `json:"name"`
-}
diff --git a/dcim_devices.go b/dcim_devices.go
deleted file mode 100644
index d7adcd2133c64f8f67a32f8eac0e1c441b40f146..0000000000000000000000000000000000000000
--- a/dcim_devices.go
+++ /dev/null
@@ -1,59 +0,0 @@
-// Copyright 2016 The go-netbox Authors.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//   http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package netbox
-
-// Device is a network device.
-type Device struct {
-	ID           int                   `json:"id"`
-	Name         string                `json:"name"`
-	DisplayName  string                `json:"display_name"`
-	DeviceType   *DeviceTypeIdentifier `json:"device_type"`
-	DeviceRole   *SimpleIdentifier     `json:"device_role"`
-	Platform     *SimpleIdentifier     `json:"platform"`
-	Serial       string                `json:"serial"`
-	Rack         *RackIdentifier       `json:"rack"`
-	Position     int                   `json:"position"`
-	Face         int                   `json:"face"`
-	ParentDevice *DeviceIdentifier     `json:"parent_device"`
-	Status       bool                  `json:"status"`
-	PrimaryIP    *IPAddressIdentifier  `json:"primary_ip"`
-	PrimaryIP4   *IPAddressIdentifier  `json:"primary_ip4"`
-	PrimaryIP6   *IPAddressIdentifier  `json:"primary_ip6"`
-	Comments     string                `json:"comments"`
-}
-
-// A DeviceIdentifier is a reduced version of a Device, returned as a nested
-// object in some top-level objects.  It contains information which can
-// be used in subsequent API calls to identify and retrieve a full Device.
-type DeviceIdentifier struct {
-	ID   int    `json:"id"`
-	Name string `json:"name"`
-}
-
-// A DeviceTypeIdentifier indicates the device type of a network device.
-type DeviceTypeIdentifier struct {
-	ID           int               `json:"id"`
-	Manufacturer *SimpleIdentifier `json:"manufacturer"`
-	Model        string            `json:"model"`
-	Slug         string            `json:"slug"`
-}
-
-// RackIdentifier represents a server rack.
-type RackIdentifier struct {
-	ID          int    `json:"id"`
-	Name        string `json:"name"`
-	FacilityID  string `json:"facility_id"`
-	DisplayName string `json:"display_name"`
-}
diff --git a/dcim_devices_test.go b/dcim_devices_test.go
deleted file mode 100644
index df3dc6bbd5987df9982aa291c147749b0c1975c5..0000000000000000000000000000000000000000
--- a/dcim_devices_test.go
+++ /dev/null
@@ -1,57 +0,0 @@
-// Copyright 2016 The go-netbox Authors.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//   http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package netbox
-
-import "reflect"
-
-func deviceEqual(a, b *Device) bool {
-	var obsAB = []struct {
-		a interface{}
-		b interface{}
-	}{
-		{a: a.DeviceType, b: b.DeviceType},
-		{a: a.DeviceRole, b: b.DeviceRole},
-		{a: a.Platform, b: b.Platform},
-		{a: a.Rack, b: b.Rack},
-		{a: a.ParentDevice, b: b.ParentDevice},
-		{a: a.ID, b: b.ID},
-		{a: a.Name, b: b.Name},
-		{a: a.DisplayName, b: b.DisplayName},
-		{a: a.Serial, b: b.Serial},
-		{a: a.Position, b: b.Position},
-		{a: a.Face, b: b.Face},
-		{a: a.Status, b: b.Status},
-		{a: a.Comments, b: b.Comments},
-		{a: a.PrimaryIP, b: b.PrimaryIP},
-		{a: a.PrimaryIP4, b: b.PrimaryIP4},
-		{a: a.PrimaryIP6, b: b.PrimaryIP6},
-	}
-	for _, o := range obsAB {
-
-		switch o.a.(type) {
-		case *IPAddressIdentifier:
-			i, j := o.a.(*IPAddressIdentifier), o.b.(*IPAddressIdentifier)
-			if ok := ipAddressIdentifiersEqual(*i, *j); !ok {
-				return false
-			}
-		default:
-			if !reflect.DeepEqual(o.a, o.b) {
-				return false
-			}
-		}
-	}
-
-	return true
-}
diff --git a/dcim_interfaces.go b/dcim_interfaces.go
deleted file mode 100644
index e1758e0f5e75675b2a992aafc231d179c3b3a537..0000000000000000000000000000000000000000
--- a/dcim_interfaces.go
+++ /dev/null
@@ -1,48 +0,0 @@
-// Copyright 2016 The go-netbox Authors.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//   http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package netbox
-
-// Interface represents an interface object.
-type Interface struct {
-	ID                 int              `json:"id"`
-	Name               string           `json:"name"`
-	FormFactor         string           `json:"form_factor"`
-	MacAddress         string           `json:"mac_address"`
-	MgmtOnly           bool             `json:"mgmt_only"`
-	Description        string           `json:"description"`
-	IsConnected        bool             `json:"is_connected"`
-	ConnectedInterface *InterfaceDetail `json:"connected_interface"`
-}
-
-// InterfaceDetail represents an interface-detail object.
-type InterfaceDetail struct {
-	ID          int               `json:"id"`
-	Device      *DeviceIdentifier `json:"device"`
-	Name        string            `json:"name"`
-	FormFactor  string            `json:"form_factor"`
-	MacAddress  string            `json:"mac_address"`
-	MgmtOnly    bool              `json:"mgmt_only"`
-	Description string            `json:"description"`
-	IsConnected bool              `json:"is_connected"`
-}
-
-// An InterfaceIdentifier is a reduced version of an Interface, returned as a
-// nested object in some top-level objects.  It contains information which can
-// be used in subsequent API calls to identify and retrieve a full Interface.
-type InterfaceIdentifier struct {
-	ID     int               `json:"id"`
-	Device *DeviceIdentifier `json:"device"`
-	Name   string            `json:"name"`
-}
diff --git a/dcim_related-connections.go b/dcim_related-connections.go
deleted file mode 100644
index 5a75f1fd5abe9ccbcdc7584652c1fd3a85e2c45e..0000000000000000000000000000000000000000
--- a/dcim_related-connections.go
+++ /dev/null
@@ -1,95 +0,0 @@
-// Copyright 2016 The go-netbox Authors.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//   http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package netbox
-
-import (
-	"errors"
-	"net/http"
-	"net/url"
-)
-
-// RelatedConnection represents components that have a related peer-device and
-// peer-interface.
-type RelatedConnection struct {
-	Device       *Device        `json:"device"`
-	ConsolePorts []*ConsolePort `json:"console-ports"`
-	Interfaces   []*Interface   `json:"interfaces"`
-	PowerPorts   []*PowerPort   `json:"power-ports"`
-}
-
-// ConsolePortIdentifier represents a reduced version of a console port.
-type ConsolePortIdentifier struct {
-	Device string `json:"device"`
-	Name   string `json:"name"`
-	Port   string `json:"port"`
-}
-
-// PowerPortIdentifier represents a reduced version of a single power port.
-type PowerPortIdentifier struct {
-	Device string `json:"device"`
-	Name   string `json:"name"`
-	Outlet string `json:"outlet"`
-}
-
-// GetRelatedConnections retrieves a RelatedConnection object from NetBox.
-func (s *DCIMService) GetRelatedConnections(
-	peerDevice, peerInterface string,
-) (*RelatedConnection, error) {
-	req, err := s.c.NewRequest(
-		http.MethodGet,
-		"/api/dcim/related-connections/",
-		&relatedConnectionsOptions{
-			peerDevice,
-			peerInterface,
-		},
-	)
-	if err != nil {
-		return nil, err
-	}
-
-	rc := new(RelatedConnection)
-	err = s.c.Do(req, rc)
-	if err != nil {
-		return nil, err
-	}
-	return rc, nil
-}
-
-// relatedConnectionsOptions is used as an argument for
-// Client.DCIM.GetRelatedConnections.
-type relatedConnectionsOptions struct {
-	PeerDevice    string
-	PeerInterface string
-}
-
-func (o *relatedConnectionsOptions) Values() (url.Values, error) {
-	err := errors.New(
-		"must provide non-zero values for both peer-device and peer-interface",
-	)
-	if o == nil {
-		return nil, err
-	}
-
-	if o.PeerDevice == "" || o.PeerInterface == "" {
-		return nil, err
-	}
-	v := url.Values{}
-
-	v.Set("peer-device", o.PeerDevice)
-
-	v.Set("peer-interface", o.PeerInterface)
-
-	return v, nil
-}
diff --git a/dcim_related-connections_test.go b/dcim_related-connections_test.go
deleted file mode 100644
index 8c8f515c5c722eb3004532fbc5f9ec07f692132f..0000000000000000000000000000000000000000
--- a/dcim_related-connections_test.go
+++ /dev/null
@@ -1,152 +0,0 @@
-// Copyright 2016 The go-netbox Authors.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//   http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package netbox
-
-import (
-	"net/http"
-	"net/url"
-	"reflect"
-	"testing"
-)
-
-func TestGetRelatedConnection(t *testing.T) {
-	want := testRelatedConnection(1)
-
-	c, done := testClient(
-		t,
-		testHandler(t, http.MethodGet, "/api/dcim/related-connections/", want),
-	)
-	defer done()
-
-	got, err := c.DCIM.GetRelatedConnections("device1", "interface1")
-	if err != nil {
-		t.Fatalf("unexpected error from Client.DCIM.GetRelatedConnections: %v", err)
-	}
-
-	if !rcEqual(want, got) {
-		t.Fatalf(
-			"unexpected related-connections payload:\n- want: %v\n- got: %v",
-			*want,
-			*got,
-		)
-	}
-}
-
-func TestGetRelatedConnectionWithOptions(t *testing.T) {
-	var happyPathTests = []struct {
-		desc string
-		o    *relatedConnectionsOptions
-		want url.Values
-	}{
-		{
-			desc: "normal string length",
-			o: &relatedConnectionsOptions{
-				PeerDevice:    "test-abc_def-0123456789",
-				PeerInterface: "test-zyx-wvu-987654321",
-			},
-			want: url.Values{
-				"peer-device":    []string{"test-abc_def-0123456789"},
-				"peer-interface": []string{"test-zyx-wvu-987654321"},
-			},
-		},
-		{
-			desc: "super long string",
-			o: &relatedConnectionsOptions{
-				PeerDevice:    "test-abc_def-01234567890000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
-				PeerInterface: "test-zyx-wvu-98765432100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
-			},
-			want: url.Values{
-				"peer-device":    []string{"test-abc_def-01234567890000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"},
-				"peer-interface": []string{"test-zyx-wvu-98765432100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"},
-			},
-		},
-		{
-			desc: "super short string",
-			o: &relatedConnectionsOptions{
-				PeerDevice:    "a",
-				PeerInterface: "z",
-			},
-			want: url.Values{
-				"peer-device":    []string{"a"},
-				"peer-interface": []string{"z"},
-			},
-		},
-	}
-	for i, test := range happyPathTests {
-		t.Logf("[%02d] happy path test %q", i, test.desc)
-
-		got, err := test.o.Values()
-		if err != nil {
-			t.Fatalf("unexpected Values error: %v", err)
-		}
-
-		if !reflect.DeepEqual(got, test.want) {
-			t.Fatalf(
-				"unexpected url.Values map:\n- want: %v\n-  got: %v",
-				test.want,
-				got,
-			)
-		}
-	}
-
-	var negativeTests = []struct {
-		desc string
-		o    *relatedConnectionsOptions
-	}{
-		{
-			desc: "only peer-device",
-			o: &relatedConnectionsOptions{
-				PeerDevice: "test-abc_def-0123456789",
-			},
-		},
-		{
-			desc: "only peer-interface",
-			o: &relatedConnectionsOptions{
-				PeerInterface: "test-zyx-wvu-987654321",
-			},
-		},
-		{
-			desc: "no peer-device or peer-interface",
-			o:    &relatedConnectionsOptions{},
-		},
-	}
-	for i, test := range negativeTests {
-		t.Logf("[%02d] negative path test %q", i, test.desc)
-
-		if _, err := test.o.Values(); err == nil {
-			t.Fatal("expected Error but got nil")
-		}
-	}
-}
-
-func rcEqual(a, b *RelatedConnection) bool {
-	if ok := deviceEqual(a.Device, b.Device); !ok {
-		return false
-	}
-
-	if !reflect.DeepEqual(a.ConsolePorts, b.ConsolePorts) {
-		return false
-	}
-
-	if !reflect.DeepEqual(a.Interfaces, b.Interfaces) {
-		return false
-	}
-
-	if !reflect.DeepEqual(a.PowerPorts, b.PowerPorts) {
-		return false
-	}
-
-	return true
-}
diff --git a/dcim_sites.go b/dcim_sites.go
deleted file mode 100644
index 11d995d1a26e4fa9f6cb6511fbe814cae6eb7f1f..0000000000000000000000000000000000000000
--- a/dcim_sites.go
+++ /dev/null
@@ -1,74 +0,0 @@
-// Copyright 2016 The go-netbox Authors.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//   http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package netbox
-
-import (
-	"fmt"
-	"net/http"
-)
-
-// A Site is a physical location where devices may reside.
-type Site struct {
-	ID              int    `json:"id"`
-	Name            string `json:"name"`
-	Slug            string `json:"slug"`
-	Facility        string `json:"facility"`
-	ASN             ASN    `json:"asn"`
-	PhysicalAddress string `json:"physical_address"`
-	ShippingAddress string `json:"shipping_address"`
-	Comments        string `json:"comments"`
-	CountPrefixes   int    `json:"count_prefixes"`
-	CountVLANs      int    `json:"count_vlans"`
-	CountRacks      int    `json:"count_racks"`
-	CountDevices    int    `json:"count_devices"`
-	CountCircuits   int    `json:"count_circuits"`
-}
-
-// A SiteIdentifier is a reduced version of a Site, returned as a nested
-// object in some top-level objects.  It contains information which can
-// be used in subsequent API calls to identify and retrieve a full Site.
-type SiteIdentifier struct {
-	ID   int    `json:"id"`
-	Name string `json:"name"`
-	Slug string `json:"slug"`
-}
-
-// GetSite retrieves a Site object from NetBox by its ID.
-func (s *DCIMService) GetSite(id int) (*Site, error) {
-	req, err := s.c.NewRequest(
-		http.MethodGet,
-		fmt.Sprintf("/api/dcim/sites/%d", id),
-		nil,
-	)
-	if err != nil {
-		return nil, err
-	}
-
-	st := new(Site)
-	err = s.c.Do(req, st)
-	return st, err
-}
-
-// ListSites retrives a list of Site objects from NetBox.
-func (s *DCIMService) ListSites() ([]*Site, error) {
-	req, err := s.c.NewRequest(http.MethodGet, "/api/dcim/sites/", nil)
-	if err != nil {
-		return nil, err
-	}
-
-	var sts []*Site
-	err = s.c.Do(req, &sts)
-	return sts, err
-}
diff --git a/dcim_sites_test.go b/dcim_sites_test.go
deleted file mode 100644
index 4a169b5667f069b0526c301491fb94740c1d9a31..0000000000000000000000000000000000000000
--- a/dcim_sites_test.go
+++ /dev/null
@@ -1,67 +0,0 @@
-// Copyright 2016 The go-netbox Authors.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//   http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package netbox
-
-import (
-	"net/http"
-	"reflect"
-	"testing"
-)
-
-func TestClientDCIMGetSite(t *testing.T) {
-	wantSite := testSite(1)
-
-	c, done := testClient(t, testHandler(t, http.MethodGet, "/api/dcim/sites/1", wantSite))
-	defer done()
-
-	gotSite, err := c.DCIM.GetSite(wantSite.ID)
-	if err != nil {
-		t.Fatalf("unexpected error from Client.DCIM.GetSite: %v", err)
-	}
-
-	if want, got := *wantSite, *gotSite; !reflect.DeepEqual(want, got) {
-		t.Fatalf("unexpected Site:\n- want: %v\n-  got: %v", want, got)
-	}
-}
-
-func TestClientDCIMListSites(t *testing.T) {
-	wantSites := []*Site{
-		testSite(1),
-		testSite(2),
-		testSite(3),
-	}
-
-	c, done := testClient(t, testHandler(t, http.MethodGet, "/api/dcim/sites/", wantSites))
-	defer done()
-
-	gotSites, err := c.DCIM.ListSites()
-	if err != nil {
-		t.Fatalf("unexpected error from Client.DCIM.ListSites: %v", err)
-	}
-
-	if want, got := derefSites(wantSites), derefSites(gotSites); !reflect.DeepEqual(want, got) {
-		t.Fatalf("unexpected Sites:\n- want: %v\n-  got: %v", want, got)
-	}
-}
-
-// derefSites is used to print values of Sites in slice, instead of memory addresses.
-func derefSites(sites []*Site) []Site {
-	s := make([]Site, len(sites))
-	for i := range sites {
-		s[i] = *sites[i]
-	}
-
-	return s
-}
diff --git a/ipam_aggregates.go b/ipam_aggregates.go
deleted file mode 100644
index 7433bf80604013b99c7b57e065fb63e97820cf02..0000000000000000000000000000000000000000
--- a/ipam_aggregates.go
+++ /dev/null
@@ -1,172 +0,0 @@
-// Copyright 2016 The go-netbox Authors.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//   http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package netbox
-
-import (
-	"encoding/json"
-	"fmt"
-	"net"
-	"net/http"
-	"net/url"
-	"strconv"
-	"time"
-)
-
-// An Aggregate is an IPv4 or IPv6 address aggregate.
-type Aggregate struct {
-	ID          int
-	Family      Family
-	Prefix      *net.IPNet
-	RIR         *RIRIdentifier
-	DateAdded   time.Time
-	Description string
-}
-
-// GetAggregate retrieves an Aggregate object from NetBox by its ID.
-func (s *IPAMService) GetAggregate(id int) (*Aggregate, error) {
-	req, err := s.c.NewRequest(
-		http.MethodGet,
-		fmt.Sprintf("/api/ipam/aggregates/%d", id),
-		nil,
-	)
-	if err != nil {
-		return nil, err
-	}
-
-	a := new(Aggregate)
-	err = s.c.Do(req, a)
-	return a, err
-}
-
-// ListAggregates retrives a list of Aggregate objects from NetBox, filtered
-// according to the parameters specified in options.
-//
-// If options is nil, all Aggregates will be retrieved.
-func (s *IPAMService) ListAggregates(options *ListAggregatesOptions) ([]*Aggregate, error) {
-	req, err := s.c.NewRequest(http.MethodGet, "/api/ipam/aggregates/", options)
-	if err != nil {
-		return nil, err
-	}
-
-	var as []*Aggregate
-	err = s.c.Do(req, &as)
-	return as, err
-}
-
-// ListAggregatesOptions is a used as an argument for Client.IPAM.ListAggregates.
-// Integer fields with an *ID suffix are preferred over their string counterparts,
-// and if both are set, only the *ID field will be used.
-type ListAggregatesOptions struct {
-	Family    Family
-	RIRID     []int
-	RIR       []string
-	DateAdded time.Time
-}
-
-// Values generates a url.Values map from the data in ListAggregatesOptions.
-func (o *ListAggregatesOptions) Values() (url.Values, error) {
-	if o == nil {
-		return nil, nil
-	}
-
-	v := url.Values{}
-
-	if o.Family != 0 {
-		v.Set("family", strconv.Itoa(int(o.Family)))
-	}
-
-	// IDs should always be preferred over string names
-
-	switch {
-	case len(o.RIRID) > 0:
-		for _, r := range o.RIRID {
-			v.Add("rir_id", strconv.Itoa(r))
-		}
-	case len(o.RIR) > 0:
-		for _, r := range o.RIR {
-			v.Add("rir", r)
-		}
-	}
-
-	if !o.DateAdded.IsZero() {
-		v.Set("date_added", o.DateAdded.Format(dateFormat))
-	}
-
-	return v, nil
-}
-
-// dateFormat is the package time equivalent of the date format used by NetBox.
-const dateFormat = "2006-01-02"
-
-// An aggregate is the raw JSON representation of an Aggregate.
-type aggregate struct {
-	ID          int            `json:"id"`
-	Family      Family         `json:"family"`
-	Prefix      string         `json:"prefix"`
-	RIR         *RIRIdentifier `json:"rir"`
-	DateAdded   string         `json:"date_added"`
-	Description string         `json:"description"`
-}
-
-// MarshalJSON marshals an Aggregate into JSON bytes.
-func (a *Aggregate) MarshalJSON() ([]byte, error) {
-	var date string
-	if !a.DateAdded.IsZero() {
-		date = a.DateAdded.Format(dateFormat)
-	}
-
-	return json.Marshal(aggregate{
-		ID:          a.ID,
-		Family:      a.Family,
-		Prefix:      a.Prefix.String(),
-		RIR:         a.RIR,
-		DateAdded:   date,
-		Description: a.Description,
-	})
-}
-
-// UnmarshalJSON unmarshals JSON bytes into an Aggregate, and verifies that
-// the contained IP address and date are valid.
-func (a *Aggregate) UnmarshalJSON(b []byte) error {
-	var raw aggregate
-	if err := json.Unmarshal(b, &raw); err != nil {
-		return err
-	}
-
-	_, prefix, err := net.ParseCIDR(raw.Prefix)
-	if err != nil {
-		return err
-	}
-
-	*a = Aggregate{
-		ID:          raw.ID,
-		Family:      raw.Family,
-		Prefix:      prefix,
-		RIR:         raw.RIR,
-		Description: raw.Description,
-	}
-
-	if raw.DateAdded == "" {
-		return nil
-	}
-
-	t, err := time.Parse(dateFormat, raw.DateAdded)
-	if err != nil {
-		return err
-	}
-	a.DateAdded = t
-
-	return nil
-}
diff --git a/ipam_aggregates_test.go b/ipam_aggregates_test.go
deleted file mode 100644
index 9366f6486f19359d35d0626b67b25585268f624e..0000000000000000000000000000000000000000
--- a/ipam_aggregates_test.go
+++ /dev/null
@@ -1,307 +0,0 @@
-// Copyright 2016 The go-netbox Authors.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//   http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package netbox
-
-import (
-	"bytes"
-	"encoding/json"
-	"net"
-	"net/http"
-	"net/url"
-	"reflect"
-	"strconv"
-	"testing"
-	"time"
-)
-
-func TestClientIPAMGetAggregate(t *testing.T) {
-	wantAggregate := testAggregate(FamilyIPv4, 1)
-
-	c, done := testClient(t, testHandler(t, http.MethodGet, "/api/ipam/aggregates/1", wantAggregate))
-	defer done()
-
-	gotAggregate, err := c.IPAM.GetAggregate(wantAggregate.ID)
-	if err != nil {
-		t.Fatalf("unexpected error from Client.IPAM.GetAggregate: %v", err)
-	}
-
-	if want, got := *wantAggregate, *gotAggregate; !aggregatesEqual(want, got) {
-		t.Fatalf("unexpected Aggregate:\n- want: %v\n-  got: %v", want, got)
-	}
-}
-
-func TestClientIPAMListAggregates(t *testing.T) {
-	wantAggregates := []*Aggregate{
-		testAggregate(FamilyIPv4, 1),
-		testAggregate(FamilyIPv6, 2),
-	}
-
-	c, done := testClient(t, testHandler(t, http.MethodGet, "/api/ipam/aggregates/", wantAggregates))
-	defer done()
-
-	gotAggregates, err := c.IPAM.ListAggregates(nil)
-	if err != nil {
-		t.Fatalf("unexpected error from Client.IPAM.ListAggregates: %v", err)
-	}
-
-	want := derefAggregates(wantAggregates)
-	got := derefAggregates(gotAggregates)
-	if !aggregateSlicesEqual(want, got) {
-		t.Fatalf("unexpected Aggregates:\n- want: %v\n-  got: %v", want, got)
-	}
-}
-
-func TestListAggregatesOptionsValues(t *testing.T) {
-	var tests = []struct {
-		desc string
-		o    *ListAggregatesOptions
-		v    url.Values
-	}{
-		{
-			desc: "empty options",
-		},
-		{
-			desc: "family only",
-			o: &ListAggregatesOptions{
-				Family: FamilyIPv4,
-			},
-			v: url.Values{
-				"family": []string{strconv.Itoa(int(FamilyIPv4))},
-			},
-		},
-		{
-			desc: "1 rir_id only",
-			o: &ListAggregatesOptions{
-				RIRID: []int{1},
-			},
-			v: url.Values{
-				"rir_id": []string{"1"},
-			},
-		},
-		{
-			desc: "3 rir_ids only",
-			o: &ListAggregatesOptions{
-				RIRID: []int{1, 2, 3},
-			},
-			v: url.Values{
-				"rir_id": []string{"1", "2", "3"},
-			},
-		},
-		{
-			desc: "1 rir only",
-			o: &ListAggregatesOptions{
-				RIR: []string{"rir"},
-			},
-			v: url.Values{
-				"rir": []string{"rir"},
-			},
-		},
-		{
-			desc: "3 rirs only",
-			o: &ListAggregatesOptions{
-				RIR: []string{"rirfoo", "rirbar", "rirbaz"},
-			},
-			v: url.Values{
-				"rir": []string{"rirfoo", "rirbar", "rirbaz"},
-			},
-		},
-		{
-			desc: "rir and rir_id, rir_id preferred",
-			o: &ListAggregatesOptions{
-				RIR:   []string{"rir"},
-				RIRID: []int{1},
-			},
-			v: url.Values{
-				"rir_id": []string{"1"},
-			},
-		},
-		{
-			desc: "date_added only",
-			o: &ListAggregatesOptions{
-				DateAdded: time.Date(2016, time.January, 22, 0, 0, 0, 0, time.UTC),
-			},
-			v: url.Values{
-				"date_added": []string{"2016-01-22"},
-			},
-		},
-		{
-			desc: "all options",
-			o: &ListAggregatesOptions{
-				Family:    FamilyIPv4,
-				RIRID:     []int{1},
-				RIR:       []string{"rir"},
-				DateAdded: time.Date(2016, time.January, 22, 0, 0, 0, 0, time.UTC),
-			},
-			v: url.Values{
-				"family":     []string{strconv.Itoa(int(FamilyIPv4))},
-				"rir_id":     []string{"1"},
-				"date_added": []string{"2016-01-22"},
-			},
-		},
-	}
-
-	for i, tt := range tests {
-		t.Logf("[%02d] test %q", i, tt.desc)
-
-		v, err := tt.o.Values()
-		if err != nil {
-			t.Fatalf("unexpected Values error: %v", err)
-		}
-
-		if want, got := tt.v, v; !reflect.DeepEqual(want, got) {
-			t.Fatalf("unexpected url.Values map:\n- want: %v\n-  got: %v",
-				want, got)
-		}
-	}
-}
-
-func TestAggregateMarshalJSON(t *testing.T) {
-	var tests = []struct {
-		desc string
-		a    *Aggregate
-		b    []byte
-	}{
-		{
-			desc: "IPv4 aggregate",
-			a:    testAggregate(FamilyIPv4, 1),
-			b:    []byte(`{"id":1,"family":4,"prefix":"8.0.0.0/8","rir":{"id":1,"name":"RIRIdentifier 1","slug":"riridentifier1"},"date_added":"2016-01-01","description":"description 1"}`),
-		},
-		{
-			desc: "IPv6 aggregate",
-			a:    testAggregate(FamilyIPv6, 2),
-			b:    []byte(`{"id":2,"family":6,"prefix":"2001::/16","rir":{"id":2,"name":"RIRIdentifier 2","slug":"riridentifier2"},"date_added":"2016-01-01","description":"description 2"}`),
-		},
-	}
-
-	for i, tt := range tests {
-		t.Logf("[%02d] test %q", i, tt.desc)
-
-		b, err := json.Marshal(tt.a)
-		if err != nil {
-			t.Fatalf("unexpected JSON marshal error: %v", err)
-		}
-
-		if want, got := tt.b, b; !bytes.Equal(want, got) {
-			t.Fatalf("unexpected JSON bytes:\n- want: %v\n-  got: %v",
-				string(want), string(got))
-		}
-	}
-}
-
-func TestAggregateUnmarshalJSON(t *testing.T) {
-	var tests = []struct {
-		desc string
-		b    []byte
-		a    *Aggregate
-		err  error
-	}{
-		{
-			desc: "invalid aggregate due to prefix",
-			b:    []byte(`{"prefix":"foo"}`),
-			err: &net.ParseError{
-				Type: "CIDR address",
-				Text: "foo",
-			},
-		},
-		{
-			desc: "invalid aggregate due to date_added",
-			b:    []byte(`{"prefix":"5.101.96.0/20","date_added":"foo"}`),
-			err: &time.ParseError{
-				Layout:     dateFormat,
-				Value:      "foo",
-				LayoutElem: "2006",
-				ValueElem:  "foo",
-			},
-		},
-		{
-			desc: "IPv4 aggregate",
-			b:    []byte(`{"id":1,"family":4,"prefix":"8.0.0.0/8","rir":{"id":1,"name":"RIRIdentifier 1","slug":"riridentifier1"},"date_added":"2016-01-01","description":"description 1"}`),
-			a:    testAggregate(FamilyIPv4, 1),
-		},
-		{
-			desc: "IPv6 aggregate",
-			b:    []byte(`{"id":2,"family":6,"prefix":"2001::/16","rir":{"id":2,"name":"RIRIdentifier 2","slug":"riridentifier2"},"date_added":"2016-01-01","description":"description 2"}`),
-			a:    testAggregate(FamilyIPv6, 2),
-		},
-	}
-
-	for i, tt := range tests {
-		t.Logf("[%02d] test %q", i, tt.desc)
-
-		a := new(Aggregate)
-		err := json.Unmarshal(tt.b, a)
-
-		if want, got := tt.err, err; !reflect.DeepEqual(want, got) {
-			t.Fatalf("unexpected error:\n- want: %v\n-  got: %v",
-				want, got)
-		}
-		if err != nil {
-			continue
-		}
-
-		if want, got := *tt.a, *a; !aggregatesEqual(want, got) {
-			t.Fatalf("unexpected Aggregate:\n- want: %v\n-  got: %v",
-				want, got)
-		}
-	}
-}
-
-func aggregateSlicesEqual(a, b []Aggregate) bool {
-	if len(a) != len(b) {
-		return false
-	}
-
-	for i := range a {
-		if !aggregatesEqual(a[i], b[i]) {
-			return false
-		}
-	}
-
-	return true
-}
-
-func aggregatesEqual(a, b Aggregate) bool {
-	if a.ID != b.ID {
-		return false
-	}
-
-	if a.Family != b.Family {
-		return false
-	}
-
-	if a.Prefix.String() != b.Prefix.String() {
-		return false
-	}
-
-	if !a.DateAdded.Equal(b.DateAdded) {
-		return false
-	}
-
-	if a.Description != b.Description {
-		return false
-	}
-
-	return true
-}
-
-// Used to print values of Aggregates in slice, instead of memory addresses.
-func derefAggregates(aggregates []*Aggregate) []Aggregate {
-	a := make([]Aggregate, len(aggregates))
-	for i := range aggregates {
-		a[i] = *aggregates[i]
-	}
-
-	return a
-}
diff --git a/ipam_ipaddresses.go b/ipam_ipaddresses.go
deleted file mode 100644
index 0c7dddd47220a7408869f12a42a1463f6d7bb01a..0000000000000000000000000000000000000000
--- a/ipam_ipaddresses.go
+++ /dev/null
@@ -1,226 +0,0 @@
-// Copyright 2016 The go-netbox Authors.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//   http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package netbox
-
-import (
-	"encoding/json"
-	"fmt"
-	"net"
-	"net/http"
-	"net/url"
-	"strconv"
-)
-
-// An IPAddress is an IPv4 or IPv6 address.
-type IPAddress struct {
-	ID          int
-	Family      Family
-	Address     *net.IPNet
-	VRF         *VRFIdentifier
-	Interface   *InterfaceIdentifier
-	Description string
-	NATInside   *IPAddressIdentifier
-	NATOutside  *IPAddressIdentifier
-}
-
-// An IPAddressIdentifier is a reduced version of a IPAddress, returned as
-// nested object in some top-level objects.  It contains information which can
-// be used in subsequent API calls to identify and retrieve a full IPAddress.
-type IPAddressIdentifier struct {
-	ID      int
-	Family  Family
-	Address *net.IPNet
-}
-
-// GetIPAddress retrieves an IPAddress object from NetBox by its ID.
-func (s *IPAMService) GetIPAddress(id int) (*IPAddress, error) {
-	req, err := s.c.NewRequest(
-		http.MethodGet,
-		fmt.Sprintf("/api/ipam/ip-addresses/%d", id),
-		nil,
-	)
-	if err != nil {
-		return nil, err
-	}
-
-	ip := new(IPAddress)
-	err = s.c.Do(req, ip)
-	return ip, err
-}
-
-// ListIPAddresses retrives a list of IPAddress objects from NetBox, filtered according
-// to the parameters specified in options.
-//
-// If options is nil, all IPAddresses will be retrieved.
-func (s *IPAMService) ListIPAddresses(options *ListIPAddressesOptions) ([]*IPAddress, error) {
-	req, err := s.c.NewRequest(http.MethodGet, "/api/ipam/ip-addresses/", options)
-	if err != nil {
-		return nil, err
-	}
-
-	var ips []*IPAddress
-	err = s.c.Do(req, &ips)
-	return ips, err
-}
-
-// An ipAddress is the raw JSON representation of an IPAddress.
-type ipAddress struct {
-	ID          int                  `json:"id"`
-	Family      Family               `json:"family"`
-	Address     string               `json:"address"`
-	VRF         *VRFIdentifier       `json:"vrf"`
-	Interface   *InterfaceIdentifier `json:"interface"`
-	Description string               `json:"description"`
-	NATInside   *IPAddressIdentifier `json:"nat_inside"`
-	NATOutside  *IPAddressIdentifier `json:"nat_outside"`
-}
-
-// MarshalJSON marshals an IPAddress into JSON bytes.
-func (ip *IPAddress) MarshalJSON() ([]byte, error) {
-	return json.Marshal(ipAddress{
-		ID:          ip.ID,
-		Family:      ip.Family,
-		Address:     ip.Address.String(),
-		VRF:         ip.VRF,
-		Interface:   ip.Interface,
-		Description: ip.Description,
-		NATInside:   ip.NATInside,
-		NATOutside:  ip.NATOutside,
-	})
-}
-
-// UnmarshalJSON unmarshals JSON bytes into an IPAddress, and verifies that
-// the contained IP address is valid.
-func (ip *IPAddress) UnmarshalJSON(b []byte) error {
-	var raw ipAddress
-	if err := json.Unmarshal(b, &raw); err != nil {
-		return err
-	}
-
-	_, ipNet, err := net.ParseCIDR(raw.Address)
-	if err != nil {
-		return err
-	}
-
-	*ip = IPAddress{
-		ID:          raw.ID,
-		Family:      raw.Family,
-		Address:     ipNet,
-		VRF:         raw.VRF,
-		Interface:   raw.Interface,
-		Description: raw.Description,
-		NATInside:   raw.NATInside,
-		NATOutside:  raw.NATOutside,
-	}
-	return nil
-}
-
-// An ipAddressIdentifier is the raw JSON representation of an IPAddressIdentifier.
-type ipAddressIdentifier struct {
-	ID      int    `json:"id"`
-	Family  Family `json:"family"`
-	Address string `json:"address"`
-}
-
-// MarshalJSON marshals an IPAddressIdentifier into JSON bytes.
-func (ip *IPAddressIdentifier) MarshalJSON() ([]byte, error) {
-	return json.Marshal(ipAddressIdentifier{
-		ID:      ip.ID,
-		Family:  ip.Family,
-		Address: ip.Address.String(),
-	})
-}
-
-// UnmarshalJSON unmarshals JSON bytes into an IPAddressIdentifier, and verifies that
-// the contained IP address is valid.
-func (ip *IPAddressIdentifier) UnmarshalJSON(b []byte) error {
-	var raw ipAddressIdentifier
-	if err := json.Unmarshal(b, &raw); err != nil {
-		return err
-	}
-
-	_, ipNet, err := net.ParseCIDR(raw.Address)
-	if err != nil {
-		return err
-	}
-
-	*ip = IPAddressIdentifier{
-		ID:      raw.ID,
-		Family:  raw.Family,
-		Address: ipNet,
-	}
-	return nil
-}
-
-// ListIPAddressesOptions is used as an argument for Client.IPAM.ListIPAddresses.
-// Integer fields with an *ID suffix are preferred over their string
-// counterparts, and if both are set, only the *ID field will be used.
-type ListIPAddressesOptions struct {
-	Family      Family
-	VRFID       []int
-	VRF         string
-	InterfaceID []int
-	DeviceID    []int
-	Device      []string
-
-	// Query is a special option which enables free-form search.
-	// For example, Query could be an IP address such as "8.8.8.8".
-	Query string
-}
-
-// Values generates a url.Values map from the data in ListIPAddressesOptions.
-func (o *ListIPAddressesOptions) Values() (url.Values, error) {
-	if o == nil {
-		return nil, nil
-	}
-
-	v := url.Values{}
-
-	if o.Family != 0 {
-		v.Set("family", strconv.Itoa(int(o.Family)))
-	}
-
-	for _, i := range o.InterfaceID {
-		v.Add("interface_id", strconv.Itoa(i))
-	}
-
-	// IDs should always be preferred over string names
-
-	switch {
-	case len(o.VRFID) > 0:
-		for _, vid := range o.VRFID {
-			v.Add("vrf_id", strconv.Itoa(vid))
-		}
-	case o.VRF != "":
-		v.Set("vrf", o.VRF)
-	}
-
-	switch {
-	case len(o.DeviceID) > 0:
-		for _, d := range o.DeviceID {
-			v.Add("device_id", strconv.Itoa(d))
-		}
-	case len(o.Device) > 0:
-		for _, d := range o.Device {
-			v.Add("device", d)
-		}
-	}
-
-	if o.Query != "" {
-		v.Set("q", o.Query)
-	}
-
-	return v, nil
-}
diff --git a/ipam_ipaddresses_test.go b/ipam_ipaddresses_test.go
deleted file mode 100644
index e761fd34b40cd855fac4b0738587e78067ff4881..0000000000000000000000000000000000000000
--- a/ipam_ipaddresses_test.go
+++ /dev/null
@@ -1,457 +0,0 @@
-// Copyright 2016 The go-netbox Authors.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//   http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package netbox
-
-import (
-	"bytes"
-	"encoding/json"
-	"net"
-	"net/http"
-	"net/url"
-	"reflect"
-	"strconv"
-	"testing"
-)
-
-func TestClientIPAMGetIPAddress(t *testing.T) {
-	wantIP := testIPAddress(FamilyIPv4, 1)
-
-	c, done := testClient(t, testHandler(t, http.MethodGet, "/api/ipam/ip-addresses/1", wantIP))
-	defer done()
-
-	gotIP, err := c.IPAM.GetIPAddress(wantIP.ID)
-	if err != nil {
-		t.Fatalf("unexpected error from Client.IPAM.GetIPAddress: %v", err)
-	}
-
-	if want, got := *wantIP, *gotIP; !ipAddressesEqual(want, got) {
-		t.Fatalf("unexpected IPAddress:\n- want: %v\n-  got: %v", want, got)
-	}
-}
-
-func TestClientIPAMListIPAddresses(t *testing.T) {
-	wantIPs := []*IPAddress{
-		testIPAddress(FamilyIPv4, 1),
-		testIPAddress(FamilyIPv6, 2),
-	}
-
-	c, done := testClient(t, testHandler(t, http.MethodGet, "/api/ipam/ip-addresses/", wantIPs))
-	defer done()
-
-	gotIPs, err := c.IPAM.ListIPAddresses(nil)
-	if err != nil {
-		t.Fatalf("unexpected error from Client.IPAM.ListIPAddresses: %v", err)
-	}
-
-	want := derefIPAddresses(wantIPs)
-	got := derefIPAddresses(gotIPs)
-	if !ipAddressesSlicesEqual(want, got) {
-		t.Fatalf("unexpected IPs:\n- want: %v\n-  got: %v", want, got)
-	}
-}
-
-func TestListIPAddressesOptionsValues(t *testing.T) {
-	var tests = []struct {
-		desc string
-		o    *ListIPAddressesOptions
-		v    url.Values
-	}{
-		{
-			desc: "empty options",
-		},
-		{
-			desc: "family only",
-			o: &ListIPAddressesOptions{
-				Family: FamilyIPv4,
-			},
-			v: url.Values{
-				"family": []string{strconv.Itoa(int(FamilyIPv4))},
-			},
-		},
-		{
-			desc: "1 vrf_id only",
-			o: &ListIPAddressesOptions{
-				VRFID: []int{1},
-			},
-			v: url.Values{
-				"vrf_id": []string{"1"},
-			},
-		},
-		{
-			desc: "3 vrf_ids only",
-			o: &ListIPAddressesOptions{
-				VRFID: []int{1, 2, 3},
-			},
-			v: url.Values{
-				"vrf_id": []string{"1", "2", "3"},
-			},
-		},
-		{
-			desc: "vrf only",
-			o: &ListIPAddressesOptions{
-				VRF: "vrf",
-			},
-			v: url.Values{
-				"vrf": []string{"vrf"},
-			},
-		},
-		{
-			desc: "vrf and vrf_id, vrf_id preferred",
-			o: &ListIPAddressesOptions{
-				VRF:   "vrf",
-				VRFID: []int{1},
-			},
-			v: url.Values{
-				"vrf_id": []string{"1"},
-			},
-		},
-		{
-			desc: "1 interface_id only",
-			o: &ListIPAddressesOptions{
-				InterfaceID: []int{2},
-			},
-			v: url.Values{
-				"interface_id": []string{"2"},
-			},
-		},
-		{
-			desc: "3 interface_ids only",
-			o: &ListIPAddressesOptions{
-				InterfaceID: []int{2, 3, 4},
-			},
-			v: url.Values{
-				"interface_id": []string{"2", "3", "4"},
-			},
-		},
-		{
-			desc: "1 device_id only",
-			o: &ListIPAddressesOptions{
-				DeviceID: []int{3},
-			},
-			v: url.Values{
-				"device_id": []string{"3"},
-			},
-		},
-		{
-			desc: "3 device_ids only",
-			o: &ListIPAddressesOptions{
-				DeviceID: []int{3, 4, 5},
-			},
-			v: url.Values{
-				"device_id": []string{"3", "4", "5"},
-			},
-		},
-		{
-			desc: "1 device only",
-			o: &ListIPAddressesOptions{
-				Device: []string{"device"},
-			},
-			v: url.Values{
-				"device": []string{"device"},
-			},
-		},
-		{
-			desc: "3 devices only",
-			o: &ListIPAddressesOptions{
-				Device: []string{"a", "b", "c"},
-			},
-			v: url.Values{
-				"device": []string{"a", "b", "c"},
-			},
-		},
-		{
-			desc: "device and device_id, device_id preferred",
-			o: &ListIPAddressesOptions{
-				Device:   []string{"device"},
-				DeviceID: []int{3},
-			},
-			v: url.Values{
-				"device_id": []string{"3"},
-			},
-		},
-		{
-			desc: "q only",
-			o: &ListIPAddressesOptions{
-				Query: "query",
-			},
-			v: url.Values{
-				"q": []string{"query"},
-			},
-		},
-		{
-			desc: "all options",
-			o: &ListIPAddressesOptions{
-				Family:      FamilyIPv4,
-				VRFID:       []int{1},
-				VRF:         "vrf",
-				InterfaceID: []int{2},
-				DeviceID:    []int{3},
-				Device:      []string{"device"},
-				Query:       "query",
-			},
-			v: url.Values{
-				"family":       []string{strconv.Itoa(int(FamilyIPv4))},
-				"vrf_id":       []string{"1"},
-				"interface_id": []string{"2"},
-				"device_id":    []string{"3"},
-				"q":            []string{"query"},
-			},
-		},
-	}
-
-	for i, tt := range tests {
-		t.Logf("[%02d] test %q", i, tt.desc)
-
-		v, err := tt.o.Values()
-		if err != nil {
-			t.Fatalf("unexpected Values error: %v", err)
-		}
-
-		if want, got := tt.v, v; !reflect.DeepEqual(want, got) {
-			t.Fatalf("unexpected url.Values map:\n- want: %v\n-  got: %v",
-				want, got)
-		}
-	}
-}
-
-func TestIPAddressMarshalJSON(t *testing.T) {
-	var tests = []struct {
-		desc string
-		ip   *IPAddress
-		b    []byte
-	}{
-		{
-			desc: "IPv4 address",
-			ip:   testIPAddress(FamilyIPv4, 1),
-			b:    []byte(`{"id":1,"family":4,"address":"8.8.8.0/24","vrf":{"id":1,"name":"VRFIdentifier 1","rd":"rd 1"},"interface":{"id":1,"device":{"id":1,"name":"DeviceIdentifier 1"},"name":"InterfaceIdentifier 1"},"description":"description 1","nat_inside":{"id":1,"family":4,"address":"8.8.8.0/24"},"nat_outside":{"id":1,"family":4,"address":"8.8.8.0/24"}}`),
-		},
-		{
-			desc: "IPv6 address",
-			ip:   testIPAddress(FamilyIPv6, 2),
-			b:    []byte(`{"id":2,"family":6,"address":"2001:4860:4860::/48","vrf":{"id":2,"name":"VRFIdentifier 2","rd":"rd 2"},"interface":{"id":2,"device":{"id":2,"name":"DeviceIdentifier 2"},"name":"InterfaceIdentifier 2"},"description":"description 2","nat_inside":{"id":2,"family":6,"address":"2001:4860:4860::/48"},"nat_outside":{"id":2,"family":6,"address":"2001:4860:4860::/48"}}`),
-		},
-	}
-
-	for i, tt := range tests {
-		t.Logf("[%02d] test %q", i, tt.desc)
-
-		b, err := json.Marshal(tt.ip)
-		if err != nil {
-			t.Fatalf("unexpected JSON marshal error: %v", err)
-		}
-
-		if want, got := tt.b, b; !bytes.Equal(want, got) {
-			t.Fatalf("unexpected JSON bytes:\n- want: %v\n-  got: %v",
-				string(want), string(got))
-		}
-	}
-}
-
-func TestIPAddressUnmarshalJSON(t *testing.T) {
-	var tests = []struct {
-		desc string
-		b    []byte
-		ip   *IPAddress
-		err  error
-	}{
-		{
-			desc: "invalid IP address",
-			b:    []byte(`{"address":"foo"}`),
-			err: &net.ParseError{
-				Type: "CIDR address",
-				Text: "foo",
-			},
-		},
-		{
-			desc: "IPv4 address",
-			b:    []byte(`{"id":1,"family":4,"address":"8.8.8.8/24","vrf":{"id":1,"name":"VRFIdentifier 1","rd":"rd 1"},"interface":{"id":1,"device":{"id":1,"name":"DeviceIdentifier 1"},"name":"InterfaceIdentifier 1"}}`),
-			ip:   testIPAddress(FamilyIPv4, 1),
-		},
-		{
-			desc: "IPv6 address",
-			b:    []byte(`{"id":2,"family":6,"address":"2001:4860:4860::8888/48","vrf":{"id":2,"name":"VRFIdentifier 2","rd":"rd 2"},"interface":{"id":2,"device":{"id":2,"name":"DeviceIdentifier 2"},"name":"InterfaceIdentifier 2"}}`),
-			ip:   testIPAddress(FamilyIPv6, 2),
-		},
-	}
-
-	for i, tt := range tests {
-		t.Logf("[%02d] test %q", i, tt.desc)
-
-		ip := new(IPAddress)
-		err := json.Unmarshal(tt.b, ip)
-
-		if want, got := tt.err, err; !reflect.DeepEqual(want, got) {
-			t.Fatalf("unexpected error:\n- want: %v\n-  got: %v",
-				want, got)
-		}
-		if err != nil {
-			continue
-		}
-
-		if want, got := *tt.ip, *ip; !ipAddressesEqual(want, got) {
-			t.Fatalf("unexpected IPAddress:\n- want: %v\n-  got: %v",
-				want, got)
-		}
-	}
-}
-
-func TestIPAddressIdentifierMarshalJSON(t *testing.T) {
-	var tests = []struct {
-		desc string
-		ip   *IPAddressIdentifier
-		b    []byte
-	}{
-		{
-			desc: "IPv4 address",
-			ip:   testIPAddressIdentifier(FamilyIPv4, 1),
-			b:    []byte(`{"id":1,"family":4,"address":"8.8.8.0/24"}`),
-		},
-		{
-			desc: "IPv6 address",
-			ip:   testIPAddressIdentifier(FamilyIPv6, 2),
-			b:    []byte(`{"id":2,"family":6,"address":"2001:4860:4860::/48"}`),
-		},
-	}
-
-	for i, tt := range tests {
-		t.Logf("[%02d] test %q", i, tt.desc)
-
-		b, err := json.Marshal(tt.ip)
-		if err != nil {
-			t.Fatalf("unexpected JSON marshal error: %v", err)
-		}
-
-		if want, got := tt.b, b; !bytes.Equal(want, got) {
-			t.Fatalf("unexpected JSON bytes:\n- want: %v\n-  got: %v",
-				string(want), string(got))
-		}
-	}
-}
-
-func TestIPAddressIdentifierUnmarshalJSON(t *testing.T) {
-	var tests = []struct {
-		desc string
-		b    []byte
-		ip   *IPAddressIdentifier
-		err  error
-	}{
-		{
-			desc: "invalid IP address",
-			b:    []byte(`{"address":"foo"}`),
-			err: &net.ParseError{
-				Type: "CIDR address",
-				Text: "foo",
-			},
-		},
-		{
-			desc: "IPv4 address",
-			b:    []byte(`{"id":1,"family":4,"address":"8.8.8.8/24"}`),
-			ip:   testIPAddressIdentifier(FamilyIPv4, 1),
-		},
-		{
-			desc: "IPv6 address",
-			b:    []byte(`{"id":2,"family":6,"address":"2001:4860:4860::8888/48"}`),
-			ip:   testIPAddressIdentifier(FamilyIPv6, 2),
-		},
-	}
-
-	for i, tt := range tests {
-		t.Logf("[%02d] test %q", i, tt.desc)
-
-		ip := new(IPAddressIdentifier)
-		err := json.Unmarshal(tt.b, ip)
-
-		if want, got := tt.err, err; !reflect.DeepEqual(want, got) {
-			t.Fatalf("unexpected error:\n- want: %v\n-  got: %v",
-				want, got)
-		}
-		if err != nil {
-			continue
-		}
-
-		if want, got := *tt.ip, *ip; !ipAddressIdentifiersEqual(want, got) {
-			t.Fatalf("unexpected IPAddressIdentifier:\n- want: %v\n-  got: %v",
-				want, got)
-		}
-	}
-}
-
-func ipAddressesSlicesEqual(a, b []IPAddress) bool {
-	if len(a) != len(b) {
-		return false
-	}
-
-	for i := range a {
-		if !ipAddressesEqual(a[i], b[i]) {
-			return false
-		}
-	}
-
-	return true
-}
-
-func ipAddressesEqual(a, b IPAddress) bool {
-	if a.ID != b.ID {
-		return false
-	}
-
-	if a.Family != b.Family {
-		return false
-	}
-
-	if !a.Address.IP.Equal(b.Address.IP) {
-		return false
-	}
-
-	if a.Address.String() != b.Address.String() {
-		return false
-	}
-
-	if !reflect.DeepEqual(a.VRF, b.VRF) {
-		return false
-	}
-
-	if !reflect.DeepEqual(a.Interface, b.Interface) {
-		return false
-	}
-
-	return true
-}
-
-func ipAddressIdentifiersEqual(a, b IPAddressIdentifier) bool {
-	if a.ID != b.ID {
-		return false
-	}
-
-	if a.Family != b.Family {
-		return false
-	}
-
-	if !a.Address.IP.Equal(b.Address.IP) {
-		return false
-	}
-
-	return true
-}
-
-// Used to print values of IPAddresses in slice, instead of memory addresses.
-func derefIPAddresses(ips []*IPAddress) []IPAddress {
-	ip := make([]IPAddress, len(ips))
-	for i := range ips {
-		ip[i] = *ips[i]
-	}
-
-	return ip
-}
diff --git a/ipam_prefixes.go b/ipam_prefixes.go
deleted file mode 100644
index 37eea55c5d32481a2d8e5b7df0259311351814d6..0000000000000000000000000000000000000000
--- a/ipam_prefixes.go
+++ /dev/null
@@ -1,213 +0,0 @@
-// Copyright 2016 The go-netbox Authors.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//   http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package netbox
-
-import (
-	"encoding/json"
-	"fmt"
-	"net"
-	"net/http"
-	"net/url"
-	"strconv"
-)
-
-// A Prefix is an IPv4 or IPv6 address prefix.
-type Prefix struct {
-	ID          int
-	Family      Family
-	Prefix      *net.IPNet
-	Site        *SiteIdentifier
-	VRF         *VRFIdentifier
-	VLAN        *VLANIdentifier
-	Status      Status
-	Role        *RoleIdentifier
-	Description string
-}
-
-// GetPrefix retrieves a Prefix object from NetBox by its ID.
-func (s *IPAMService) GetPrefix(id int) (*Prefix, error) {
-	req, err := s.c.NewRequest(
-		http.MethodGet,
-		fmt.Sprintf("/api/ipam/prefixes/%d", id),
-		nil,
-	)
-	if err != nil {
-		return nil, err
-	}
-
-	p := new(Prefix)
-	err = s.c.Do(req, p)
-	return p, err
-}
-
-// ListPrefixes retrives a list of Prefix objects from NetBox, filtered
-// according to the parameters specified in options.
-//
-// If options is nil, all Prefixes will be retrieved.
-func (s *IPAMService) ListPrefixes(options *ListPrefixesOptions) ([]*Prefix, error) {
-	req, err := s.c.NewRequest(http.MethodGet, "/api/ipam/prefixes/", options)
-	if err != nil {
-		return nil, err
-	}
-
-	var ps []*Prefix
-	err = s.c.Do(req, &ps)
-	return ps, err
-}
-
-// ListPrefixesOptions is used as an argument for Client.IPAM.ListPrefixes.
-// Integer fields with an *ID suffix are preferred over their string
-// counterparts, and if both are set, only the *ID field will be used.
-// In addition, VLANID is preferred over the site-specific VLANVID.
-type ListPrefixesOptions struct {
-	Family  Family
-	SiteID  []int
-	Site    []string
-	VRFID   int
-	VRF     string
-	VLANID  []int
-	VLANVID VLANID
-	Status  int
-	RoleID  []int
-	Role    []string
-	Parent  *net.IPNet
-
-	// Query is a special option which enables free-form search.
-	// For example, Query could be an IP address such as "8.8.8.8".
-	Query string
-}
-
-// Values generates a url.Values map from the data in ListPrefixesOptions.
-func (o *ListPrefixesOptions) Values() (url.Values, error) {
-	if o == nil {
-		return nil, nil
-	}
-
-	v := url.Values{}
-
-	if o.Family != 0 {
-		v.Set("family", strconv.Itoa(int(o.Family)))
-	}
-
-	// IDs should always be preferred over string names
-
-	switch {
-	case len(o.SiteID) > 0:
-		for _, s := range o.SiteID {
-			v.Add("site_id", strconv.Itoa(s))
-		}
-	case len(o.Site) > 0:
-		for _, s := range o.Site {
-			v.Add("site", s)
-		}
-	}
-
-	switch {
-	case o.VRFID != 0:
-		v.Set("vrf_id", strconv.Itoa(o.VRFID))
-	case o.VRF != "":
-		v.Set("vrf", o.VRF)
-	}
-
-	// Also prefer VLAN's NetBox ID over its VLAN VID
-	switch {
-	case len(o.VLANID) > 0:
-		for _, vid := range o.VLANID {
-			v.Add("vlan_id", strconv.Itoa(vid))
-		}
-	case o.VLANVID != 0:
-		v.Set("vlan_vid", strconv.Itoa(int(o.VLANVID)))
-	}
-
-	if o.Status != 0 {
-		v.Set("status", strconv.Itoa(o.Status))
-	}
-
-	switch {
-	case len(o.RoleID) > 0:
-		for _, r := range o.RoleID {
-			v.Add("role_id", strconv.Itoa(r))
-		}
-	case len(o.Role) > 0:
-		for _, r := range o.Role {
-			v.Add("role", r)
-		}
-	}
-
-	if o.Parent != nil {
-		v.Set("parent", o.Parent.String())
-	}
-
-	if o.Query != "" {
-		v.Set("q", o.Query)
-	}
-
-	return v, nil
-}
-
-// A prefix is the raw JSON representation of a Prefix.
-type prefix struct {
-	ID          int             `json:"id"`
-	Family      Family          `json:"family"`
-	Prefix      string          `json:"prefix"`
-	Site        *SiteIdentifier `json:"site"`
-	VRF         *VRFIdentifier  `json:"vrf"`
-	VLAN        *VLANIdentifier `json:"vlan"`
-	Status      Status          `json:"status"`
-	Role        *RoleIdentifier `json:"role"`
-	Description string          `json:"description"`
-}
-
-// MarshalJSON marshals an Prefix into JSON bytes.
-func (p *Prefix) MarshalJSON() ([]byte, error) {
-	return json.Marshal(prefix{
-		ID:          p.ID,
-		Family:      p.Family,
-		Prefix:      p.Prefix.String(),
-		Site:        p.Site,
-		VRF:         p.VRF,
-		VLAN:        p.VLAN,
-		Status:      p.Status,
-		Role:        p.Role,
-		Description: p.Description,
-	})
-}
-
-// UnmarshalJSON unmarshals JSON bytes into an Prefix, and verifies that
-// the contained IP address is valid.
-func (p *Prefix) UnmarshalJSON(b []byte) error {
-	var raw prefix
-	if err := json.Unmarshal(b, &raw); err != nil {
-		return err
-	}
-
-	_, prefix, err := net.ParseCIDR(raw.Prefix)
-	if err != nil {
-		return err
-	}
-
-	*p = Prefix{
-		ID:          raw.ID,
-		Family:      raw.Family,
-		Prefix:      prefix,
-		Site:        raw.Site,
-		VRF:         raw.VRF,
-		VLAN:        raw.VLAN,
-		Status:      raw.Status,
-		Role:        raw.Role,
-		Description: raw.Description,
-	}
-	return nil
-}
diff --git a/ipam_prefixes_test.go b/ipam_prefixes_test.go
deleted file mode 100644
index f431f1e1498f279ede1241332d75bffca1bacb30..0000000000000000000000000000000000000000
--- a/ipam_prefixes_test.go
+++ /dev/null
@@ -1,460 +0,0 @@
-// Copyright 2016 The go-netbox Authors.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//   http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package netbox
-
-import (
-	"bytes"
-	"encoding/json"
-	"net"
-	"net/http"
-	"net/url"
-	"reflect"
-	"strconv"
-	"testing"
-)
-
-func TestClientIPAMGetPrefix(t *testing.T) {
-	wantPrefix := testPrefix(FamilyIPv4, 1)
-
-	c, done := testClient(t, testHandler(t, http.MethodGet, "/api/ipam/prefixes/1", wantPrefix))
-	defer done()
-
-	gotPrefix, err := c.IPAM.GetPrefix(wantPrefix.ID)
-	if err != nil {
-		t.Fatalf("unexpected error from Client.IPAM.GetPrefix: %v", err)
-	}
-
-	if want, got := *wantPrefix, *gotPrefix; !prefixesEqual(want, got) {
-		t.Fatalf("unexpected Prefix:\n- want: %v\n-  got: %v", want, got)
-	}
-}
-
-func TestClientIPAMListPrefixes(t *testing.T) {
-	wantPrefixes := []*Prefix{
-		testPrefix(FamilyIPv4, 1),
-		testPrefix(FamilyIPv6, 2),
-	}
-
-	c, done := testClient(t, testHandler(t, http.MethodGet, "/api/ipam/prefixes/", wantPrefixes))
-	defer done()
-
-	gotPrefixes, err := c.IPAM.ListPrefixes(nil)
-	if err != nil {
-		t.Fatalf("unexpected error from Client.IPAM.ListPrefixes: %v", err)
-	}
-
-	want := derefPrefixes(wantPrefixes)
-	got := derefPrefixes(gotPrefixes)
-	if !prefixSlicesEqual(want, got) {
-		t.Fatalf("unexpected Prefixes:\n- want: %v\n-  got: %v", want, got)
-	}
-}
-
-func TestListPrefixesOptionsValues(t *testing.T) {
-	var tests = []struct {
-		desc string
-		o    *ListPrefixesOptions
-		v    url.Values
-	}{
-		{
-			desc: "empty options",
-		},
-		{
-			desc: "family only",
-			o: &ListPrefixesOptions{
-				Family: FamilyIPv4,
-			},
-			v: url.Values{
-				"family": []string{strconv.Itoa(int(FamilyIPv4))},
-			},
-		},
-		{
-			desc: "1 site_id only",
-			o: &ListPrefixesOptions{
-				SiteID: []int{1},
-			},
-			v: url.Values{
-				"site_id": []string{"1"},
-			},
-		},
-		{
-			desc: "3 site_ids only",
-			o: &ListPrefixesOptions{
-				SiteID: []int{1, 2, 3},
-			},
-			v: url.Values{
-				"site_id": []string{"1", "2", "3"},
-			},
-		},
-		{
-			desc: "1 site only",
-			o: &ListPrefixesOptions{
-				Site: []string{"site"},
-			},
-			v: url.Values{
-				"site": []string{"site"},
-			},
-		},
-		{
-			desc: "3 sites only",
-			o: &ListPrefixesOptions{
-				Site: []string{"sitefoo", "sitebar", "sitebaz"},
-			},
-			v: url.Values{
-				"site": []string{"sitefoo", "sitebar", "sitebaz"},
-			},
-		},
-		{
-			desc: "site and site_id, site_id preferred",
-			o: &ListPrefixesOptions{
-				Site:   []string{"site"},
-				SiteID: []int{1},
-			},
-			v: url.Values{
-				"site_id": []string{"1"},
-			},
-		},
-		{
-			desc: "vrf_id only",
-			o: &ListPrefixesOptions{
-				VRFID: 2,
-			},
-			v: url.Values{
-				"vrf_id": []string{"2"},
-			},
-		},
-		{
-			desc: "vrf only",
-			o: &ListPrefixesOptions{
-				VRF: "vrf",
-			},
-			v: url.Values{
-				"vrf": []string{"vrf"},
-			},
-		},
-		{
-			desc: "vrf and vrf_id, vrf_id preferred",
-			o: &ListPrefixesOptions{
-				VRF:   "vrf",
-				VRFID: 2,
-			},
-			v: url.Values{
-				"vrf_id": []string{"2"},
-			},
-		},
-		{
-			desc: "1 vlan_id only",
-			o: &ListPrefixesOptions{
-				VLANID: []int{3},
-			},
-			v: url.Values{
-				"vlan_id": []string{"3"},
-			},
-		},
-		{
-			desc: "3 vlan_ids only",
-			o: &ListPrefixesOptions{
-				VLANID: []int{3, 4, 5},
-			},
-			v: url.Values{
-				"vlan_id": []string{"3", "4", "5"},
-			},
-		},
-		{
-			desc: "vlan_vid only",
-			o: &ListPrefixesOptions{
-				VLANVID: 4094,
-			},
-			v: url.Values{
-				"vlan_vid": []string{"4094"},
-			},
-		},
-		{
-			desc: "vlan and vlan_id, vlan_id preferred",
-			o: &ListPrefixesOptions{
-				VLANVID: 4094,
-				VLANID:  []int{3},
-			},
-			v: url.Values{
-				"vlan_id": []string{"3"},
-			},
-		},
-		{
-			desc: "status only",
-			o: &ListPrefixesOptions{
-				Status: 4,
-			},
-			v: url.Values{
-				"status": []string{"4"},
-			},
-		},
-		{
-			desc: "1 role_id only",
-			o: &ListPrefixesOptions{
-				RoleID: []int{5},
-			},
-			v: url.Values{
-				"role_id": []string{"5"},
-			},
-		},
-		{
-			desc: "3 role_ids only",
-			o: &ListPrefixesOptions{
-				RoleID: []int{5, 6, 7},
-			},
-			v: url.Values{
-				"role_id": []string{"5", "6", "7"},
-			},
-		},
-		{
-			desc: "1 role only",
-			o: &ListPrefixesOptions{
-				Role: []string{"role"},
-			},
-			v: url.Values{
-				"role": []string{"role"},
-			},
-		},
-		{
-			desc: "3 roles only",
-			o: &ListPrefixesOptions{
-				Role: []string{"rolefoo", "rolebar", "rolebaz"},
-			},
-			v: url.Values{
-				"role": []string{"rolefoo", "rolebar", "rolebaz"},
-			},
-		},
-		{
-			desc: "role and role_id, role_id preferred",
-			o: &ListPrefixesOptions{
-				Role:   []string{"role"},
-				RoleID: []int{5},
-			},
-			v: url.Values{
-				"role_id": []string{"5"},
-			},
-		},
-		{
-			desc: "parent only",
-			o: &ListPrefixesOptions{
-				Parent: &net.IPNet{
-					IP:   net.ParseIP("::1"),
-					Mask: net.CIDRMask(128, 128),
-				},
-			},
-			v: url.Values{
-				"parent": []string{"::1/128"},
-			},
-		},
-		{
-			desc: "q only",
-			o: &ListPrefixesOptions{
-				Query: "query",
-			},
-			v: url.Values{
-				"q": []string{"query"},
-			},
-		},
-		{
-			desc: "all options",
-			o: &ListPrefixesOptions{
-				Family:  FamilyIPv4,
-				SiteID:  []int{1},
-				Site:    []string{"site"},
-				VRFID:   2,
-				VRF:     "vrf",
-				VLANID:  []int{3},
-				VLANVID: 4094,
-				Status:  4,
-				RoleID:  []int{5},
-				Role:    []string{"role"},
-				Parent: &net.IPNet{
-					IP:   net.ParseIP("::1"),
-					Mask: net.CIDRMask(128, 128),
-				},
-				Query: "query",
-			},
-			v: url.Values{
-				"family":  []string{strconv.Itoa(int(FamilyIPv4))},
-				"site_id": []string{"1"},
-				"vrf_id":  []string{"2"},
-				"vlan_id": []string{"3"},
-				"status":  []string{"4"},
-				"role_id": []string{"5"},
-				"parent":  []string{"::1/128"},
-				"q":       []string{"query"},
-			},
-		},
-	}
-
-	for i, tt := range tests {
-		t.Logf("[%02d] test %q", i, tt.desc)
-
-		v, err := tt.o.Values()
-		if err != nil {
-			t.Fatalf("unexpected Values error: %v", err)
-		}
-
-		if want, got := tt.v, v; !reflect.DeepEqual(want, got) {
-			t.Fatalf("unexpected url.Values map:\n- want: %v\n-  got: %v",
-				want, got)
-		}
-	}
-}
-
-func TestPrefixMarshalJSON(t *testing.T) {
-	var tests = []struct {
-		desc string
-		p    *Prefix
-		b    []byte
-	}{
-		{
-			desc: "IPv4 prefix",
-			p:    testPrefix(FamilyIPv4, 1),
-			b:    []byte(`{"id":1,"family":4,"prefix":"8.8.0.0/16","site":{"id":1,"name":"SiteIdentifier 1","slug":"siteidentifier1"},"vrf":{"id":1,"name":"VRFIdentifier 1","rd":"rd 1"},"vlan":{"id":1,"vid":1,"name":"vlanidentifier 1","display_name":"VLANIdentifier 1"},"status":1,"role":{"id":1,"name":"RoleIdentifier 1","slug":"roleidentifier1"},"description":"description 1"}`),
-		},
-		{
-			desc: "IPv6 prefix",
-			p:    testPrefix(FamilyIPv6, 2),
-			b:    []byte(`{"id":2,"family":6,"prefix":"2001:4860::/32","site":{"id":2,"name":"SiteIdentifier 2","slug":"siteidentifier2"},"vrf":{"id":2,"name":"VRFIdentifier 2","rd":"rd 2"},"vlan":{"id":2,"vid":2,"name":"vlanidentifier 2","display_name":"VLANIdentifier 2"},"status":1,"role":{"id":2,"name":"RoleIdentifier 2","slug":"roleidentifier2"},"description":"description 2"}`),
-		},
-	}
-
-	for i, tt := range tests {
-		t.Logf("[%02d] test %q", i, tt.desc)
-
-		b, err := json.Marshal(tt.p)
-		if err != nil {
-			t.Fatalf("unexpected JSON marshal error: %v", err)
-		}
-
-		if want, got := tt.b, b; !bytes.Equal(want, got) {
-			t.Fatalf("unexpected JSON bytes:\n- want: %v\n-  got: %v",
-				string(want), string(got))
-		}
-	}
-}
-
-func TestPrefixUnmarshalJSON(t *testing.T) {
-	var tests = []struct {
-		desc string
-		b    []byte
-		p    *Prefix
-		err  error
-	}{
-		{
-			desc: "invalid prefix",
-			b:    []byte(`{"prefix":"foo"}`),
-			err: &net.ParseError{
-				Type: "CIDR address",
-				Text: "foo",
-			},
-		},
-		{
-			desc: "IPv4 prefix",
-			b:    []byte(`{"id":1,"family":4,"prefix":"8.8.0.0/16","site":{"id":1,"name":"SiteIdentifier 1","slug":"siteidentifier1"},"vrf":{"id":1,"name":"VRFIdentifier 1","rd":"rd 1"},"vlan":{"id":1,"vid":1,"name":"vlanidentifier 1","display_name":"VLANIdentifier 1"},"status":1,"role":{"id":1,"name":"RoleIdentifier 1","slug":"roleidentifier1"},"description":"description 1"}`),
-			p:    testPrefix(FamilyIPv4, 1),
-		},
-		{
-			desc: "IPv6 prefix",
-			b:    []byte(`{"id":2,"family":6,"prefix":"2001:4860::/32","site":{"id":2,"name":"SiteIdentifier 2","slug":"siteidentifier2"},"vrf":{"id":2,"name":"VRFIdentifier 2","rd":"rd 2"},"vlan":{"id":2,"vid":2,"name":"vlanidentifier 2","display_name":"VLANIdentifier 2"},"status":1,"role":{"id":2,"name":"RoleIdentifier 2","slug":"roleidentifier2"},"description":"description 2"}`),
-			p:    testPrefix(FamilyIPv6, 2),
-		},
-	}
-
-	for i, tt := range tests {
-		t.Logf("[%02d] test %q", i, tt.desc)
-
-		p := new(Prefix)
-		err := json.Unmarshal(tt.b, p)
-
-		if want, got := tt.err, err; !reflect.DeepEqual(want, got) {
-			t.Fatalf("unexpected error:\n- want: %v\n-  got: %v",
-				want, got)
-		}
-		if err != nil {
-			continue
-		}
-
-		if want, got := *tt.p, *p; !prefixesEqual(want, got) {
-			t.Fatalf("unexpected Prefix:\n- want: %v\n-  got: %v",
-				want, got)
-		}
-	}
-}
-
-func prefixSlicesEqual(a, b []Prefix) bool {
-	if len(a) != len(b) {
-		return false
-	}
-
-	for i := range a {
-		if !prefixesEqual(a[i], b[i]) {
-			return false
-		}
-	}
-
-	return true
-}
-
-func prefixesEqual(a, b Prefix) bool {
-	if a.ID != b.ID {
-		return false
-	}
-
-	if a.Family != b.Family {
-		return false
-	}
-
-	if a.Prefix.String() != b.Prefix.String() {
-		return false
-	}
-
-	if !reflect.DeepEqual(a.Site, b.Site) {
-		return false
-	}
-
-	if !reflect.DeepEqual(a.VRF, b.VRF) {
-		return false
-	}
-
-	if !reflect.DeepEqual(a.VLAN, b.VLAN) {
-		return false
-	}
-
-	if a.Status != b.Status {
-		return false
-	}
-
-	if !reflect.DeepEqual(a.Role, b.Role) {
-		return false
-	}
-
-	if a.Description != b.Description {
-		return false
-	}
-
-	return true
-}
-
-// Used to print values of Prefixs in slice, instead of memory addresses.
-func derefPrefixes(prefixes []*Prefix) []Prefix {
-	p := make([]Prefix, len(prefixes))
-	for i := range prefixes {
-		p[i] = *prefixes[i]
-	}
-
-	return p
-}
diff --git a/ipam_rirs.go b/ipam_rirs.go
deleted file mode 100644
index d9c19d6114beddca11f4409cdf4a99b7da345181..0000000000000000000000000000000000000000
--- a/ipam_rirs.go
+++ /dev/null
@@ -1,66 +0,0 @@
-// Copyright 2016 The go-netbox Authors.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//   http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package netbox
-
-import (
-	"fmt"
-	"net/http"
-)
-
-// An RIR is a Regional Internet Registry which manages allocation of IP
-// addresses.
-type RIR struct {
-	ID   int    `json:"id"`
-	Name string `json:"name"`
-	Slug string `json:"slug"`
-}
-
-// An RIRIdentifier is an RIR returned as a nested object in some top-level
-// objects.  Though RIR and RIRIdentifier currently share the same fields,
-// this may not always be the case.  It contains information which can be
-// used in subsequent API calls to identify and retrieve a full RIR.
-type RIRIdentifier struct {
-	ID   int    `json:"id"`
-	Name string `json:"name"`
-	Slug string `json:"slug"`
-}
-
-// GetRIR retrieves an RIR object from NetBox by its ID.
-func (s *IPAMService) GetRIR(id int) (*RIR, error) {
-	req, err := s.c.NewRequest(
-		http.MethodGet,
-		fmt.Sprintf("/api/ipam/rirs/%d", id),
-		nil,
-	)
-	if err != nil {
-		return nil, err
-	}
-
-	r := new(RIR)
-	err = s.c.Do(req, r)
-	return r, err
-}
-
-// ListRIRs retrives a list of RIR objects from NetBox.
-func (s *IPAMService) ListRIRs() ([]*RIR, error) {
-	req, err := s.c.NewRequest(http.MethodGet, "/api/ipam/rirs/", nil)
-	if err != nil {
-		return nil, err
-	}
-
-	var rs []*RIR
-	err = s.c.Do(req, &rs)
-	return rs, err
-}
diff --git a/ipam_rirs_test.go b/ipam_rirs_test.go
deleted file mode 100644
index 1a0fd994d3ddb5f9bdb13a59bd4520ef37557f1f..0000000000000000000000000000000000000000
--- a/ipam_rirs_test.go
+++ /dev/null
@@ -1,67 +0,0 @@
-// Copyright 2016 The go-netbox Authors.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//   http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package netbox
-
-import (
-	"net/http"
-	"reflect"
-	"testing"
-)
-
-func TestClientIPAMGetRIR(t *testing.T) {
-	wantRIR := testRIR(1)
-
-	c, done := testClient(t, testHandler(t, http.MethodGet, "/api/ipam/rirs/1", wantRIR))
-	defer done()
-
-	gotRIR, err := c.IPAM.GetRIR(wantRIR.ID)
-	if err != nil {
-		t.Fatalf("unexpected error from Client.IPAM.GetRIR: %v", err)
-	}
-
-	if want, got := *wantRIR, *gotRIR; !reflect.DeepEqual(want, got) {
-		t.Fatalf("unexpected RIR:\n- want: %v\n-  got: %v", want, got)
-	}
-}
-
-func TestClientIPAMListRIRs(t *testing.T) {
-	wantRIRs := []*RIR{
-		testRIR(1),
-		testRIR(2),
-		testRIR(3),
-	}
-
-	c, done := testClient(t, testHandler(t, http.MethodGet, "/api/ipam/rirs/", wantRIRs))
-	defer done()
-
-	gotRIRs, err := c.IPAM.ListRIRs()
-	if err != nil {
-		t.Fatalf("unexpected error from Client.IPAM.ListRIRs: %v", err)
-	}
-
-	if want, got := derefRIRs(wantRIRs), derefRIRs(gotRIRs); !reflect.DeepEqual(want, got) {
-		t.Fatalf("unexpected RIRs:\n- want: %v\n-  got: %v", want, got)
-	}
-}
-
-// derefRIRs is used to print values of RIRs in slice, instead of memory addresses.
-func derefRIRs(rirs []*RIR) []RIR {
-	r := make([]RIR, len(rirs))
-	for i := range rirs {
-		r[i] = *rirs[i]
-	}
-
-	return r
-}
diff --git a/ipam_roles.go b/ipam_roles.go
deleted file mode 100644
index 36b7aab86f2c4220be4416d457354cf288eae272..0000000000000000000000000000000000000000
--- a/ipam_roles.go
+++ /dev/null
@@ -1,67 +0,0 @@
-// Copyright 2016 The go-netbox Authors.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//   http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package netbox
-
-import (
-	"fmt"
-	"net/http"
-)
-
-// A Role is a role tag which can be applied to an object such as a
-// Prefix or VLAN.  Role values are typically used to indicate the role
-// of an object, such as infrastructure, management, customer, etc.
-type Role struct {
-	ID     int    `json:"id"`
-	Name   string `json:"name"`
-	Slug   string `json:"slug"`
-	Weight int    `json:"weight"`
-}
-
-// A RoleIdentifier is a reduced version of a Role, returned as a nested
-// object in some top-level objects.  It contains information which can
-// be used in subsequent API calls to identify and retrieve a full Role.
-type RoleIdentifier struct {
-	ID   int    `json:"id"`
-	Name string `json:"name"`
-	Slug string `json:"slug"`
-}
-
-// GetRole retrieves a Role object from NetBox by its ID.
-func (s *IPAMService) GetRole(id int) (*Role, error) {
-	req, err := s.c.NewRequest(
-		http.MethodGet,
-		fmt.Sprintf("/api/ipam/roles/%d", id),
-		nil,
-	)
-	if err != nil {
-		return nil, err
-	}
-
-	r := new(Role)
-	err = s.c.Do(req, r)
-	return r, err
-}
-
-// ListRoles retrieves a list of Role objects from NetBox.
-func (s *IPAMService) ListRoles() ([]*Role, error) {
-	req, err := s.c.NewRequest(http.MethodGet, "/api/ipam/roles/", nil)
-	if err != nil {
-		return nil, err
-	}
-
-	var rs []*Role
-	err = s.c.Do(req, &rs)
-	return rs, err
-}
diff --git a/ipam_roles_test.go b/ipam_roles_test.go
deleted file mode 100644
index 49e759b2457ef4c97f1ced20c3695c5044e41d91..0000000000000000000000000000000000000000
--- a/ipam_roles_test.go
+++ /dev/null
@@ -1,67 +0,0 @@
-// Copyright 2016 The go-netbox Authors.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//   http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package netbox
-
-import (
-	"net/http"
-	"reflect"
-	"testing"
-)
-
-func TestClientIPAMGetRole(t *testing.T) {
-	wantRole := testRole(1)
-
-	c, done := testClient(t, testHandler(t, http.MethodGet, "/api/ipam/roles/1", wantRole))
-	defer done()
-
-	gotRole, err := c.IPAM.GetRole(wantRole.ID)
-	if err != nil {
-		t.Fatalf("unexpected error from Client.IPAM.GetRole: %v", err)
-	}
-
-	if want, got := *wantRole, *gotRole; !reflect.DeepEqual(want, got) {
-		t.Fatalf("unexpected Role:\n- want: %v\n-  got: %v", want, got)
-	}
-}
-
-func TestClientIPAMListRoles(t *testing.T) {
-	wantRoles := []*Role{
-		testRole(1),
-		testRole(2),
-		testRole(3),
-	}
-
-	c, done := testClient(t, testHandler(t, http.MethodGet, "/api/ipam/roles/", wantRoles))
-	defer done()
-
-	gotRoles, err := c.IPAM.ListRoles()
-	if err != nil {
-		t.Fatalf("unexpected error from Client.IPAM.ListRoles: %v", err)
-	}
-
-	if want, got := derefRoles(wantRoles), derefRoles(gotRoles); !reflect.DeepEqual(want, got) {
-		t.Fatalf("unexpected Roles:\n- want: %v\n-  got: %v", want, got)
-	}
-}
-
-// derefRoles is used to print values of Roles in slice, instead of memory addresses.
-func derefRoles(roles []*Role) []Role {
-	r := make([]Role, len(roles))
-	for i := range roles {
-		r[i] = *roles[i]
-	}
-
-	return r
-}
diff --git a/ipam_vlans.go b/ipam_vlans.go
deleted file mode 100644
index 39c912830eed80a341f19acf3b455c25252202bb..0000000000000000000000000000000000000000
--- a/ipam_vlans.go
+++ /dev/null
@@ -1,150 +0,0 @@
-// Copyright 2016 The go-netbox Authors.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//   http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package netbox
-
-import (
-	"fmt"
-	"net/http"
-	"net/url"
-	"strconv"
-)
-
-// A VLANID is a 12 bit integer VLAN ID.  Use its Valid method to determine
-// if the contained value is a valid VLAN ID.
-type VLANID int
-
-// Valid determines if a VLANID contains a valid VLAN ID.
-func (v VLANID) Valid() bool {
-	// Cannot be less than 0, cannot be greater than 4096.
-	return v >= 0 && v <= 4096
-}
-
-// A VLAN is a Virtual LAN object which can be assigned to a Site.
-type VLAN struct {
-	ID          int             `json:"id"`
-	Site        *SiteIdentifier `json:"site"`
-	VID         VLANID          `json:"vid"`
-	Name        string          `json:"name"`
-	Status      Status          `json:"status"`
-	Role        *RoleIdentifier `json:"role"`
-	DisplayName string          `json:"display_name"`
-}
-
-// A VLANIdentifier is a reduced version of a VLAN, returned as a nested
-// object in some top-level objects.  It contains information which can
-// be used in subsequent API calls to identify and retrieve a full VLAN.
-type VLANIdentifier struct {
-	ID          int    `json:"id"`
-	VID         VLANID `json:"vid"`
-	Name        string `json:"name"`
-	DisplayName string `json:"display_name"`
-}
-
-// GetVLAN retrieves a VLAN object from NetBox by its ID.
-func (s *IPAMService) GetVLAN(id int) (*VLAN, error) {
-	req, err := s.c.NewRequest(
-		http.MethodGet,
-		fmt.Sprintf("/api/ipam/vlans/%d", id),
-		nil,
-	)
-	if err != nil {
-		return nil, err
-	}
-
-	vlan := new(VLAN)
-	err = s.c.Do(req, vlan)
-	return vlan, err
-}
-
-// ListVLANs retrives a list of VLAN objects from NetBox, filtered according
-// to the parameters specified in options.
-//
-// If options is nil, all VLANs will be retrieved.
-func (s *IPAMService) ListVLANs(options *ListVLANsOptions) ([]*VLAN, error) {
-	req, err := s.c.NewRequest(http.MethodGet, "/api/ipam/vlans/", options)
-	if err != nil {
-		return nil, err
-	}
-
-	var vlans []*VLAN
-	err = s.c.Do(req, &vlans)
-	return vlans, err
-}
-
-// ListVLANsOptions is used as an argument for Client.IPAM.ListVLANs.
-// Integer fields with an *ID suffix are preferred over their string
-// counterparts, and if both are set, only the *ID field will be used.
-type ListVLANsOptions struct {
-	Name     string
-	Role     []string
-	RoleID   []int
-	Site     []string
-	SiteID   []int
-	Status   []string
-	StatusID int
-	VID      VLANID
-}
-
-// Values generates a url.Values map from the data in ListVLANsOptions.
-func (o *ListVLANsOptions) Values() (url.Values, error) {
-	if o == nil {
-		return nil, nil
-	}
-
-	v := url.Values{}
-
-	if o.Name != "" {
-		v.Set("name", o.Name)
-	}
-
-	// IDs should always be preferred over string names
-
-	switch {
-	case len(o.RoleID) > 0:
-		for _, r := range o.RoleID {
-			v.Add("role_id", strconv.Itoa(r))
-		}
-	case len(o.Role) > 0:
-		for _, r := range o.Role {
-			v.Add("role", r)
-		}
-	}
-
-	switch {
-	case len(o.SiteID) > 0:
-		for _, s := range o.SiteID {
-			v.Add("site_id", strconv.Itoa(s))
-		}
-	case len(o.Site) > 0:
-		for _, s := range o.Site {
-			v.Add("site", s)
-		}
-	}
-
-	switch {
-	case o.StatusID != 0:
-		v.Set("status_id", strconv.Itoa(o.StatusID))
-	case len(o.Status) > 0:
-		for _, s := range o.Status {
-			v.Add("status", s)
-		}
-	}
-
-	if o.VID != 0 {
-		v.Set("vid", strconv.Itoa(int(o.VID)))
-	}
-
-	return v, nil
-}
diff --git a/ipam_vlans_test.go b/ipam_vlans_test.go
deleted file mode 100644
index 468b122a9845f87957c6300fb9334ce52c9312aa..0000000000000000000000000000000000000000
--- a/ipam_vlans_test.go
+++ /dev/null
@@ -1,297 +0,0 @@
-// Copyright 2016 The go-netbox Authors.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//   http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package netbox
-
-import (
-	"math"
-	"net/http"
-	"net/url"
-	"reflect"
-	"testing"
-)
-
-func TestVLANIDValid(t *testing.T) {
-	var tests = []struct {
-		id VLANID
-		ok bool
-	}{
-		{
-			id: math.MinInt64,
-		},
-		{
-			id: -1,
-		},
-		{
-			id: 4097,
-		},
-		{
-			id: math.MaxInt64,
-		},
-		{
-			id: 0,
-			ok: true,
-		},
-		{
-			id: 4096,
-			ok: true,
-		},
-	}
-
-	for _, tt := range tests {
-		if want, got := tt.ok, tt.id.Valid(); want != got {
-			t.Fatalf("unexpected VLANID(%d).Valid():\n- want: %v\n-  got: %v",
-				tt.id, want, got)
-		}
-	}
-}
-
-func TestClientIPAMGetVLAN(t *testing.T) {
-	wantVLAN := testVLAN(1)
-
-	c, done := testClient(t, testHandler(t, http.MethodGet, "/api/ipam/vlans/1", wantVLAN))
-	defer done()
-
-	gotVLAN, err := c.IPAM.GetVLAN(wantVLAN.ID)
-	if err != nil {
-		t.Fatalf("unexpected error from Client.IPAM.GetVLAN: %v", err)
-	}
-
-	if want, got := *wantVLAN, *gotVLAN; !reflect.DeepEqual(want, got) {
-		t.Fatalf("unexpected VLAN:\n- want: %v\n-  got: %v", want, got)
-	}
-}
-
-func TestClientIPAMListVLANs(t *testing.T) {
-	wantVLANs := []*VLAN{
-		testVLAN(1),
-		testVLAN(2),
-		testVLAN(3),
-	}
-
-	c, done := testClient(t, testHandler(t, http.MethodGet, "/api/ipam/vlans/", wantVLANs))
-	defer done()
-
-	gotVLANs, err := c.IPAM.ListVLANs(nil)
-	if err != nil {
-		t.Fatalf("unexpected error from Client.IPAM.ListVLANs: %v", err)
-	}
-
-	if want, got := derefVLANs(wantVLANs), derefVLANs(gotVLANs); !reflect.DeepEqual(want, got) {
-		t.Fatalf("unexpected VLANs:\n- want: %v\n-  got: %v", want, got)
-	}
-}
-
-func TestListVLANsOptionsValues(t *testing.T) {
-	var tests = []struct {
-		desc string
-		o    *ListVLANsOptions
-		v    url.Values
-	}{
-		{
-			desc: "empty options",
-		},
-		{
-			desc: "name only",
-			o: &ListVLANsOptions{
-				Name: "name",
-			},
-			v: url.Values{
-				"name": []string{"name"},
-			},
-		},
-		{
-			desc: "1 role_id only",
-			o: &ListVLANsOptions{
-				RoleID: []int{1},
-			},
-			v: url.Values{
-				"role_id": []string{"1"},
-			},
-		},
-		{
-			desc: "3 role_ids only",
-			o: &ListVLANsOptions{
-				RoleID: []int{1, 2, 3},
-			},
-			v: url.Values{
-				"role_id": []string{"1", "2", "3"},
-			},
-		},
-		{
-			desc: "1 role only",
-			o: &ListVLANsOptions{
-				Role: []string{"role"},
-			},
-			v: url.Values{
-				"role": []string{"role"},
-			},
-		},
-		{
-			desc: "3 roles only",
-			o: &ListVLANsOptions{
-				Role: []string{"rolefoo", "rolebar", "rolebaz"},
-			},
-			v: url.Values{
-				"role": []string{"rolefoo", "rolebar", "rolebaz"},
-			},
-		},
-		{
-			desc: "role and role_id, role_id preferred",
-			o: &ListVLANsOptions{
-				Role:   []string{"role"},
-				RoleID: []int{1},
-			},
-			v: url.Values{
-				"role_id": []string{"1"},
-			},
-		},
-		{
-			desc: "1 site_id only",
-			o: &ListVLANsOptions{
-				SiteID: []int{2},
-			},
-			v: url.Values{
-				"site_id": []string{"2"},
-			},
-		},
-		{
-			desc: "3 site_ids only",
-			o: &ListVLANsOptions{
-				SiteID: []int{2, 3, 4},
-			},
-			v: url.Values{
-				"site_id": []string{"2", "3", "4"},
-			},
-		},
-		{
-			desc: "1 site only",
-			o: &ListVLANsOptions{
-				Site: []string{"site"},
-			},
-			v: url.Values{
-				"site": []string{"site"},
-			},
-		},
-		{
-			desc: "3 sites only",
-			o: &ListVLANsOptions{
-				Site: []string{"sitefoo", "sitebar", "sitebaz"},
-			},
-			v: url.Values{
-				"site": []string{"sitefoo", "sitebar", "sitebaz"},
-			},
-		},
-		{
-			desc: "site and site_id, site_id preferred",
-			o: &ListVLANsOptions{
-				Site:   []string{"site"},
-				SiteID: []int{2},
-			},
-			v: url.Values{
-				"site_id": []string{"2"},
-			},
-		},
-		{
-			desc: "status_id only",
-			o: &ListVLANsOptions{
-				StatusID: 3,
-			},
-			v: url.Values{
-				"status_id": []string{"3"},
-			},
-		},
-		{
-			desc: "1 status only",
-			o: &ListVLANsOptions{
-				Status: []string{"status"},
-			},
-			v: url.Values{
-				"status": []string{"status"},
-			},
-		},
-		{
-			desc: "3 statuses only",
-			o: &ListVLANsOptions{
-				Status: []string{"statusfoo", "statusbar", "statusbaz"},
-			},
-			v: url.Values{
-				"status": []string{"statusfoo", "statusbar", "statusbaz"},
-			},
-		},
-		{
-			desc: "status and status_id, status_id preferred",
-			o: &ListVLANsOptions{
-				Status:   []string{"status"},
-				StatusID: 3,
-			},
-			v: url.Values{
-				"status_id": []string{"3"},
-			},
-		},
-		{
-			desc: "vid only",
-			o: &ListVLANsOptions{
-				VID: 4,
-			},
-			v: url.Values{
-				"vid": []string{"4"},
-			},
-		},
-		{
-			desc: "all options",
-			o: &ListVLANsOptions{
-				Name:     "name",
-				Role:     []string{"role"},
-				RoleID:   []int{1},
-				Site:     []string{"site"},
-				SiteID:   []int{2},
-				Status:   []string{"status"},
-				StatusID: 3,
-				VID:      4,
-			},
-			v: url.Values{
-				"name":      []string{"name"},
-				"role_id":   []string{"1"},
-				"site_id":   []string{"2"},
-				"status_id": []string{"3"},
-				"vid":       []string{"4"},
-			},
-		},
-	}
-
-	for i, tt := range tests {
-		t.Logf("[%02d] test %q", i, tt.desc)
-
-		v, err := tt.o.Values()
-		if err != nil {
-			t.Fatalf("unexpected Values error: %v", err)
-		}
-
-		if want, got := tt.v, v; !reflect.DeepEqual(want, got) {
-			t.Fatalf("unexpected url.Values map:\n- want: %v\n-  got: %v",
-				want, got)
-		}
-	}
-}
-
-// Used to print values of VLANs in slice, instead of memory addresses.
-func derefVLANs(vlans []*VLAN) []VLAN {
-	v := make([]VLAN, len(vlans))
-	for i := range vlans {
-		v[i] = *vlans[i]
-	}
-
-	return v
-}
diff --git a/ipam_vrfs.go b/ipam_vrfs.go
deleted file mode 100644
index e414306448d4751864a4f720b26bf22e4310689d..0000000000000000000000000000000000000000
--- a/ipam_vrfs.go
+++ /dev/null
@@ -1,65 +0,0 @@
-// Copyright 2016 The go-netbox Authors.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//   http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package netbox
-
-import (
-	"fmt"
-	"net/http"
-)
-
-// A VRF is a Virtual Routing and Forwarding device.
-type VRF struct {
-	ID          int    `json:"id"`
-	Name        string `json:"name"`
-	RD          string `json:"rd"`
-	Description string `json:"description"`
-}
-
-// A VRFIdentifier is a VRF returned as a nested object in some top-level
-// objects.  It contains information which can be used in subsequent API
-// calls to identify and retrieve a full VRF.
-type VRFIdentifier struct {
-	ID   int    `json:"id"`
-	Name string `json:"name"`
-	RD   string `json:"rd"`
-}
-
-// GetVRF retrieves a VRF object from NetBox by its ID.
-func (s *IPAMService) GetVRF(id int) (*VRF, error) {
-	req, err := s.c.NewRequest(
-		http.MethodGet,
-		fmt.Sprintf("/api/ipam/vrfs/%d", id),
-		nil,
-	)
-	if err != nil {
-		return nil, err
-	}
-
-	v := new(VRF)
-	err = s.c.Do(req, v)
-	return v, err
-}
-
-// ListVRFs retrives a list of VRF objects from NetBox.
-func (s *IPAMService) ListVRFs() ([]*VRF, error) {
-	req, err := s.c.NewRequest(http.MethodGet, "/api/ipam/vrfs/", nil)
-	if err != nil {
-		return nil, err
-	}
-
-	var vs []*VRF
-	err = s.c.Do(req, &vs)
-	return vs, err
-}
diff --git a/ipam_vrfs_test.go b/ipam_vrfs_test.go
deleted file mode 100644
index dc3159fb5e623a04e7c1f5a84cd42b4144cc56f1..0000000000000000000000000000000000000000
--- a/ipam_vrfs_test.go
+++ /dev/null
@@ -1,67 +0,0 @@
-// Copyright 2016 The go-netbox Authors.
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-//   http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package netbox
-
-import (
-	"net/http"
-	"reflect"
-	"testing"
-)
-
-func TestClientIPAMGetVRF(t *testing.T) {
-	wantVRF := testVRF(1)
-
-	c, done := testClient(t, testHandler(t, http.MethodGet, "/api/ipam/vrfs/1", wantVRF))
-	defer done()
-
-	gotVRF, err := c.IPAM.GetVRF(wantVRF.ID)
-	if err != nil {
-		t.Fatalf("unexpected error from Client.IPAM.GetVRF: %v", err)
-	}
-
-	if want, got := *wantVRF, *gotVRF; !reflect.DeepEqual(want, got) {
-		t.Fatalf("unexpected VRF:\n- want: %v\n-  got: %v", want, got)
-	}
-}
-
-func TestClientIPAMListVRFs(t *testing.T) {
-	wantVRFs := []*VRF{
-		testVRF(1),
-		testVRF(2),
-		testVRF(3),
-	}
-
-	c, done := testClient(t, testHandler(t, http.MethodGet, "/api/ipam/vrfs/", wantVRFs))
-	defer done()
-
-	gotVRFs, err := c.IPAM.ListVRFs()
-	if err != nil {
-		t.Fatalf("unexpected error from Client.IPAM.ListVRFs: %v", err)
-	}
-
-	if want, got := derefVRFs(wantVRFs), derefVRFs(gotVRFs); !reflect.DeepEqual(want, got) {
-		t.Fatalf("unexpected VRFs:\n- want: %v\n-  got: %v", want, got)
-	}
-}
-
-// derefVRFs is used to print values of VRFs in slice, instead of memory addresses.
-func derefVRFs(vrfs []*VRF) []VRF {
-	v := make([]VRF, len(vrfs))
-	for i := range vrfs {
-		v[i] = *vrfs[i]
-	}
-
-	return v
-}
diff --git a/netbox/client.go b/netbox/client.go
new file mode 100644
index 0000000000000000000000000000000000000000..194079a2cbebf2eac8414ee5895dcac28b69d4be
--- /dev/null
+++ b/netbox/client.go
@@ -0,0 +1,212 @@
+// Copyright 2017 The go-netbox Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package netbox
+
+import (
+	"bytes"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"net/http"
+	"net/url"
+	"strings"
+)
+
+// A Client is a NetBox client.  It can be used to retrieve network and
+// datacenter infrastructure information from a NetBox server.
+type Client struct {
+	// DCIM provides access to methods in NetBox's DCIM API.
+	DCIM *DCIMService
+
+	// IPAM provides access to methods in NetBox's IPAM API.
+	IPAM *IPAMService
+
+	// Tenancy provides access to methods in NetBox's Tenancy API.
+	Tenancy *TenancyService
+
+	u      *url.URL
+	client *http.Client
+}
+
+// NewClient returns a new instance of a NetBox client.  addr specifies the address
+// of the NetBox server, and client specifies an optional HTTP client to use
+// for requests.
+//
+// If client is nil, a default HTTP client will be used.
+func NewClient(addr string, client *http.Client) (*Client, error) {
+	if client == nil {
+		client = &http.Client{}
+	}
+
+	// Append trailing slash there is none. This is necessary
+	// to be able to concat url parts in a correct manner.
+	// See NewRequest
+	if !strings.HasSuffix(addr, "/") {
+		addr = addr + "/"
+	}
+
+	u, err := url.Parse(addr)
+	if err != nil {
+		return nil, err
+	}
+
+	c := &Client{
+		u:      u,
+		client: client,
+	}
+
+	c.DCIM = &DCIMService{c: c}
+	c.IPAM = &IPAMService{c: c}
+	c.Tenancy = NewTenancyService(c)
+
+	return c, nil
+}
+
+// NewRequest creates a HTTP request using the input HTTP method, URL
+// endpoint, and a Valuer which creates URL parameters for the request.
+//
+// If a nil Valuer is specified, no query parameters will be sent with the
+// request.
+func (c *Client) NewRequest(method string, endpoint string, options Valuer) (*http.Request, error) {
+	return c.NewDataRequest(method, endpoint, options, nil)
+}
+
+// NewDataRequest creates a HTTP request using the input HTTP method, URL
+// endpoint, a Valuer which creates URL parameters for the request, and
+// a io.Reader as the body of the request.
+//
+// If a nil Valuer is specified, no query parameters will be sent with the
+// request.
+//
+// If a nil io.Reader is specified, no body will be sent with the request.
+func (c *Client) NewDataRequest(method string, endpoint string, options Valuer, body io.Reader) (*http.Request, error) {
+	// Allow specifying a base path for API requests, so if a NetBox server
+	// resides at a path like http://example.com/netbox/, API requests will
+	// be sent to http://example.com/netbox/api/...
+	//
+	// Enables support of: https://github.com/digitalocean/netbox/issues/212.
+	//
+	// Remove leading slash if there is one. This is necessary to be able to
+	// concat url parts in a correct manner. We can not use path.Join here,
+	// because this always trims the trailing slash, which causes the
+	// Do function to always run into 301 and then retry the correct
+	// Location. With GET, it does work with one useless request, but it breaks
+	// each other http method.
+	// Doing this, because out-of-tree extensions are more robust. If someone
+	// implements an own API-call, we do not override parts of c.u, even if
+	// the caller uses "/api/...".
+	rel, err := url.Parse(strings.TrimLeft(endpoint, "/"))
+	if err != nil {
+		return nil, err
+	}
+
+	u := c.u.ResolveReference(rel)
+
+	// If no valuer specified, create a request with no query parameters
+	if options == nil {
+		return http.NewRequest(method, u.String(), body)
+	}
+
+	values, err := options.Values()
+	if err != nil {
+		return nil, err
+	}
+	u.RawQuery = values.Encode()
+
+	return http.NewRequest(method, u.String(), body)
+}
+
+// NewJSONRequest creates a HTTP request using the input HTTP method, URL
+// endpoint, a Valuer which creates URL parameters for the request, and
+// an io.Reader as the body of the request.
+//
+// If a nil Valuer is specified, no query parameters will be sent with the
+// request.
+//
+// The body parameter is marshaled to JSON and sent as a HTTP request body.
+// Body must not be nil.
+func (c *Client) NewJSONRequest(method string, endpoint string, options Valuer, body interface{}) (*http.Request, error) {
+	if body == nil {
+		return nil, errors.New("expected body to be not nil")
+	}
+
+	b := new(bytes.Buffer)
+	err := json.NewEncoder(b).Encode(body)
+	if err != nil {
+		return nil, err
+	}
+
+	req, err := c.NewDataRequest(
+		method,
+		endpoint,
+		options,
+		b,
+	)
+	if err != nil {
+		return nil, err
+	}
+
+	req.Header.Set("Content-Type", "application/json; charset=utf-8")
+	return req, nil
+}
+
+// Do executes an HTTP request and if v is not nil, Do unmarshals result
+// JSON onto v.
+func (c *Client) Do(req *http.Request, v interface{}) error {
+	res, err := c.client.Do(req)
+	if err != nil {
+		return err
+	}
+	defer func() {
+		_ = res.Body.Close()
+	}()
+	// Test if the request was successful.
+	// If not, do not try to decode the body. This would result in
+	// misleading error messages
+	err = httpStatusOK(res)
+	if err != nil {
+		return err
+	}
+
+	if v == nil {
+		return nil
+	}
+
+	return json.NewDecoder(res.Body).Decode(v)
+}
+
+// httpStatusOK tests if the StatusCode of res is smaller than 300. Tries to
+// Unmarshal the response into json, and returns only the detail
+// if this exists. Otherwise returns the status code with the raw Body data.
+func httpStatusOK(res *http.Response) error {
+	if res.StatusCode >= http.StatusMultipleChoices {
+		errDetail := struct {
+			Detail string `json:"detail"`
+		}{}
+		bodyData, err := ioutil.ReadAll(res.Body)
+		if err != nil {
+			return fmt.Errorf("%d - %v", res.StatusCode, err)
+		}
+		err = json.Unmarshal(bodyData, &errDetail)
+		if err == nil && errDetail.Detail != "" {
+			return fmt.Errorf("%d - %s", res.StatusCode, errDetail.Detail)
+		}
+
+		return fmt.Errorf("%d - %s", res.StatusCode, bodyData)
+	}
+	return nil
+}
diff --git a/netbox/client_test.go b/netbox/client_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..4170a338e1a19b4b25ca7353360e2c6cc3ea42d0
--- /dev/null
+++ b/netbox/client_test.go
@@ -0,0 +1,291 @@
+// Copyright 2017 The go-netbox Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package netbox
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"net/http/httptest"
+	"net/url"
+	"reflect"
+	"strconv"
+	"testing"
+)
+
+func TestClientBadJSON(t *testing.T) {
+	c, done := testClient(t, func(w http.ResponseWriter, r *http.Request) {
+		w.Write([]byte("foo"))
+	})
+	defer done()
+
+	req, err := c.NewRequest(http.MethodGet, "/", nil)
+	if err != nil {
+		t.Fatal("expected no error, but an error returned")
+	}
+
+	// Pass empty struct to trigger JSON unmarshaling path
+	var v struct{}
+
+	err = c.Do(req, &v)
+	if _, ok := err.(*json.SyntaxError); !ok {
+		t.Fatalf("unexpected error type: %T", err)
+	}
+}
+
+func TestClientBadStatusCode(t *testing.T) {
+	var tests = []struct {
+		desc       string
+		data       []byte
+		statusCode int
+		want       error
+	}{
+		{
+			desc:       "403, but no json result",
+			data:       []byte("foo"),
+			statusCode: http.StatusForbidden,
+			want:       errors.New("403 - foo"),
+		},
+		{
+			desc:       "403, with json, but without detail",
+			data:       []byte(`{"error_msg": "some error occurred"}`),
+			statusCode: http.StatusForbidden,
+			want:       errors.New(`403 - {"error_msg": "some error occurred"}`),
+		},
+		{
+			desc:       "500, but correct json",
+			data:       []byte(`{"detail": "some error occurred"}`),
+			statusCode: http.StatusInternalServerError,
+			want:       errors.New("500 - some error occurred"),
+		},
+	}
+
+	for i, tt := range tests {
+		t.Run(fmt.Sprintf("[%d] %s", i, tt.desc), func(t *testing.T) {
+			c, done := testClient(t, func(w http.ResponseWriter, r *http.Request) {
+				w.WriteHeader(tt.statusCode)
+				w.Write(tt.data)
+			})
+			defer done()
+
+			req, err := c.NewRequest(http.MethodGet, "/", nil)
+			if err != nil {
+				t.Fatal("expected no error, but an error returned")
+			}
+
+			var v struct{}
+			err = c.Do(req, &v)
+			if want, got := tt.want, err; !reflect.DeepEqual(want, got) {
+				t.Fatalf("expected error:\n- want: %v\n-  got: %v", want, got)
+			}
+		})
+	}
+}
+
+func TestNewJSONRequest(t *testing.T) {
+	c, done := testClient(t, func(w http.ResponseWriter, r *http.Request) {
+		w.Write([]byte("foo"))
+	})
+	defer done()
+	wantBody := "{\"id\":1,\"name\":\"Test 1\"}\n"
+	wantHeader := "application/json; charset=utf-8"
+
+	req, err := c.NewJSONRequest(http.MethodPost, "/", nil, &struct {
+		ID   int    `json:"id"`
+		Name string `json:"name"`
+	}{
+		ID:   1,
+		Name: "Test 1",
+	})
+	if err != nil {
+		t.Fatal("expected no error, but an error returned")
+	}
+
+	res, err := ioutil.ReadAll(req.Body)
+	if err != nil {
+		t.Fatal("expected no error, but an error returned")
+	}
+	if want, got := wantBody, string(res); got != want {
+		t.Fatalf("unexpected body:\n- want: %v\n-  got: %v", want, got)
+	}
+
+	if want, got := wantHeader, req.Header.Get("Content-Type"); got != want {
+		t.Fatalf("unexpected body:\n- want: %v\n-  got: %v", want, got)
+	}
+
+	req, err = c.NewJSONRequest(http.MethodPost, "/", nil, nil)
+	if err == nil {
+		t.Fatal("expected an error, but there was none")
+	}
+	if req != nil {
+		t.Fatalf("expected a nil request, but got %v", req)
+	}
+}
+
+func TestClientQueryParameters(t *testing.T) {
+	c := &Client{
+		u:      &url.URL{},
+		client: &http.Client{},
+	}
+
+	const (
+		wantFoo = "foo"
+		wantBar = 1
+	)
+
+	req, err := c.NewRequest(http.MethodGet, "/", testValuer{
+		Foo: wantFoo,
+		Bar: wantBar,
+	})
+	if err != nil {
+		t.Fatal("expected an error, but no error returned")
+	}
+
+	q := req.URL.Query()
+	if want, got := 2, len(q); want != got {
+		t.Fatalf("unexpected number of query parameters:\n- want: %v\n-  got: %v",
+			want, got)
+	}
+
+	if want, got := wantFoo, q.Get("foo"); want != got {
+		t.Fatalf("unexpected foo:\n- want: %v\n-  got: %v", want, got)
+	}
+
+	if want, got := strconv.Itoa(wantBar), q.Get("bar"); want != got {
+		t.Fatalf("unexpected bar:\n- want: %v\n-  got: %v", want, got)
+	}
+}
+
+func TestClientPrependBaseURLPath(t *testing.T) {
+	u, err := url.Parse("http://example.com/netbox/")
+	if err != nil {
+		t.Fatalf("unexpected error: %v", err)
+	}
+
+	c := &Client{
+		u:      u,
+		client: &http.Client{},
+	}
+
+	req, err := c.NewRequest(http.MethodGet, "/api/ipam/vlans", nil)
+	if err != nil {
+		t.Fatal("expected an error, but no error returned")
+	}
+
+	if want, got := "/netbox/api/ipam/vlans", req.URL.Path; want != got {
+		t.Fatalf("unexpected URL path:\n- want: %q\n-  got: %q",
+			want, got)
+	}
+}
+
+type testValuer struct {
+	Foo string
+	Bar int
+}
+
+func (q testValuer) Values() (url.Values, error) {
+	v := url.Values{}
+
+	if q.Foo != "" {
+		v.Set("foo", q.Foo)
+	}
+
+	if q.Bar != 0 {
+		v.Set("bar", strconv.Itoa(q.Bar))
+	}
+
+	return v, nil
+}
+
+func testClient(t *testing.T, fn func(w http.ResponseWriter, r *http.Request)) (*Client, func()) {
+	s := httptest.NewServer(http.HandlerFunc(fn))
+
+	c, err := NewClient(s.URL, nil)
+	if err != nil {
+		t.Fatalf("error creating Client: %v", err)
+	}
+
+	return c, func() { s.Close() }
+}
+
+func testHandler(t *testing.T, method string, path string, v interface{}) http.HandlerFunc {
+	return testStatusHandler(t, method, path, v, 0)
+}
+
+func testStatusHandler(t *testing.T, method string, path string, v interface{}, statusCode int) http.HandlerFunc {
+	return func(w http.ResponseWriter, r *http.Request) {
+		if want, got := method, r.Method; want != got {
+			t.Fatalf("unexpected HTTP method:\n- want: %v\n-  got: %v", want, got)
+		}
+
+		if want, got := path, r.URL.Path; want != got {
+			t.Fatalf("unexpected URL path:\n- want: %v\n-  got: %v", want, got)
+		}
+
+		if statusCode > 0 {
+			w.WriteHeader(statusCode)
+		}
+
+		if err := json.NewEncoder(w).Encode(v); err != nil {
+			t.Fatalf("error while encoding JSON: %v", err)
+		}
+	}
+}
+
+func testTenantGroupCreate(n int) *TenantGroup {
+	return &TenantGroup{
+		Name: fmt.Sprintf("Tenant Group %d", n),
+		Slug: fmt.Sprintf("tenant-group-%d", n),
+	}
+}
+
+func testTenantGroup(n int) *TenantGroup {
+	return &TenantGroup{
+		ID:   n,
+		Name: fmt.Sprintf("Tenant Group %d", n),
+		Slug: fmt.Sprintf("tenant-group-%d", n),
+	}
+}
+
+func testTenant(n int) *Tenant {
+	return testTenantWithGroup(n, testTenantGroup(n))
+}
+
+func testTenantCreate(n int) *Tenant {
+	return testTenantWithGroupCreate(n, testTenantGroup(n))
+}
+
+func testTenantWithGroupCreate(n int, t *TenantGroup) *Tenant {
+	return &Tenant{
+		Name:        fmt.Sprintf("Tenant %d", n),
+		Slug:        fmt.Sprintf("tenant-%d", n),
+		Description: fmt.Sprintf("Tenant %d Description", n),
+		Comments:    fmt.Sprintf("Tenant %d Comments", n),
+		Group:       t,
+	}
+}
+
+func testTenantWithGroup(n int, t *TenantGroup) *Tenant {
+	return &Tenant{
+		ID:          n,
+		Name:        fmt.Sprintf("Tenant %d", n),
+		Slug:        fmt.Sprintf("tenant-%d", n),
+		Description: fmt.Sprintf("Tenant %d Description", n),
+		Comments:    fmt.Sprintf("Tenant %d Comments", n),
+		Group:       t,
+	}
+}
diff --git a/dcim.go b/netbox/dcim.go
similarity index 95%
rename from dcim.go
rename to netbox/dcim.go
index 22fc5858bdfbbe6cc2b53803e37a2c61371d0494..a7f4dffd91eef822c77d72c6a028753fd08aec37 100644
--- a/dcim.go
+++ b/netbox/dcim.go
@@ -1,4 +1,4 @@
-// Copyright 2016 The go-netbox Authors.
+// Copyright 2017 The go-netbox Authors.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
diff --git a/dcim_poweroutlet.go b/netbox/doc.go
similarity index 67%
rename from dcim_poweroutlet.go
rename to netbox/doc.go
index e75b708bd5d0ed7c0c8d08f10a7719bf1169cbc2..558f7233145bec714e06b8eb29a7d28d999827fb 100644
--- a/dcim_poweroutlet.go
+++ b/netbox/doc.go
@@ -1,4 +1,4 @@
-// Copyright 2016 The go-netbox Authors.
+// Copyright 2017 The go-netbox Authors.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,11 +12,6 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+// Package netbox provides an API 2.0 client for DigitalOcean's NetBox IPAM
+// and DCIM service.
 package netbox
-
-// PowerOutletIdentifier represents a reduced version of a power outlet object.
-type PowerOutletIdentifier struct {
-	ID     int               `json:"id"`
-	Device *DeviceIdentifier `json:"device"`
-	Name   string            `json:"name"`
-}
diff --git a/netbox/generate_basic_tests.go b/netbox/generate_basic_tests.go
new file mode 100644
index 0000000000000000000000000000000000000000..80d879b73416b7dbfdba24b4e68b82c80ef04028
--- /dev/null
+++ b/netbox/generate_basic_tests.go
@@ -0,0 +1,383 @@
+// Copyright 2017 The go-netbox Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//+build ignore
+
+package main
+
+import (
+	"bytes"
+	"flag"
+	"fmt"
+	"go/format"
+	"log"
+	"os"
+	"text/template"
+	"time"
+)
+
+func main() {
+	typeName := flag.String("type-name", "Example", "Name of the type to use (e.g. TenantGroup).")
+	serviceName := flag.String("service-name", "ExampleService", "Name of the service to create (e.g. TenantGroupsService).")
+	endpoint := flag.String("endpoint", "tenancy", "Name of the endpoint (e.g. dcim, ipam, tenancy).")
+	service := flag.String("service", "example", "Name of the service below endpoint (e.g. tenant-groups).")
+	clientEndpoint := flag.String("client-endpoint", "Tenancy", "Name of the client endpoint (e.g. DCIM, IPAM, Tenancy).")
+	clientService := flag.String("client-service", "TenantGroups", "Name of the client service (e.g. TenantGroups, Tenants).")
+	withoutListOpts := flag.Bool("without-list-opts", false, "Disable list options for this endpoint.")
+
+	flag.Parse()
+
+	b := &bytes.Buffer{}
+	functionsTemplate.Execute(b, struct {
+		Timestamp      time.Time
+		TypeName       string
+		ServiceName    string
+		Endpoint       string
+		Service        string
+		ClientEndpoint string
+		ClientService  string
+		ListOpts       bool
+		JSONTag        func(string) string
+	}{
+		Timestamp:      time.Now(),
+		TypeName:       *typeName,
+		ServiceName:    *serviceName,
+		Endpoint:       *endpoint,
+		Service:        *service,
+		ClientEndpoint: *clientEndpoint,
+		ClientService:  *clientService,
+		ListOpts:       !*withoutListOpts,
+		JSONTag:        func(name string) string { return "`json:\"" + name + "\"`" },
+	})
+
+	// go fmt
+	res, err := format.Source(b.Bytes())
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	f, err := os.Create(fmt.Sprintf("%s_%s_basic_test.go", *endpoint, *service))
+	if err != nil {
+		log.Fatal(err)
+	}
+	defer f.Close()
+	_, err = f.Write(res)
+	if err != nil {
+		log.Fatal(err)
+	}
+}
+
+var functionsTemplate = template.Must(template.New("").Parse(`// Copyright 2017 The go-netbox Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Code generated by generate_basic_tests.go. DO NOT EDIT.
+
+package netbox
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"net/http"
+	"reflect"
+	"testing"
+)
+
+// Using this to override MarshalJSON
+// In all cases when posting data to netbox-API, the {{ .TypeName }}.MarshalJSON is what you want,
+// but not here as a return in testHandler
+type serverData{{ .TypeName }} {{ .TypeName }}
+
+func convertToServerData{{ .TypeName }}(data []*{{ .TypeName }}) []*serverData{{ .TypeName }} {
+	dataWant := make([]*serverData{{ .TypeName }}, len(data))
+	for i := range data {
+		tmp := serverData{{ .TypeName }}(*data[i])
+		dataWant[i] = &tmp
+	}
+	return dataWant
+}
+
+func TestBasic{{ .TypeName }}Get(t *testing.T) {
+	var tests = []struct {
+		desc      string
+		want      *{{ .TypeName }}
+	}{
+		{
+			desc:      "Simple {{ .TypeName }}",
+			want:      test{{ .TypeName }}(1),
+		},
+	}
+
+	for i, tt := range tests {
+		t.Run(fmt.Sprintf("[%d] %s", i, tt.desc), func(t *testing.T) {
+			serverData := serverData{{ .TypeName }}(*tt.want)
+
+			c, done := testClient(t, testHandler(t, http.MethodGet, "/api/{{ .Endpoint }}/{{ .Service }}/1/", &serverData))
+			defer done()
+
+			res, err := c.{{ .ClientEndpoint }}.{{ .ClientService }}.Get(1)
+			if err != nil {
+				t.Fatalf("unexpected error from Client.{{ .ClientEndpoint }}.{{ .ClientService }}.Get: %v", err)
+			}
+
+			if want, got := tt.want, res; !reflect.DeepEqual(want, got) {
+				t.Fatalf("unexpected {{ .TypeName }}:\n- want: %v\n-  got: %v", want, got)
+			}
+		})
+	}
+}
+
+func TestBasic{{ .TypeName }}Get404(t *testing.T) {
+	c, done := testClient(t, testStatusHandler(t, http.MethodGet, "/api/{{ .Endpoint }}/{{ .Service }}/1/", &struct {
+		Detail string {{ call .JSONTag "detail" }}
+	}{
+		Detail: "Not found.",
+	},
+		http.StatusNotFound))
+	defer done()
+
+	res, err := c.{{ .ClientEndpoint }}.{{ .ClientService }}.Get(1)
+	errstr := "404 - Not found."
+	if want, got := errors.New(errstr), err; !reflect.DeepEqual(want, got) {
+		t.Fatalf("unexpected error from Client.{{ .ClientEndpoint }}.{{ .ClientService }}.Get:\n- want: %v\n-  got: %v", want, got)
+	}
+
+	if res != nil {
+		t.Fatalf("unexpected result:\n- want: %v\n-  got: %v", nil, res)
+	}
+}
+
+func TestBasicListExtract{{ .TypeName }}(t *testing.T) {
+	want := []*{{ .TypeName }}{
+		test{{ .TypeName }}(1),
+		test{{ .TypeName }}(2),
+	}
+	serverWant := convertToServerData{{ .TypeName }}(want)
+	serverData, _ := json.Marshal(serverWant)
+	c, done := testClient(t, testHandler(t, http.MethodGet, "/api/{{ .Endpoint }}/{{ .Service }}/", &pageData{
+		Count:       2,
+		NextURL:     "",
+		PreviousURL: "",
+		Results:     serverData,
+	}))
+	defer done()
+
+	{{ if .ListOpts -}}
+	page := c.{{ .ClientEndpoint }}.{{ .ClientService }}.List(nil)
+	{{ else }}
+	page := c.{{ .ClientEndpoint }}.{{ .ClientService }}.List()
+	{{ end }}
+	if page == nil {
+		t.Fatalf("unexpexted result from c.{{ .ClientEndpoint }}.{{ .ClientService }}.List.")
+	}
+
+	got := []*{{ .TypeName }}{}
+	counter := 0
+	for page.Next() {
+		var err error
+		got, err = c.{{ .ClientEndpoint }}.{{ .ClientService }}.Extract(page)
+		if err != nil {
+			t.Fatalf("unexpected error from c.{{ .ClientEndpoint }}.{{ .ClientService }}.Extract: %v", err)
+		}
+		counter = counter + 1
+		if counter > 2 { // Safe guard
+			break
+		}
+	}
+	if counter != 1 {
+		t.Fatalf("unexpected page count:\n- want: 1\n-  got: %d", counter)
+	}
+
+	if !reflect.DeepEqual(want, got) {
+		t.Fatalf("unexpected result:\n- want: %v\n-  got: %v", want, got)
+	}
+
+	if page.Err() != nil {
+		t.Fatalf("unexpected error from page:\n- want: %v\n-  got: %v", want, got)
+	}
+}
+
+func TestBasicCreate{{ .TypeName }}(t *testing.T) {
+	var tests = []struct {
+		desc       string
+		data       *{{ .TypeName }}
+		want       int
+		serverData interface{}
+		status     int
+		errstr     string
+	}{
+		{
+			desc:       "Create with ID 0",
+			data:       test{{ .TypeName }}Create(1),
+			want:       1,
+			status:     0,
+			errstr:     "",
+			serverData: test{{ .TypeName }}(1),
+		},
+		{
+			desc:   "Create duplicate",
+			data:   test{{ .TypeName }}Create(1),
+			want:   0,
+			status: http.StatusBadRequest,
+			errstr: "400 - {\"name\":[\"{{ .ServiceName }} with this name already exists.\"]}\n",
+			serverData: &struct {
+				Name []string {{ call .JSONTag "name" }}
+			}{
+				Name: []string{"{{ .ServiceName }} with this name already exists."},
+			},
+		},
+	}
+
+	for i, tt := range tests {
+		t.Run(fmt.Sprintf("[%d] %s", i, tt.desc), func(t *testing.T) {
+			c, done := testClient(t, testStatusHandler(t, http.MethodPost, "/api/{{ .Endpoint }}/{{ .Service }}/", tt.serverData, tt.status))
+			defer done()
+
+			var terr error
+			if tt.errstr != "" {
+				terr = errors.New(tt.errstr) // Using errstr and initialize real err here, to satisfy golint
+			}
+
+			res, err := c.{{ .ClientEndpoint }}.{{ .ClientService }}.Create(tt.data)
+			if want, got := terr, err; !reflect.DeepEqual(want, got) {
+				t.Fatalf("unexpected error:\n- want: %v\n-  got: %v", want, got)
+			}
+			if want, got := tt.want, res; !reflect.DeepEqual(want, got) {
+				t.Fatalf("unexpected {{ .TypeName }}:\n- want: %v\n-  got: %v", want, got)
+			}
+		})
+	}
+}
+
+func TestBasicUpdate{{ .TypeName }}(t *testing.T) {
+	var tests = []struct {
+		desc       string
+		data       *{{ .TypeName }}
+		want       int
+		serverData interface{}
+		status     int
+		errstr     string
+	}{
+		{
+			desc:       "Update with ID 1",
+			data:       test{{ .TypeName }}(1),
+			want:       1,
+			serverData: test{{ .TypeName }}(1),
+			status:     0,
+			errstr:     "",
+		},
+		{
+			desc: "Update not found",
+			data: test{{ .TypeName }}(1),
+			want: 0,
+			serverData: &struct {
+				Detail string
+			}{
+				Detail: "Not found.",
+			},
+			status: http.StatusNotFound,
+			errstr: "404 - Not found.",
+		},
+		{
+			desc: "Update to duplicate",
+			data: test{{ .TypeName }}(1),
+			want: 0,
+			serverData: &struct {
+				Name []string {{ call .JSONTag "name" }}
+			}{
+				Name: []string{"{{ .ServiceName }} with this name already exists."},
+			},
+			status: http.StatusBadRequest,
+			errstr: "400 - {\"name\":[\"{{ .ServiceName }} with this name already exists.\"]}\n",
+		},
+	}
+
+	for i, tt := range tests {
+		t.Run(fmt.Sprintf("[%d] %s", i, tt.desc), func(t *testing.T) {
+			c, done := testClient(t, testStatusHandler(t, http.MethodPatch, "/api/{{ .Endpoint }}/{{ .Service }}/1/", tt.serverData, tt.status))
+			defer done()
+
+			var terr error
+			if tt.errstr != "" {
+				terr = errors.New(tt.errstr) // Using errstr and initialize real err here, to satisfy golint
+			}
+
+			res, err := c.{{ .ClientEndpoint }}.{{ .ClientService }}.Update(tt.data)
+			if want, got := terr, err; !reflect.DeepEqual(want, got) {
+				t.Fatalf("unexpected error:\n- want: %v\n-  got: %v", want, got)
+			}
+			if want, got := tt.want, res; !reflect.DeepEqual(want, got) {
+				t.Fatalf("unexpected {{ .TypeName }}:\n- want: %v\n-  got: %v", want, got)
+			}
+		})
+	}
+}
+
+func TestBasicDelete{{ .TypeName }}(t *testing.T) {
+	var tests = []struct {
+		desc       string
+		data       *{{ .TypeName }}
+		serverData interface{}
+		status     int
+		errstr     string
+	}{
+		{
+			desc:       "Delete ID 1",
+			data:       test{{ .TypeName }}(1),
+			serverData: test{{ .TypeName }}(1),
+			status:     0,
+			errstr:     "",
+		},
+		{
+			desc: "Delete not Found",
+			data: test{{ .TypeName }}(1),
+			serverData: &struct {
+				Detail string {{ call .JSONTag "detail" }}
+			}{
+				Detail: "Not found.",
+			},
+			status: http.StatusNotFound,
+			errstr: "404 - Not found.",
+		},
+	}
+
+	for i, tt := range tests {
+		t.Run(fmt.Sprintf("[%d] %s", i, tt.desc), func(t *testing.T) {
+			c, done := testClient(t, testStatusHandler(t, http.MethodDelete, "/api/{{ .Endpoint }}/{{ .Service }}/1/", tt.serverData, tt.status))
+			defer done()
+
+			var terr error
+			if tt.errstr != "" {
+				terr = errors.New(tt.errstr) // Using errstr and initialize real err here, to satisfy golint
+			}
+
+			err := c.{{ .ClientEndpoint }}.{{ .ClientService }}.Delete(tt.data)
+			if want, got := terr, err; !reflect.DeepEqual(want, got) {
+				t.Fatalf("unexpected error:\n- want: %v\n-  got: %v", want, got)
+			}
+		})
+	}
+}
+`))
diff --git a/netbox/generate_functions.go b/netbox/generate_functions.go
new file mode 100644
index 0000000000000000000000000000000000000000..9411f8c3b8c218d65d1319d8d8bed90f6d907839
--- /dev/null
+++ b/netbox/generate_functions.go
@@ -0,0 +1,204 @@
+// Copyright 2017 The go-netbox Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+//+build ignore
+
+package main
+
+import (
+	"bytes"
+	"flag"
+	"fmt"
+	"go/format"
+	"log"
+	"os"
+	"text/template"
+	"time"
+)
+
+func main() {
+	typeName := flag.String("type-name", "Example", "Name of the type to use (e.g. TenantGroup).")
+	serviceName := flag.String("service-name", "ExampleService", "Name of the service to create (e.g. TenantGroupsService).")
+	endpoint := flag.String("endpoint", "tenancy", "Name of the endpoint (e.g. dcim, ipam, tenancy).")
+	service := flag.String("service", "example", "Name of the service below endpoint (e.g. tenant-groups).")
+	updateTypeName := flag.String("update-type-name", "", "Name of the type to use for creates and updates, to change the marshal behavior. Default typeName.")
+	withoutListOpts := flag.Bool("without-list-opts", false, "Disable list options for this endpoint.")
+
+	flag.Parse()
+
+	if *updateTypeName == "" {
+		*updateTypeName = *typeName
+	}
+
+	b := &bytes.Buffer{}
+	functionsTemplate.Execute(b, struct {
+		Timestamp      time.Time
+		TypeName       string
+		UpdateTypeName string
+		ServiceName    string
+		Endpoint       string
+		Service        string
+		ListOpts       bool
+	}{
+		Timestamp:      time.Now(),
+		TypeName:       *typeName,
+		UpdateTypeName: *updateTypeName,
+		ServiceName:    *serviceName,
+		Endpoint:       *endpoint,
+		Service:        *service,
+		ListOpts:       !*withoutListOpts,
+	})
+
+	// go fmt
+	res, err := format.Source(b.Bytes())
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	f, err := os.Create(fmt.Sprintf("%s_%s.go", *endpoint, *service))
+	if err != nil {
+		log.Fatal(err)
+	}
+	defer f.Close()
+	_, err = f.Write(res)
+	if err != nil {
+		log.Fatal(err)
+	}
+}
+
+var functionsTemplate = template.Must(template.New("").Parse(`// Copyright 2017 The go-netbox Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Code generated by generate_functions.go. DO NOT EDIT.
+
+package netbox
+
+import (
+	"encoding/json"
+	"fmt"
+	"net/http"
+)
+
+// {{ .ServiceName }} is used in a Client to access NetBox's {{ .Endpoint }}/{{ .Service }} API methods.
+type {{ .ServiceName }} struct {
+	c *Client
+}
+
+// Get retrieves an {{ .TypeName }} object from NetBox by its ID.
+func (s *{{ .ServiceName }}) Get(id int) (*{{ .TypeName }}, error) {
+	req, err := s.c.NewRequest(
+		http.MethodGet,
+		fmt.Sprintf("api/{{ .Endpoint }}/{{ .Service }}/%d/", id),
+		nil,
+	)
+	if err != nil {
+		return nil, err
+	}
+
+	t := new({{ .TypeName }})
+	err = s.c.Do(req, t)
+	if err != nil {
+		return nil, err
+	}
+	return t, nil
+}
+
+// List returns a Page associated with an NetBox API Endpoint.
+{{ if .ListOpts -}}
+func (s *{{ .ServiceName }}) List(options *List{{ .TypeName }}Options) *Page {
+	return NewPage(s.c, "api/{{ .Endpoint }}/{{ .Service }}/", options)
+}
+{{ else -}}
+func (s *{{ .ServiceName }}) List() *Page {
+	return NewPage(s.c, "api/{{ .Endpoint }}/{{ .Service }}/", nil)
+}
+{{ end }}
+
+// Extract retrives a list of {{ .TypeName }} objects from page.
+func (s *{{ .ServiceName }}) Extract(page *Page) ([]*{{ .TypeName }}, error) {
+	if err := page.Err(); err != nil {
+		return nil, err
+	}
+
+	var groups []*{{ .TypeName }}
+	if err := json.Unmarshal(page.data.Results, &groups); err != nil {
+		return nil, err
+	}
+	return groups, nil
+}
+
+// Create creates a new {{ .TypeName }} object in NetBox and returns the ID of the new object.
+func (s *{{ .ServiceName }}) Create(data *{{ .TypeName }}) (int, error) {
+	req, err := s.c.NewJSONRequest(http.MethodPost, "api/{{ .Endpoint }}/{{ .Service }}/", nil, data)
+	if err != nil {
+		return 0, err
+	}
+
+	g := new({{ .UpdateTypeName }})
+	err = s.c.Do(req, g)
+	if err != nil {
+		return 0, err
+	}
+	return g.ID, nil
+}
+
+// Update changes an existing {{ .TypeName }} object in NetBox, and returns the ID of the new object.
+func (s *{{ .ServiceName }}) Update(data *{{ .TypeName }}) (int, error) {
+	req, err := s.c.NewJSONRequest(
+		http.MethodPatch,
+		fmt.Sprintf("api/{{ .Endpoint }}/{{ .Service }}/%d/", data.ID),
+		nil,
+		data,
+	)
+	if err != nil {
+		return 0, err
+	}
+
+	// g is just used to verify correct api result.
+	// data is not changed, because the g is not the full representation that one would
+	// get with Get. But if the response was unmarshaled into {{ .UpdateTypeName }} correctly,
+	// everything went fine, and we do not need to update data.
+	g := new({{ .UpdateTypeName }})
+	err = s.c.Do(req, g)
+	if err != nil {
+		return 0, err
+	}
+	return g.ID, nil
+}
+
+// Delete deletes an existing {{ .TypeName }} object from NetBox.
+func (s *{{ .ServiceName }}) Delete(data *{{ .TypeName }}) error {
+	req, err := s.c.NewRequest(
+		http.MethodDelete,
+		fmt.Sprintf("api/{{ .Endpoint }}/{{ .Service }}/%d/", data.ID),
+		nil,
+	)
+	if err != nil {
+		return err
+	}
+
+	return s.c.Do(req, nil)
+}
+`))
diff --git a/ipam.go b/netbox/ipam.go
similarity index 94%
rename from ipam.go
rename to netbox/ipam.go
index dabd3d07dcb196157c6b08f3278d5e8ee81d99e9..c61d3fc77a8a4a4b3d9c34bba1bde735680543da 100644
--- a/ipam.go
+++ b/netbox/ipam.go
@@ -1,4 +1,4 @@
-// Copyright 2016 The go-netbox Authors.
+// Copyright 2017 The go-netbox Authors.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
diff --git a/netbox.go b/netbox/netbox.go
similarity index 98%
rename from netbox.go
rename to netbox/netbox.go
index 0ff0fb8175615bdd1491937d51fa4598428fd68f..298eeebb1c66647ec22a54572630d37df9e5eadc 100644
--- a/netbox.go
+++ b/netbox/netbox.go
@@ -1,4 +1,4 @@
-// Copyright 2016 The go-netbox Authors.
+// Copyright 2017 The go-netbox Authors.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
diff --git a/netbox_test.go b/netbox/netbox_test.go
similarity index 60%
rename from netbox_test.go
rename to netbox/netbox_test.go
index f3bd44cf210e6b33f3db127fe067ec1b9d7187a8..f7e8d5dd6b8aaa33631d2a2af8bd87c152727a01 100644
--- a/netbox_test.go
+++ b/netbox/netbox_test.go
@@ -1,4 +1,4 @@
-// Copyright 2016 The go-netbox Authors.
+// Copyright 2017 The go-netbox Authors.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -25,33 +25,46 @@ import (
 
 func TestFamilyValid(t *testing.T) {
 	var tests = []struct {
-		f  Family
-		ok bool
+		desc string
+		f    Family
+		ok   bool
 	}{
 		{
-			f: math.MinInt64,
+			desc: "Test math.MinInt64",
+			f:    math.MinInt64,
 		},
 		{
-			f: math.MaxInt64,
+			desc: "Test math.MaxInt64",
+			f:    math.MaxInt64,
 		},
 		{
-			f:  FamilyIPv4,
-			ok: true,
+			desc: "Test FamilyIPv4",
+			f:    FamilyIPv4,
+			ok:   true,
 		},
 		{
-			f:  FamilyIPv6,
-			ok: true,
+			desc: "Test FamilyIPv6",
+			f:    FamilyIPv6,
+			ok:   true,
 		},
 	}
 
-	for _, tt := range tests {
-		if want, got := tt.ok, tt.f.Valid(); want != got {
-			t.Fatalf("unexpected Family(%d).Valid():\n- want: %v\n-  got: %v",
-				tt.f, want, got)
-		}
+	for i, tt := range tests {
+		t.Run(fmt.Sprintf("[%d] %s", i, tt.desc), func(t *testing.T) {
+			if want, got := tt.ok, tt.f.Valid(); want != got {
+				t.Fatalf("unexpected Family(%d).Valid():\n- want: %v\n-  got: %v",
+					tt.f, want, got)
+			}
+		})
 	}
 }
 
+type testSimple struct {
+	ID   int    `json:"id"`
+	Name string `json:"name"`
+	Slug string `json:"slug"`
+}
+
 // ExampleNewClient demonstrates usage of the Client type.
 func ExampleNewClient() {
 	// Sets up a minimal, mocked NetBox server
@@ -64,33 +77,31 @@ func ExampleNewClient() {
 		panic(fmt.Sprintf("failed to create netbox.Client: %v", err))
 	}
 
-	// Retrieve an IPAddress with ID 1
-	ip, err := c.IPAM.GetIPAddress(1)
+	res := testSimple{}
+	req, err := c.NewRequest(http.MethodGet, "/", nil)
 	if err != nil {
-		panic(fmt.Sprintf("failed to retrieve IP address: %v", err))
+		panic(err)
 	}
 
-	fmt.Printf("IP #%03d: %s (%s)\n", ip.ID, ip.Address.String(), ip.Family)
+	err = c.Do(req, &res)
+	if err != nil {
+		panic(err)
+	}
 
-	// Output:
-	// IP #001: 192.168.1.1/32 (IPv4)
+	fmt.Printf("%v\n", res)
 }
 
 // exampleServer creates a test HTTP server which returns its address and
 // can be closed using the returned closure.
 func exampleServer() (string, func()) {
 	s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
-		ip := struct {
-			ID      int    `json:"id"`
-			Family  Family `json:"family"`
-			Address string `json:"address"`
-		}{
-			ID:      1,
-			Family:  FamilyIPv4,
-			Address: "192.168.1.1/32",
+		simple := testSimple{
+			ID:   1,
+			Name: "Test 1",
+			Slug: "test-1",
 		}
 
-		_ = json.NewEncoder(w).Encode(ip)
+		_ = json.NewEncoder(w).Encode(simple)
 	}))
 
 	return s.URL, func() { s.Close() }
diff --git a/netbox/page.go b/netbox/page.go
new file mode 100644
index 0000000000000000000000000000000000000000..23e2d5d152db0632b7ebd13d9f2ca6296f79491e
--- /dev/null
+++ b/netbox/page.go
@@ -0,0 +1,174 @@
+// Copyright 2017 The go-netbox Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package netbox
+
+import (
+	"encoding/json"
+	"errors"
+	"net/http"
+	"net/url"
+	"strconv"
+)
+
+// A Page contains all necessary information about the current position
+// in a paged result. It is used to walk over all pages from List calls.
+type Page struct {
+	limit    int
+	offset   int
+	done     bool
+	c        *Client
+	data     pageData
+	endpoint string
+	options  Valuer
+	err      error
+}
+
+// NewPage Returns a new Page to walk over List calls.
+func NewPage(client *Client, endpoint string, options Valuer) *Page {
+	return &Page{
+		limit:    50, // netbox server default
+		c:        client,
+		endpoint: endpoint,
+		options:  options,
+	}
+}
+
+// pageData is the internal representation of a page.
+type pageData struct {
+	Count       int             `json:"count"`
+	NextURL     string          `json:"next"`
+	PreviousURL string          `json:"previous"`
+	Results     json.RawMessage `json:"results"`
+}
+
+// Values implements the Valuer interface. One could pass options
+// to a List call. These must be extended by limit and offset to
+// get the appropriate next page.
+func (p *Page) Values() (url.Values, error) {
+	if p == nil {
+		return nil, errors.New("page not defined")
+	}
+	v := url.Values{}
+	if p.options != nil {
+		opts, err := p.options.Values()
+		if err != nil {
+			return nil, err
+		}
+		if opts != nil {
+			v = opts
+		}
+	}
+	v.Set("limit", strconv.Itoa(p.limit))
+	v.Set("offset", strconv.Itoa(p.offset))
+
+	return v, nil
+}
+
+// Next advances to the next page of API results.  When Next returns false,
+// no more results are available.
+func (p *Page) Next() bool {
+	if p == nil {
+		return false
+	}
+	if p.done {
+		return false
+	}
+
+	req, err := p.c.NewRequest(http.MethodGet, p.endpoint, p)
+	if err != nil {
+		p.err = err
+		return false
+	}
+
+	p.data = pageData{}
+	err = p.c.Do(req, &p.data)
+	if err != nil {
+		p.err = err
+		return false
+	}
+
+	if p.data.NextURL == "" {
+		// We are on the last page, so we still need to return true to
+		// indicate that there is still data to process. But we set
+		// p.done to true, so the next "Next()" returns false.
+		p.done = true
+	} else {
+		p.setNextURL(p.data.NextURL)
+	}
+	return true
+
+}
+
+// setNext sets limit and offset parameter for the next page.
+func (p *Page) setNext(limit int, offset int) {
+	p.limit = limit
+	p.offset = offset
+}
+
+// setNextURL extracts limit and offset from the nextURL, obtained from the result.
+// Under the hood, it uses setNext to finally set those parameters.
+func (p *Page) setNextURL(urlStr string) {
+	nextURL, err := url.Parse(urlStr)
+	if err != nil {
+		// We dont want to cancel this run, since there is data,
+		// but do not want to run into Next
+		p.setErr(err)
+		return
+	}
+
+	query, err := url.ParseQuery(nextURL.RawQuery)
+	if err != nil {
+		// Same like above
+		p.setErr(err)
+		return
+	}
+
+	limits := query["limit"]
+	offsets := query["offset"]
+	if len(limits) == 0 {
+		p.setErr(errors.New("no such query parameter limit"))
+		return
+	}
+	if len(offsets) == 0 {
+		p.setErr(errors.New("no such query parameter offset"))
+		return
+	}
+
+	limit, err := strconv.Atoi(limits[0])
+	if err != nil {
+		p.setErr(err)
+		return
+	}
+
+	offset, err := strconv.Atoi(offsets[0])
+	if err != nil {
+		p.setErr(err)
+		return
+	}
+	p.setNext(limit, offset)
+}
+
+// Err returns the internal err field. This should be called right after
+// the for to get any errors occured during Next()
+func (p *Page) Err() error {
+	return p.err
+}
+
+// setErr sets an internal err field.
+func (p *Page) setErr(err error) {
+	if p.err == nil {
+		p.err = err
+	}
+}
diff --git a/netbox/page_test.go b/netbox/page_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..0af570f712ab175cbd5be1ac82a711bc40d141d2
--- /dev/null
+++ b/netbox/page_test.go
@@ -0,0 +1,244 @@
+// Copyright 2017 The go-netbox Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package netbox
+
+import (
+	"errors"
+	"fmt"
+	"net/http"
+	"net/url"
+	"reflect"
+	"strconv"
+	"testing"
+)
+
+type testOptions struct{}
+
+func (t *testOptions) Values() (url.Values, error) {
+	return url.Values{
+		"Hello": []string{"World"},
+		"limit": []string{"30"},
+	}, nil
+}
+
+func TestPageValues(t *testing.T) {
+
+	var tests = []struct {
+		desc string
+		page *Page
+		want url.Values
+		err  error
+	}{
+		{
+			desc: "nil page",
+			page: nil,
+			want: nil,
+			err:  errors.New("page not defined"),
+		},
+		{
+			desc: "options nil",
+			page: NewPage(nil, "/", nil),
+			want: url.Values{
+				"limit":  []string{"50"},
+				"offset": []string{"0"},
+			},
+			err: nil,
+		},
+		{
+			desc: "options set",
+			page: NewPage(nil, "/", &testOptions{}),
+			want: url.Values{
+				"Hello":  []string{"World"},
+				"limit":  []string{"50"},
+				"offset": []string{"0"},
+			},
+			err: nil,
+		},
+	}
+
+	for i, tt := range tests {
+		t.Run(fmt.Sprintf("[%d] %s", i, tt.desc), func(t *testing.T) {
+			res, err := tt.page.Values()
+			if want, got := tt.err, err; !reflect.DeepEqual(want, got) {
+				t.Fatalf("unecpected error:\n- want: %v\n-  got: %v", want, got)
+			}
+			if want, got := tt.want, res; !reflect.DeepEqual(want, got) {
+				t.Fatalf("unexpected values:\n- want: %v\n-  got: %v", want, got)
+			}
+		})
+	}
+}
+
+func TestNext(t *testing.T) {
+	var pages = []pageData{
+		{
+			Count:       99,
+			NextURL:     "http://example.com/?limit=50&offset=50",
+			PreviousURL: "",
+			Results:     []byte("{\"Hello\": \"World\"}"),
+		},
+		{
+			Count:       99,
+			NextURL:     "",
+			PreviousURL: "http://example.com/?limit=50&offset=0",
+			Results:     []byte("{\"Hello\": \"World\"}"),
+		},
+	}
+
+	c, done := testClient(t, testHandler(t, http.MethodGet, "/", pages[0]))
+	defer done()
+
+	p := NewPage(c, "/", nil)
+	if !p.Next() {
+		t.Fatal("Expected another page, got none.")
+	}
+	p.c, done = testClient(t, testHandler(t, http.MethodGet, "/", pages[1]))
+	defer done()
+	if !p.Next() {
+		t.Fatal("Expected another page, got none.")
+	}
+	if p.Next() {
+		t.Fatal("Did not expect another page, but got one.")
+	}
+	if err := p.Err(); err != nil {
+		t.Fatalf("Did not expect an error: %v", err)
+	}
+}
+
+func TestPageWithError(t *testing.T) {
+	c, done := testClient(t, func(w http.ResponseWriter, r *http.Request) {
+		w.WriteHeader(http.StatusForbidden)
+		w.Write([]byte("foo"))
+	})
+	defer done()
+
+	p := NewPage(c, "/", &testOptions{})
+	if p.Next() {
+		t.Fatal("Did not expect any page, but got one.")
+	}
+	if want, got := errors.New("403 - foo"), p.Err(); !reflect.DeepEqual(want, got) {
+		t.Fatalf("unexpected error:\n- want: %v\n-  got: %v", want, got)
+	}
+}
+
+func TestErr(t *testing.T) {
+	var tests = []struct {
+		desc string
+		set  error
+		want error
+	}{
+		{
+			desc: "Err nil",
+			set:  nil,
+			want: nil,
+		},
+		{
+			desc: "Err set",
+			set:  errors.New("This is broken"),
+			want: errors.New("This is broken"),
+		},
+	}
+	for i, tt := range tests {
+		t.Run(fmt.Sprintf("[%d] %s", i, tt.desc), func(t *testing.T) {
+			p := NewPage(nil, "/", nil)
+			p.setErr(tt.set)
+			if want, got := tt.want, p.Err(); !reflect.DeepEqual(want, got) {
+				t.Fatalf("unexpected error:\n- want: %v\n-  got: %v", want, got)
+			}
+		})
+	}
+}
+
+func TestSetNext(t *testing.T) {
+	p := NewPage(nil, "/", nil)
+	p.setNext(99, 88)
+
+	if want, got := 99, p.limit; want != got {
+		t.Fatalf("unexpected limit:\n- want: %v\n-  got: %v", want, got)
+	}
+	if want, got := 88, p.offset; want != got {
+		t.Fatalf("unexpected offset:\n- want: %v\n-  got: %v", want, got)
+	}
+}
+
+func TestSetNextURL(t *testing.T) {
+	var tests = []struct {
+		desc       string
+		url        string
+		wantOffset int
+		wantLimit  int
+		err        error
+	}{
+		{
+			desc:       "No Params returned",
+			url:        "http://example.com",
+			wantOffset: 0,
+			wantLimit:  50,
+			err:        errors.New("no such query parameter limit"),
+		},
+		{
+			desc:       "No Offset returned",
+			url:        "http://example.com?limit=20",
+			wantOffset: 0,
+			wantLimit:  50,
+			err:        errors.New("no such query parameter offset"),
+		},
+		{
+			desc:       "Limit not an int",
+			url:        "http://example.com?limit=hello&offset=world",
+			wantOffset: 0,
+			wantLimit:  50,
+			err: &strconv.NumError{
+				Func: "Atoi",
+				Num:  "hello",
+				Err:  strconv.ErrSyntax,
+			},
+		},
+		{
+			desc:       "Offset not an int",
+			url:        "http://example.com?limit=50&offset=world",
+			wantOffset: 0,
+			wantLimit:  50,
+			err: &strconv.NumError{
+				Func: "Atoi",
+				Num:  "world",
+				Err:  strconv.ErrSyntax,
+			},
+		},
+		{
+			desc:       "Correct limit and offset",
+			url:        "http://example.com?limit=99&offset=88",
+			wantOffset: 88,
+			wantLimit:  99,
+			err:        nil,
+		},
+	}
+
+	for i, tt := range tests {
+		t.Run(fmt.Sprintf("[%d] %s", i, tt.desc), func(t *testing.T) {
+			p := NewPage(nil, "/", nil)
+			p.setNextURL(tt.url)
+			if want, got := tt.wantOffset, p.offset; want != got {
+				t.Fatalf("unexpected offset:\n- want: %v\n-  got: %v", want, got)
+			}
+			if want, got := tt.wantLimit, p.limit; want != got {
+				t.Fatalf("unexpected limit:\n- want: %v\n-  got: %v", want, got)
+			}
+			if want, got := tt.err, p.Err(); reflect.TypeOf(want) != reflect.TypeOf(got) {
+				t.Fatalf("unexpected error:\n- want: %v\n-  got: %v", want, got)
+			}
+		})
+	}
+}
diff --git a/dcim_powerports.go b/netbox/tenancy.go
similarity index 52%
rename from dcim_powerports.go
rename to netbox/tenancy.go
index 5e606263df7115976b1cf5a9cf585e6f596f3876..b44124f2a8a1211831becba9475d548d1cb2e81f 100644
--- a/dcim_powerports.go
+++ b/netbox/tenancy.go
@@ -1,4 +1,4 @@
-// Copyright 2016 The go-netbox Authors.
+// Copyright 2017 The go-netbox Authors.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -14,10 +14,22 @@
 
 package netbox
 
-// PowerPort represents a power port object.
-type PowerPort struct {
-	ID               int                    `json:"id"`
-	Name             string                 `json:"name"`
-	PowerOutlet      *PowerOutletIdentifier `json:"power_outlet"`
-	ConnectionStatus bool                   `json:"connection_status"`
+// A TenancyService is udes in a Client to access NetBox's Tenancy API methods.
+type TenancyService struct {
+	c            *Client
+	TenantGroups *TenantGroupsService
+	Tenants      *TenantsService
+}
+
+// NewTenancyService returns a TenancyService initialized with all sub-services.
+func NewTenancyService(client *Client) *TenancyService {
+	return &TenancyService{
+		c: client,
+		TenantGroups: &TenantGroupsService{
+			c: client,
+		},
+		Tenants: &TenantsService{
+			c: client,
+		},
+	}
 }
diff --git a/netbox/tenancy_tenant-groups.go b/netbox/tenancy_tenant-groups.go
new file mode 100644
index 0000000000000000000000000000000000000000..8e1a62ad2a14fbf0aeb041c8f16596f2318da7b4
--- /dev/null
+++ b/netbox/tenancy_tenant-groups.go
@@ -0,0 +1,118 @@
+// Copyright 2017 The go-netbox Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Code generated by generate_functions.go. DO NOT EDIT.
+
+package netbox
+
+import (
+	"encoding/json"
+	"fmt"
+	"net/http"
+)
+
+// TenantGroupsService is used in a Client to access NetBox's tenancy/tenant-groups API methods.
+type TenantGroupsService struct {
+	c *Client
+}
+
+// Get retrieves an TenantGroup object from NetBox by its ID.
+func (s *TenantGroupsService) Get(id int) (*TenantGroup, error) {
+	req, err := s.c.NewRequest(
+		http.MethodGet,
+		fmt.Sprintf("api/tenancy/tenant-groups/%d/", id),
+		nil,
+	)
+	if err != nil {
+		return nil, err
+	}
+
+	t := new(TenantGroup)
+	err = s.c.Do(req, t)
+	if err != nil {
+		return nil, err
+	}
+	return t, nil
+}
+
+// List returns a Page associated with an NetBox API Endpoint.
+func (s *TenantGroupsService) List() *Page {
+	return NewPage(s.c, "api/tenancy/tenant-groups/", nil)
+}
+
+// Extract retrives a list of TenantGroup objects from page.
+func (s *TenantGroupsService) Extract(page *Page) ([]*TenantGroup, error) {
+	if err := page.Err(); err != nil {
+		return nil, err
+	}
+
+	var groups []*TenantGroup
+	if err := json.Unmarshal(page.data.Results, &groups); err != nil {
+		return nil, err
+	}
+	return groups, nil
+}
+
+// Create creates a new TenantGroup object in NetBox and returns the ID of the new object.
+func (s *TenantGroupsService) Create(data *TenantGroup) (int, error) {
+	req, err := s.c.NewJSONRequest(http.MethodPost, "api/tenancy/tenant-groups/", nil, data)
+	if err != nil {
+		return 0, err
+	}
+
+	g := new(TenantGroup)
+	err = s.c.Do(req, g)
+	if err != nil {
+		return 0, err
+	}
+	return g.ID, nil
+}
+
+// Update changes an existing TenantGroup object in NetBox, and returns the ID of the new object.
+func (s *TenantGroupsService) Update(data *TenantGroup) (int, error) {
+	req, err := s.c.NewJSONRequest(
+		http.MethodPatch,
+		fmt.Sprintf("api/tenancy/tenant-groups/%d/", data.ID),
+		nil,
+		data,
+	)
+	if err != nil {
+		return 0, err
+	}
+
+	// g is just used to verify correct api result.
+	// data is not changed, because the g is not the full representation that one would
+	// get with Get. But if the response was unmarshaled into TenantGroup correctly,
+	// everything went fine, and we do not need to update data.
+	g := new(TenantGroup)
+	err = s.c.Do(req, g)
+	if err != nil {
+		return 0, err
+	}
+	return g.ID, nil
+}
+
+// Delete deletes an existing TenantGroup object from NetBox.
+func (s *TenantGroupsService) Delete(data *TenantGroup) error {
+	req, err := s.c.NewRequest(
+		http.MethodDelete,
+		fmt.Sprintf("api/tenancy/tenant-groups/%d/", data.ID),
+		nil,
+	)
+	if err != nil {
+		return err
+	}
+
+	return s.c.Do(req, nil)
+}
diff --git a/netbox/tenancy_tenant-groups_basic_test.go b/netbox/tenancy_tenant-groups_basic_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..2e3322e38d8a969f7887bec47ef8ac5ee61ed8c7
--- /dev/null
+++ b/netbox/tenancy_tenant-groups_basic_test.go
@@ -0,0 +1,299 @@
+// Copyright 2017 The go-netbox Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Code generated by generate_functions.go. DO NOT EDIT.
+
+package netbox
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"net/http"
+	"reflect"
+	"testing"
+)
+
+// Using this to override MarshalJSON
+// In all cases when posting data to netbox-API, the TenantGroup.MarshalJSON is what you want,
+// but not here as a return in testHandler
+type serverDataTenantGroup TenantGroup
+
+func convertToServerDataTenantGroup(data []*TenantGroup) []*serverDataTenantGroup {
+	dataWant := make([]*serverDataTenantGroup, len(data))
+	for i := range data {
+		tmp := serverDataTenantGroup(*data[i])
+		dataWant[i] = &tmp
+	}
+	return dataWant
+}
+
+func TestBasicTenantGroupGet(t *testing.T) {
+	var tests = []struct {
+		desc string
+		want *TenantGroup
+	}{
+		{
+			desc: "Simple TenantGroup",
+			want: testTenantGroup(1),
+		},
+	}
+
+	for i, tt := range tests {
+		t.Run(fmt.Sprintf("[%d] %s", i, tt.desc), func(t *testing.T) {
+			serverData := serverDataTenantGroup(*tt.want)
+
+			c, done := testClient(t, testHandler(t, http.MethodGet, "/api/tenancy/tenant-groups/1/", &serverData))
+			defer done()
+
+			res, err := c.Tenancy.TenantGroups.Get(1)
+			if err != nil {
+				t.Fatalf("unexpected error from Client.Tenancy.TenantGroups.Get: %v", err)
+			}
+
+			if want, got := tt.want, res; !reflect.DeepEqual(want, got) {
+				t.Fatalf("unexpected TenantGroup:\n- want: %v\n-  got: %v", want, got)
+			}
+		})
+	}
+}
+
+func TestBasicTenantGroupGet404(t *testing.T) {
+	c, done := testClient(t, testStatusHandler(t, http.MethodGet, "/api/tenancy/tenant-groups/1/", &struct {
+		Detail string `json:"detail"`
+	}{
+		Detail: "Not found.",
+	},
+		http.StatusNotFound))
+	defer done()
+
+	res, err := c.Tenancy.TenantGroups.Get(1)
+	errstr := "404 - Not found."
+	if want, got := errors.New(errstr), err; !reflect.DeepEqual(want, got) {
+		t.Fatalf("unexpected error from Client.Tenancy.TenantGroups.Get:\n- want: %v\n-  got: %v", want, got)
+	}
+
+	if res != nil {
+		t.Fatalf("unexpected result:\n- want: %v\n-  got: %v", nil, res)
+	}
+}
+
+func TestBasicListExtractTenantGroup(t *testing.T) {
+	want := []*TenantGroup{
+		testTenantGroup(1),
+		testTenantGroup(2),
+	}
+	serverWant := convertToServerDataTenantGroup(want)
+	serverData, _ := json.Marshal(serverWant)
+	c, done := testClient(t, testHandler(t, http.MethodGet, "/api/tenancy/tenant-groups/", &pageData{
+		Count:       2,
+		NextURL:     "",
+		PreviousURL: "",
+		Results:     serverData,
+	}))
+	defer done()
+
+	page := c.Tenancy.TenantGroups.List()
+
+	if page == nil {
+		t.Fatalf("unexpexted result from c.Tenancy.TenantGroups.List.")
+	}
+
+	got := []*TenantGroup{}
+	counter := 0
+	for page.Next() {
+		var err error
+		got, err = c.Tenancy.TenantGroups.Extract(page)
+		if err != nil {
+			t.Fatalf("unexpected error from c.Tenancy.TenantGroups.Extract: %v", err)
+		}
+		counter = counter + 1
+		if counter > 2 { // Safe guard
+			break
+		}
+	}
+	if counter != 1 {
+		t.Fatalf("unexpected page count:\n- want: 1\n-  got: %d", counter)
+	}
+
+	if !reflect.DeepEqual(want, got) {
+		t.Fatalf("unexpected result:\n- want: %v\n-  got: %v", want, got)
+	}
+
+	if page.Err() != nil {
+		t.Fatalf("unexpected error from page:\n- want: %v\n-  got: %v", want, got)
+	}
+}
+
+func TestBasicCreateTenantGroup(t *testing.T) {
+	var tests = []struct {
+		desc       string
+		data       *TenantGroup
+		want       int
+		serverData interface{}
+		status     int
+		errstr     string
+	}{
+		{
+			desc:       "Create with ID 0",
+			data:       testTenantGroupCreate(1),
+			want:       1,
+			status:     0,
+			errstr:     "",
+			serverData: testTenantGroup(1),
+		},
+		{
+			desc:   "Create duplicate",
+			data:   testTenantGroupCreate(1),
+			want:   0,
+			status: http.StatusBadRequest,
+			errstr: "400 - {\"name\":[\"TenantGroupsService with this name already exists.\"]}\n",
+			serverData: &struct {
+				Name []string `json:"name"`
+			}{
+				Name: []string{"TenantGroupsService with this name already exists."},
+			},
+		},
+	}
+
+	for i, tt := range tests {
+		t.Run(fmt.Sprintf("[%d] %s", i, tt.desc), func(t *testing.T) {
+			c, done := testClient(t, testStatusHandler(t, http.MethodPost, "/api/tenancy/tenant-groups/", tt.serverData, tt.status))
+			defer done()
+
+			var terr error
+			if tt.errstr != "" {
+				terr = errors.New(tt.errstr) // Using errstr and initialize real err here, to satisfy golint
+			}
+
+			res, err := c.Tenancy.TenantGroups.Create(tt.data)
+			if want, got := terr, err; !reflect.DeepEqual(want, got) {
+				t.Fatalf("unexpected error:\n- want: %v\n-  got: %v", want, got)
+			}
+			if want, got := tt.want, res; !reflect.DeepEqual(want, got) {
+				t.Fatalf("unexpected TenantGroup:\n- want: %v\n-  got: %v", want, got)
+			}
+		})
+	}
+}
+
+func TestBasicUpdateTenantGroup(t *testing.T) {
+	var tests = []struct {
+		desc       string
+		data       *TenantGroup
+		want       int
+		serverData interface{}
+		status     int
+		errstr     string
+	}{
+		{
+			desc:       "Update with ID 1",
+			data:       testTenantGroup(1),
+			want:       1,
+			serverData: testTenantGroup(1),
+			status:     0,
+			errstr:     "",
+		},
+		{
+			desc: "Update not found",
+			data: testTenantGroup(1),
+			want: 0,
+			serverData: &struct {
+				Detail string
+			}{
+				Detail: "Not found.",
+			},
+			status: http.StatusNotFound,
+			errstr: "404 - Not found.",
+		},
+		{
+			desc: "Update to duplicate",
+			data: testTenantGroup(1),
+			want: 0,
+			serverData: &struct {
+				Name []string `json:"name"`
+			}{
+				Name: []string{"TenantGroupsService with this name already exists."},
+			},
+			status: http.StatusBadRequest,
+			errstr: "400 - {\"name\":[\"TenantGroupsService with this name already exists.\"]}\n",
+		},
+	}
+
+	for i, tt := range tests {
+		t.Run(fmt.Sprintf("[%d] %s", i, tt.desc), func(t *testing.T) {
+			c, done := testClient(t, testStatusHandler(t, http.MethodPatch, "/api/tenancy/tenant-groups/1/", tt.serverData, tt.status))
+			defer done()
+
+			var terr error
+			if tt.errstr != "" {
+				terr = errors.New(tt.errstr) // Using errstr and initialize real err here, to satisfy golint
+			}
+
+			res, err := c.Tenancy.TenantGroups.Update(tt.data)
+			if want, got := terr, err; !reflect.DeepEqual(want, got) {
+				t.Fatalf("unexpected error:\n- want: %v\n-  got: %v", want, got)
+			}
+			if want, got := tt.want, res; !reflect.DeepEqual(want, got) {
+				t.Fatalf("unexpected TenantGroup:\n- want: %v\n-  got: %v", want, got)
+			}
+		})
+	}
+}
+
+func TestBasicDeleteTenantGroup(t *testing.T) {
+	var tests = []struct {
+		desc       string
+		data       *TenantGroup
+		serverData interface{}
+		status     int
+		errstr     string
+	}{
+		{
+			desc:       "Delete ID 1",
+			data:       testTenantGroup(1),
+			serverData: testTenantGroup(1),
+			status:     0,
+			errstr:     "",
+		},
+		{
+			desc: "Delete not Found",
+			data: testTenantGroup(1),
+			serverData: &struct {
+				Detail string `json:"detail"`
+			}{
+				Detail: "Not found.",
+			},
+			status: http.StatusNotFound,
+			errstr: "404 - Not found.",
+		},
+	}
+
+	for i, tt := range tests {
+		t.Run(fmt.Sprintf("[%d] %s", i, tt.desc), func(t *testing.T) {
+			c, done := testClient(t, testStatusHandler(t, http.MethodDelete, "/api/tenancy/tenant-groups/1/", tt.serverData, tt.status))
+			defer done()
+
+			var terr error
+			if tt.errstr != "" {
+				terr = errors.New(tt.errstr) // Using errstr and initialize real err here, to satisfy golint
+			}
+
+			err := c.Tenancy.TenantGroups.Delete(tt.data)
+			if want, got := terr, err; !reflect.DeepEqual(want, got) {
+				t.Fatalf("unexpected error:\n- want: %v\n-  got: %v", want, got)
+			}
+		})
+	}
+}
diff --git a/netbox/tenancy_tenant-groups_test.go b/netbox/tenancy_tenant-groups_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..351110344a90abdfce71e44f165528b4c22c95a8
--- /dev/null
+++ b/netbox/tenancy_tenant-groups_test.go
@@ -0,0 +1,83 @@
+// Copyright 2017 The go-netbox Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package netbox
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"reflect"
+	"testing"
+)
+
+func TestTenantGroupUnmarshalJSON(t *testing.T) {
+	var tests = []struct {
+		desc string
+		data []byte
+		want *TenantGroup
+	}{
+		{
+			desc: "full",
+			data: []byte(`{ "id": 1, "name": "Tenant Group 1", "slug": "tenant-group-1", "custom_fields": {} }`),
+			want: testTenantGroup(1),
+		},
+	}
+
+	for i, tt := range tests {
+		t.Run(fmt.Sprintf("[%d] %s", i, tt.desc), func(t *testing.T) {
+			result := new(TenantGroup)
+			err := json.Unmarshal(tt.data, result)
+			if err != nil {
+				t.Fatalf("unexpected error from TenantGroup.UnmarshalJSON: %v", err)
+			}
+
+			if want, got := tt.want, result; !reflect.DeepEqual(want, got) {
+				t.Fatalf("unexpected TenantGroup:\n- want: %v\n-  got: %v", want, got)
+			}
+		})
+	}
+}
+
+func TestTenantGroupMarshalJSON(t *testing.T) {
+	var tests = []struct {
+		desc string
+		data *TenantGroup
+		want []byte
+	}{
+		{
+			desc: "With TenantGroup.ID",
+			data: testTenantGroup(1),
+			want: []byte(`{"id":1,"name":"Tenant Group 1","slug":"tenant-group-1"}`),
+		},
+		{
+			desc: "No TenantGroup.ID",
+			data: testTenantGroup(0),
+			want: []byte(`{"name":"Tenant Group 0","slug":"tenant-group-0"}`),
+		},
+	}
+
+	for i, tt := range tests {
+		t.Run(fmt.Sprintf("[%d] %s", i, tt.desc), func(t *testing.T) {
+			result, err := json.Marshal(tt.data)
+			if err != nil {
+				t.Fatalf("unexpected error from TenantGroup.MarshalJSON: %v", err)
+			}
+
+			if want, got := tt.want, result; bytes.Compare(want, got) != 0 {
+				t.Fatalf("unexpected TenantGroup:\n- want: %s\n-  got: %s", want, got)
+			}
+		})
+	}
+}
diff --git a/netbox/tenancy_tenant-groups_types.go b/netbox/tenancy_tenant-groups_types.go
new file mode 100644
index 0000000000000000000000000000000000000000..813ef83d5829e3894af75bf591d3016cb6bcaa8f
--- /dev/null
+++ b/netbox/tenancy_tenant-groups_types.go
@@ -0,0 +1,25 @@
+// Copyright 2017 The go-netbox Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package netbox
+
+// A TenantGroup is a representation of netbox tenant-groups
+type TenantGroup struct {
+	ID   int    `json:"id,omitempty"`
+	Name string `json:"name"`
+	Slug string `json:"slug"`
+}
+
+//go:generate go run generate_functions.go -type-name TenantGroup -service-name TenantGroupsService -endpoint tenancy -service tenant-groups -without-list-opts
+//go:generate go run generate_basic_tests.go -type-name TenantGroup -service-name TenantGroupsService -endpoint tenancy -service tenant-groups -client-endpoint Tenancy -client-service TenantGroups -without-list-opts
diff --git a/netbox/tenancy_tenants.go b/netbox/tenancy_tenants.go
new file mode 100644
index 0000000000000000000000000000000000000000..0c265acf8db817f25fa03143c90327f1bdfa3def
--- /dev/null
+++ b/netbox/tenancy_tenants.go
@@ -0,0 +1,118 @@
+// Copyright 2017 The go-netbox Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Code generated by generate_functions.go. DO NOT EDIT.
+
+package netbox
+
+import (
+	"encoding/json"
+	"fmt"
+	"net/http"
+)
+
+// TenantsService is used in a Client to access NetBox's tenancy/tenants API methods.
+type TenantsService struct {
+	c *Client
+}
+
+// Get retrieves an Tenant object from NetBox by its ID.
+func (s *TenantsService) Get(id int) (*Tenant, error) {
+	req, err := s.c.NewRequest(
+		http.MethodGet,
+		fmt.Sprintf("api/tenancy/tenants/%d/", id),
+		nil,
+	)
+	if err != nil {
+		return nil, err
+	}
+
+	t := new(Tenant)
+	err = s.c.Do(req, t)
+	if err != nil {
+		return nil, err
+	}
+	return t, nil
+}
+
+// List returns a Page associated with an NetBox API Endpoint.
+func (s *TenantsService) List(options *ListTenantOptions) *Page {
+	return NewPage(s.c, "api/tenancy/tenants/", options)
+}
+
+// Extract retrives a list of Tenant objects from page.
+func (s *TenantsService) Extract(page *Page) ([]*Tenant, error) {
+	if err := page.Err(); err != nil {
+		return nil, err
+	}
+
+	var groups []*Tenant
+	if err := json.Unmarshal(page.data.Results, &groups); err != nil {
+		return nil, err
+	}
+	return groups, nil
+}
+
+// Create creates a new Tenant object in NetBox and returns the ID of the new object.
+func (s *TenantsService) Create(data *Tenant) (int, error) {
+	req, err := s.c.NewJSONRequest(http.MethodPost, "api/tenancy/tenants/", nil, data)
+	if err != nil {
+		return 0, err
+	}
+
+	g := new(updateTenant)
+	err = s.c.Do(req, g)
+	if err != nil {
+		return 0, err
+	}
+	return g.ID, nil
+}
+
+// Update changes an existing Tenant object in NetBox, and returns the ID of the new object.
+func (s *TenantsService) Update(data *Tenant) (int, error) {
+	req, err := s.c.NewJSONRequest(
+		http.MethodPatch,
+		fmt.Sprintf("api/tenancy/tenants/%d/", data.ID),
+		nil,
+		data,
+	)
+	if err != nil {
+		return 0, err
+	}
+
+	// g is just used to verify correct api result.
+	// data is not changed, because the g is not the full representation that one would
+	// get with Get. But if the response was unmarshaled into updateTenant correctly,
+	// everything went fine, and we do not need to update data.
+	g := new(updateTenant)
+	err = s.c.Do(req, g)
+	if err != nil {
+		return 0, err
+	}
+	return g.ID, nil
+}
+
+// Delete deletes an existing Tenant object from NetBox.
+func (s *TenantsService) Delete(data *Tenant) error {
+	req, err := s.c.NewRequest(
+		http.MethodDelete,
+		fmt.Sprintf("api/tenancy/tenants/%d/", data.ID),
+		nil,
+	)
+	if err != nil {
+		return err
+	}
+
+	return s.c.Do(req, nil)
+}
diff --git a/netbox/tenancy_tenants_basic_test.go b/netbox/tenancy_tenants_basic_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..e26756f54541e2f987bbaf66b60977033db5b968
--- /dev/null
+++ b/netbox/tenancy_tenants_basic_test.go
@@ -0,0 +1,299 @@
+// Copyright 2017 The go-netbox Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Code generated by generate_functions.go. DO NOT EDIT.
+
+package netbox
+
+import (
+	"encoding/json"
+	"errors"
+	"fmt"
+	"net/http"
+	"reflect"
+	"testing"
+)
+
+// Using this to override MarshalJSON
+// In all cases when posting data to netbox-API, the Tenant.MarshalJSON is what you want,
+// but not here as a return in testHandler
+type serverDataTenant Tenant
+
+func convertToServerDataTenant(data []*Tenant) []*serverDataTenant {
+	dataWant := make([]*serverDataTenant, len(data))
+	for i := range data {
+		tmp := serverDataTenant(*data[i])
+		dataWant[i] = &tmp
+	}
+	return dataWant
+}
+
+func TestBasicTenantGet(t *testing.T) {
+	var tests = []struct {
+		desc string
+		want *Tenant
+	}{
+		{
+			desc: "Simple Tenant",
+			want: testTenant(1),
+		},
+	}
+
+	for i, tt := range tests {
+		t.Run(fmt.Sprintf("[%d] %s", i, tt.desc), func(t *testing.T) {
+			serverData := serverDataTenant(*tt.want)
+
+			c, done := testClient(t, testHandler(t, http.MethodGet, "/api/tenancy/tenants/1/", &serverData))
+			defer done()
+
+			res, err := c.Tenancy.Tenants.Get(1)
+			if err != nil {
+				t.Fatalf("unexpected error from Client.Tenancy.Tenants.Get: %v", err)
+			}
+
+			if want, got := tt.want, res; !reflect.DeepEqual(want, got) {
+				t.Fatalf("unexpected Tenant:\n- want: %v\n-  got: %v", want, got)
+			}
+		})
+	}
+}
+
+func TestBasicTenantGet404(t *testing.T) {
+	c, done := testClient(t, testStatusHandler(t, http.MethodGet, "/api/tenancy/tenants/1/", &struct {
+		Detail string `json:"detail"`
+	}{
+		Detail: "Not found.",
+	},
+		http.StatusNotFound))
+	defer done()
+
+	res, err := c.Tenancy.Tenants.Get(1)
+	errstr := "404 - Not found."
+	if want, got := errors.New(errstr), err; !reflect.DeepEqual(want, got) {
+		t.Fatalf("unexpected error from Client.Tenancy.Tenants.Get:\n- want: %v\n-  got: %v", want, got)
+	}
+
+	if res != nil {
+		t.Fatalf("unexpected result:\n- want: %v\n-  got: %v", nil, res)
+	}
+}
+
+func TestBasicListExtractTenant(t *testing.T) {
+	want := []*Tenant{
+		testTenant(1),
+		testTenant(2),
+	}
+	serverWant := convertToServerDataTenant(want)
+	serverData, _ := json.Marshal(serverWant)
+	c, done := testClient(t, testHandler(t, http.MethodGet, "/api/tenancy/tenants/", &pageData{
+		Count:       2,
+		NextURL:     "",
+		PreviousURL: "",
+		Results:     serverData,
+	}))
+	defer done()
+
+	page := c.Tenancy.Tenants.List(nil)
+
+	if page == nil {
+		t.Fatalf("unexpexted result from c.Tenancy.Tenants.List.")
+	}
+
+	got := []*Tenant{}
+	counter := 0
+	for page.Next() {
+		var err error
+		got, err = c.Tenancy.Tenants.Extract(page)
+		if err != nil {
+			t.Fatalf("unexpected error from c.Tenancy.Tenants.Extract: %v", err)
+		}
+		counter = counter + 1
+		if counter > 2 { // Safe guard
+			break
+		}
+	}
+	if counter != 1 {
+		t.Fatalf("unexpected page count:\n- want: 1\n-  got: %d", counter)
+	}
+
+	if !reflect.DeepEqual(want, got) {
+		t.Fatalf("unexpected result:\n- want: %v\n-  got: %v", want, got)
+	}
+
+	if page.Err() != nil {
+		t.Fatalf("unexpected error from page:\n- want: %v\n-  got: %v", want, got)
+	}
+}
+
+func TestBasicCreateTenant(t *testing.T) {
+	var tests = []struct {
+		desc       string
+		data       *Tenant
+		want       int
+		serverData interface{}
+		status     int
+		errstr     string
+	}{
+		{
+			desc:       "Create with ID 0",
+			data:       testTenantCreate(1),
+			want:       1,
+			status:     0,
+			errstr:     "",
+			serverData: testTenant(1),
+		},
+		{
+			desc:   "Create duplicate",
+			data:   testTenantCreate(1),
+			want:   0,
+			status: http.StatusBadRequest,
+			errstr: "400 - {\"name\":[\"TenantsService with this name already exists.\"]}\n",
+			serverData: &struct {
+				Name []string `json:"name"`
+			}{
+				Name: []string{"TenantsService with this name already exists."},
+			},
+		},
+	}
+
+	for i, tt := range tests {
+		t.Run(fmt.Sprintf("[%d] %s", i, tt.desc), func(t *testing.T) {
+			c, done := testClient(t, testStatusHandler(t, http.MethodPost, "/api/tenancy/tenants/", tt.serverData, tt.status))
+			defer done()
+
+			var terr error
+			if tt.errstr != "" {
+				terr = errors.New(tt.errstr) // Using errstr and initialize real err here, to satisfy golint
+			}
+
+			res, err := c.Tenancy.Tenants.Create(tt.data)
+			if want, got := terr, err; !reflect.DeepEqual(want, got) {
+				t.Fatalf("unexpected error:\n- want: %v\n-  got: %v", want, got)
+			}
+			if want, got := tt.want, res; !reflect.DeepEqual(want, got) {
+				t.Fatalf("unexpected Tenant:\n- want: %v\n-  got: %v", want, got)
+			}
+		})
+	}
+}
+
+func TestBasicUpdateTenant(t *testing.T) {
+	var tests = []struct {
+		desc       string
+		data       *Tenant
+		want       int
+		serverData interface{}
+		status     int
+		errstr     string
+	}{
+		{
+			desc:       "Update with ID 1",
+			data:       testTenant(1),
+			want:       1,
+			serverData: testTenant(1),
+			status:     0,
+			errstr:     "",
+		},
+		{
+			desc: "Update not found",
+			data: testTenant(1),
+			want: 0,
+			serverData: &struct {
+				Detail string
+			}{
+				Detail: "Not found.",
+			},
+			status: http.StatusNotFound,
+			errstr: "404 - Not found.",
+		},
+		{
+			desc: "Update to duplicate",
+			data: testTenant(1),
+			want: 0,
+			serverData: &struct {
+				Name []string `json:"name"`
+			}{
+				Name: []string{"TenantsService with this name already exists."},
+			},
+			status: http.StatusBadRequest,
+			errstr: "400 - {\"name\":[\"TenantsService with this name already exists.\"]}\n",
+		},
+	}
+
+	for i, tt := range tests {
+		t.Run(fmt.Sprintf("[%d] %s", i, tt.desc), func(t *testing.T) {
+			c, done := testClient(t, testStatusHandler(t, http.MethodPatch, "/api/tenancy/tenants/1/", tt.serverData, tt.status))
+			defer done()
+
+			var terr error
+			if tt.errstr != "" {
+				terr = errors.New(tt.errstr) // Using errstr and initialize real err here, to satisfy golint
+			}
+
+			res, err := c.Tenancy.Tenants.Update(tt.data)
+			if want, got := terr, err; !reflect.DeepEqual(want, got) {
+				t.Fatalf("unexpected error:\n- want: %v\n-  got: %v", want, got)
+			}
+			if want, got := tt.want, res; !reflect.DeepEqual(want, got) {
+				t.Fatalf("unexpected Tenant:\n- want: %v\n-  got: %v", want, got)
+			}
+		})
+	}
+}
+
+func TestBasicDeleteTenant(t *testing.T) {
+	var tests = []struct {
+		desc       string
+		data       *Tenant
+		serverData interface{}
+		status     int
+		errstr     string
+	}{
+		{
+			desc:       "Delete ID 1",
+			data:       testTenant(1),
+			serverData: testTenant(1),
+			status:     0,
+			errstr:     "",
+		},
+		{
+			desc: "Delete not Found",
+			data: testTenant(1),
+			serverData: &struct {
+				Detail string `json:"detail"`
+			}{
+				Detail: "Not found.",
+			},
+			status: http.StatusNotFound,
+			errstr: "404 - Not found.",
+		},
+	}
+
+	for i, tt := range tests {
+		t.Run(fmt.Sprintf("[%d] %s", i, tt.desc), func(t *testing.T) {
+			c, done := testClient(t, testStatusHandler(t, http.MethodDelete, "/api/tenancy/tenants/1/", tt.serverData, tt.status))
+			defer done()
+
+			var terr error
+			if tt.errstr != "" {
+				terr = errors.New(tt.errstr) // Using errstr and initialize real err here, to satisfy golint
+			}
+
+			err := c.Tenancy.Tenants.Delete(tt.data)
+			if want, got := terr, err; !reflect.DeepEqual(want, got) {
+				t.Fatalf("unexpected error:\n- want: %v\n-  got: %v", want, got)
+			}
+		})
+	}
+}
diff --git a/netbox/tenancy_tenants_test.go b/netbox/tenancy_tenants_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..b46f9ead3bf34212a9b2ea5b7c1e0471825a3cde
--- /dev/null
+++ b/netbox/tenancy_tenants_test.go
@@ -0,0 +1,194 @@
+// Copyright 2017 The go-netbox Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package netbox
+
+import (
+	"bytes"
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"net/url"
+	"reflect"
+	"testing"
+)
+
+func TestTenantGet(t *testing.T) {
+	var tests = []struct {
+		desc      string
+		want      *Tenant
+		wantGroup *TenantGroup
+	}{
+		{
+			desc:      "Without TenantGroup",
+			want:      testTenantWithGroup(1, nil),
+			wantGroup: nil,
+		},
+		{
+			desc:      "With TenantGroup",
+			want:      testTenantWithGroup(1, testTenantGroup(1)),
+			wantGroup: testTenantGroup(1),
+		},
+	}
+
+	for i, tt := range tests {
+		t.Run(fmt.Sprintf("[%d] %s", i, tt.desc), func(t *testing.T) {
+			serverData := serverDataTenant(*tt.want)
+
+			c, done := testClient(t, testHandler(t, http.MethodGet, "/api/tenancy/tenants/1/", &serverData))
+			defer done()
+
+			res, err := c.Tenancy.Tenants.Get(1)
+			if err != nil {
+				t.Fatalf("unexpected error from Client.Tenancy.Tenants.Get: %v", err)
+			}
+
+			if want, got := tt.want, res; !reflect.DeepEqual(want, got) {
+				t.Fatalf("unexpected Tenant:\n- want: %v\n-  got: %v", want, got)
+			}
+			if want, got := tt.wantGroup, res.Group; !reflect.DeepEqual(want, got) {
+				t.Fatalf("unexpected TenantGroup:\n- want: %v\n-  got: %v", want, got)
+			}
+		})
+	}
+}
+
+func TestTenantUnmarshalJSON(t *testing.T) {
+	var tests = []struct {
+		desc string
+		data []byte
+		want *Tenant
+	}{
+		{
+			desc: "Nil Group",
+			data: []byte(`{ "id": 1, "name": "Tenant 1", "slug": "tenant-1", "group": null, "description": "Tenant 1 Description", "comments": "Tenant 1 Comments", "custom_fields": {} }`),
+			want: testTenantWithGroup(1, nil),
+		},
+		{
+			desc: "With Group",
+			data: []byte(`{ "id": 1, "name": "Tenant 1", "slug": "tenant-1", "group": {"id": 1, "name": "Tenant Group 1", "slug": "tenant-group-1"}, "description": "Tenant 1 Description", "comments": "Tenant 1 Comments", "custom_fields": {} }`),
+			want: testTenant(1),
+		},
+	}
+
+	for i, tt := range tests {
+		t.Run(fmt.Sprintf("[%d] %s", i, tt.desc), func(t *testing.T) {
+			result := new(Tenant)
+			err := json.Unmarshal(tt.data, result)
+			if err != nil {
+				t.Fatalf("unexpected error from Tenant.UnmarshalJSON: %v", err)
+			}
+
+			if want, got := tt.want, result; !reflect.DeepEqual(want, got) {
+				t.Fatalf("unexpected Tenant:\n- want: %v\n-  got: %v", want, got)
+			}
+		})
+	}
+}
+
+func TestTenantMarshalJSON(t *testing.T) {
+	var tests = []struct {
+		desc string
+		data *Tenant
+		want []byte
+	}{
+		{
+			desc: "Nil Group",
+			data: testTenantWithGroup(1, nil),
+			want: []byte(`{"id":1,"name":"Tenant 1","slug":"tenant-1","description":"Tenant 1 Description","comments":"Tenant 1 Comments"}`),
+		},
+		{
+			desc: "With Group",
+			data: testTenant(1),
+			want: []byte(`{"id":1,"name":"Tenant 1","slug":"tenant-1","description":"Tenant 1 Description","comments":"Tenant 1 Comments","group":1}`),
+		},
+		{
+			desc: "No Tenant.ID",
+			data: testTenantWithGroup(0, nil),
+			want: []byte(`{"name":"Tenant 0","slug":"tenant-0","description":"Tenant 0 Description","comments":"Tenant 0 Comments"}`),
+		},
+	}
+
+	for i, tt := range tests {
+		t.Run(fmt.Sprintf("[%d] %s", i, tt.desc), func(t *testing.T) {
+			result, err := json.Marshal(tt.data)
+			if err != nil {
+				t.Fatalf("unexpected error from Tenant.MarshalJSON: %v", err)
+			}
+
+			if want, got := tt.want, result; bytes.Compare(want, got) != 0 {
+				t.Fatalf("unexpected Tenant:\n- want: %s\n-  got: %s", want, got)
+			}
+		})
+	}
+}
+
+func TestListTenantOptions(t *testing.T) {
+	var tests = []struct {
+		desc string
+		o    *ListTenantOptions
+		v    url.Values
+	}{
+		{
+			desc: "empty options",
+		},
+		{
+			desc: "full options",
+			o: &ListTenantOptions{
+				Name:    "Hello",
+				IDIn:    "1,2,3",
+				GroupID: 1,
+				Query:   "World",
+			},
+			v: url.Values{
+				"name":     []string{"Hello"},
+				"id__in":   []string{"1,2,3"},
+				"group_id": []string{"1"},
+				"q":        []string{"World"},
+			},
+		},
+		{
+			desc: "group vs group_id",
+			o: &ListTenantOptions{
+				GroupID: 1,
+				Group:   "Group1",
+			},
+			v: url.Values{
+				"group_id": []string{"1"},
+			},
+		},
+		{
+			desc: "group name",
+			o: &ListTenantOptions{
+				Group: "Group 1",
+			},
+			v: url.Values{
+				"group": []string{"Group 1"},
+			},
+		},
+	}
+
+	for i, tt := range tests {
+		t.Run(fmt.Sprintf("[%d] %s", i, tt.desc), func(t *testing.T) {
+			v, err := tt.o.Values()
+			if err != nil {
+				t.Fatalf("unexpected Values error: %v", err)
+			}
+
+			if want, got := tt.v, v; !reflect.DeepEqual(want, got) {
+				t.Fatalf("unexpected url.Values map:\n- want: %v\n-  got: %v", want, got)
+			}
+		})
+	}
+}
diff --git a/netbox/tenancy_tenants_types.go b/netbox/tenancy_tenants_types.go
new file mode 100644
index 0000000000000000000000000000000000000000..ea64d3de37e63a22a20e0a8c569eb76452e09eec
--- /dev/null
+++ b/netbox/tenancy_tenants_types.go
@@ -0,0 +1,103 @@
+// Copyright 2017 The go-netbox Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package netbox
+
+import (
+	"encoding/json"
+	"net/url"
+	"strconv"
+)
+
+// A Tenant is a representation of netbox tenants
+type Tenant struct {
+	ID          int          `json:"id,omitempty"`
+	Name        string       `json:"name"`
+	Slug        string       `json:"slug"`
+	Description string       `json:"description"`
+	Comments    string       `json:"comments"`
+	Group       *TenantGroup `json:"Group"`
+}
+
+// A updateTenant is the internal representation of tenants
+// needed to POST/PUT/PATCH to Netbox API
+type updateTenant struct {
+	ID          int    `json:"id,omitempty"`
+	Name        string `json:"name"`
+	Slug        string `json:"slug"`
+	Description string `json:"description"`
+	Comments    string `json:"comments"`
+	Group       int    `json:"group,omitempty"`
+}
+
+// MarshalJSON marshals an Tenant into JSON bytes.
+func (t *Tenant) MarshalJSON() ([]byte, error) {
+	group := 0
+	if t.Group != nil {
+		group = t.Group.ID
+	}
+	return json.Marshal(updateTenant{
+		ID:          t.ID,
+		Name:        t.Name,
+		Slug:        t.Slug,
+		Description: t.Description,
+		Comments:    t.Comments,
+		Group:       group,
+	})
+}
+
+// ListTenantOptions is used as an argument for Client.Tenancy.Tenant.List.
+// Integer fields with an *ID suffix are preferred over their string
+// counterparts, and if both are set, only the *ID field will be used.
+type ListTenantOptions struct {
+	Name    string
+	IDIn    string
+	GroupID int
+	Group   string
+
+	Query string
+}
+
+// Values generates a url.Values map from the data in ListTenantOptions.
+func (o *ListTenantOptions) Values() (url.Values, error) {
+	if o == nil {
+		return nil, nil
+	}
+
+	v := url.Values{}
+
+	switch {
+	case o.GroupID != 0:
+		v.Set("group_id", strconv.Itoa(o.GroupID))
+	case o.Group != "":
+		v.Set("group", o.Group)
+	}
+
+	if o.Name != "" {
+		v.Set("name", o.Name)
+	}
+
+	if o.IDIn != "" {
+		v.Set("id__in", o.IDIn)
+	}
+
+	if o.Query != "" {
+		v.Set("q", o.Query)
+	}
+
+	return v, nil
+}
+
+//go:generate go run generate_functions.go -type-name Tenant -update-type-name updateTenant -service-name TenantsService -endpoint tenancy -service tenants
+//go:generate go run generate_basic_tests.go -type-name Tenant -service-name TenantsService -endpoint tenancy -service tenants -client-endpoint Tenancy -client-service Tenants
diff --git a/scripts/golint.sh b/scripts/golint.sh
index 54696d72fd6450d3365a0c7a1049c528302acb4a..ae7c53f46d933964f2f0396a94031e96487dc83a 100755
--- a/scripts/golint.sh
+++ b/scripts/golint.sh
@@ -1,12 +1,14 @@
 #!/bin/bash
 
 # Verify that all files are correctly golint'd.
-EXIT=0
-GOLINT=$(golint ./...)
+set -e -o nounset -o pipefail
+counter=0
+while read -r line; do
+	echo $line
+	: $((counter++))
+done < <(golint ./...)
 
-if [[ ! -z $GOLINT ]]; then
-	echo $GOLINT
-	EXIT=1
+if ((counter == 0)); then
+	exit 0
 fi
-
-exit $EXIT
+exit 1
diff --git a/scripts/licensecheck.sh b/scripts/licensecheck.sh
index 2230a8874d84d084ceb86c1c94e9f22b9406c8e9..ab31f0a75c236a8eee486f72b590cf848baa9183 100755
--- a/scripts/licensecheck.sh
+++ b/scripts/licensecheck.sh
@@ -2,8 +2,8 @@
 
 # Verify that the correct license block is present in all Go source
 # files.
-read -r -d '' EXPECTED <<EndOfLicense
-// Copyright 2016 The go-netbox Authors.
+IFS=$'\n' read -r -d '' -a EXPECTED <<EndOfLicense
+// Copyright 2017 The go-netbox Authors.
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -17,15 +17,22 @@ read -r -d '' EXPECTED <<EndOfLicense
 // See the License for the specific language governing permissions and
 // limitations under the License.
 EndOfLicense
+AUTHOR_REGEX='^// Copyright 20[0-9][0-9] The go-netbox Authors\.$'
 
 # Scan each Go source file for license.
 EXIT=0
 GOFILES=$(find . -name "*.go")
 
 for FILE in $GOFILES; do
-	BLOCK=$(head -n 14 $FILE)
+	IFS=$'\n' read -r -d '' -a BLOCK < <(head -n 14 $FILE)
 
-	if [ "$BLOCK" != "$EXPECTED" ]; then
+	tmp_block=${BLOCK[@]:1}
+	tmp_expected=${EXPECTED[@]:1}
+	if [[ $tmp_block != $tmp_expected ]]; then
+		echo "file missing license: $FILE"
+		EXIT=1
+	fi
+	if ! [[ "${BLOCK[0]}" =~ $AUTHOR_REGEX ]]; then
 		echo "file missing license: $FILE"
 		EXIT=1
 	fi