Swagger UI openapi.json openapi.yaml
Guide

Invoice Emission Setup

The setup phase: storing credentials (pass-gov, pass-invoices, certificate), running the orchestrator, and the validation pipeline that flips emission_status to active.

TL;DR

One orchestrator (company setup in eNotas), three credential stores (pass-gov, pass-invoices, certificate), one explicit validation call. After that, emission_status = active and the fiscal entity is ready to emit.

FLOW A — Credentials first, then setup

1

Store credentials

POST /fiscal-entities/{id}/pass-invoices and/or POST /fiscal-entities/{id}/certificate — credentials saved locally. Auto-validates against government API.

2

Company setup

POST /fiscal-entities/{id}/invoices-emission — creates eNotas company account, sends existing credentials to eNotas. Sets emission_status = missing_validation.

3

Validate emission

POST /fiscal-entities/{id}/invoices-emission/validate — runs a real R$0,10 test NFS-e. Flips emission_status to active on success.

FLOW B — Setup first, then credentials

1

Company setup

POST /fiscal-entities/{id}/invoices-emission — creates eNotas company account. No credentials exist yet. Sets emission_status = missing_auth.

2

Store credentials

POST /fiscal-entities/{id}/pass-invoices and/or POST /fiscal-entities/{id}/certificate — credentials saved locally and sent to eNotas immediately (provider account exists). Sets emission_status = missing_validation.

3

Validate emission

POST /fiscal-entities/{id}/invoices-emission/validate — runs a real R$0,10 test NFS-e. Flips emission_status to active on success.

Architecture

The Brazilian invoice emission stack is layered:

Your app
Contabeleza API
accounting service
eNotas
Prefeitura

The three stores hold secrets. The setup call is an orchestration that hands those secrets to eNotas in the right shape. The validation call is a smoke test that proves the whole chain works end-to-end before the client starts emitting real invoices.

Endpoints

Credential stores

MethodPathPurpose
GET/fiscal-entities/:id/pass-govGet pass-gov record (password blanked unless ?show_pass=true + admin)
POST/fiscal-entities/:id/pass-govCreate pass-gov (triggers async validation)
PATCH/fiscal-entities/:id/pass-govUpdate pass-gov (password change triggers re-validation; password: "" clears & inactivates)
PATCH/fiscal-entities/:id/pass-gov/validateRe-trigger async validation manually
GET/fiscal-entities/:id/pass-invoicesGet pass-invoices record
POST/fiscal-entities/:id/pass-invoicesCreate pass-invoices (triggers credential validation)
PATCH/fiscal-entities/:id/pass-invoicesUpdate pass-invoices (password change triggers re-test; password: "" clears & inactivates)
GET/fiscal-entities/:id/certificateGet certificate metadata (returns null if none)
POST/fiscal-entities/:id/certificateUpload PFX (multipart, password + data)
PATCH/fiscal-entities/:id/certificateUpdate PFX password only (file optional)
DELETE/fiscal-entities/:id/certificateSoft-delete (deactivates + unlinks from seller)
GET/fiscal-entities/:id/certificate/downloadDownload the PFX (base64)

Setup & validation

MethodPathPurpose
POST/fiscal-entities/:id/invoices-emissionRun the setup orchestrator. Idempotent.
POST/fiscal-entities/:id/invoices-emission/validateRun the validation pipeline (emits a R$0,10 test invoice)
POST/fiscal-entities/:id/invoices-emission/deactivateDeactivate the eNotas provider account
GET/fiscal-entities/:id/validation-statusGet the latest validation result

pass-gov — government portal password

The password used by eNotas to access the gov.br / prefeitura portal on behalf of the MEI. Used to fetch the DAS receipt and other official documents. It does NOT affect emission_status — you can emit invoices without it.

Record structure

  • password — encrypted at rest.
  • statusactive | inactive.
  • validation-statusnone | validating | succeeded | failed.
  • validation-errors — array of error objects.

Encryption

Encrypted at rest. Decrypted on read in production environments.

Async validation flow

client
Sends POST or PATCH with password set.
accounting
Encrypts the password, sets validation_statuses = validating.
accounting
Triggers an async worker to validate the credentials against the government portal.
accounting (async)
Decrypts password, fetches CNPJ + owner's CPF, calls the government MEI consultation API.
accounting
Maps errors to validation_errors[] array. Writes final status: succeeded or failed.

JSON:API shape

{
  "data": {
    "id": "1",
    "type": "fiscal-entity-pass-govs",
    "attributes": {
      "password": "",
      "status": "active",
      "validation-status": "validating",
      "validation-errors": [{"code": "...", "message": "..."}],
      "validation-at": "2024-10-15T12:00:00Z",
      "timestamps": {}
    },
    "relationships": {
      "fiscal-entity": { "data": { "type": "fiscal-entities", "id": "123" } }
    }
  }
}

Password is blanked in all responses unless ?show_pass=true is used. show_pass=true is only honored for the admin role.

Clearing the password

Sending PATCH .../pass-gov with password: "" clears the stored password, sets status to inactive, and resets validation-status to none. The create endpoint (POST .../pass-gov) requires a non-empty password and rejects empty with 400 PasswordRequired.

pass-invoices — NFSe provider credentials

The credentials eNotas uses to authenticate against the city's invoice system. Required for emission — without it, emission_status stays missing_auth until credentials are sent.

Record structure

  • authentication-typepass_only | token | user_and_pass.
  • username — only for user_and_pass. Stored in plaintext (eNotas needs to return it).
  • password — encrypted at rest. For token, this field holds the API token.
  • statusactive | inactive.

authentication_type values

TypeFields sent to eNotasUse case
pass_only (default)TaxData.passwordOne shared password per CNPJ.
tokenTaxData.tokenAPI token. password field holds the token string.
user_and_passTaxData.user + TaxData.passwordUsername + password. Most common for MEI prefeitura portals.

The authentication_type value determines which fields are sent to eNotas when a provider account exists (or during setup) as shown in the table above.

Credential validation (auto-triggered)

When you POST or PATCH with a new password, the system triggers an async credential validation — testing the credentials against the government revenue API:

  1. Sets validation_status = validating.
  2. Sends an async worker to validate credentials against the revenue API.
  3. On success, sets validation_status = succeeded. On failure, sets validation_status = failed with error details.

Credential validation is separate from emission validation (the test invoice). Credential validation checks if the password works against the government API. Emission validation (POST /invoices-emission/validate) tests the full eNotas pipeline with a real R$0,10 NFS-e.

Sending to eNotas

If a provider account already exists (e.g. FLOW B), pass-invoices credentials are sent to eNotas immediately on create/update, setting emission_status = missing_validation. If no provider account exists (e.g. FLOW A), credentials are stored locally only — the setup orchestrator will send them when it creates the company.

JSON:API shape

{
  "data": {
    "id": "1",
    "type": "fiscal-entity-pass-invoices",
    "attributes": {
      "authentication-type": "user_and_pass",
      "username": "meu-cnpj-aqui",
      "password": "",
      "status": "active",
      "timestamps": {}
    },
    "relationships": {
      "fiscal-entity": { "data": { "type": "fiscal-entities", "id": "123" } }
    }
  }
}

No DELETE endpoint — to "remove" you PATCH with password="" which clears the stored password, sets status to inactive, and persists the change atomically. The create endpoint (POST .../pass-invoices) requires a non-empty password and rejects empty with 400 PasswordRequired.

Digital certificate (.pfx)

Signs the NFS-e XML. Optional but recommended for production. The file is stored securely; the password is encrypted at rest.

Size limit: 5000 KB. MIME: application/x-pkcs12 or application/pkcs12. Anything else is rejected before reaching the service.

What the service does on upload

  1. Decodes and validates the PFX.
  2. Maps errors: wrong passwordInvalidCertificatePassword (400), decode errorInvalidCertificateFormat (400), emptyCertificateEmpty (400).
  3. Extracts the expiration date as "YYYY-MM-DD".
  4. Stores the file securely with encryption.
  5. Encrypts the password and stores an integrity hash.
  6. Links the certificate onto the fiscal entity.

Multipart example

curl -X POST 'https://api.contabeleza.com.br/fiscal-entities/123/certificate' \
  -H "Authorization: Bearer $TOKEN" \
  -H "ilm-business: 900001" \
  -F "password=cert123456" \
  -F "data=@/path/to/certificate.pfx;type=application/x-pkcs12"

Storage

Stored securely in primary storage. Legacy records are transparently migrated to the current store on read.

Soft delete

DELETE removes the stored file, appends inactive to the status history, and unlinks the cert from the seller. The database row is preserved. A subsequent POST reactivates the existing record rather than inserting a new one.

The setup orchestrator — POST /invoices-emission

Idempotent — safe to re-run after any change.

The setup orchestrator creates/updates the eNotas provider account. If pass-invoices and/or certificate credentials already exist locally, they are sent to eNotas at this point. If no credentials exist, emission_status is set to missing_auth.
accounting
Looks up the fiscal entity by id.
accounting → eNotas
Sends company data: name, email, tax_number, address, tax_data. eNotas returns a provider account id.
accounting
Checks for existing pass-invoices credentials. If found and active, sends them to eNotas via SetAuthentication.
accounting
Checks for existing certificate. If found and active, sends it to eNotas via SetCertificate.
accounting
Sets emission_status: missing_validation if credentials were sent, missing_auth if none. Resets emission_validation_statuses to none.

After setup, the caller must:

  1. If emission_status = missing_auth: configure credentials via the dedicated endpoints (pass-invoices, certificate). They will be sent to eNotas automatically since the provider account now exists.
  2. Trigger POST /invoices-emission/validate to run the emission validation pipeline.

Validation pipeline — POST /invoices-emission/validate

An explicit smoke test. The service creates a real R$0,10 NFS-e through the same eNotas path, waits for the callback, and immediately cancels the test invoice on success. The result is stored in a rolling history (max 3 entries).

Flow

  1. A tiny test invoice is built with amount: 10 (R$0,10).
  2. The invoice is emitted through the same pipeline as a normal invoice.
  3. The callback is routed internally to the validation handler rather than the caller's callback.
  4. On eNotas success, validation is marked succeeded and cancellation is triggered asynchronously.
  5. On eNotas failure, the validation is marked failed with the error metadata.

emission-status state machine

Four states:

missing_setup
missing_auth
missing_validation
active
inactive

FLOW A skips missing_auth — setup with existing credentials goes directly to missing_validation. FLOW B goes through missing_auth first.

StateMeaningWhat to do
missing_setupJust created; no eNotas companyCall POST /invoices-emission
missing_autheNotas company created; no credentials sent yetStore pass-invoices and/or certificate — they'll be sent to eNotas automatically
missing_validationCredentials sent to eNotas; emission validation not yet succeededCall POST /invoices-emission/validate
activeReady to emit (emission validation succeeded)
inactiveManually deactivatedRe-run setup; upload a fresh cert if expired

Emission validation statuses (canonical state machine)

Driven by the emission_validation_statuses JSON field — a timestamped array of entries, one of:

  • none — credentials have changed (or initial state). emission_status must be missing_validation or missing_auth.
  • validating — a test invoice is in flight. emission_status remains missing_validation.
  • succeeded — the test invoice was emitted and accepted. emission_status becomes active.
  • failed — the test invoice failed. emission_status stays missing_validation; check last_validation.

Whenever a credential is created, updated, or deactivated, the system appends none to emission_validation_statuses and forces emission_status = missing_validation — the previous validation no longer applies because the credentials changed.

missing_auth as an error code

In addition to being an emission_status value, missing_auth is also an error code returned by POST /invoices-emission/validate and invoice creation when no valid auth method is configured (neither pass-invoices nor certificate has validation_status = succeeded).

Read-only status fields

The serializer exposes three computed status fields on the fiscal-entities response so the UI can show what is and isn't valid:

FieldValuesMeaning
status_pass_govsvalid / invalid / none / not-validated / processing-validationIs pass-gov usable for fetching DAS?
status_pass_invoicesvalid / invalid / none / not-validated / processing-validationIs pass-invoices usable for emission?
status_certificatevalid / invalid / expired / none / not-validated / processing-validationIs the digital certificate usable for signing?

valid = the credential is active and its last validation status is succeeded. invalid = the credential exists but its last validation status is failed. none = the credential has not been configured (no stored data). not-validated = the credential exists but has never been validated. processing-validation = a validation attempt is currently in flight. expired on certificates takes precedence over invalid when the cert is active but past its expiration date. On a GET /fiscal-entities/{id}, if the active certificate is expired, the system automatically sets emission_status = missing_validation and resets emission_validation_statuses to none (because the credentials effectively changed).

The guard at emission time is strict: returns 400 invoices.emission_not_active if EmissionStatus != "active". The only exception is a validation test invoice when the status is missing_validation — that's exactly the test invoice created by the emission validation pipeline. So the UI must always show the current state and block the "emit" action when it's not active.

Failure modes

SymptomCauseWhat to do
404 CertificateNotFoundNo certificate on fileShow the upload screen (cert is optional but useful for production)
400 InvalidCertificateFormatPFX corrupt or expiredAsk user to re-export from cert authority
400 InvalidCertificatePasswordWrong passwordAdd a "forgot cert password" flow
500 EMP0002CNPJ already exists in eNotasNo action — service retries automatically with update path
500 decrypt passwordStale encrypted record after key rotationForce a PATCH to re-encrypt with the current key
400 IdInvalidURL has :id placeholder or non-numeric idSubstitute the actual path parameter
show_pass ignoredCaller is not adminOnly admin role can see plaintext passwords

Ship checklist

  1. ☐ Call POST /invoices-emission to create the eNotas provider account (status becomes missing_validation)
  2. ☐ Create the pass-gov record (optional — needed for DAS fetching)
  3. ☐ Create the pass-invoices record (required for emission)
  4. ☐ Upload the certificate (optional but recommended for production)
  5. ☐ Run POST /invoices-emission/validate to confirm the setup works end-to-end (flips emission_status to active on success)
  6. ☐ Show the emission_status badge and the three computed status fields (status_pass_govs, status_pass_invoices, status_certificate) in the UI
  7. ☐ Block the "emit" action when emission_status != "active"
  8. ☐ On the fiscal-entities list page, treat status_certificate == "expired" as a hard block (forces missing_validation on next GET)
missing_setup missing_validation active inactive

Once emission_status = active, head to the Invoice Emission guide for the actual emission flow.