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
emission_status = active and the fiscal entity is ready to emit.
FLOW A — Credentials first, then setup
Store credentials
POST /fiscal-entities/{id}/pass-invoices and/or POST /fiscal-entities/{id}/certificate — credentials saved locally. Auto-validates against government API.
Company setup
POST /fiscal-entities/{id}/invoices-emission — creates eNotas company account, sends existing credentials to eNotas. Sets emission_status = missing_validation.
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
Company setup
POST /fiscal-entities/{id}/invoices-emission — creates eNotas company account. No credentials exist yet. Sets emission_status = missing_auth.
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.
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:
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
| Method | Path | Purpose |
|---|---|---|
| GET | /fiscal-entities/:id/pass-gov | Get pass-gov record (password blanked unless ?show_pass=true + admin) |
| POST | /fiscal-entities/:id/pass-gov | Create pass-gov (triggers async validation) |
| PATCH | /fiscal-entities/:id/pass-gov | Update pass-gov (password change triggers re-validation; password: "" clears & inactivates) |
| PATCH | /fiscal-entities/:id/pass-gov/validate | Re-trigger async validation manually |
| GET | /fiscal-entities/:id/pass-invoices | Get pass-invoices record |
| POST | /fiscal-entities/:id/pass-invoices | Create pass-invoices (triggers credential validation) |
| PATCH | /fiscal-entities/:id/pass-invoices | Update pass-invoices (password change triggers re-test; password: "" clears & inactivates) |
| GET | /fiscal-entities/:id/certificate | Get certificate metadata (returns null if none) |
| POST | /fiscal-entities/:id/certificate | Upload PFX (multipart, password + data) |
| PATCH | /fiscal-entities/:id/certificate | Update PFX password only (file optional) |
| DELETE | /fiscal-entities/:id/certificate | Soft-delete (deactivates + unlinks from seller) |
| GET | /fiscal-entities/:id/certificate/download | Download the PFX (base64) |
Setup & validation
| Method | Path | Purpose |
|---|---|---|
| POST | /fiscal-entities/:id/invoices-emission | Run the setup orchestrator. Idempotent. |
| POST | /fiscal-entities/:id/invoices-emission/validate | Run the validation pipeline (emits a R$0,10 test invoice) |
| POST | /fiscal-entities/:id/invoices-emission/deactivate | Deactivate the eNotas provider account |
| GET | /fiscal-entities/:id/validation-status | Get 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.status—active|inactive.validation-status—none|validating|succeeded|failed.validation-errors— array of error objects.
Encryption
Encrypted at rest. Decrypted on read in production environments.
Async validation flow
POST or PATCH with password set.validation_statuses = validating.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-type—pass_only|token|user_and_pass.username— only foruser_and_pass. Stored in plaintext (eNotas needs to return it).password— encrypted at rest. Fortoken, this field holds the API token.status—active|inactive.
authentication_type values
| Type | Fields sent to eNotas | Use case |
|---|---|---|
pass_only (default) | TaxData.password | One shared password per CNPJ. |
token | TaxData.token | API token. password field holds the token string. |
user_and_pass | TaxData.user + TaxData.password | Username + 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:
- Sets
validation_status = validating. - Sends an async worker to validate credentials against the revenue API.
- On success, sets
validation_status = succeeded. On failure, setsvalidation_status = failedwith 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.
application/x-pkcs12 or application/pkcs12. Anything else is rejected before reaching the service.
What the service does on upload
- Decodes and validates the PFX.
- Maps errors: wrong password →
InvalidCertificatePassword(400), decode error →InvalidCertificateFormat(400), empty →CertificateEmpty(400). - Extracts the expiration date as
"YYYY-MM-DD". - Stores the file securely with encryption.
- Encrypts the password and stores an integrity hash.
- 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.
emission_status is set to missing_auth.
SetAuthentication.SetCertificate.emission_status: missing_validation if credentials were sent, missing_auth if none. Resets emission_validation_statuses to none.After setup, the caller must:
- 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. - Trigger
POST /invoices-emission/validateto 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
- A tiny test invoice is built with
amount: 10(R$0,10). - The invoice is emitted through the same pipeline as a normal invoice.
- The callback is routed internally to the validation handler rather than the caller's callback.
- On eNotas success, validation is marked
succeededand cancellation is triggered asynchronously. - On eNotas failure, the validation is marked
failedwith the error metadata.
emission-status state machine
Four states:
FLOW A skips missing_auth — setup with existing credentials goes directly to missing_validation. FLOW B goes through missing_auth first.
| State | Meaning | What to do |
|---|---|---|
missing_setup | Just created; no eNotas company | Call POST /invoices-emission |
missing_auth | eNotas company created; no credentials sent yet | Store pass-invoices and/or certificate — they'll be sent to eNotas automatically |
missing_validation | Credentials sent to eNotas; emission validation not yet succeeded | Call POST /invoices-emission/validate |
active | Ready to emit (emission validation succeeded) | — |
inactive | Manually deactivated | Re-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_statusmust bemissing_validationormissing_auth.validating— a test invoice is in flight.emission_statusremainsmissing_validation.succeeded— the test invoice was emitted and accepted.emission_statusbecomesactive.failed— the test invoice failed.emission_statusstaysmissing_validation; checklast_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:
| Field | Values | Meaning |
|---|---|---|
status_pass_govs | valid / invalid / none / not-validated / processing-validation | Is pass-gov usable for fetching DAS? |
status_pass_invoices | valid / invalid / none / not-validated / processing-validation | Is pass-invoices usable for emission? |
status_certificate | valid / invalid / expired / none / not-validated / processing-validation | Is 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
| Symptom | Cause | What to do |
|---|---|---|
404 CertificateNotFound | No certificate on file | Show the upload screen (cert is optional but useful for production) |
400 InvalidCertificateFormat | PFX corrupt or expired | Ask user to re-export from cert authority |
400 InvalidCertificatePassword | Wrong password | Add a "forgot cert password" flow |
500 EMP0002 | CNPJ already exists in eNotas | No action — service retries automatically with update path |
500 decrypt password | Stale encrypted record after key rotation | Force a PATCH to re-encrypt with the current key |
400 IdInvalid | URL has :id placeholder or non-numeric id | Substitute the actual path parameter |
show_pass ignored | Caller is not admin | Only admin role can see plaintext passwords |
Ship checklist
- ☐ Call POST /invoices-emission to create the eNotas provider account (status becomes
missing_validation) - ☐ Create the pass-gov record (optional — needed for DAS fetching)
- ☐ Create the pass-invoices record (required for emission)
- ☐ Upload the certificate (optional but recommended for production)
- ☐ Run POST /invoices-emission/validate to confirm the setup works end-to-end (flips
emission_statustoactiveon success) - ☐ Show the
emission_statusbadge and the three computed status fields (status_pass_govs,status_pass_invoices,status_certificate) in the UI - ☐ Block the "emit" action when
emission_status != "active" - ☐ On the
fiscal-entitieslist page, treatstatus_certificate == "expired"as a hard block (forcesmissing_validationon next GET)
Once emission_status = active, head to the Invoice Emission guide for the actual emission flow.