A Go client library to consume Zuora API.
This is a WIP and has minimal endpoints covered but it is really easy to add new ones.
- Go >1.7
- Zuora client ID (Use Environment variables as best practice)
- Zuora client secret (Use Environment variables as best practice)
- Zuora api url (Use Environment variables as best practice)
package main
import (
"context"
"fmt"
"log"
"net/http"
"os"
"github.com/hyeomans/zuora"
"github.com/joho/godotenv"
)
func main() {
err := godotenv.Load()
if err != nil {
log.Fatal("Error loading .env file")
}
zuoraClientID := os.Getenv("ZUORA_CLIENT_ID")
zuoraClientSecret := os.Getenv("ZUORA_CLIENT_SECRET")
zuoraURL := os.Getenv("ZUORA_URL")
zuoraAPI := zuora.NewAPI(&http.Client{}, zuoraURL, zuoraClientID, zuoraClientSecret)
ctx := context.Background()
object, err := zuoraAPI.AccountsService.Summary(ctx, "customerAccount")
if err != nil {
log.Fatal(err)
} else {
fmt.Printf("%+v\n", object)
}
}
package main
import (
"context"
"fmt"
"log"
"net"
"net/http"
"os"
"time"
"github.com/hyeomans/zuora"
"github.com/joho/godotenv"
)
func main() {
err := godotenv.Load()
if err != nil {
log.Fatal("Error loading .env file")
}
zuoraClientID := os.Getenv("ZUORA_CLIENT_ID")
zuoraClientSecret := os.Getenv("ZUORA_CLIENT_SECRET")
zuoraURL := os.Getenv("ZUORA_URL")
httpClient := newHTTPClient()
zuoraAPI := zuora.NewAPI(httpClient, zuoraURL, zuoraClientID, zuoraClientSecret)
ctx := context.Background()
object, err := zuoraAPI.AccountsService.Summary(ctx, "customerAccount")
if err != nil {
log.Fatal(err)
} else {
fmt.Printf("%+v\n", object)
}
}
func newHTTPClient() *http.Client {
keepAliveTimeout := 600 * time.Second
timeout := 2 * time.Second
defaultTransport := &http.Transport{
Dial: (&net.Dialer{
KeepAlive: keepAliveTimeout,
}).Dial,
MaxIdleConns: 100,
MaxIdleConnsPerHost: 100,
}
return &http.Client{
Transport: defaultTransport,
Timeout: timeout,
}
}
By default this package uses an in-memory backing store, but you can bring your own backing store, you only need to fullfill the interface:
//TokenStorer handles token renewal with two simple methods.
//Token() returns a boolean to indicate a token is valid and if valid, it will return the active token.
//Update() causes a side-effect to update a token in whichever backing store you choose.
type TokenStorer interface {
Token() (bool, *Token)
Update(*Token)
}
Zuora API is not consistent with their error responses, this package tries to unify all error responses in a single one. One of the most important error responses from Zuora is Request exceeded limit and this package follows "Errors as behaviour" to identify when this happens.
package main
import (
"context"
"fmt"
"log"
"net/http"
"os"
"github.com/hyeomans/zuora"
"github.com/joho/godotenv"
)
func main() {
err := godotenv.Load()
if err != nil {
log.Fatal("Error loading .env file")
}
zuoraClientID := os.Getenv("ZUORA_CLIENT_ID")
zuoraClientSecret := os.Getenv("ZUORA_CLIENT_SECRET")
zuoraURL := os.Getenv("ZUORA_URL")
zuoraAPI := zuora.NewAPI(&http.Client{}, zuoraURL, zuoraClientID, zuoraClientSecret)
ctx := context.Background()
object, err := zuoraAPI.AccountsService.Summary(ctx, "customerAccount")
if err != nil {
temporary, ok := result.(err)
if ok && temporary.Temporary() {
fmt.Println("You could continue making requests after cool off time")
} else if ok && temporary.Temporary() {
log.Fatal("This is not a temporary error, modify your request")
} else {
log.Fatalf("an error ocurred %v", err)
}
} else {
fmt.Printf("%+v\n", object)
}
}
Errors that are temporary according to this package are:
http.StatusTooManyRequests
http.StatusLocked
http.StatusInternalServerError
More about error as behaviour: https://dave.cheney.net/2014/12/24/inspecting-errors https://www.ardanlabs.com/blog/2014/10/error-handling-in-go-part-i.html
There could be a possibilty to that a response has multiple error messages encoded as described by Zuora documentation:
If the JSON success field is false, a JSON "reasons" array is included in the response body with at least one set of code and message attributes that can be used to code a response.
Example:
{
"success": false,
"processId": "3F7EA3FD706C7E7C",
"reasons": [
{
"code": 53100020,
"message": " {com.zuora.constraints.either_or_both}"
},
{
"code": 53100320,
"message": "'termType' value should be one of: TERMED, EVERGREEN"
}
]
}
The problem this presents is that, if you have a Request exceeded limit inside here, you might take different approaches to handle it. This package resolves this issue be setting a priority on errors, here is the list from highest (top) to lowest priority (bottom):
http.StatusTooManyRequests
http.StatusUnauthorized
http.StatusForbidden
http.StatusNotFound
http.StatusLocked
http.StatusInternalServerError
http.StatusBadRequest //<-- If not in list, is considered a BadRequest
Zuora allows to query tables by using a query language they call ZOQL, this package contains a helper struct/function to make easier to query whatever table you want.
Here is an example where:
- We wrap the API client
- Create a custom struct to Unmarshal the raw response from our Query endpoint:
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"github.com/hyeomans/zuora"
"github.com/joho/godotenv"
)
func main() {
err := godotenv.Load()
if err != nil {
log.Fatal("Error loading .env file")
}
zuoraClientID := os.Getenv("ZUORA_CLIENT_ID")
zuoraClientSecret := os.Getenv("ZUORA_CLIENT_SECRET")
zuoraURL := os.Getenv("ZUORA_URL")
zuoraAPI := zuora.NewAPI(&http.Client{}, zuoraURL, zuoraClientID, zuoraClientSecret)
ctx := context.Background()
myWrapper := myZuoraClient{zuoraAPI: zuoraAPI}
products, err := myWrapper.GetProducts(ctx)
if err != nil {
log.Fatal(err)
} else {
fmt.Printf("%+v\n", products)
}
}
type myZuoraClient struct {
zuoraAPI *zuora.API
}
type products struct {
Records []zuora.Product `json:"records"`
}
//GetProducts Returns all products
func (m *myZuoraClient) GetProducts(ctx context.Context) ([]zuora.Product, error) {
fields := []string{"ID", "Name"}
zoqlComposer := zuora.NewZoqlComposer("Product", fields)
rawProducts, err := m.zuoraAPI.ActionsService.Query(ctx, zoqlComposer)
if err != nil {
return nil, err
}
jsonResponse := products{}
if err := json.Unmarshal(rawProducts, &jsonResponse); err != nil {
return nil, err
}
return jsonResponse.Records, nil
}
ZoqlComposer
uses functional options that allow you to compose a query that require:
- Single filter
- OR Filter
- AND filter
- Combination of those 3
Here is a minimal example:
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"github.com/hyeomans/zuora"
"github.com/joho/godotenv"
)
func main() {
err := godotenv.Load()
if err != nil {
log.Fatal("Error loading .env file")
}
zuoraClientID := os.Getenv("ZUORA_CLIENT_ID")
zuoraClientSecret := os.Getenv("ZUORA_CLIENT_SECRET")
zuoraURL := os.Getenv("ZUORA_URL")
zuoraAPI := zuora.NewAPI(&http.Client{}, zuoraURL, zuoraClientID, zuoraClientSecret)
ctx := context.Background()
myWrapper := myZuoraClient{zuoraAPI: zuoraAPI}
products, err := myWrapper.GetProductById(ctx, "an-id")
if err != nil {
log.Fatal(err)
} else {
fmt.Printf("%+v\n", products)
}
}
type myZuoraClient struct {
zuoraAPI *zuora.API
}
type products struct {
Records []zuora.Product `json:"records"`
}
//GetProductById Will return a product by ID with many filters.
func (m *myZuoraClient) GetProductById(ctx context.Context, id string) ([]zuora.Product, error) {
fields := []string{"ID", "Name"}
filter := zuora.QueryFilter{Key: "ID", Value: id}
singleFilter := zuora.QueryWithFilter(filter)
andFilter := zuora.QueryWithAndFilter([]zuora.QueryFilter{filter, filter})
orFilter := zuora.QueryWithOrFilter([]zuora.QueryFilter{filter, filter})
zoqlComposer := zuora.NewZoqlComposer("Product", fields, singleFilter, andFilter, orFilter)
fmt.Println(zoqlComposer) //You can print to see returning query.
//{ "queryString" : "select ID, Name from Product where ID = 'an-id' and ID = 'an-id' and ID = 'an-id' or ID = 'an-id' or ID = 'an-id'" }
rawProducts, err := m.zuoraAPI.ActionsService.Query(ctx, zoqlComposer)
if err != nil {
return nil, err
}
jsonResponse := products{}
if err := json.Unmarshal(rawProducts, &jsonResponse); err != nil {
return nil, err
}
return jsonResponse.Records, nil
}
Zuora reference | How to call it | Link |
---|---|---|
Get account summary | AccountsService.Summary(...) | https://www.zuora.com/developer/api-reference/#operation/GET_AccountSummary |
Zuora reference | How to call it | Link |
---|---|---|
Query | ActionsService.Query(...) | https://www.zuora.com/developer/api-reference/#operation/Action_POSTquery |
Zuora reference | How to call it | Link |
---|---|---|
Get billing documents | BillingDocumentsService.Get(...) | https://www.zuora.com/developer/api-reference/#operation/GET_BillingDocuments |
Zuora reference | How to call it | Link |
---|---|---|
Describe object | DescribeService.Model(...) | https://www.zuora.com/developer/api-reference/#tag/Describe |
Zuora reference | How to call it | Link |
---|---|---|
CRUD: Get payment | PaymentsService.ByIdThroughObject | https://www.zuora.com/developer/api-reference/#operation/Object_GETPayment |
Zuora reference | How to call it | Link |
---|---|---|
CRUD: Retrieve Product | ProductsService.Get(...) | https://www.zuora.com/developer/api-reference/#operation/Object_GETProduct |
Zuora reference | How to call it | Link |
---|---|---|
Get subscriptions by account | SubscriptionsService.ByKey(...) | https://www.zuora.com/developer/api-reference/#operation/GET_SubscriptionsByKey |
CRUD: Retrieve Subscription | SubscriptionsService.ByAccount(...) | https://www.zuora.com/developer/api-reference/#operation/Object_GETSubscription |
CRUD: Update Subscription | SubscriptionsService.Update(...) | https://www.zuora.com/developer/api-reference/#operation/Object_PUTSubscription |
CRUD: Update Subscription | SubscriptionsService.UpdateFull(...) | https://www.zuora.com/developer/api-reference/#operation/Object_PUTSubscription |
Cancel subscription | SubscriptionsService.Cancel(...) | https://www.zuora.com/developer/api-reference/#operation/PUT_CancelSubscription |