qs.NotaFiscal.Pack
2.0.1
dotnet add package qs.NotaFiscal.Pack --version 2.0.1
NuGet\Install-Package qs.NotaFiscal.Pack -Version 2.0.1
<PackageReference Include="qs.NotaFiscal.Pack" Version="2.0.1" />
<PackageVersion Include="qs.NotaFiscal.Pack" Version="2.0.1" />
<PackageReference Include="qs.NotaFiscal.Pack" />
paket add qs.NotaFiscal.Pack --version 2.0.1
#r "nuget: qs.NotaFiscal.Pack, 2.0.1"
#:package qs.NotaFiscal.Pack@2.0.1
#addin nuget:?package=qs.NotaFiscal.Pack&version=2.0.1
#tool nuget:?package=qs.NotaFiscal.Pack&version=2.0.1
qs.NotaFiscal.Pack
SDK oficial em .NET para integração de parceiros externos com a plataforma QS.NOTAS — emissão e cancelamento de NFS-e via API REST, além de recebimento padronizado de webhooks de eventos.
📑 Índice
- Visão geral
- Instalação
- Configuração
- Uso — Emissão de NF (UC-02)
- Uso — Cancelamento de NF (UC-04)
- Recebimento de Webhook (UC-03)
- Tratamento de erros
- CorrelationId — estratégia
- Resiliência (retry / circuit breaker)
- Observabilidade
- Testes
- Compatibilidade
- Princípios de design
- Licença
🎯 Visão geral
O SDK encapsula as chamadas ao endpoint público do QS.NOTAS (/api/parceiros/*) e padroniza
o contrato do webhook, evitando que cada parceiro reimplemente serialização, autenticação
por X-API-KEY, controle de CorrelationId e tratamento dos payloads de resposta.
Casos de uso cobertos:
| UC | Descrição | Método |
|---|---|---|
| UC-02 | Emitir NF | IQsNotaFiscalClient.EmitirAsync |
| UC-04 | Cancelar NF | IQsNotaFiscalClient.CancelarAsync |
| UC-03 | Processar webhook recebido | QsNotaFiscalWebhookParser |
Todas as operações são assíncronas, retornam HTTP 202 no sucesso da submissão e o
status final chega via webhook. O SDK não lança exceção em erros de negócio (401,
409, 422) — use QsNotaFiscalResult<T>.
📦 Instalação
dotnet add package qs.NotaFiscal.Pack
Ou via PackageReference:
<PackageReference Include="qs.NotaFiscal.Pack" Version="1.0.0" />
⚙️ Configuração
Opção 1 — appsettings.json (recomendado)
{
"QsNotaFiscal": {
"BaseUrl": "https://api.qsnotas.com.br/",
"ApiKey": "sk_live_AAAA-BBBB-CCCC-DDDD-EEEEEEEEEEEEEEEE",
"Timeout": "00:00:30",
"UserAgent": "MinhaEmpresa/2.4 (+https://minhaempresa.com)"
}
}
// Program.cs
using qs.NotaFiscal.Pack.DependencyInjection;
builder.Services.AddQsNotaFiscalClient(builder.Configuration);
Opção 2 — Code-first
builder.Services.AddQsNotaFiscalClient(opts =>
{
opts.BaseUrl = "https://api.qsnotas.com.br/";
opts.ApiKey = Environment.GetEnvironmentVariable("QSNOTAS_API_KEY")!;
opts.Timeout = TimeSpan.FromSeconds(30);
});
🔐 Segurança: nunca comite a
ApiKey. Use variáveis de ambiente,User Secretsem dev ou um secret manager (Azure Key Vault, AWS Secrets Manager) em produção.
O extension method devolve um IHttpClientBuilder — encadeie políticas de retry,
circuit breaker, delegating handlers ou mocks nele (ver Resiliência).
📤 Uso — Emissão de NF (UC-02)
using qs.NotaFiscal.Pack.Abstractions;
using qs.NotaFiscal.Pack.Models.Requests;
public sealed class EmissaoService
{
private readonly IQsNotaFiscalClient _client;
public EmissaoService(IQsNotaFiscalClient client) => _client = client;
public async Task EmitirAsync(CancellationToken ct)
{
var req = new EmitirNotaFiscalRequest
{
CorrelationId = $"PARCEIRO-{DateTime.UtcNow:yyyy}-{Guid.NewGuid():N}",
Emissor = "12345678000190",
DataCompetencia = DateTime.Today,
DataVencimento = DateTime.Today.AddDays(10),
Descricao = "Consultoria em Tecnologia da Informação",
CodigoListaServicos = "1.07",
NaturezaOperacao = 1,
Valor = 5000.00m,
EstadoTributacao = "MG",
MunicipioTributacao = "3106200", // código IBGE do município de tributação
Tomador = new Tomador
{
CpfCnpj = "12345678000199",
RazaoSocial = "Empresa Tomadora Ltda",
Email = "financeiro@tomadora.com.br",
Logradouro = "Av. Afonso Pena",
Numero = "1000",
Bairro = "Centro",
Cidade = "Belo Horizonte", // grafia exata (com acentos)
Estado = "MG", // UF (2 letras)
Cep = "30130921",
CodigoIbge = "3106200" // opcional — ver seção abaixo
},
Imposto = new Imposto
{
Valor = 5000.00m,
BaseCalculoPercentual = 100,
IssPercentual = 5.0m,
IssRetido = false
}
};
var result = await _client.EmitirAsync(req, ct);
if (result.IsSuccess)
{
var resp = result.Value!;
Console.WriteLine($"Aceita. NotaFiscalId={resp.NotaFiscalId}, Correlation={resp.CorrelationId}");
// → Status final virá via webhook (ver seção abaixo).
return;
}
// Erros de negócio (401, 409, 422) — sem exceção.
Console.WriteLine($"Falha HTTP {result.HttpStatus}: {result.ErrorMessage}");
}
}
O que esperar:
| HTTP | Código de erro | Significado |
|---|---|---|
| 202 Accepted | — | Requisição aceita. NotaFiscalId e CorrelationId retornados. Status final via webhook. |
| 400 Bad Request | — | CorrelationId ausente. |
| 401 Unauthorized | — | API-KEY inválida ou ausente. |
| 409 Conflict | — | CorrelationId já utilizado. |
| 422 Unprocessable Entity | TOMADOR_INCOMPLETO |
Dados obrigatórios do tomador faltando (RazaoSocial, Logradouro, Cidade, Estado, Cep). |
| 422 Unprocessable Entity | CPFCNPJ_INVALIDO |
CPF/CNPJ com dígito verificador inválido. |
| 422 Unprocessable Entity | CIDADE_OBRIGATORIA |
Cidade/Estado ausentes e CodigoIbge também ausente. |
| 422 Unprocessable Entity | CIDADE_NAO_CADASTRADA |
Município não encontrado na base de cidades do QS.NOTAS — ver próxima seção. |
| 422 Unprocessable Entity | CODIGO_IBGE_INVALIDO |
CodigoIbge informado não corresponde a nenhum município da tabela. |
🏙️ Código IBGE do município — duas formas de informar
A partir da v1.0 o SDK aceita o município do tomador de duas formas. Use a que for mais conveniente ao seu cadastro interno:
1. Via CodigoIbge (recomendado quando você já tem o código no seu cadastro):
Tomador = new Tomador
{
// … demais campos …
CodigoIbge = "3106200" // 7 dígitos, fonte IBGE
}
Quando CodigoIbge é informado, o QS.NOTAS apenas valida sua existência na tabela
interna de cidades e o utiliza diretamente — sem lookup por nome.
2. Via Cidade + Estado (resolução automática):
Tomador = new Tomador
{
// … demais campos …
Cidade = "Belo Horizonte", // grafia exata (com acentos)
Estado = "MG" // UF, 2 letras maiúsculas
// CodigoIbge ausente → QS.NOTAS resolve a partir de Cidade+Estado
}
Regra de precedência: se ambos forem enviados, CodigoIbge prevalece e os
campos Cidade/Estado são usados apenas para cadastro/exibição.
Implicações da resolução por nome:
- Grafia importa.
"Belo Horizonte"resolve;"belo horizonte","Belo horizonte"ou"Bh"podem não resolver. Use sempre a grafia oficial, com acentos. - UF obrigatória e exata. Dois caracteres maiúsculos (
"MG", não"Minas Gerais"). - Falha se o município não existe na base. Resposta: HTTP 422 com código
CIDADE_NAO_CADASTRADA. Se isso acontecer, prefira enviar viaCodigoIbgeou contate o suporte — pode ser cidade recém-emancipada.
💡 Dica: o campo
MunicipioTributacaodoEmitirNotaFiscalRequestsempre é o código IBGE (7 dígitos) do município onde o ISS é devido — não aceita nome. É independente do município do tomador.
Como tratar no seu código:
var result = await _client.EmitirAsync(req, ct);
if (result.IsFailure && result.Errors.Any(e =>
e.Codigo == "CIDADE_NAO_CADASTRADA" || e.Codigo == "CODIGO_IBGE_INVALIDO"))
{
// Trate como erro de cadastro: revise os dados do tomador antes de reenviar.
_logger.LogError("Município inválido: {Msg}", result.ErrorMessage);
throw new InvalidOperationException(result.ErrorMessage);
}
❌ Uso — Cancelamento de NF (UC-04)
var req = new CancelarNotaFiscalRequest
{
CorrelationId = $"PARCEIRO-CANCEL-{Guid.NewGuid():N}",
NumeroNota = 1234,
Motivo = "Serviço não prestado"
};
var result = await _client.CancelarAsync(req, ct);
if (result.IsFailure)
{
if (result.HttpStatus == 404)
throw new InvalidOperationException("Nota não encontrada.");
if (result.HttpStatus == 422)
throw new InvalidOperationException($"Prazo expirado ou dados inválidos: {result.ErrorMessage}");
}
Restrições:
- Somente NFs com status
Emitidapodem ser canceladas. - O prazo legal de cancelamento varia por município.
- Uma NF só pode ser cancelada pelo AccountID que a emitiu.
📥 Recebimento de Webhook (UC-03)
Configure no painel do QS.NOTAS (Configurações → Webhook) a URL do seu endpoint e
a API-KEY que o QS.NOTAS deve apresentar ao chamá-lo.
Exemplo ASP.NET Core (Minimal API)
using qs.NotaFiscal.Pack.Webhook;
using qs.NotaFiscal.Pack.Models.Webhook;
var parser = app.Services.GetRequiredService<QsNotaFiscalWebhookParser>();
app.MapPut("/webhooks/qsnotas", async (HttpContext ctx, IConfiguration cfg) =>
{
// 1. Autenticar — compare em tempo constante para evitar timing attacks.
var expected = cfg["QsNotaFiscal:WebhookApiKey"]!;
var received = ctx.Request.Headers["X-API-KEY"].ToString();
if (!parser.IsApiKeyValid(received, expected))
return Results.Unauthorized();
// 2. Desserializar.
WebhookEventoPayload payload;
try
{
payload = await parser.ParseAsync(ctx.Request.Body, ctx.RequestAborted);
}
catch
{
return Results.BadRequest("Payload inválido.");
}
// 3. Processar (idempotente: o QS.NOTAS faz retries em não-200).
switch (payload.Status)
{
case StatusEventoWebhook.Emitida:
await MarcarComoEmitida(payload.CorrelationId, payload.NumeroNota, payload.Protocolo);
break;
case StatusEventoWebhook.Cancelada:
await MarcarComoCancelada(payload.CorrelationId);
break;
case StatusEventoWebhook.Rejeitada:
case StatusEventoWebhook.Erro:
await RegistrarFalha(payload.CorrelationId, payload.Erros);
break;
}
return Results.Ok();
});
Exemplo com Controller clássico
[ApiController]
[Route("webhooks/qsnotas")]
public sealed class QsNotasWebhookController : ControllerBase
{
private readonly QsNotaFiscalWebhookParser _parser;
private readonly string _expectedKey;
public QsNotasWebhookController(QsNotaFiscalWebhookParser parser, IConfiguration cfg)
{
_parser = parser;
_expectedKey = cfg["QsNotaFiscal:WebhookApiKey"]!;
}
[HttpPut]
public async Task<IActionResult> Receive(CancellationToken ct)
{
if (!_parser.IsApiKeyValid(Request.Headers[QsNotaFiscalWebhookParser.ApiKeyHeader], _expectedKey))
return Unauthorized();
var payload = await _parser.ParseAsync(Request.Body, ct);
// ... seu processamento aqui (salvar no banco, disparar evento interno, etc.)
return Ok();
}
}
⚠️ Idempotência é obrigatória
O QS.NOTAS re-envia (backoff exponencial, até 5 tentativas) se o seu endpoint não
retornar HTTP 200. Sempre processe o webhook como idempotente: chave = CorrelationId.
if (await _db.WebhookJaProcessado(payload.CorrelationId)) return Results.Ok();
Política de retry do QS.NOTAS
| Tentativa | Atraso acumulado |
|---|---|
| 1 | imediata |
| 2 | +1 min |
| 3 | +5 min |
| 4 | +30 min |
| 5 | +2 h |
Após 5 falhas, a entrega é marcada como FalhaDefinitiva no painel do QS.NOTAS.
🚨 Tratamento de erros
O SDK segue o princípio: resultado esperado → Result; imprevisto → exceção.
| Situação | Tipo retornado |
|---|---|
| HTTP 2xx | QsNotaFiscalResult<T>.IsSuccess == true |
| HTTP 4xx (erro de negócio) | QsNotaFiscalResult<T>.IsFailure == true com lista de Errors |
| HTTP 5xx após retry | QsNotaFiscalResult<T>.IsFailure == true (5xx chega aqui se você esgotar retries) |
| Timeout / rede | throw QsNotaFiscalException |
| Corpo JSON malformado | throw QsNotaFiscalException |
ArgumentException em parâmetros inválidos (null, CorrelationId vazio) |
throw ArgumentException |
try
{
var result = await _client.EmitirAsync(req, ct);
if (result.IsFailure)
{
_logger.LogWarning("Erro {Status}: {Msg}", result.HttpStatus, result.ErrorMessage);
return;
}
// sucesso…
}
catch (QsNotaFiscalException ex)
{
// infra (rede, DNS, timeout) — reemita depois.
_logger.LogError(ex, "Falha infra ao chamar QS.NOTAS.");
}
🔑 CorrelationId — estratégia
O CorrelationId é a chave idempotente da operação.
Regras:
- Você decide o formato (ex.:
PARCEIRO-<ano>-<id-interno>). - Deve ser único por emissão (reenviar o mesmo →
HTTP 409). - O cancelamento pode ter um CorrelationId diferente da emissão.
- O QS.NOTAS devolve o mesmo CorrelationId em todos os webhooks da NF.
Boas práticas:
- Gere com prefixo identificável (
MINHA-EMPRESA-ajuda em troubleshooting cross-sistema). - Nunca reuse. Se precisar reemitir após falha, gere um novo.
- Armazene junto da sua entidade local — é como você correlaciona o webhook de volta.
🔁 Resiliência (retry / circuit breaker)
O SDK não impõe políticas de resiliência — você pluga no pipeline do HttpClient.
Com Microsoft.Extensions.Http.Resilience (recomendado .NET 8+)
dotnet add package Microsoft.Extensions.Http.Resilience
services.AddQsNotaFiscalClient(configuration)
.AddStandardResilienceHandler(); // retry + circuit breaker + timeout
Com Polly (tradicional)
services.AddQsNotaFiscalClient(configuration)
.AddTransientHttpErrorPolicy(p => p.WaitAndRetryAsync(
retryCount: 3,
sleepDurationProvider: attempt => TimeSpan.FromSeconds(Math.Pow(2, attempt))));
❗ Cuidado: retries automáticos não são seguros em operações que criam recurso (emissão). Use o CorrelationId como garantia — a API rejeita duplicatas com 409, então um retry após timeout ambíguo fica protegido.
📊 Observabilidade
O SDK emite logs via ILogger<QsNotaFiscalClient> (níveis Warning / Error).
Nada de logs Information verbosos — você controla seu próprio nível de ruído.
Para capturar detalhes finos do HTTP (headers, corpo, timing), habilite o logger
nativo do HttpClient:
{
"Logging": {
"LogLevel": {
"System.Net.Http.HttpClient.IQsNotaFiscalClient": "Information"
}
}
}
Para distributed tracing (OpenTelemetry), o HttpClient já instrumenta spans
automaticamente — basta registrar o HttpClient instrumentor:
services.AddOpenTelemetry()
.WithTracing(t => t.AddHttpClientInstrumentation());
🧪 Testes
IQsNotaFiscalClient é uma interface — stub com seu mocker favorito:
// NSubstitute
var client = Substitute.For<IQsNotaFiscalClient>();
client.EmitirAsync(Arg.Any<EmitirNotaFiscalRequest>(), default)
.Returns(QsNotaFiscalResult<EmitirNotaFiscalResponse>.Ok(
new EmitirNotaFiscalResponse { CorrelationId = "X", NotaFiscalId = Guid.NewGuid() }, 202));
Para testes de integração com HttpClient, use Microsoft.AspNetCore.Mvc.Testing
ou um DelegatingHandler customizado plugado via AddHttpMessageHandler.
🧬 Compatibilidade
| Target | Status | Observação |
|---|---|---|
| netstandard2.0 | ✅ | Compat máxima (NETFx 4.6.1+, Mono, Xamarin). |
| net6.0 | ✅ | LTS. |
| net8.0 | ✅ | Recomendado. |
Dependências principais:
Microsoft.Extensions.Http8.0.0Microsoft.Extensions.Options.DataAnnotations8.0.0System.Text.Json8.0.5 (apenas para netstandard2.0)
🏗️ Princípios de design
- Typed HttpClient via
IHttpClientFactory— gestão correta do ciclo de vida doHttpClient(evitaSocketExceptionpor esgotamento de portas). - Options pattern com validação por
DataAnnotationseValidateOnStart— falha no boot se a configuração estiver incompleta. - Result type para erros esperados, exceção para inesperados — clean-code PoEAA.
- Sem dependências pesadas — nenhum Polly, AutoMapper ou Newtonsoft embutido. O parceiro escolhe o que plugar.
- Imutabilidade onde pertinente (erros são
readonly, result ésealed). - Cancellation token em todos os métodos async.
- XML docs em toda a superfície pública — IntelliSense rico.
- XML comments + Source Link — navegação até o fonte no debug.
- Multi-target para alcançar a base instalada real do ecossistema .NET.
🛠️ Suporte
- 📧 suporte@qsnotas.com.br
- 🐛 Issues:
github.com/<org>/qsnotafiscal/issues - 📚 Docs API:
docs/integracoes-parceiro/integracao-parceiros.md
📄 Licença
MIT — veja o arquivo LICENSE no repositório.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 is compatible. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- Microsoft.Extensions.Http (>= 8.0.0)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 8.0.0)
- Microsoft.Extensions.Options.DataAnnotations (>= 8.0.0)
- System.Text.Json (>= 8.0.5)
-
net6.0
- Microsoft.Extensions.Http (>= 8.0.0)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 8.0.0)
- Microsoft.Extensions.Options.DataAnnotations (>= 8.0.0)
-
net8.0
- Microsoft.Extensions.Http (>= 8.0.0)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 8.0.0)
- Microsoft.Extensions.Options.DataAnnotations (>= 8.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.