Go SDK
Send invoices to Complyance from a Go application, from a first sample payload to a submitted invoice in sandbox. You need Go 1.18 or later and an API key for your workspace.
Install
go get github.com/complyance-io/complyance-go-sdk/v3@v3.0.5-betav3.0.5-beta is the latest version, and it is a beta release. The latest stable release, v3.0.3, does not have SubmitJSONSingle or GetDocumentStatus, which this page uses.
Some older guides use the module path github.com/complyance/go-sdk. That module does not exist. Use github.com/complyance-io/complyance-go-sdk/v3, with /v3 at the end.
The code lives in the module’s pkg package. The examples on this page import it as sdk:
import sdk "github.com/complyance-io/complyance-go-sdk/v3/pkg"Configure the SDK
Configure the SDK once, when your application starts. Every call after that uses the same key.
package main
import (
"log"
"os"
sdk "github.com/complyance-io/complyance-go-sdk/v3/pkg"
)
const (
apiHost = "https://prod.gets.complyance.io"
sourceName = "acme-erp"
sourceVersion = "1.0.0"
)
func main() {
cfg := sdk.NewSDKConfig(
os.Getenv("COMPLYANCE_API_KEY"),
sdk.EnvironmentSandbox,
nil, // sources
nil, // retry settings: nil uses the defaults
)
if err := sdk.Configure(cfg); err != nil {
log.Fatal(err)
}
// The examples below go here.
}- Read the key from the
COMPLYANCE_API_KEYenvironment variable. Never write it in your code. sdk.EnvironmentSandboxis for a sandbox key. Usesdk.EnvironmentProductionwith a production key when you go live.apiHostis the API’s address, for sandbox and production alike. Theenvironmentin each request, and the key, decide which one you reach.sourceNameandsourceVersionname the source your invoices come from, as set up in your workspace. Every request sends them.
This version of the SDK keeps its configuration for the whole process, so its functions take no client value and no context.Context.
How this page sends requests
The SDK’s PushToUnifyV2 function sends the earlier Unify request format, and in this version its response carries only a status and a message, not the document ID or the validation result. So this page sends invoices with sdk.SubmitJSONSingle instead. It sends a request body you build, in the current format described in Submit and retrieve, with the new-api: true header, and returns the whole response as a map[string]interface{}.
SubmitJSONSingle sends each request once. It does not retry, and it does not check the country: the API accepts the countries listed in Countries. The SDK’s own functions, such as PushToUnifyV2, accept only Saudi Arabia (sdk.CountrySA), Malaysia (sdk.CountryMY) and the UAE (sdk.CountryAE) in sandbox and production, and have no country value for Oman.
Send a sample payload for mapping
Before you can submit invoices, Complyance needs to know how your fields correspond to GETS, the Complyance standard invoice format. Send one invoice exactly as your system produces it, with the purpose mapping. It is not validated or sent to a tax authority. The purposes are described in Submit and retrieve.
Add "encoding/json" to the imports in main.go, then:
// Your own invoice data, in your system's format
payload := map[string]interface{}{
"invoice_no": "INV-2026-0142",
"invoice_date": "2026-09-18",
"currency": "AED",
"seller_name": "Acme Trading LLC",
"customer_name": "Gulf Office Supplies LLC",
"lines": []map[string]interface{}{
{"description": "Office chairs", "qty": 4, "unit_price": 250.00, "vat_rate": 5},
},
"total": 1050.00,
}
request := map[string]interface{}{
"country": "AE",
"environment": "sandbox",
"purpose": "mapping",
"source": sourceName + ":" + sourceVersion,
"documentType": map[string]interface{}{
"base": "tax_invoice",
"modifiers": []string{},
},
"payload": payload,
}
body, err := json.Marshal(request)
if err != nil {
log.Fatal(err)
}
result, err := sdk.SubmitJSONSingle(body, apiHost, true)
if err != nil {
log.Fatal(err)
}
log.Println(result["message"])You should see:
Payload saved for mapping.Next, open the Integration engine in the Developer portal. In Create an integration, pick the payload you sent at Payload Selection, then map your fields to GETS and test your mapping.
Submit an invoice
Once the integration is tested and linked to your source, send invoices from the same source with the purpose invoicing. Send the payload in the same format you used for mapping; Complyance converts it with your integration.
request["purpose"] = "invoicing"
body, err = json.Marshal(request)
if err != nil {
log.Fatal(err)
}
result, err = sdk.SubmitJSONSingle(body, apiHost, true)
if err != nil {
// See "Handle errors" below.
log.Fatal(err)
}
if problems, _ := result["errors"].([]interface{}); len(problems) > 0 {
// The invoice failed validation
for _, item := range problems {
problem, _ := item.(map[string]interface{})
log.Printf("%v at %v: %v", problem["code"], problem["payloadPath"], problem["message"])
}
} else {
log.Println("Document ID:", result["documentId"])
}documentType is the GETS document type: a base, such as tax_invoice or credit_note, and the modifiers that apply. For a UAE export invoice, use "modifiers": []string{"export"}. Which bases and modifiers a country accepts is in Base and modifiers, and which fields each one requires is in the field reference.
A validated invoice returns its documentId. Keep it to check the status later.
{
"documentId": "01K5EXAMPLE00000000000142",
"documentNumber": "INV-2026-0142",
"message": "Your invoice was validated and is ready for submission.",
"Base64XML": "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4…"
}An invoice that fails validation comes back as HTTP 422. SubmitJSONSingle returns it as a result, not as an error, with errors listing every problem. Each one has a code, the GETS field in getsPath, the field in your payload in payloadPath, and a message. Fix every one, then send the invoice again.
Run your invoices in sandbox first, and check them against the test cases in a testbed before you switch to production. For production, use a production key, sdk.EnvironmentProduction and "environment": "production".
Check a document’s status
Delivery to the tax authority or the Peppol network continues after the response. Ask for a document’s current status with its documentId:
status, err := sdk.GetDocumentStatus("01K5EXAMPLE00000000000142")
if err != nil {
log.Fatal(err)
}
data, _ := status["data"].(map[string]interface{})
log.Println(data["state"], data["isTerminal"])Check again until isTerminal is true. Every state and field is described in the documents reference. Checking the status is how you learn whether the invoice was accepted.
Retrieve purchase invoices
Purchase invoices are the invoices your suppliers send you. Get one by its documentId:
invoice, err := sdk.GetPurchaseInvoice("01JEXAMPLE0000000000000142")
if err != nil {
log.Fatal(err)
}
invoiceData, _ := invoice["data"].(map[string]interface{})
log.Println(invoiceData["documentNumber"], invoiceData["state"])To list the invoices received in a date range, pass the query parameters as a map. When hasMore is true, send the same request again with cursor set to nextCursor:
filters := map[string]string{
"from": "2026-09-01",
"to": "2026-09-30",
"limit": "100",
}
for {
page, err := sdk.ListPurchaseInvoices(filters)
if err != nil {
log.Fatal(err)
}
pageData, _ := page["data"].(map[string]interface{})
items, _ := pageData["items"].([]interface{})
for _, item := range items {
inv, _ := item.(map[string]interface{})
log.Println(inv["documentId"], inv["invoiceNumber"], inv["supplierName"])
}
hasMore, _ := pageData["hasMore"].(bool)
next, _ := pageData["nextCursor"].(string)
if !hasMore || next == "" {
break
}
filters["cursor"] = next
}Both functions return the response body as a map[string]interface{}. Every field is described in Get a purchase invoice and List purchase invoices.
Handle errors
When the API refuses a request with any status other than 422, SubmitJSONSingle returns an *sdk.SDKError. Its ErrorDetail holds the HTTP status as httpStatus and the response body as responseBody. Any other error means the request could not be sent. This function, in the same package as main.go, sends one invoice and reports each case:
package main
import (
"errors"
"fmt"
"log"
sdk "github.com/complyance-io/complyance-go-sdk/v3/pkg"
)
// submitInvoice sends one request body and returns the document ID.
func submitInvoice(body []byte) (string, error) {
result, err := sdk.SubmitJSONSingle(body, apiHost, true)
var sdkErr *sdk.SDKError
if errors.As(err, &sdkErr) && sdkErr.ErrorDetail != nil {
detail := sdkErr.ErrorDetail
return "", fmt.Errorf("refused with HTTP %v: %v",
detail.GetContextValue("httpStatus"), detail.GetContextValue("responseBody"))
}
if err != nil {
return "", fmt.Errorf("not sent: %w", err)
}
if problems, _ := result["errors"].([]interface{}); len(problems) > 0 {
for _, item := range problems {
problem, _ := item.(map[string]interface{})
log.Printf("%v at %v: %v", problem["code"], problem["payloadPath"], problem["message"])
}
return "", errors.New("the invoice failed validation")
}
documentID, _ := result["documentId"].(string)
return documentID, nil
}| Status | What it means | What to do |
|---|---|---|
401 | The API key is missing, invalid, revoked or expired. | Check that COMPLYANCE_API_KEY is set where your application runs. |
403 | The key belongs to a different environment from environment in the request. | Use a sandbox key with "environment": "sandbox" and a production key with "environment": "production". |
404 | The source is not in your workspace. | Check sourceName and sourceVersion. |
429 | Too many requests. | Wait, then send the request again. SubmitJSONSingle does not retry for you. |
What each error from the API means is in Errors.