diff --git a/README.md b/README.md
index d22f081..234c2dc 100644
--- a/README.md
+++ b/README.md
@@ -204,7 +204,7 @@ psu stack deploy asd --endpoint primary --log-level debug
 The output would look like:
 
 ```text
-DEBU[0000] Getting endpoint's Docker info     endpoint=5
+DEBU[0000] Getting endpoint's Docker info     endpoint=primary
 DEBU[0000] Getting stack                      endpoint=primary stack=asd
 DEBU[0000] Stack not found                    stack=asd
 INFO[0000] Creating stack                     endpoint=primary stack=asd
diff --git a/client/auth.go b/client/auth.go
new file mode 100644
index 0000000..6a9d57c
--- /dev/null
+++ b/client/auth.go
@@ -0,0 +1,38 @@
+package client
+
+import "net/http"
+
+// AuthenticateUserOptions represents options passed to PortainerClient.AuthenticateUser()
+type AuthenticateUserOptions struct {
+	Username string
+	Password string
+}
+
+// AuthenticateUserRequest represents the body of a request to POST /auth
+type AuthenticateUserRequest struct {
+	Username string
+	Password string
+}
+
+// AuthenticateUserResponse represents the body of a response for a request to POST /auth
+type AuthenticateUserResponse struct {
+	Jwt string
+}
+
+func (n *portainerClientImp) AuthenticateUser(options AuthenticateUserOptions) (token string, err error) {
+	reqBody := AuthenticateUserRequest{
+		Username: options.Username,
+		Password: options.Password,
+	}
+
+	respBody := AuthenticateUserResponse{}
+
+	err = n.doJSON("auth", http.MethodPost, http.Header{}, &reqBody, &respBody)
+	if err != nil {
+		return
+	}
+
+	token = respBody.Jwt
+
+	return
+}
diff --git a/client/auth_test.go b/client/auth_test.go
new file mode 100644
index 0000000..8b8f36d
--- /dev/null
+++ b/client/auth_test.go
@@ -0,0 +1,92 @@
+package client
+
+import (
+	"net/http"
+	"net/http/httptest"
+	"net/url"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func Test_portainerClientImp_AuthenticateUser(t *testing.T) {
+	type fields struct {
+		server *httptest.Server
+	}
+	type args struct {
+		options AuthenticateUserOptions
+	}
+	tests := []struct {
+		name      string
+		fields    fields
+		args      args
+		wantToken string
+		wantErr   bool
+	}{
+		{
+			name: "valid username and password authenticates (happy path)",
+			fields: fields{
+				server: httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+					assert.Equal(t, req.Method, http.MethodPost)
+					assert.Equal(t, req.RequestURI, "/api/auth")
+
+					var body map[string]interface{}
+					err := readRequestBodyAsJSON(req, &body)
+					assert.Nil(t, err)
+
+					assert.NotNil(t, body["Username"])
+					assert.Equal(t, body["Username"], "admin")
+					assert.NotNil(t, body["Password"])
+					assert.Equal(t, body["Password"], "a")
+
+					writeResponseBodyAsJSON(w, map[string]interface{}{
+						"jwt": "token",
+					})
+				})),
+			},
+			args: args{
+				options: AuthenticateUserOptions{
+					Username: "admin",
+					Password: "a",
+				},
+			},
+			wantToken: "token",
+		},
+		{
+			name: "invalid username and password does not authenticate",
+			fields: fields{
+				server: httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+					w.WriteHeader(http.StatusUnprocessableEntity)
+					writeResponseBodyAsJSON(w, map[string]interface{}{
+						"Err":     "Invalid credentials",
+						"Details": "Unauthorized",
+					})
+				})),
+			},
+			args: args{
+				options: AuthenticateUserOptions{
+					Username: "admin",
+					Password: "a",
+				},
+			},
+			wantErr: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			tt.fields.server.Start()
+			defer tt.fields.server.Close()
+
+			apiURL, _ := url.Parse(tt.fields.server.URL + "/api/")
+
+			n := &portainerClientImp{
+				httpClient: tt.fields.server.Client(),
+				url:        apiURL,
+			}
+
+			gotToken, err := n.AuthenticateUser(tt.args.options)
+			assert.Equal(t, tt.wantErr, err != nil)
+			assert.Equal(t, tt.wantToken, gotToken)
+		})
+	}
+}
diff --git a/client/client.go b/client/client.go
index 66264de..668a35d 100644
--- a/client/client.go
+++ b/client/client.go
@@ -3,23 +3,16 @@ package client
 import (
 	"bytes"
 	"encoding/json"
-	"errors"
-	"fmt"
 	"io"
-	"io/ioutil"
 	"net/http"
 	"net/url"
 
 	portainer "github.com/portainer/portainer/api"
 )
 
-type StackListFilter struct {
-	SwarmId    string               `json:",omitempty"`
-	EndpointId portainer.EndpointID `json:",omitempty"`
-}
-
+// Config represents a Portainer client configuration
 type Config struct {
-	Url           *url.URL
+	URL           *url.URL
 	User          string
 	Password      string
 	Token         string
@@ -27,39 +20,40 @@ type Config struct {
 	DoNotUseToken bool
 }
 
+// PortainerClient represents a Portainer API client
 type PortainerClient interface {
-	// Authenticate a user to get an auth token
-	Authenticate() (token string, err error)
+	// AuthenticateUser a user to get an auth token
+	AuthenticateUser(options AuthenticateUserOptions) (token string, err error)
 
 	// Get endpoints
-	GetEndpoints() ([]portainer.Endpoint, error)
+	EndpointList() ([]portainer.Endpoint, error)
 
 	// Get endpoint groups
-	GetEndpointGroups() ([]portainer.EndpointGroup, error)
+	EndpointGroupList() ([]portainer.EndpointGroup, error)
 
 	// Get stacks, optionally filtered by swarmId and endpointId
-	GetStacks(swarmId string, endpointId portainer.EndpointID) ([]portainer.Stack, error)
+	StackList(options StackListOptions) ([]portainer.Stack, error)
 
 	// Create swarm stack
-	CreateSwarmStack(stackName string, environmentVariables []portainer.Pair, stackFileContent string, swarmClusterId string, endpointId portainer.EndpointID) (stack portainer.Stack, err error)
+	StackCreateSwarm(options StackCreateSwarmOptions) (stack portainer.Stack, err error)
 
 	// Create compose stack
-	CreateComposeStack(stackName string, environmentVariables []portainer.Pair, stackFileContent string, endpointId portainer.EndpointID) (stack portainer.Stack, err error)
+	StackCreateCompose(options StackCreateComposeOptions) (stack portainer.Stack, err error)
 
 	// Update stack
-	UpdateStack(stack portainer.Stack, environmentVariables []portainer.Pair, stackFileContent string, prune bool, endpointId portainer.EndpointID) error
+	StackUpdate(options StackUpdateOptions) error
 
 	// Delete stack
-	DeleteStack(stackId portainer.StackID) error
+	StackDelete(stackID portainer.StackID) error
 
 	// Get stack file content
-	GetStackFileContent(stackId portainer.StackID) (content string, err error)
+	StackFileInspect(stackID portainer.StackID) (content string, err error)
 
 	// Get endpoint Docker info
-	GetEndpointDockerInfo(endpointId portainer.EndpointID) (info map[string]interface{}, err error)
+	EndpointDockerInfo(endpointID portainer.EndpointID) (info map[string]interface{}, err error)
 
 	// Get Portainer status info
-	GetStatus() (portainer.Status, error)
+	Status() (portainer.Status, error)
 
 	// Run a function before sending a request to Portainer
 	BeforeRequest(hook func(req *http.Request) (err error))
@@ -68,7 +62,7 @@ type PortainerClient interface {
 	AfterResponse(hook func(resp *http.Response) (err error))
 
 	// Proxy proxies a request to /endpoint/{id}/docker and returns its result
-	Proxy(endpointId portainer.EndpointID, req *http.Request) (resp *http.Response, err error)
+	Proxy(endpointID portainer.EndpointID, req *http.Request) (resp *http.Response, err error)
 }
 
 type portainerClientImp struct {
@@ -78,40 +72,18 @@ type portainerClientImp struct {
 	password           string
 	token              string
 	userAgent          string
-	doNotUseToken      bool
 	beforeRequestHooks []func(req *http.Request) (err error)
 	afterResponseHooks []func(resp *http.Response) (err error)
 }
 
-// Check if an http.Response object has errors
-func checkResponseForErrors(resp *http.Response) error {
-	if 300 <= resp.StatusCode {
-		// Guess it's a GenericError
-		respBody := GenericError{}
-		err := json.NewDecoder(resp.Body).Decode(&respBody)
-		if err != nil {
-			// It's not a GenericError
-			bodyBytes, err := ioutil.ReadAll(resp.Body)
-			defer resp.Body.Close()
-			if err != nil {
-				return err
-			}
-			resp.Body = ioutil.NopCloser(bytes.NewReader(bodyBytes))
-			return errors.New(string(bodyBytes))
-		}
-		return &respBody
-	}
-	return nil
-}
-
 // Do an http request
-func (n *portainerClientImp) do(uri, method string, request io.Reader, requestType string, headers http.Header) (resp *http.Response, err error) {
-	requestUrl, err := n.url.Parse(uri)
+func (n *portainerClientImp) do(uri, method string, requestBody io.Reader, headers http.Header) (resp *http.Response, err error) {
+	requestURL, err := n.url.Parse(uri)
 	if err != nil {
 		return
 	}
 
-	req, err := http.NewRequest(method, requestUrl.String(), request)
+	req, err := http.NewRequest(method, requestURL.String(), requestBody)
 	if err != nil {
 		return
 	}
@@ -120,20 +92,8 @@ func (n *portainerClientImp) do(uri, method string, request io.Reader, requestTy
 		req.Header = headers
 	}
 
-	if request != nil {
-		req.Header.Set("Content-Type", requestType)
-		req.Header.Set("User-Agent", n.userAgent)
-	}
-
-	if !n.doNotUseToken {
-		if n.token == "" {
-			n.token, err = n.Authenticate()
-			if err != nil {
-				return
-			}
-		}
-		req.Header.Set("Authorization", "Bearer "+n.token)
-	}
+	// Set user agent header
+	req.Header.Set("User-Agent", n.userAgent)
 
 	// Run all "before request" hooks
 	for i := 0; i < len(n.beforeRequestHooks); i++ {
@@ -164,26 +124,46 @@ func (n *portainerClientImp) do(uri, method string, request io.Reader, requestTy
 	return
 }
 
-// Do a JSON http request
-func (n *portainerClientImp) doJSON(uri, method string, request interface{}, response interface{}) error {
-	var body io.Reader
+func (n *portainerClientImp) doWithToken(uri, method string, requestBody io.Reader, headers http.Header) (resp *http.Response, err error) {
+	// Ensure there is an auth token
+	if n.token == "" {
+		n.token, err = n.AuthenticateUser(AuthenticateUserOptions{
+			Username: n.user,
+			Password: n.password,
+		})
+		if err != nil {
+			return
+		}
+	}
+	headers.Set("Authorization", "Bearer "+n.token)
 
-	if request != nil {
-		reqBodyBytes, err := json.Marshal(request)
+	return n.do(uri, method, requestBody, headers)
+}
+
+// Do a JSON http request
+func (n *portainerClientImp) doJSON(uri, method string, headers http.Header, requestBody interface{}, responseBody interface{}) error {
+	// Encode request body, if any
+	var body io.Reader
+	if requestBody != nil {
+		reqBodyBytes, err := json.Marshal(requestBody)
 		if err != nil {
 			return err
 		}
 		body = bytes.NewReader(reqBodyBytes)
 	}
 
-	resp, err := n.do(uri, method, body, "application/json", nil)
+	// Set content type header
+	headers.Set("Content-Type", "application/json")
+
+	resp, err := n.do(uri, method, body, headers)
 	if err != nil {
 		return err
 	}
 
-	if response != nil {
+	// Decode response body, if any
+	if responseBody != nil {
 		d := json.NewDecoder(resp.Body)
-		err := d.Decode(response)
+		err := d.Decode(responseBody)
 		if err != nil {
 			return err
 		}
@@ -192,6 +172,23 @@ func (n *portainerClientImp) doJSON(uri, method string, request interface{}, res
 	return nil
 }
 
+// Do a JSON http request with an auth token
+func (n *portainerClientImp) doJSONWithToken(uri, method string, headers http.Header, request interface{}, response interface{}) (err error) {
+	// Ensure there is an auth token
+	if n.token == "" {
+		n.token, err = n.AuthenticateUser(AuthenticateUserOptions{
+			Username: n.user,
+			Password: n.password,
+		})
+		if err != nil {
+			return
+		}
+	}
+	headers.Set("Authorization", "Bearer "+n.token)
+
+	return n.doJSON(uri, method, headers, request, response)
+}
+
 func (n *portainerClientImp) BeforeRequest(hook func(req *http.Request) (err error)) {
 	n.beforeRequestHooks = append(n.beforeRequestHooks, hook)
 }
@@ -200,124 +197,11 @@ func (n *portainerClientImp) AfterResponse(hook func(resp *http.Response) (err e
 	n.afterResponseHooks = append(n.afterResponseHooks, hook)
 }
 
-func (n *portainerClientImp) Authenticate() (token string, err error) {
-	reqBody := AuthenticateUserRequest{
-		Username: n.user,
-		Password: n.password,
-	}
-
-	respBody := AuthenticateUserResponse{}
-
-	previousDoNotUseTokenValue := n.doNotUseToken
-	n.doNotUseToken = true
-
-	err = n.doJSON("auth", http.MethodPost, &reqBody, &respBody)
-	if err != nil {
-		return
-	}
-
-	n.doNotUseToken = previousDoNotUseTokenValue
-
-	token = respBody.Jwt
-
-	return
-}
-
-func (n *portainerClientImp) GetEndpoints() (endpoints []portainer.Endpoint, err error) {
-	err = n.doJSON("endpoints", http.MethodGet, nil, &endpoints)
-	return
-}
-
-func (n *portainerClientImp) GetEndpointGroups() (endpointGroups []portainer.EndpointGroup, err error) {
-	err = n.doJSON("endpoint_groups", http.MethodGet, nil, &endpointGroups)
-	return
-}
-
-func (n *portainerClientImp) GetStacks(swarmId string, endpointId portainer.EndpointID) (stacks []portainer.Stack, err error) {
-	filter := StackListFilter{
-		SwarmId:    swarmId,
-		EndpointId: endpointId,
-	}
-
-	filterJsonBytes, _ := json.Marshal(filter)
-	filterJsonString := string(filterJsonBytes)
-
-	err = n.doJSON(fmt.Sprintf("stacks?filters=%s", filterJsonString), http.MethodGet, nil, &stacks)
-	return
-}
-
-func (n *portainerClientImp) CreateSwarmStack(stackName string, environmentVariables []portainer.Pair, stackFileContent string, swarmClusterId string, endpointId portainer.EndpointID) (stack portainer.Stack, err error) {
-	reqBody := StackCreateRequest{
-		Name:             stackName,
-		Env:              environmentVariables,
-		SwarmID:          swarmClusterId,
-		StackFileContent: stackFileContent,
-	}
-
-	err = n.doJSON(fmt.Sprintf("stacks?type=%v&method=%s&endpointId=%v", 1, "string", endpointId), http.MethodPost, &reqBody, &stack)
-	return
-}
-
-func (n *portainerClientImp) CreateComposeStack(stackName string, environmentVariables []portainer.Pair, stackFileContent string, endpointId portainer.EndpointID) (stack portainer.Stack, err error) {
-	reqBody := StackCreateRequest{
-		Name:             stackName,
-		Env:              environmentVariables,
-		StackFileContent: stackFileContent,
-	}
-
-	err = n.doJSON(fmt.Sprintf("stacks?type=%v&method=%s&endpointId=%v", 2, "string", endpointId), http.MethodPost, &reqBody, &stack)
-	return
-}
-
-func (n *portainerClientImp) UpdateStack(stack portainer.Stack, environmentVariables []portainer.Pair, stackFileContent string, prune bool, endpointId portainer.EndpointID) (err error) {
-	reqBody := StackUpdateRequest{
-		Env:              environmentVariables,
-		StackFileContent: stackFileContent,
-		Prune:            prune,
-	}
-
-	err = n.doJSON(fmt.Sprintf("stacks/%v?endpointId=%v", stack.ID, endpointId), http.MethodPut, &reqBody, nil)
-	return
-}
-
-func (n *portainerClientImp) DeleteStack(stackId portainer.StackID) (err error) {
-	err = n.doJSON(fmt.Sprintf("stacks/%d", stackId), http.MethodDelete, nil, nil)
-	return
-}
-
-func (n *portainerClientImp) GetStackFileContent(stackId portainer.StackID) (content string, err error) {
-	var respBody StackFileInspectResponse
-
-	err = n.doJSON(fmt.Sprintf("stacks/%v/file", stackId), http.MethodGet, nil, &respBody)
-	if err != nil {
-		return
-	}
-
-	content = respBody.StackFileContent
-
-	return
-}
-
-func (n *portainerClientImp) GetEndpointDockerInfo(endpointId portainer.EndpointID) (info map[string]interface{}, err error) {
-	err = n.doJSON(fmt.Sprintf("endpoints/%v/docker/info", endpointId), http.MethodGet, nil, &info)
-	return
-}
-
-func (n *portainerClientImp) GetStatus() (status portainer.Status, err error) {
-	err = n.doJSON("status", http.MethodGet, nil, &status)
-	return
-}
-
-func (n *portainerClientImp) Proxy(endpointId portainer.EndpointID, req *http.Request) (resp *http.Response, err error) {
-	resp, err = n.do(fmt.Sprintf("endpoints/%v/docker%s", endpointId, req.RequestURI), req.Method, req.Body, "", req.Header)
-	return
-}
-
-// Create a new client
+// NewClient creates a new Portainer API client
 func NewClient(httpClient *http.Client, config Config) PortainerClient {
 	return &portainerClientImp{
 		httpClient: httpClient,
-		url:        config.Url,
+		url:        config.URL,
 		user:       config.User,
 		password:   config.Password,
 		token:      config.Token,
diff --git a/client/client_test.go b/client/client_test.go
index d752efa..c69af0c 100644
--- a/client/client_test.go
+++ b/client/client_test.go
@@ -3,6 +3,7 @@ package client
 import (
 	"encoding/json"
 	"fmt"
+	"io"
 	"io/ioutil"
 	"net/http"
 	"net/http/httptest"
@@ -12,62 +13,389 @@ import (
 	"github.com/stretchr/testify/assert"
 )
 
-func readRequestBodyAsJson(req *http.Request, body *map[string]interface{}) (err error) {
+func readRequestBodyAsJSON(req *http.Request, body *map[string]interface{}) (err error) {
 	bodyBytes, err := ioutil.ReadAll(req.Body)
 	defer req.Body.Close()
 	err = json.Unmarshal(bodyBytes, body)
 	return
 }
 
-func writeResponseBodyAsJson(w http.ResponseWriter, body map[string]interface{}) (err error) {
+func writeResponseBodyAsJSON(w http.ResponseWriter, body map[string]interface{}) (err error) {
 	bodyBytes, err := json.Marshal(body)
 	fmt.Fprintln(w, string(bodyBytes))
 	return
 }
 
 func TestNewClient(t *testing.T) {
-	apiUrl, _ := url.Parse("http://validurl.com/api")
+	apiURL, _ := url.Parse("http://validurl.com/api")
 
 	validClient := NewClient(http.DefaultClient, Config{
-		Url: apiUrl,
+		URL: apiURL,
 	})
 	assert.NotNil(t, validClient)
 }
 
-func TestClientAuthenticates(t *testing.T) {
-	ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
-		var body map[string]interface{}
-		err := readRequestBodyAsJson(req, &body)
+func Test_portainerClientImp_do(t *testing.T) {
+	type fields struct {
+		user               string
+		password           string
+		token              string
+		userAgent          string
+		beforeRequestHooks []func(req *http.Request) (err error)
+		afterResponseHooks []func(resp *http.Response) (err error)
+		server             *httptest.Server
+		beforeFunctionCall func(t *testing.T, tt *fields)
+	}
+	type args struct {
+		uri         string
+		method      string
+		requestBody io.Reader
+		headers     http.Header
+	}
+	tests := []struct {
+		name          string
+		fields        fields
+		args          args
+		wantRespCheck func(resp *http.Response) bool
+		wantErr       bool
+	}{
+		{
+			name: "error on bad URI",
+			fields: fields{
+				server: httptest.NewUnstartedServer(nil),
+			},
+			args: args{
+				uri: string(0x7f),
+			},
+			wantErr: true,
+		},
+		{
+			name: "error on bad method",
+			fields: fields{
+				server: httptest.NewUnstartedServer(nil),
+			},
+			args: args{
+				method: "WOLOLO?",
+			},
+			wantErr: true,
+		},
+		{
+			name: "extra headers are added",
+			fields: fields{
+				server: httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+					assert.Equal(t, req.Header.Get("Some-Header"), "value")
+				})),
+			},
+			args: args{
+				headers: http.Header{
+					"Some-Header": []string{
+						"value",
+					},
+				},
+			},
+		},
+		{
+			name: "returns error on http error",
+			fields: fields{
+				token:  "token",
+				server: httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {})),
+				beforeFunctionCall: func(t *testing.T, tt *fields) {
+					tt.server.Close()
+				},
+			},
+			wantErr: true,
+		},
+		{
+			name: "returns error on response error",
+			fields: fields{
+				server: httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+					w.WriteHeader(http.StatusInternalServerError)
+				})),
+			},
+			wantErr: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			tt.fields.server.Start()
+			defer tt.fields.server.Close()
 
-		assert.Equal(t, req.Method, http.MethodPost)
-		assert.Equal(t, req.RequestURI, "/api/auth")
-		assert.NotNil(t, req.Header["Content-Type"])
-		assert.NotNil(t, req.Header["Content-Type"][0])
-		assert.Equal(t, req.Header["Content-Type"][0], "application/json")
-		assert.NotNil(t, req.Header["User-Agent"])
-		assert.NotNil(t, req.Header["User-Agent"][0])
-		assert.Equal(t, req.Header["User-Agent"][0], "GE007")
-		assert.Nil(t, err)
-		assert.NotNil(t, body["Username"])
-		assert.Equal(t, body["Username"], "admin")
-		assert.NotNil(t, body["Password"])
-		assert.Equal(t, body["Password"], "a")
+			apiURL, _ := url.Parse(tt.fields.server.URL + "/api/")
 
-		writeResponseBodyAsJson(w, map[string]interface{}{
-			"jwt": "somerandomtoken",
+			n := &portainerClientImp{
+				httpClient:         tt.fields.server.Client(),
+				url:                apiURL,
+				user:               tt.fields.user,
+				password:           tt.fields.password,
+				token:              tt.fields.token,
+				userAgent:          tt.fields.userAgent,
+				beforeRequestHooks: tt.fields.beforeRequestHooks,
+				afterResponseHooks: tt.fields.afterResponseHooks,
+			}
+
+			if tt.fields.beforeFunctionCall != nil {
+				tt.fields.beforeFunctionCall(t, &tt.fields)
+			}
+			gotResp, err := n.do(tt.args.uri, tt.args.method, tt.args.requestBody, tt.args.headers)
+
+			assert.Equal(t, tt.wantErr, err != nil)
+			if tt.wantRespCheck != nil {
+				assert.True(t, tt.wantRespCheck(gotResp))
+			}
 		})
-	}))
-	defer ts.Close()
-
-	apiUrl, _ := url.Parse(ts.URL + "/api/")
-
-	customClient := NewClient(ts.Client(), Config{
-		Url:       apiUrl,
-		User:      "admin",
-		Password:  "a",
-		UserAgent: "GE007",
-	})
-	token, err := customClient.Authenticate()
-	assert.Nil(t, err)
-	assert.Equal(t, token, "somerandomtoken")
+	}
+}
+
+func Test_portainerClientImp_doJSON(t *testing.T) {
+	type fields struct {
+		httpClient *http.Client
+		url        *url.URL
+		server     *httptest.Server
+	}
+	type args struct {
+		uri          string
+		method       string
+		headers      http.Header
+		requestBody  interface{}
+		responseBody interface{}
+	}
+	tests := []struct {
+		name         string
+		fields       fields
+		args         args
+		wantRespBody interface{}
+		wantErr      bool
+	}{
+		{
+			name: "request is made with application/json content type and expected JSON object as body",
+			fields: fields{
+				server: httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+					assert.Equal(t, "/api/stacks", req.RequestURI)
+					assert.Equal(t, http.MethodPost, req.Method)
+					assert.Equal(t, "application/json", req.Header.Get("Content-Type"))
+
+					var body map[string]interface{}
+					err := readRequestBodyAsJSON(req, &body)
+					assert.Nil(t, err)
+
+					assert.Equal(t, map[string]interface{}{
+						"key1": "value1",
+					}, body)
+
+					writeResponseBodyAsJSON(w, map[string]interface{}{
+						"key2": "value2",
+					})
+				})),
+			},
+			args: args{
+				uri:     "stacks",
+				method:  http.MethodPost,
+				headers: http.Header{},
+				requestBody: map[string]interface{}{
+					"key1": "value1",
+				},
+				responseBody: map[string]interface{}{},
+			},
+			wantRespBody: map[string]interface{}{
+				"key2": "value2",
+			},
+		},
+		{
+			name: "invalid JSON object as request body causes an error",
+			fields: fields{
+				server: httptest.NewUnstartedServer(nil),
+			},
+			args: args{
+				uri:         "stacks",
+				method:      http.MethodPost,
+				headers:     http.Header{},
+				requestBody: func() {},
+			},
+			wantErr: true,
+		},
+		{
+			name: "invalid JSON object as response body causes an error",
+			fields: fields{
+				server: httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+					fmt.Fprint(w, "not a JSON object")
+				})),
+			},
+			args: args{
+				uri:          "stacks",
+				method:       http.MethodPost,
+				headers:      http.Header{},
+				responseBody: map[string]interface{}{},
+			},
+			wantRespBody: map[string]interface{}{},
+			wantErr:      true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			tt.fields.server.Start()
+			defer tt.fields.server.Close()
+
+			apiURL, _ := url.Parse(tt.fields.server.URL + "/api/")
+
+			n := &portainerClientImp{
+				httpClient: tt.fields.server.Client(),
+				url:        apiURL,
+			}
+
+			err := n.doJSON(tt.args.uri, tt.args.method, tt.args.headers, &tt.args.requestBody, &tt.args.responseBody)
+			assert.Equal(t, tt.wantErr, err != nil)
+			assert.Equal(t, tt.wantRespBody, tt.args.responseBody)
+		})
+	}
+}
+
+func Test_portainerClientImp_doJSONWithToken(t *testing.T) {
+	type fields struct {
+		httpClient *http.Client
+		url        *url.URL
+		user       string
+		password   string
+		token      string
+		server     *httptest.Server
+	}
+	type args struct {
+		uri          string
+		method       string
+		headers      http.Header
+		requestBody  interface{}
+		responseBody interface{}
+	}
+	tests := []struct {
+		name         string
+		fields       fields
+		args         args
+		wantRespBody interface{}
+		wantErr      bool
+	}{
+		{
+			name: "when a token is present, it is used",
+			fields: fields{
+				token: "token",
+				server: httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+					assert.Equal(t, "/api/stacks", req.RequestURI)
+					assert.Equal(t, http.MethodPost, req.Method)
+					assert.Equal(t, "Bearer token", req.Header.Get("Authorization"))
+
+					var body map[string]interface{}
+					err := readRequestBodyAsJSON(req, &body)
+					assert.Nil(t, err)
+
+					assert.Equal(t, map[string]interface{}{
+						"key1": "value1",
+					}, body)
+
+					writeResponseBodyAsJSON(w, map[string]interface{}{
+						"key2": "value2",
+					})
+				})),
+			},
+			args: args{
+				uri:     "stacks",
+				method:  http.MethodPost,
+				headers: http.Header{},
+				requestBody: map[string]interface{}{
+					"key1": "value1",
+				},
+				responseBody: map[string]interface{}{},
+			},
+			wantRespBody: map[string]interface{}{
+				"key2": "value2",
+			},
+		},
+		{
+			name: "when a token is not present, a new one is obtained and used",
+			fields: fields{
+				user:     "admin",
+				password: "a",
+				server: httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+					switch req.RequestURI {
+					case "/api/auth":
+						assert.Equal(t, "", req.Header.Get("Authorization"))
+
+						writeResponseBodyAsJSON(w, map[string]interface{}{
+							"jwt": "token",
+						})
+					case "/api/stacks":
+						assert.Equal(t, "/api/stacks", req.RequestURI)
+						assert.Equal(t, http.MethodPost, req.Method)
+						assert.Equal(t, "Bearer token", req.Header.Get("Authorization"))
+
+						var body map[string]interface{}
+						err := readRequestBodyAsJSON(req, &body)
+						assert.Nil(t, err)
+
+						assert.Equal(t, map[string]interface{}{
+							"key1": "value1",
+						}, body)
+
+						writeResponseBodyAsJSON(w, map[string]interface{}{
+							"key2": "value2",
+						})
+					}
+				})),
+			},
+			args: args{
+				uri:     "stacks",
+				method:  http.MethodPost,
+				headers: http.Header{},
+				requestBody: map[string]interface{}{
+					"key1": "value1",
+				},
+				responseBody: map[string]interface{}{},
+			},
+			wantRespBody: map[string]interface{}{
+				"key2": "value2",
+			},
+		},
+		{
+			name: "when authentication error occurs, an error is returned",
+			fields: fields{
+				user:     "admin",
+				password: "a",
+				server: httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
+					w.WriteHeader(http.StatusUnprocessableEntity)
+					writeResponseBodyAsJSON(w, map[string]interface{}{
+						"Err":     "Invalid credentials",
+						"Details": "Unauthorized",
+					})
+				})),
+			},
+			args: args{
+				uri:     "stacks",
+				method:  http.MethodPost,
+				headers: http.Header{},
+				requestBody: map[string]interface{}{
+					"key1": "value1",
+				},
+				responseBody: map[string]interface{}{},
+			},
+			wantRespBody: map[string]interface{}{},
+			wantErr:      true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			tt.fields.server.Start()
+			defer tt.fields.server.Close()
+
+			apiURL, _ := url.Parse(tt.fields.server.URL + "/api/")
+
+			n := &portainerClientImp{
+				httpClient: tt.fields.server.Client(),
+				url:        apiURL,
+				user:       tt.fields.user,
+				password:   tt.fields.password,
+				token:      tt.fields.token,
+			}
+
+			err := n.doJSONWithToken(tt.args.uri, tt.args.method, tt.args.headers, &tt.args.requestBody, &tt.args.responseBody)
+			assert.Equal(t, tt.wantErr, err != nil)
+			assert.Equal(t, tt.wantRespBody, tt.args.responseBody)
+		})
+	}
 }
diff --git a/client/endpointGroup_list.go b/client/endpointGroup_list.go
new file mode 100644
index 0000000..89de541
--- /dev/null
+++ b/client/endpointGroup_list.go
@@ -0,0 +1,12 @@
+package client
+
+import (
+	"net/http"
+
+	portainer "github.com/portainer/portainer/api"
+)
+
+func (n *portainerClientImp) EndpointGroupList() (endpointGroups []portainer.EndpointGroup, err error) {
+	err = n.doJSONWithToken("endpoint_groups", http.MethodGet, http.Header{}, nil, &endpointGroups)
+	return
+}
diff --git a/client/endpoint_docker_info.go b/client/endpoint_docker_info.go
new file mode 100644
index 0000000..83fb90e
--- /dev/null
+++ b/client/endpoint_docker_info.go
@@ -0,0 +1,13 @@
+package client
+
+import (
+	"fmt"
+	"net/http"
+
+	portainer "github.com/portainer/portainer/api"
+)
+
+func (n *portainerClientImp) EndpointDockerInfo(endpointID portainer.EndpointID) (info map[string]interface{}, err error) {
+	err = n.doJSONWithToken(fmt.Sprintf("endpoints/%v/docker/info", endpointID), http.MethodGet, http.Header{}, nil, &info)
+	return
+}
diff --git a/client/endpoint_docker_proxy.go b/client/endpoint_docker_proxy.go
new file mode 100644
index 0000000..ce493a3
--- /dev/null
+++ b/client/endpoint_docker_proxy.go
@@ -0,0 +1,12 @@
+package client
+
+import (
+	"fmt"
+	"net/http"
+
+	portainer "github.com/portainer/portainer/api"
+)
+
+func (n *portainerClientImp) Proxy(endpointID portainer.EndpointID, req *http.Request) (resp *http.Response, err error) {
+	return n.doWithToken(fmt.Sprintf("endpoints/%v/docker%s", endpointID, req.RequestURI), req.Method, req.Body, req.Header)
+}
diff --git a/client/endpoint_list.go b/client/endpoint_list.go
new file mode 100644
index 0000000..2aac0e9
--- /dev/null
+++ b/client/endpoint_list.go
@@ -0,0 +1,12 @@
+package client
+
+import (
+	"net/http"
+
+	portainer "github.com/portainer/portainer/api"
+)
+
+func (n *portainerClientImp) EndpointList() (endpoints []portainer.Endpoint, err error) {
+	err = n.doJSONWithToken("endpoints", http.MethodGet, http.Header{}, nil, &endpoints)
+	return
+}
diff --git a/client/errors.go b/client/errors.go
new file mode 100644
index 0000000..6a77642
--- /dev/null
+++ b/client/errors.go
@@ -0,0 +1,16 @@
+package client
+
+import "fmt"
+
+// GenericError represents the body of a generic error returned by the Portainer API
+type GenericError struct {
+	Err     string
+	Details string
+}
+
+func (e *GenericError) Error() string {
+	if e.Details != "" {
+		return fmt.Sprintf("%s: %s", e.Err, e.Details)
+	}
+	return fmt.Sprintf("%s", e.Err)
+}
diff --git a/client/errors_test.go b/client/errors_test.go
new file mode 100644
index 0000000..a0ebaef
--- /dev/null
+++ b/client/errors_test.go
@@ -0,0 +1,56 @@
+package client
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestGenericError_Error(t *testing.T) {
+	type fields struct {
+		Err     string
+		Details string
+	}
+	tests := []struct {
+		name   string
+		fields fields
+		want   string
+	}{
+		{
+			name: "error with message and details",
+			fields: fields{
+				Err:     "error",
+				Details: "details",
+			},
+			want: "error: details",
+		},
+		{
+			name: "error with message and no details",
+			fields: fields{
+				Err: "error",
+			},
+			want: "error",
+		},
+		{
+			name: "error with no error message and details",
+			fields: fields{
+				Details: "details",
+			},
+			want: ": details",
+		},
+		{
+			name:   "error with no error message and no details",
+			fields: fields{},
+			want:   "",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			e := &GenericError{
+				Err:     tt.fields.Err,
+				Details: tt.fields.Details,
+			}
+			assert.Equal(t, tt.want, e.Error())
+		})
+	}
+}
diff --git a/client/portainerTypes.go b/client/portainerTypes.go
deleted file mode 100644
index 68f2f15..0000000
--- a/client/portainerTypes.go
+++ /dev/null
@@ -1,57 +0,0 @@
-package client
-
-import (
-	"fmt"
-
-	portainer "github.com/portainer/portainer/api"
-)
-
-func GetTranslatedStackType(s portainer.Stack) string {
-	switch s.Type {
-	case 1:
-		return "swarm"
-	case 2:
-		return "compose"
-	default:
-		return ""
-	}
-}
-
-type StackCreateRequest struct {
-	Name             string
-	SwarmID          string
-	StackFileContent string
-	Env              []portainer.Pair `json:",omitempty"`
-}
-
-type StackUpdateRequest struct {
-	StackFileContent string
-	Env              []portainer.Pair `json:",omitempty"`
-	Prune            bool
-}
-
-type StackFileInspectResponse struct {
-	StackFileContent string
-}
-
-type GenericError struct {
-	Err     string
-	Details string
-}
-
-func (e *GenericError) Error() string {
-	if e.Details != "" {
-		return fmt.Sprintf("%s: %s", e.Err, e.Details)
-	} else {
-		return fmt.Sprintf("%s", e.Err)
-	}
-}
-
-type AuthenticateUserRequest struct {
-	Username string
-	Password string
-}
-
-type AuthenticateUserResponse struct {
-	Jwt string
-}
diff --git a/client/stackFile_inspect.go b/client/stackFile_inspect.go
new file mode 100644
index 0000000..7ef6720
--- /dev/null
+++ b/client/stackFile_inspect.go
@@ -0,0 +1,26 @@
+package client
+
+import (
+	"fmt"
+	"net/http"
+
+	portainer "github.com/portainer/portainer/api"
+)
+
+// StackFileInspectResponse represents the body of a response for a request to GET /stack/{id}/file
+type StackFileInspectResponse struct {
+	StackFileContent string
+}
+
+func (n *portainerClientImp) StackFileInspect(stackID portainer.StackID) (content string, err error) {
+	var respBody StackFileInspectResponse
+
+	err = n.doJSONWithToken(fmt.Sprintf("stacks/%v/file", stackID), http.MethodGet, http.Header{}, nil, &respBody)
+	if err != nil {
+		return
+	}
+
+	content = respBody.StackFileContent
+
+	return
+}
diff --git a/client/stack_create.go b/client/stack_create.go
new file mode 100644
index 0000000..d93011c
--- /dev/null
+++ b/client/stack_create.go
@@ -0,0 +1,56 @@
+package client
+
+import (
+	"fmt"
+	"net/http"
+
+	portainer "github.com/portainer/portainer/api"
+)
+
+// StackCreateComposeOptions represents options passed to PortainerClient.StackCreateCompose()
+type StackCreateComposeOptions struct {
+	StackName            string
+	EnvironmentVariables []portainer.Pair
+	StackFileContent     string
+	EndpointID           portainer.EndpointID
+}
+
+// StackCreateRequest represents the body of a request to POST /stacks
+type StackCreateRequest struct {
+	Name             string
+	SwarmID          string
+	StackFileContent string
+	Env              []portainer.Pair `json:",omitempty"`
+}
+
+// StackCreateSwarmOptions represents options passed to PortainerClient.StackCreateSwarm()
+type StackCreateSwarmOptions struct {
+	StackName            string
+	EnvironmentVariables []portainer.Pair
+	StackFileContent     string
+	SwarmClusterID       string
+	EndpointID           portainer.EndpointID
+}
+
+func (n *portainerClientImp) StackCreateCompose(options StackCreateComposeOptions) (stack portainer.Stack, err error) {
+	reqBody := StackCreateRequest{
+		Name:             options.StackName,
+		Env:              options.EnvironmentVariables,
+		StackFileContent: options.StackFileContent,
+	}
+
+	err = n.doJSONWithToken(fmt.Sprintf("stacks?type=%v&method=%s&endpointId=%v", 2, "string", options.EndpointID), http.MethodPost, http.Header{}, &reqBody, &stack)
+	return
+}
+
+func (n *portainerClientImp) StackCreateSwarm(options StackCreateSwarmOptions) (stack portainer.Stack, err error) {
+	reqBody := StackCreateRequest{
+		Name:             options.StackName,
+		Env:              options.EnvironmentVariables,
+		SwarmID:          options.SwarmClusterID,
+		StackFileContent: options.StackFileContent,
+	}
+
+	err = n.doJSONWithToken(fmt.Sprintf("stacks?type=%v&method=%s&endpointId=%v", 1, "string", options.EndpointID), http.MethodPost, http.Header{}, &reqBody, &stack)
+	return
+}
diff --git a/client/stack_delete.go b/client/stack_delete.go
new file mode 100644
index 0000000..f67fc20
--- /dev/null
+++ b/client/stack_delete.go
@@ -0,0 +1,13 @@
+package client
+
+import (
+	"fmt"
+	"net/http"
+
+	portainer "github.com/portainer/portainer/api"
+)
+
+func (n *portainerClientImp) StackDelete(stackID portainer.StackID) (err error) {
+	err = n.doJSONWithToken(fmt.Sprintf("stacks/%d", stackID), http.MethodDelete, http.Header{}, nil, nil)
+	return
+}
diff --git a/client/stack_list.go b/client/stack_list.go
new file mode 100644
index 0000000..7b74c6d
--- /dev/null
+++ b/client/stack_list.go
@@ -0,0 +1,28 @@
+package client
+
+import (
+	"encoding/json"
+	"fmt"
+	"net/http"
+
+	portainer "github.com/portainer/portainer/api"
+)
+
+// StackListFilter represents a filter for a stack list
+type StackListFilter struct {
+	SwarmID    string               `json:"SwarmId,omitempty"`
+	EndpointID portainer.EndpointID `json:"EndpointId,omitempty"`
+}
+
+// StackListOptions represents options passed to PortainerClient.StackList()
+type StackListOptions struct {
+	Filter StackListFilter
+}
+
+func (n *portainerClientImp) StackList(options StackListOptions) (stacks []portainer.Stack, err error) {
+	filterJSONBytes, _ := json.Marshal(options.Filter)
+	filterJSONString := string(filterJSONBytes)
+
+	err = n.doJSONWithToken(fmt.Sprintf("stacks?filters=%s", filterJSONString), http.MethodGet, http.Header{}, nil, &stacks)
+	return
+}
diff --git a/client/stack_update.go b/client/stack_update.go
new file mode 100644
index 0000000..45ea6b8
--- /dev/null
+++ b/client/stack_update.go
@@ -0,0 +1,35 @@
+package client
+
+import (
+	"fmt"
+	"net/http"
+
+	portainer "github.com/portainer/portainer/api"
+)
+
+// StackUpdateOptions represents options passed to PortainerClient.StackUpdate()
+type StackUpdateOptions struct {
+	Stack                portainer.Stack
+	EnvironmentVariables []portainer.Pair
+	StackFileContent     string
+	Prune                bool
+	EndpointID           portainer.EndpointID
+}
+
+// StackUpdateRequest represents the body of a request to PUT /stacks/{id}
+type StackUpdateRequest struct {
+	StackFileContent string
+	Env              []portainer.Pair `json:",omitempty"`
+	Prune            bool
+}
+
+func (n *portainerClientImp) StackUpdate(options StackUpdateOptions) (err error) {
+	reqBody := StackUpdateRequest{
+		Env:              options.EnvironmentVariables,
+		StackFileContent: options.StackFileContent,
+		Prune:            options.Prune,
+	}
+
+	err = n.doJSONWithToken(fmt.Sprintf("stacks/%v?endpointId=%v", options.Stack.ID, options.EndpointID), http.MethodPut, http.Header{}, &reqBody, nil)
+	return
+}
diff --git a/client/status.go b/client/status.go
new file mode 100644
index 0000000..62744bd
--- /dev/null
+++ b/client/status.go
@@ -0,0 +1,12 @@
+package client
+
+import (
+	"net/http"
+
+	portainer "github.com/portainer/portainer/api"
+)
+
+func (n *portainerClientImp) Status() (status portainer.Status, err error) {
+	err = n.doJSONWithToken("status", http.MethodGet, http.Header{}, nil, &status)
+	return
+}
diff --git a/client/utils.go b/client/utils.go
new file mode 100644
index 0000000..173b31f
--- /dev/null
+++ b/client/utils.go
@@ -0,0 +1,44 @@
+package client
+
+import (
+	"bytes"
+	"encoding/json"
+	"errors"
+	"io/ioutil"
+	"net/http"
+
+	portainer "github.com/portainer/portainer/api"
+)
+
+// GetTranslatedStackType returns a stack's Type field (int) translated to it's human readable form (string)
+func GetTranslatedStackType(t portainer.StackType) string {
+	switch t {
+	case portainer.DockerSwarmStack:
+		return "swarm"
+	case portainer.DockerComposeStack:
+		return "compose"
+	default:
+		return ""
+	}
+}
+
+// Check if an http.Response object has errors
+func checkResponseForErrors(resp *http.Response) error {
+	if 300 <= resp.StatusCode {
+		// Guess it's a GenericError
+		respBody := GenericError{}
+		err := json.NewDecoder(resp.Body).Decode(&respBody)
+		if err != nil {
+			// It's not a GenericError
+			bodyBytes, err := ioutil.ReadAll(resp.Body)
+			defer resp.Body.Close()
+			if err != nil {
+				return err
+			}
+			resp.Body = ioutil.NopCloser(bytes.NewReader(bodyBytes))
+			return errors.New(string(bodyBytes))
+		}
+		return &respBody
+	}
+	return nil
+}
diff --git a/client/utils_test.go b/client/utils_test.go
new file mode 100644
index 0000000..a4f225e
--- /dev/null
+++ b/client/utils_test.go
@@ -0,0 +1,97 @@
+package client
+
+import (
+	"bytes"
+	"encoding/json"
+	"io/ioutil"
+	"net/http"
+	"testing"
+
+	portainer "github.com/portainer/portainer/api"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestGetTranslatedStackType(t *testing.T) {
+	type args struct {
+		t portainer.StackType
+	}
+	tests := []struct {
+		name string
+		args args
+		want string
+	}{
+		{
+			name: "swarm stack type",
+			args: args{
+				t: portainer.DockerSwarmStack,
+			},
+			want: "swarm",
+		},
+		{
+			name: "compose stack type",
+			args: args{
+				t: portainer.DockerComposeStack,
+			},
+			want: "compose",
+		},
+		{
+			name: "unknown stack type",
+			args: args{
+				t: 100,
+			},
+			want: "",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert.Equal(t, tt.want, GetTranslatedStackType(tt.args.t))
+		})
+	}
+}
+
+func Test_checkResponseForErrors(t *testing.T) {
+	type args struct {
+		resp *http.Response
+	}
+	tests := []struct {
+		name    string
+		args    args
+		wantErr bool
+	}{
+		{
+			name: "generic error",
+			args: args{
+				resp: func() (resp *http.Response) {
+					resp = &http.Response{
+						StatusCode: http.StatusNotFound,
+					}
+					bodyBytes, _ := json.Marshal(map[string]interface{}{
+						"Err":     "Error",
+						"Details": "Not found",
+					})
+					resp.Body = ioutil.NopCloser(bytes.NewReader(bodyBytes))
+					return
+				}(),
+			},
+			wantErr: true,
+		},
+		{
+			name: "non generic error",
+			args: args{
+				resp: func() (resp *http.Response) {
+					resp = &http.Response{
+						StatusCode: http.StatusNotFound,
+						Body:       ioutil.NopCloser(bytes.NewReader([]byte("Err"))),
+					}
+					return
+				}(),
+			},
+			wantErr: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			assert.Equal(t, tt.wantErr, checkResponseForErrors(tt.args.resp) != nil)
+		})
+	}
+}
diff --git a/cmd/configList.go b/cmd/configList.go
index 5c76e20..2942758 100644
--- a/cmd/configList.go
+++ b/cmd/configList.go
@@ -77,9 +77,9 @@ var configListCmd = &cobra.Command{
 			common.CheckError(flushErr)
 		case "json":
 			// Print configs in a json format
-			statusJsonBytes, err := json.Marshal(configs)
+			statusJSONBytes, err := json.Marshal(configs)
 			common.CheckError(err)
-			fmt.Println(string(statusJsonBytes))
+			fmt.Println(string(statusJSONBytes))
 		default:
 			// Print configs in a custom format
 			template, templateParsingErr := template.New("configTpl").Parse(viper.GetString("config.list.format"))
diff --git a/cmd/endpointGroupInspect.go b/cmd/endpointGroupInspect.go
index 606b47a..cc3a1ba 100644
--- a/cmd/endpointGroupInspect.go
+++ b/cmd/endpointGroupInspect.go
@@ -44,9 +44,9 @@ var endpointGroupInspectCmd = &cobra.Command{
 				common.CheckError(err)
 			case "json":
 				// Print endpoint group in a json format
-				endpointJsonBytes, err := json.Marshal(endpointGroup)
+				endpointJSONBytes, err := json.Marshal(endpointGroup)
 				common.CheckError(err)
-				fmt.Println(string(endpointJsonBytes))
+				fmt.Println(string(endpointJSONBytes))
 			default:
 				// Print endpoint group in a custom format
 				template, err := template.New("endpointGroupTpl").Parse(viper.GetString("endpoint.group.inspect.format"))
diff --git a/cmd/endpointGroupList.go b/cmd/endpointGroupList.go
index 5722d8f..953c90e 100644
--- a/cmd/endpointGroupList.go
+++ b/cmd/endpointGroupList.go
@@ -30,7 +30,7 @@ var endpointGroupListCmd = &cobra.Command{
 		common.CheckError(err)
 
 		logrus.Debug("Getting endpoint groups")
-		endpointGroups, err := client.GetEndpointGroups()
+		endpointGroups, err := client.EndpointGroupList()
 		common.CheckError(err)
 
 		switch viper.GetString("endpoint.group.list.format") {
@@ -55,9 +55,9 @@ var endpointGroupListCmd = &cobra.Command{
 			common.CheckError(flushErr)
 		case "json":
 			// Print endpoint groups in a json format
-			statusJsonBytes, err := json.Marshal(endpointGroups)
+			statusJSONBytes, err := json.Marshal(endpointGroups)
 			common.CheckError(err)
-			fmt.Println(string(statusJsonBytes))
+			fmt.Println(string(statusJSONBytes))
 		default:
 			// Print endpoint groups in a custom format
 			template, templateParsingErr := template.New("endpointGroupTpl").Parse(viper.GetString("endpoint.group.list.format"))
diff --git a/cmd/endpointInspect.go b/cmd/endpointInspect.go
index cdce6b4..1fb98eb 100644
--- a/cmd/endpointInspect.go
+++ b/cmd/endpointInspect.go
@@ -74,9 +74,9 @@ var endpointInspectCmd = &cobra.Command{
 			common.CheckError(err)
 		case "json":
 			// Print endpoint in a json format
-			endpointJsonBytes, err := json.Marshal(endpoint)
+			endpointJSONBytes, err := json.Marshal(endpoint)
 			common.CheckError(err)
-			fmt.Println(string(endpointJsonBytes))
+			fmt.Println(string(endpointJSONBytes))
 		default:
 			// Print endpoint in a custom format
 			template, err := template.New("endpointTpl").Parse(viper.GetString("endpoint.inspect.format"))
diff --git a/cmd/endpointList.go b/cmd/endpointList.go
index ac23979..9ae8720 100644
--- a/cmd/endpointList.go
+++ b/cmd/endpointList.go
@@ -30,7 +30,7 @@ var endpointListCmd = &cobra.Command{
 		common.CheckError(err)
 
 		logrus.Debug("Getting endpoints")
-		endpoints, err := client.GetEndpoints()
+		endpoints, err := client.EndpointList()
 		common.CheckError(err)
 
 		switch viper.GetString("endpoint.list.format") {
@@ -71,9 +71,9 @@ var endpointListCmd = &cobra.Command{
 			common.CheckError(flushErr)
 		case "json":
 			// Print endpoints in a json format
-			statusJsonBytes, err := json.Marshal(endpoints)
+			statusJSONBytes, err := json.Marshal(endpoints)
 			common.CheckError(err)
-			fmt.Println(string(statusJsonBytes))
+			fmt.Println(string(statusJSONBytes))
 		default:
 			// Print endpoints in a custom format
 			template, templateParsingErr := template.New("endpointTpl").Parse(viper.GetString("endpoint.list.format"))
diff --git a/cmd/login.go b/cmd/login.go
index f0fad9b..4f157fb 100644
--- a/cmd/login.go
+++ b/cmd/login.go
@@ -3,6 +3,8 @@ package cmd
 import (
 	"fmt"
 
+	"github.com/greenled/portainer-stack-utils/client"
+
 	"github.com/greenled/portainer-stack-utils/common"
 	"github.com/sirupsen/logrus"
 	"github.com/spf13/cobra"
@@ -15,14 +17,17 @@ var loginCmd = &cobra.Command{
 	Short: "Log in to a Portainer instance",
 	Run: func(cmd *cobra.Command, args []string) {
 		// Get auth token
-		client, err := common.GetDefaultClient()
+		portainerClient, err := common.GetDefaultClient()
 		common.CheckError(err)
 
 		user := viper.GetString("user")
 		logrus.WithFields(logrus.Fields{
 			"user": user,
 		}).Debug("Getting auth token")
-		authToken, err := client.Authenticate()
+		authToken, err := portainerClient.AuthenticateUser(client.AuthenticateUserOptions{
+			Username: viper.GetString("user"),
+			Password: viper.GetString("password"),
+		})
 		common.CheckError(err)
 
 		if viper.GetBool("login.print") {
diff --git a/cmd/stackDeploy.go b/cmd/stackDeploy.go
index b715a87..dfdde9e 100644
--- a/cmd/stackDeploy.go
+++ b/cmd/stackDeploy.go
@@ -3,6 +3,8 @@ package cmd
 import (
 	"io/ioutil"
 
+	"github.com/greenled/portainer-stack-utils/client"
+
 	portainer "github.com/portainer/portainer/api"
 
 	"github.com/sirupsen/logrus"
@@ -53,7 +55,10 @@ var stackDeployCmd = &cobra.Command{
 			common.CheckError(endpointRetrievalErr)
 		}
 
-		endpointSwarmClusterId, selectionErr := common.GetEndpointSwarmClusterId(endpoint.ID)
+		logrus.WithFields(logrus.Fields{
+			"endpoint": endpoint.Name,
+		}).Debug("Getting endpoint's Docker info")
+		endpointSwarmClusterID, selectionErr := common.GetEndpointSwarmClusterID(endpoint.ID)
 		if selectionErr == nil {
 			// It's a swarm cluster
 		} else if selectionErr == common.ErrStackClusterNotFound {
@@ -67,7 +72,7 @@ var stackDeployCmd = &cobra.Command{
 			"stack":    stackName,
 			"endpoint": endpoint.Name,
 		}).Debug("Getting stack")
-		retrievedStack, stackRetrievalErr := common.GetStackByName(stackName, endpointSwarmClusterId, endpoint.ID)
+		retrievedStack, stackRetrievalErr := common.GetStackByName(stackName, endpointSwarmClusterID, endpoint.ID)
 		if stackRetrievalErr == nil {
 			// We are updating an existing stack
 			logrus.WithFields(logrus.Fields{
@@ -84,7 +89,7 @@ var stackDeployCmd = &cobra.Command{
 				logrus.WithFields(logrus.Fields{
 					"stack": retrievedStack.Name,
 				}).Debug("Getting stack file content")
-				stackFileContent, stackFileContentRetrievalErr = portainerClient.GetStackFileContent(retrievedStack.ID)
+				stackFileContent, stackFileContentRetrievalErr = portainerClient.StackFileInspect(retrievedStack.ID)
 				common.CheckError(stackFileContentRetrievalErr)
 			}
 
@@ -112,7 +117,13 @@ var stackDeployCmd = &cobra.Command{
 			logrus.WithFields(logrus.Fields{
 				"stack": retrievedStack.Name,
 			}).Info("Updating stack")
-			err := portainerClient.UpdateStack(retrievedStack, newEnvironmentVariables, stackFileContent, viper.GetBool("stack.deploy.prune"), endpoint.ID)
+			err := portainerClient.StackUpdate(client.StackUpdateOptions{
+				Stack:                retrievedStack,
+				EnvironmentVariables: newEnvironmentVariables,
+				StackFileContent:     stackFileContent,
+				Prune:                viper.GetBool("stack.deploy.prune"),
+				EndpointID:           endpoint.ID,
+			})
 			common.CheckError(err)
 		} else if stackRetrievalErr == common.ErrStackNotFound {
 			// We are deploying a new stack
@@ -126,13 +137,19 @@ var stackDeployCmd = &cobra.Command{
 			stackFileContent, loadingErr := loadStackFile(viper.GetString("stack.deploy.stack-file"))
 			common.CheckError(loadingErr)
 
-			if endpointSwarmClusterId != "" {
+			if endpointSwarmClusterID != "" {
 				// It's a swarm cluster
 				logrus.WithFields(logrus.Fields{
 					"stack":    stackName,
 					"endpoint": endpoint.Name,
 				}).Info("Creating stack")
-				stack, deploymentErr := portainerClient.CreateSwarmStack(stackName, loadedEnvironmentVariables, stackFileContent, endpointSwarmClusterId, endpoint.ID)
+				stack, deploymentErr := portainerClient.StackCreateSwarm(client.StackCreateSwarmOptions{
+					StackName:            stackName,
+					EnvironmentVariables: loadedEnvironmentVariables,
+					StackFileContent:     stackFileContent,
+					SwarmClusterID:       endpointSwarmClusterID,
+					EndpointID:           endpoint.ID,
+				})
 				common.CheckError(deploymentErr)
 				logrus.WithFields(logrus.Fields{
 					"stack":    stack.Name,
@@ -145,7 +162,12 @@ var stackDeployCmd = &cobra.Command{
 					"stack":    stackName,
 					"endpoint": endpoint.Name,
 				}).Info("Creating stack")
-				stack, deploymentErr := portainerClient.CreateComposeStack(stackName, loadedEnvironmentVariables, stackFileContent, endpoint.ID)
+				stack, deploymentErr := portainerClient.StackCreateCompose(client.StackCreateComposeOptions{
+					StackName:            stackName,
+					EnvironmentVariables: loadedEnvironmentVariables,
+					StackFileContent:     stackFileContent,
+					EndpointID:           endpoint.ID,
+				})
 				common.CheckError(deploymentErr)
 				logrus.WithFields(logrus.Fields{
 					"stack":    stack.Name,
diff --git a/cmd/stackInspect.go b/cmd/stackInspect.go
index 1e1c5e7..829ea01 100644
--- a/cmd/stackInspect.go
+++ b/cmd/stackInspect.go
@@ -24,7 +24,7 @@ var stackInspectCmd = &cobra.Command{
 	Args:    cobra.ExactArgs(1),
 	Run: func(cmd *cobra.Command, args []string) {
 		stackName := args[0]
-		var endpointSwarmClusterId string
+		var endpointSwarmClusterID string
 		var stack portainer.Stack
 
 		var endpoint portainer.Endpoint
@@ -47,15 +47,18 @@ var stackInspectCmd = &cobra.Command{
 			common.CheckError(endpointRetrievalErr)
 		}
 
+		logrus.WithFields(logrus.Fields{
+			"endpoint": endpoint.Name,
+		}).Debug("Getting endpoint's Docker info")
 		var selectionErr, stackRetrievalErr error
-		endpointSwarmClusterId, selectionErr = common.GetEndpointSwarmClusterId(endpoint.ID)
+		endpointSwarmClusterID, selectionErr = common.GetEndpointSwarmClusterID(endpoint.ID)
 		if selectionErr == nil {
 			// It's a swarm cluster
 			logrus.WithFields(logrus.Fields{
 				"stack":    stackName,
 				"endpoint": endpoint.Name,
 			}).Debug("Getting stack")
-			stack, stackRetrievalErr = common.GetStackByName(stackName, endpointSwarmClusterId, endpoint.ID)
+			stack, stackRetrievalErr = common.GetStackByName(stackName, endpointSwarmClusterID, endpoint.ID)
 		} else if selectionErr == common.ErrStackClusterNotFound {
 			// It's not a swarm cluster
 			logrus.WithFields(logrus.Fields{
@@ -84,7 +87,7 @@ var stackInspectCmd = &cobra.Command{
 					"%v\t%s\t%v\t%s",
 					stack.ID,
 					stack.Name,
-					client.GetTranslatedStackType(stack),
+					client.GetTranslatedStackType(stack.Type),
 					endpoint.Name,
 				))
 				common.CheckError(err)
@@ -92,9 +95,9 @@ var stackInspectCmd = &cobra.Command{
 				common.CheckError(flushErr)
 			case "json":
 				// Print stack in a json format
-				stackJsonBytes, err := json.Marshal(stack)
+				stackJSONBytes, err := json.Marshal(stack)
 				common.CheckError(err)
-				fmt.Println(string(stackJsonBytes))
+				fmt.Println(string(stackJSONBytes))
 			default:
 				// Print stack in a custom format
 				template, templateParsingErr := template.New("stackTpl").Parse(viper.GetString("stack.inspect.format"))
diff --git a/cmd/stackList.go b/cmd/stackList.go
index a663c5c..88cdfa8 100644
--- a/cmd/stackList.go
+++ b/cmd/stackList.go
@@ -33,31 +33,43 @@ var stackListCmd = &cobra.Command{
 		portainerClient, err := common.GetClient()
 		common.CheckError(err)
 
-		endpoints, endpointsRetrievalErr := portainerClient.GetEndpoints()
+		endpoints, endpointsRetrievalErr := portainerClient.EndpointList()
 		common.CheckError(endpointsRetrievalErr)
 
-		var endpointSwarmClusterId string
+		var endpointSwarmClusterID string
 		var stacks []portainer.Stack
 		if endpointName := viper.GetString("stack.list.endpoint"); endpointName != "" {
 			// Get endpoint by name
 			endpoint, endpointRetrievalErr := common.GetEndpointFromListByName(endpoints, endpointName)
 			common.CheckError(endpointRetrievalErr)
 
+			logrus.WithFields(logrus.Fields{
+				"endpoint": endpoint.Name,
+			}).Debug("Getting endpoint's Docker info")
 			var selectionErr error
-			endpointSwarmClusterId, selectionErr = common.GetEndpointSwarmClusterId(endpoint.ID)
+			endpointSwarmClusterID, selectionErr = common.GetEndpointSwarmClusterID(endpoint.ID)
 			if selectionErr == nil {
 				// It's a swarm cluster
 				logrus.WithFields(logrus.Fields{
 					"endpoint": endpoint.Name,
 				}).Debug("Getting stacks")
-				stacks, err = portainerClient.GetStacks(endpointSwarmClusterId, endpoint.ID)
+				stacks, err = portainerClient.StackList(client.StackListOptions{
+					Filter: client.StackListFilter{
+						SwarmID:    endpointSwarmClusterID,
+						EndpointID: endpoint.ID,
+					},
+				})
 				common.CheckError(err)
 			} else if selectionErr == common.ErrStackClusterNotFound {
 				// It's not a swarm cluster
 				logrus.WithFields(logrus.Fields{
 					"endpoint": endpoint.Name,
 				}).Debug("Getting stacks")
-				stacks, err = portainerClient.GetStacks("", endpoint.ID)
+				stacks, err = portainerClient.StackList(client.StackListOptions{
+					Filter: client.StackListFilter{
+						EndpointID: endpoint.ID,
+					},
+				})
 				common.CheckError(err)
 			} else {
 				// Something else happened
@@ -65,7 +77,7 @@ var stackListCmd = &cobra.Command{
 			}
 		} else {
 			logrus.Debug("Getting stacks")
-			stacks, err = portainerClient.GetStacks("", 0)
+			stacks, err = portainerClient.StackList(client.StackListOptions{})
 			common.CheckError(err)
 		}
 
@@ -80,13 +92,13 @@ var stackListCmd = &cobra.Command{
 			})
 			common.CheckError(err)
 			for _, s := range stacks {
-				stackEndpoint, err := common.GetEndpointFromListById(endpoints, s.EndpointID)
+				stackEndpoint, err := common.GetEndpointFromListByID(endpoints, s.EndpointID)
 				common.CheckError(err)
 				_, err = fmt.Fprintln(writer, fmt.Sprintf(
 					"%v\t%s\t%v\t%s",
 					s.ID,
 					s.Name,
-					client.GetTranslatedStackType(s),
+					client.GetTranslatedStackType(s.Type),
 					stackEndpoint.Name,
 				))
 				common.CheckError(err)
@@ -95,9 +107,9 @@ var stackListCmd = &cobra.Command{
 			common.CheckError(flushErr)
 		case "json":
 			// Print stacks in a json format
-			stacksJsonBytes, err := json.Marshal(stacks)
+			stacksJSONBytes, err := json.Marshal(stacks)
 			common.CheckError(err)
-			fmt.Println(string(stacksJsonBytes))
+			fmt.Println(string(stacksJSONBytes))
 		default:
 			// Print stacks in a custom format
 			template, templateParsingErr := template.New("stackTpl").Parse(viper.GetString("stack.list.format"))
diff --git a/cmd/stackRemove.go b/cmd/stackRemove.go
index 0eeee59..72d9f63 100644
--- a/cmd/stackRemove.go
+++ b/cmd/stackRemove.go
@@ -22,7 +22,7 @@ var stackRemoveCmd = &cobra.Command{
 		common.CheckError(clientRetrievalErr)
 
 		stackName := args[0]
-		var endpointSwarmClusterId string
+		var endpointSwarmClusterID string
 		var stack portainer.Stack
 
 		var endpoint portainer.Endpoint
@@ -45,15 +45,18 @@ var stackRemoveCmd = &cobra.Command{
 			common.CheckError(endpointRetrievalErr)
 		}
 
+		logrus.WithFields(logrus.Fields{
+			"endpoint": endpoint.Name,
+		}).Debug("Getting endpoint's Docker info")
 		var selectionErr, stackRetrievalErr error
-		endpointSwarmClusterId, selectionErr = common.GetEndpointSwarmClusterId(endpoint.ID)
+		endpointSwarmClusterID, selectionErr = common.GetEndpointSwarmClusterID(endpoint.ID)
 		if selectionErr == nil {
 			// It's a swarm cluster
 			logrus.WithFields(logrus.Fields{
 				"stack":    stackName,
 				"endpoint": endpoint.Name,
 			}).Debug("Getting stack")
-			stack, stackRetrievalErr = common.GetStackByName(stackName, endpointSwarmClusterId, endpoint.ID)
+			stack, stackRetrievalErr = common.GetStackByName(stackName, endpointSwarmClusterID, endpoint.ID)
 		} else if selectionErr == common.ErrStackClusterNotFound {
 			// It's not a swarm cluster
 			logrus.WithFields(logrus.Fields{
@@ -68,13 +71,13 @@ var stackRemoveCmd = &cobra.Command{
 
 		if stackRetrievalErr == nil {
 			// The stack exists
-			stackId := stack.ID
+			stackID := stack.ID
 
 			logrus.WithFields(logrus.Fields{
 				"stack":    stackName,
 				"endpoint": endpoint.Name,
 			}).Info("Removing stack")
-			err := portainerClient.DeleteStack(stackId)
+			err := portainerClient.StackDelete(stackID)
 			common.CheckError(err)
 			logrus.WithFields(logrus.Fields{
 				"stack":    stack.Name,
diff --git a/cmd/status.go b/cmd/status.go
index 3534a6c..abe67dc 100644
--- a/cmd/status.go
+++ b/cmd/status.go
@@ -26,7 +26,7 @@ var statusCmd = &cobra.Command{
 		client, err := common.GetClient()
 		common.CheckError(err)
 
-		respBody, err := client.GetStatus()
+		respBody, err := client.Status()
 		common.CheckError(err)
 
 		switch viper.GetString("status.format") {
@@ -53,9 +53,9 @@ var statusCmd = &cobra.Command{
 			common.CheckError(flushErr)
 		case "json":
 			// Print status in a json format
-			statusJsonBytes, err := json.Marshal(respBody)
+			statusJSONBytes, err := json.Marshal(respBody)
 			common.CheckError(err)
-			fmt.Println(string(statusJsonBytes))
+			fmt.Println(string(statusJSONBytes))
 		default:
 			// Print status in a custom format
 			template, templateParsingErr := template.New("statusTpl").Parse(viper.GetString("status.format"))
diff --git a/common/client.go b/common/client.go
index 742d3f9..57baef3 100644
--- a/common/client.go
+++ b/common/client.go
@@ -18,7 +18,7 @@ import (
 
 var cachedClient client.PortainerClient
 
-// Get the cached client or a new one
+// GetClient returns the cached Portainer API client. If none is present, creates and returns a new one).
 func GetClient() (c client.PortainerClient, err error) {
 	if cachedClient == nil {
 		cachedClient, err = GetDefaultClient()
@@ -29,14 +29,14 @@ func GetClient() (c client.PortainerClient, err error) {
 	return cachedClient, nil
 }
 
-// Get the default client
+// GetDefaultClient returns a new Portainer API client with the default configuration
 func GetDefaultClient() (c client.PortainerClient, err error) {
 	config, err := GetDefaultClientConfig()
 	if err != nil {
 		return
 	}
 
-	c = client.NewClient(GetDefaultHttpClient(), config)
+	c = client.NewClient(GetDefaultHTTPClient(), config)
 
 	c.BeforeRequest(func(req *http.Request) (err error) {
 		var bodyString string
@@ -82,15 +82,15 @@ func GetDefaultClient() (c client.PortainerClient, err error) {
 	return
 }
 
-// Get the default config for a client
+// GetDefaultClientConfig returns the default configuration for a Portainer API client
 func GetDefaultClientConfig() (config client.Config, err error) {
-	apiUrl, err := url.Parse(strings.TrimRight(viper.GetString("url"), "/") + "/api/")
+	apiURL, err := url.Parse(strings.TrimRight(viper.GetString("url"), "/") + "/api/")
 	if err != nil {
 		return
 	}
 
 	config = client.Config{
-		Url:           apiUrl,
+		URL:           apiURL,
 		User:          viper.GetString("user"),
 		Password:      viper.GetString("password"),
 		Token:         viper.GetString("auth-token"),
@@ -101,8 +101,8 @@ func GetDefaultClientConfig() (config client.Config, err error) {
 	return
 }
 
-// Get the default http client for a Portainer client
-func GetDefaultHttpClient() *http.Client {
+// GetDefaultHTTPClient returns the default HTTP client for a Portainer API client
+func GetDefaultHTTPClient() *http.Client {
 	return &http.Client{
 		Timeout: viper.GetDuration("timeout"),
 		Transport: &http.Transport{
diff --git a/common/configs.go b/common/configs.go
index 59a2348..0c18842 100644
--- a/common/configs.go
+++ b/common/configs.go
@@ -8,6 +8,7 @@ import (
 	"github.com/spf13/viper"
 )
 
+// LoadCofig loads the configuration file currently used by viper into a new viper instance
 func LoadCofig() (v *viper.Viper, err error) {
 	// Set config file name
 	var configFile string
@@ -34,6 +35,7 @@ func LoadCofig() (v *viper.Viper, err error) {
 	return
 }
 
+// CheckConfigKeyExists checks a given configuration key exists in the default viper
 func CheckConfigKeyExists(key string) (keyExists bool) {
 	for _, k := range viper.AllKeys() {
 		if k == key {
diff --git a/common/printing.go b/common/printing.go
index 218d210..cfe163b 100644
--- a/common/printing.go
+++ b/common/printing.go
@@ -7,6 +7,7 @@ import (
 	"text/tabwriter"
 )
 
+// NewTabWriter returns a new tabwriter.Writer
 func NewTabWriter(headers []string) (*tabwriter.Writer, error) {
 	writer := tabwriter.NewWriter(os.Stdout, 20, 2, 3, ' ', 0)
 	_, err := fmt.Fprintln(writer, strings.Join(headers, "\t"))
diff --git a/common/utils.go b/common/utils.go
index b8dbd5a..1b34702 100644
--- a/common/utils.go
+++ b/common/utils.go
@@ -4,10 +4,13 @@ import (
 	"fmt"
 	"reflect"
 
+	"github.com/greenled/portainer-stack-utils/client"
+
 	portainer "github.com/portainer/portainer/api"
 	"github.com/sirupsen/logrus"
 )
 
+// Common errors
 const (
 	ErrStackNotFound             = Error("Stack not found")
 	ErrStackClusterNotFound      = Error("Stack cluster not found")
@@ -29,6 +32,7 @@ func (e Error) Error() string {
 	return string(e)
 }
 
+// GetDefaultEndpoint returns the default endpoint (if only one endpoint exists)
 func GetDefaultEndpoint() (endpoint portainer.Endpoint, err error) {
 	portainerClient, err := GetClient()
 	if err != nil {
@@ -36,7 +40,7 @@ func GetDefaultEndpoint() (endpoint portainer.Endpoint, err error) {
 	}
 
 	logrus.Debug("Getting endpoints")
-	endpoints, err := portainerClient.GetEndpoints()
+	endpoints, err := portainerClient.EndpointList()
 	if err != nil {
 		return
 	}
@@ -53,13 +57,20 @@ func GetDefaultEndpoint() (endpoint portainer.Endpoint, err error) {
 	return
 }
 
-func GetStackByName(name string, swarmId string, endpointId portainer.EndpointID) (stack portainer.Stack, err error) {
+// GetStackByName returns a stack by its name from the (endpoint filtered) list
+// of all stacks
+func GetStackByName(name string, swarmID string, endpointID portainer.EndpointID) (stack portainer.Stack, err error) {
 	portainerClient, err := GetClient()
 	if err != nil {
 		return
 	}
 
-	stacks, err := portainerClient.GetStacks(swarmId, endpointId)
+	stacks, err := portainerClient.StackList(client.StackListOptions{
+		Filter: client.StackListFilter{
+			SwarmID:    swarmID,
+			EndpointID: endpointID,
+		},
+	})
 	if err != nil {
 		return
 	}
@@ -73,13 +84,15 @@ func GetStackByName(name string, swarmId string, endpointId portainer.EndpointID
 	return
 }
 
+// GetEndpointByName returns an endpoint by its name from the list of all
+// endpoints
 func GetEndpointByName(name string) (endpoint portainer.Endpoint, err error) {
 	portainerClient, err := GetClient()
 	if err != nil {
 		return
 	}
 
-	endpoints, err := portainerClient.GetEndpoints()
+	endpoints, err := portainerClient.EndpointList()
 	if err != nil {
 		return
 	}
@@ -93,13 +106,15 @@ func GetEndpointByName(name string) (endpoint portainer.Endpoint, err error) {
 	return
 }
 
+// GetEndpointGroupByName returns an endpoint group by its name from the list
+// of all endpoint groups
 func GetEndpointGroupByName(name string) (endpointGroup portainer.EndpointGroup, err error) {
 	portainerClient, err := GetClient()
 	if err != nil {
 		return
 	}
 
-	endpointGroups, err := portainerClient.GetEndpointGroups()
+	endpointGroups, err := portainerClient.EndpointGroupList()
 	if err != nil {
 		return
 	}
@@ -113,7 +128,9 @@ func GetEndpointGroupByName(name string) (endpointGroup portainer.EndpointGroup,
 	return
 }
 
-func GetEndpointFromListById(endpoints []portainer.Endpoint, id portainer.EndpointID) (endpoint portainer.Endpoint, err error) {
+// GetEndpointFromListByID returns an endpoint by its id from a list of
+// endpoints
+func GetEndpointFromListByID(endpoints []portainer.Endpoint, id portainer.EndpointID) (endpoint portainer.Endpoint, err error) {
 	for i := range endpoints {
 		if endpoints[i].ID == id {
 			return endpoints[i], err
@@ -122,6 +139,8 @@ func GetEndpointFromListById(endpoints []portainer.Endpoint, id portainer.Endpoi
 	return endpoint, ErrEndpointNotFound
 }
 
+// GetEndpointFromListByName returns an endpoint by its name from a list of
+// endpoints
 func GetEndpointFromListByName(endpoints []portainer.Endpoint, name string) (endpoint portainer.Endpoint, err error) {
 	for i := range endpoints {
 		if endpoints[i].Name == name {
@@ -131,17 +150,15 @@ func GetEndpointFromListByName(endpoints []portainer.Endpoint, name string) (end
 	return endpoint, ErrEndpointNotFound
 }
 
-func GetEndpointSwarmClusterId(endpointId portainer.EndpointID) (endpointSwarmClusterId string, err error) {
+// GetEndpointSwarmClusterID returns an endpoint's swarm cluster id
+func GetEndpointSwarmClusterID(endpointID portainer.EndpointID) (endpointSwarmClusterID string, err error) {
 	// Get docker information for endpoint
 	portainerClient, err := GetClient()
 	if err != nil {
 		return
 	}
 
-	logrus.WithFields(logrus.Fields{
-		"endpoint": endpointId,
-	}).Debug("Getting endpoint's Docker info")
-	result, err := portainerClient.GetEndpointDockerInfo(endpointId)
+	result, err := portainerClient.EndpointDockerInfo(endpointID)
 	if err != nil {
 		return
 	}
@@ -149,7 +166,7 @@ func GetEndpointSwarmClusterId(endpointId portainer.EndpointID) (endpointSwarmCl
 	// Get swarm (if any) information for endpoint
 	id, selectionErr := selectValue(result, []string{"Swarm", "Cluster", "ID"})
 	if selectionErr == nil {
-		endpointSwarmClusterId = id.(string)
+		endpointSwarmClusterID = id.(string)
 	} else if selectionErr == valueNotFoundError {
 		err = ErrStackClusterNotFound
 	} else {
@@ -170,6 +187,7 @@ func selectValue(jsonMap map[string]interface{}, jsonPath []string) (interface{}
 	}
 }
 
+// GetFormatHelp returns the help string for --format flags
 func GetFormatHelp(v interface{}) (r string) {
 	typeOfV := reflect.TypeOf(v)
 	r = fmt.Sprintf(`
diff --git a/version/version.go b/version/version.go
index 734f199..3308067 100644
--- a/version/version.go
+++ b/version/version.go
@@ -19,6 +19,7 @@ var (
 	buildDate string
 )
 
+// BuildVersionString returns the tool's version
 func BuildVersionString() string {
 	osArch := runtime.GOOS + "/" + runtime.GOARCH
 
@@ -33,6 +34,8 @@ func BuildVersionString() string {
 	return fmt.Sprintf("%s %s %s BuildDate: %s", programName, version, osArch, buildDate)
 }
 
+// BuildUseAgentString returns the tool's User-Agent in requests to the
+// Portainer API
 func BuildUseAgentString() string {
 	var theVersion = version
 	if theVersion == "" {