stackin
SDKs

SDKs

  • One class, three methods

    An Invoice client with issue(), consult(), and cancel() — nothing else to instantiate.

  • Typed models

    Each item is a typed Product, with Brazil-specific fiscal fields validated client-side before the request leaves.

  • Same contract in every language

    Python, Go, and PHP follow the same API contract — methods, errors, and resources behave identically, so switching languages doesn't mean relearning stackin.

Just getting started?

Create a free account, generate an SDK key, and issue your first document in homologation — it's free and unlimited, no card required.

Copy
pip install stackin-python-sdk

Authentication

The SDK authenticates with an SDK key — pass it once when creating the client, the library handles every request after that. You never construct a URL or set a header yourself.

Getting an SDK key

  1. 1
    Sign up and log in at .
  2. 2
    Create a company — this is required before a key.
  3. 3
    Go to Settings → API key in the dashboard, and leave context set to sdk. No certificate needed for this step — a key can be created and used right away; a certificate is only required later, when actually issuing a document.
  4. 4
    Name the key and choose its environment (homologation or production, defaults to homologation) — this is fixed for the key's whole lifetime, it can't be changed later.
  5. 5
    Copy the key immediately — it's shown , at creation time, and can't be retrieved again. Losing it means revoking it and creating a new one.
Copy
from stackin import Invoice

client = Invoice(api_key="$STACKIN_API_KEY")

A company can hold up to 3 keys total (any mix of sdk/api context and homologation/production environment), so you can keep separate keys per surface or environment.

A missing or invalid token gets a 401 — see below.

Environments

Every SDK key is created for exactly one environment — or or — chosen when you generate it in the dashboard, and fixed for that key's lifetime. There's no environment field in SDK calls: the key you authenticate with decides where the document goes.

HomologationTest environment

Free and unlimited — it's the real SEFAZ/ADN test environment, not a mock — so integrate and test fully before switching to production.

ProductionLive environment

For real documents. Make sure everything works in homologation before switching.

A company can hold keys for both environments at once (up to 3 total).

Errors

The SDKs translate each failure into a distinct type: APIError for whatever the API answered (carrying status and detail), ConnectionFailedError for a network failure before any response, and the language's own validation error for what's caught client-side, before the request leaves.

Copy
from stackin import APIError, ConnectionFailedError

try:
    result = client.issue(...)
except ValueError as error:
    # Caught before the request left — empty items, missing ncm/cfop,
    # incomplete recipient_address on an NFE.
    print(f"Invalid request: {error}")
except APIError as error:
    # 4xx/5xx from the API. 502 carries the authorizer's own message.
    print(f"[{error.status_code}] {error.detail}")
except ConnectionFailedError as error:
    print(f"Could not reach the API: {error}")
HTTPErrorWhen
400Invalid configurationThe issuer's configuration (certificate, address, fiscal fields) is missing or invalid.
401UnauthorizedThe Bearer token is missing, expired, invalid, or wasn't created for the host you're calling (api.stackin.io vs sdk.stackin.io).
402Quota reachedThe company's plan quota for production invoices was reached and the plan has no overage price (trial).
409Operation conflictThe requested operation doesn't apply to that document_type (e.g. an nfse-only operation on an nfe).
422Validation errorThe request body failed validation — a required field is missing or malformed.
501Not implementedSigning for that document type/country isn't implemented on the server yet.
502Authorizer rejectedAuthorizer rejectionThe authorizer (SEFAZ/state or ADN) rejected the request. detail.message carries the authorizer's own error, and detail.invoice_id (issuance only) lets you look the rejected attempt up.

Authorizer rejections

When the SEFAZ or ADN rejects an operation, the response includes extra information.

APIError's detail carries these fields when the authorizer rejects — use detail.invoice_id to look the attempt up later.

Copy
{
  "detail": {
    "message": "IE do emitente inválida",
    "invoice_id": "inv_8f3c9b2e6a7d"
  }
}
detail.message
The message returned by the authorizer.
detail.invoice_id
Identifies the issuance attempt.
Endpoints

Invoices

Issues a new document. items[].product.br (ncm, cfop) is required for , ignored for , ignored for — a service has no tax classification code, so nfse only reads description and amount off each item.

The same call works for nfse — just change document_type.

Copy
from stackin import Address, DocumentType, Invoice
from stackin.br import Product

client = Invoice(api_key="$STACKIN_API_KEY")

result = client.issue(
    document_type=DocumentType.NFE,
    client_name="Buyer Company Ltd",
    tax_id="11222333000181",
    items=[
        Product(
            description="Rosa Holambra Vermelha",
            amount=112.44,
            ncm="06031100",
            cfop="5102",
        )
    ],
    recipient_address=Address(
        street="Rua das Flores",
        number="1200",
        neighborhood="Centro",
        city="Joinville",
        state="SC",
        zip_code="89201100",
        city_code="4209102",
    ),
)

Looks up a document's current status directly at the authorizer by its access key. Requires as a query param.

Copy
result = client.consult(
    document_type=DocumentType.NFE,
    access_key="42260831112223330001815500000012341123456789",
)

Cancels a previously authorized document. must be at least 15 characters for nfe (the authorizer's xJust field requires it). Cancelling doesn't refund plan quota — the authorizer capacity was already spent on issuance.

Copy
result = client.cancel(
    document_type=DocumentType.NFE,
    access_key="42260831112223330001815500000012341123456789",
    reason="Duplicate order, cancelled by the buyer within 24h",
)

Retries a failed submission by the document's local `id` (not its access key — a rejected invoice doesn't have one). Useful for retrying after fixing a configuration problem or an authorizer outage. Consumes credit like a fresh issuance, since it's a new transmission.

Copy
result = client.reissue(
    invoice_id="9f2c1e3a-4b5d-6e7f-8a9b-0c1d2e3f4a5b",
)