# Python SDK

Send invoices to Complyance from a Python application with the Python SDK, from a sample payload for mapping to a submitted invoice in sandbox. You need Python 3.7 or later and a sandbox API key.

> **Before you start**
>
> - Python 3.7 or later.
> - A Complyance workspace, with access to the **Developer portal**.
> - A sandbox API key. See [API keys](https://docs.complyance.io/one-platform/developer-portal/api-keys/) and [Authentication and environments](https://docs.complyance.io/api-reference/authentication/).
> - Work in sandbox until your invoices pass. Switch to production only when you go live.

## Install

The package is `io-complyance-unify-sdk` on PyPI. Install version `3.0.2b0`, a beta release, by its exact version. Later releases on PyPI, from `3.0.3b0` to `3.0.11b0`, raise a `SyntaxError` when you import them.

**pip**

```bash title="Install the SDK"
pip install io-complyance-unify-sdk==3.0.2b0
```

**Poetry**

```bash title="Install the SDK"
poetry add io-complyance-unify-sdk==3.0.2b0
```

**uv**

```bash title="Install the SDK"
uv add io-complyance-unify-sdk==3.0.2b0
```

You import it as `complyance_sdk`. Some older guides name the package `complyance-sdk`. That package does not exist on PyPI; install `io-complyance-unify-sdk`.

## Configure the SDK

Set your key as an environment variable, so it never appears in your code:

```bash title="Set your key"
export COMPLYANCE_API_KEY='paste-your-sandbox-key'
```

Then configure the SDK once, when your application starts:

```python title="Configure the SDK"
import os

from complyance_sdk import Environment, GETSUnifySDK, SDKConfig, Source, SourceType

api_key = os.environ.get("COMPLYANCE_API_KEY")
if not api_key:
    raise RuntimeError("Set COMPLYANCE_API_KEY before you start.")

config = SDKConfig(
    api_key=api_key,
    environment=Environment.SANDBOX,
    sources=[Source("acme-erp", "1.0", SourceType.FIRST_PARTY)],
)

GETSUnifySDK.configure(config)
```

- `Environment.SANDBOX` sends requests to sandbox. Use `Environment.PRODUCTION` with a production key when you go live. A key works only in the environment it was created for.
- `Source` names the system your invoices come from, such as your ERP, by a name and a version. Use the same name and version in every request from that system.

The SDK is synchronous: each call returns when the response arrives. The examples below run after this configuration, in the same module.

### Retries

The SDK tries a request up to five times when the network fails or the API answers `408`, `429`, `500`, `502`, `503` or `504`. To change this, pass a retry configuration as `retry_config`:

```python title="Choose a retry configuration"
from complyance_sdk import RetryConfig

config = SDKConfig(
    api_key=api_key,
    environment=Environment.SANDBOX,
    sources=[Source("acme-erp", "1.0", SourceType.FIRST_PARTY)],
    retry_config=RetryConfig.conservative_retry(),
)
```

The presets are `RetryConfig.default_config()`, `RetryConfig.conservative_retry()`, `RetryConfig.aggressive_retry()` and `RetryConfig.no_retry()`, which sends each request once.

## Send a sample payload for mapping

Before you can submit invoices, Complyance needs to know how your fields match GETS, the Complyance standard invoice format. Send one invoice in your own format with the purpose `Purpose.MAPPING`. Complyance stores it as a sample payload that you map in the Developer portal; it is not validated or sent to a tax authority. The purposes are described in [Submit and retrieve](https://docs.complyance.io/api-reference/unify/submit-and-retrieve/#purpose).

```python title="Send a sample payload"
from complyance_sdk import (
    BASE,
    Country,
    GETSUnifySDK,
    GetsDocumentType,
    Mode,
    Operation,
    Purpose,
)

payload = {
    "invoice_data": {
        "document_number": "INV-2026-0142",
        "invoice_date": "2026-09-18",
        "currency_code": "AED",
        "total_amount": 10500.0,
    },
    "seller_info": {"seller_name": "Acme Trading LLC", "country_code": "AE"},
    "buyer_info": {"buyer_name": "Gulf Retail LLC", "buyer_country": "AE"},
    "line_items": [
        {"line_id": "1", "item_name": "Office chair", "quantity": 10, "unit_price": 1000.0},
    ],
}

document_type = GetsDocumentType.builder().base(BASE.TAX_INVOICE).build()

mapping = GETSUnifySDK.push_to_unify_v2(
    source_name="acme-erp",
    source_version="1.0",
    document_type_v2=document_type,
    country=Country.AE,
    operation=Operation.SINGLE,
    mode=Mode.DOCUMENTS,
    purpose=Purpose.MAPPING,
    payload=payload,
)

print(mapping.status, mapping.data.payload.payload_id)
```

Send a payload with every field your system can produce, so you can map all of them. The SDK reads the response into an object; this is the response it reads, shortened:

```json title="Sample payload received"
{
  "status": "success",
  "message": "Unify request processed successfully",
  "data": {
    "processing": {
      "status": "completed"
    }
  }
}
```

In Python, the same values are `mapping.status`, `mapping.data.payload.payload_id` and `mapping.data.processing.status`.

Next, in the Developer portal:

1. [Create an integration](https://docs.complyance.io/one-platform/developer-portal/integration-engine/create/) and pick this sample payload.
2. [Map your fields to GETS](https://docs.complyance.io/one-platform/developer-portal/integration-engine/gets-mapping/) until the required fields are covered.
3. [Link your source](https://docs.complyance.io/one-platform/developer-portal/source-management/) (`acme-erp`, version `1.0`) to the integration.

## Submit an invoice

Once your source is linked to an integration, send invoices from the same source with the purpose `Purpose.INVOICING`. Complyance applies your mapping, checks the invoice and continues delivery for its country.

```python title="Submit an invoice"
response = GETSUnifySDK.push_to_unify_v2(
    source_name="acme-erp",
    source_version="1.0",
    document_type_v2=document_type,
    country=Country.AE,
    operation=Operation.SINGLE,
    mode=Mode.DOCUMENTS,
    purpose=Purpose.INVOICING,
    payload=payload,
)

validation = response.data.validation if response.data else None
if validation and validation.overall_success is False:
    for error in validation.errors:
        print(error.code, error.path, error.message)
elif response.data and response.data.document:
    print("Document ID:", response.data.document.document_id)
```

- The source name and version must match the source you linked to the integration.
- `document_type` comes from the mapping step. The builder sets the GETS document type: a base, such as `BASE.TAX_INVOICE` or `BASE.CREDIT_NOTE`, and any modifiers that apply. For a UAE export invoice, add `.modifiers([MODIFIER.EXPORT])` before `.build()`. The document type decides which fields are required; the modifiers each country accepts are in [Base and modifiers](https://docs.complyance.io/api-reference/gets/base-and-modifiers/).
- `Country.AE` is the country the invoice is issued in. In sandbox and production the SDK sends invoices for `Country.SA`, `Country.MY`, `Country.AE`, `Country.BE` and `Country.DE`. It has no country value for Oman.

This version of the SDK sends the earlier Unify request format, without the `new-api` header, so the answer comes back in the earlier response format described in [SDKs](https://docs.complyance.io/sdks/#response-formats). `response.status` is `"success"` when Complyance received the request, even if the invoice failed its checks. Always read `response.data.validation` as well; the SDK reads its `success` field into `validation.overall_success`. An invoice that failed shows each problem with its code:

```json title="Invoice failed validation"
{
  "status": "success",
  "data": {
    "document": {
      "documentId": "01K5EXAMPLE00000000000142"
    },
    "validation": {
      "success": false,
      "errors": [
        {
          "code": "GETS-LINE-005",
          "message": "Line item price is required",
          "path": ["lineItems", 0, "price", "amount"]
        }
      ]
    }
  }
}
```

`path` points to the GETS field. Fix the field in your payload or in your mapping, then send the invoice again. Keep the `documentId` to check the document later.

If the request still fails after its retries, `response.status` is `"queued"`. The SDK keeps the request and sends it again itself, so do not send that invoice again.

Run your mapping against test cases in a [testbed](https://docs.complyance.io/one-platform/developer-portal/testbeds/) before you switch to production.

## Check a document’s status

Delivery to the tax authority or the Peppol network continues after the response. This version of the SDK has no method to read a document, so ask the API for it with the `documentId`. The example uses `httpx`, which is installed with the SDK:

```python title="Check a document's status"
import os

import httpx

document_id = "01K5EXAMPLE00000000000142"

result = httpx.get(
    f"https://prod.gets.complyance.io/api/v3/documents/{document_id}",
    params={"type": "sales"},
    headers={
        "Authorization": f"Bearer {os.environ['COMPLYANCE_API_KEY']}",
        "Accept": "application/json",
    },
)
result.raise_for_status()
print(result.json())
```

Every state and field is described in the [documents reference](https://docs.complyance.io/api-reference/unify/documents/). Checking the status is how you learn whether the invoice was accepted.

## Retrieve purchase invoices

Purchase invoices are the invoices your suppliers send you. This version of the SDK does not retrieve them, so call the API in the same way. Your API key decides the workspace and environment.

List the purchase invoices received in a date range, one page at a time:

```python title="List purchase invoices"
import os

import httpx

headers = {
    "Authorization": f"Bearer {os.environ['COMPLYANCE_API_KEY']}",
    "Accept": "application/json",
}
params = {"type": "purchases", "from": "2026-09-01", "to": "2026-09-30", "limit": 100}

with httpx.Client(base_url="https://prod.gets.complyance.io", headers=headers) as client:
    while True:
        page = client.get("/api/v3/documents", params=params)
        page.raise_for_status()
        data = page.json()["data"]
        for item in data["items"]:
            print(item["documentId"], item["invoiceNumber"], item["supplierName"])
        if not data["hasMore"]:
            break
        params["cursor"] = data["nextCursor"]
```

Keep the same dates and limit for every page, and pass `nextCursor` back unchanged. Then get one invoice by its `documentId`:

```python title="Get a purchase invoice"
invoice = httpx.get(
    "https://prod.gets.complyance.io/api/v3/documents/01JEXAMPLE0000000000000142",
    params={"type": "purchases"},
    headers=headers,
)
invoice.raise_for_status()
print(invoice.json()["data"]["invoice"])
```

The fields are described in [List purchase invoices](https://docs.complyance.io/api-reference/purchases/list/) and [Get a purchase invoice](https://docs.complyance.io/api-reference/purchases/get/).

## Handle errors

The SDK raises an `SDKException` when it cannot send a request or the API refuses it. Its `error_detail` holds the `code`, a `message` and a `suggestion`.

```python title="Handle errors"
from complyance_sdk import SDKException

try:
    response = GETSUnifySDK.push_to_unify_v2(
        source_name="acme-erp",
        source_version="1.0",
        document_type_v2=document_type,
        country=Country.AE,
        operation=Operation.SINGLE,
        mode=Mode.DOCUMENTS,
        purpose=Purpose.INVOICING,
        payload=payload,
    )
    print(response.status)
except SDKException as exc:
    detail = exc.error_detail
    print(detail.code, detail.message, detail.suggestion)
    print(detail.context.get("originalError"))
```

When the API refuses a request, `detail.code` is `ErrorCode.MAX_RETRIES_EXCEEDED`, and `detail.context["originalError"]` names the reason. The errors people hit most:

- **`AUTHENTICATION_FAILED`** in `originalError`: the key was refused because it is invalid, revoked or expired. Check that `COMPLYANCE_API_KEY` is set where your application runs, and use a sandbox key with `Environment.SANDBOX` and a production key with `Environment.PRODUCTION`.
- **`MISSING_FIELD`** with `Source name is required` or `Source version is required`: an invoicing request was sent without a source. Pass the source name and version you linked to your integration.
- **`INVALID_ARGUMENT`** with `Country not allowed for production environment`: the country is not one the SDK sends to. Use one of the countries listed in [Submit an invoice](https://docs.complyance.io/sdks/python/#submit-an-invoice).

Validation problems in the invoice itself do not raise an exception; they come back in `response.data.validation`, as shown in [Submit an invoice](https://docs.complyance.io/sdks/python/#submit-an-invoice). What each error means, and what to do, is in [Unify errors](https://docs.complyance.io/api-reference/unify/errors/).

Next, try a request without writing code in the [API playground](https://docs.complyance.io/api/?operation=submitDocument).

Source: https://docs.complyance.io/sdks/python/
