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

Install the SDKbash
go get github.com/complyance-io/complyance-go-sdk/v3@v3.0.5-beta

v3.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 the SDKgo
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.

main.gogo
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_KEY environment variable. Never write it in your code.
  • sdk.EnvironmentSandbox is for a sandbox key. Use sdk.EnvironmentProduction with a production key when you go live.
  • apiHost is the API’s address, for sandbox and production alike. The environment in each request, and the key, decide which one you reach.
  • sourceName and sourceVersion name 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:

Send a sample payloadgo
// 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:

Output
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.

Submit an invoicego
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.

Invoice validatedjson
{
  "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:

Check a document's statusgo
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:

Get a purchase invoicego
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:

List purchase invoicesgo
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:

errors.gogo
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
}
StatusWhat it meansWhat to do
401The API key is missing, invalid, revoked or expired.Check that COMPLYANCE_API_KEY is set where your application runs.
403The 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".
404The source is not in your workspace.Check sourceName and sourceVersion.
429Too many requests.Wait, then send the request again. SubmitJSONSingle does not retry for you.

What each error from the API means is in Errors.