Conferir a alíquota de ICMS antes de emitir
Antes de emitir cada nota, consulte a alíquota de ICMS do NCM de cada item na UF de destino e avise quando a confianca for a_confirmar. A consulta não recebe dado da sua empresa.
1. Um NCM
GET /v1/icms/aliquotas?ncm=&uf=&inicio=&fim= devolve a alíquota período a período. O exemplo completo, com tratamento de 429, está em Primeira chamada:
- cURL
- TypeScript
- Python
- PHP
- C#
- Java
- Delphi
#!/bin/sh
# Consulta a alíquota de ICMS de um NCM e trata o 429 (espera o Retry-After).
# Precisa de RMF_API_URL (comece pelo sandbox) e RMF_API_KEY no ambiente. Requer curl e jq.
set -eu
: "${RMF_API_URL:?defina RMF_API_URL (sandbox: https://sandbox.api.rmfestrategia.com/v1)}"
: "${RMF_API_KEY:?defina RMF_API_KEY (chave rmf_test_ no sandbox)}"
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
for tentativa in 1 2 3 4 5; do
# RMF_SIMULAR_429=1 pede ao sandbox um 429 na primeira tentativa (X-Sandbox-Simular: limite), para ver a espera funcionar
simular=""; [ "$tentativa" = 1 ] && [ "${RMF_SIMULAR_429:-}" = 1 ] && simular="X-Sandbox-Simular: limite"
status=$(curl -sS -G "$RMF_API_URL/icms/aliquotas" \
--data-urlencode ncm=85414121 --data-urlencode uf=SP \
--data-urlencode inicio=2026-10 --data-urlencode fim=2026-10 \
-H "Authorization: Bearer $RMF_API_KEY" -H 'Accept: application/json' ${simular:+-H "$simular"} \
-D "$tmp/cabecalhos" -o "$tmp/corpo" -w '%{http_code}')
if [ "$status" = 429 ]; then
espera=$(awk -F': ' 'tolower($1)=="retry-after" {print int($2)}' "$tmp/cabecalhos")
echo "429: aguardando ${espera:-1}s (tentativa $tentativa)" >&2
sleep "${espera:-1}"
continue
fi
if [ "$status" -ge 400 ]; then # application/problem+json
jq -r '"\(.codigo) (\(.request_id)): \(.detail)"' "$tmp/corpo" >&2
exit 1
fi
jq -r '.periodos[0] | "aliquota_interna=\(.aliquota_interna) aliquota_st=\(.aliquota_st) confianca=\(.confianca)"' "$tmp/corpo"
exit 0
done
echo "limite_excedido: tente mais tarde" >&2
exit 1
// Consulta a alíquota de ICMS de um NCM e trata o 429 (espera o Retry-After).
// Node 22.18 ou mais novo (fetch nativo; roda o .ts direto). Precisa de RMF_API_URL e RMF_API_KEY.
const BASE = process.env.RMF_API_URL;
const CHAVE = process.env.RMF_API_KEY;
if (!BASE || !CHAVE) {
console.error("defina RMF_API_URL (sandbox: https://sandbox.api.rmfestrategia.com/v1) e RMF_API_KEY");
process.exit(2);
}
type Periodo = {
inicio: string;
fim: string;
aliquota_interna: number;
aliquota_st: number | null;
na_st: boolean | null;
confianca: "confirmada" | "provavel" | "a_confirmar";
};
async function consultarAliquota(ncm: string, uf: string, competencia: string): Promise<Periodo> {
const url = new URL(`${BASE}/icms/aliquotas`);
url.search = new URLSearchParams({ ncm, uf, inicio: competencia, fim: competencia }).toString();
for (let tentativa = 1; tentativa <= 5; tentativa++) {
// RMF_SIMULAR_429=1 pede ao sandbox um 429 na primeira tentativa (X-Sandbox-Simular: limite)
const simular = tentativa === 1 && process.env.RMF_SIMULAR_429 === "1" ? { "X-Sandbox-Simular": "limite" } : {};
const resp = await fetch(url, {
headers: { Authorization: `Bearer ${CHAVE}`, Accept: "application/json", ...simular },
});
if (resp.status === 429) {
const espera = Number(resp.headers.get("Retry-After") ?? "1");
console.error(`429: aguardando ${espera}s (tentativa ${tentativa})`);
await new Promise((r) => setTimeout(r, espera * 1000));
continue;
}
if (!resp.ok) {
const erro = await resp.json(); // application/problem+json
throw new Error(`${erro.codigo} (${erro.request_id}): ${erro.detail}`);
}
const corpo = await resp.json();
return corpo.periodos[0];
}
throw new Error("limite_excedido: tente mais tarde");
}
const p = await consultarAliquota("85414121", "SP", "2026-10");
console.log(`aliquota_interna=${p.aliquota_interna} aliquota_st=${p.aliquota_st} confianca=${p.confianca}`);
"""Consulta a alíquota de ICMS de um NCM e trata o 429 (espera o Retry-After).
Python 3.9 ou mais novo, com requests. Precisa de RMF_API_URL e RMF_API_KEY no ambiente."""
import os
import sys
import time
import requests
BASE = os.environ.get("RMF_API_URL")
CHAVE = os.environ.get("RMF_API_KEY")
if not BASE or not CHAVE:
sys.exit("defina RMF_API_URL (sandbox: https://sandbox.api.rmfestrategia.com/v1) e RMF_API_KEY")
def consultar_aliquota(ncm: str, uf: str, competencia: str) -> dict:
params = {"ncm": ncm, "uf": uf, "inicio": competencia, "fim": competencia}
headers = {"Authorization": f"Bearer {CHAVE}", "Accept": "application/json"}
for tentativa in range(1, 6):
# RMF_SIMULAR_429=1 pede ao sandbox um 429 na primeira tentativa (X-Sandbox-Simular: limite)
extra = {"X-Sandbox-Simular": "limite"} if tentativa == 1 and os.environ.get("RMF_SIMULAR_429") == "1" else {}
resp = requests.get(f"{BASE}/icms/aliquotas", params=params, headers={**headers, **extra}, timeout=15)
if resp.status_code == 429:
espera = int(resp.headers.get("Retry-After", "1"))
print(f"429: aguardando {espera}s (tentativa {tentativa})", file=sys.stderr)
time.sleep(espera)
continue
if not resp.ok:
erro = resp.json() # application/problem+json
raise RuntimeError(f"{erro['codigo']} ({erro['request_id']}): {erro.get('detail')}")
return resp.json()["periodos"][0]
raise RuntimeError("limite_excedido: tente mais tarde")
p = consultar_aliquota("85414121", "SP", "2026-10")
print(f"aliquota_interna={p['aliquota_interna']} aliquota_st={p['aliquota_st']} confianca={p['confianca']}")
<?php
// Consulta a alíquota de ICMS de um NCM e trata o 429 (espera o Retry-After).
// PHP 8.1 ou mais novo, extensão curl. Precisa de RMF_API_URL e RMF_API_KEY no ambiente.
$base = getenv('RMF_API_URL');
$chave = getenv('RMF_API_KEY');
if (!$base || !$chave) {
fwrite(STDERR, "defina RMF_API_URL (sandbox: https://sandbox.api.rmfestrategia.com/v1) e RMF_API_KEY\n");
exit(2);
}
function consultarAliquota(string $base, string $chave, string $ncm, string $uf, string $competencia): array
{
$url = $base . '/icms/aliquotas?' . http_build_query([
'ncm' => $ncm, 'uf' => $uf, 'inicio' => $competencia, 'fim' => $competencia,
]);
for ($tentativa = 1; $tentativa <= 5; $tentativa++) {
$retryAfter = 1;
$ch = curl_init($url);
$cabecalhos = ["Authorization: Bearer $chave", 'Accept: application/json'];
// RMF_SIMULAR_429=1 pede ao sandbox um 429 na primeira tentativa (X-Sandbox-Simular: limite)
if ($tentativa === 1 && getenv('RMF_SIMULAR_429') === '1') {
$cabecalhos[] = 'X-Sandbox-Simular: limite';
}
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => $cabecalhos,
CURLOPT_HEADERFUNCTION => function ($ch, $linha) use (&$retryAfter) {
if (stripos($linha, 'Retry-After:') === 0) {
$retryAfter = (int) trim(substr($linha, 12));
}
return strlen($linha);
},
]);
$corpo = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($status === 429) {
fwrite(STDERR, "429: aguardando {$retryAfter}s (tentativa $tentativa)\n");
sleep($retryAfter);
continue;
}
$json = json_decode($corpo, true);
if ($status >= 400) { // application/problem+json
throw new RuntimeException("{$json['codigo']} ({$json['request_id']}): {$json['detail']}");
}
return $json['periodos'][0];
}
throw new RuntimeException('limite_excedido: tente mais tarde');
}
$p = consultarAliquota($base, $chave, '85414121', 'SP', '2026-10');
echo 'aliquota_interna=', $p['aliquota_interna'], ' aliquota_st=', $p['aliquota_st'] ?? 'null',
' confianca=', $p['confianca'], PHP_EOL;
// Consulta a alíquota de ICMS de um NCM e trata o 429 (espera o Retry-After).
// .NET 8 ou mais novo, top-level statements. Precisa de RMF_API_URL e RMF_API_KEY no ambiente.
using System.Net;
using System.Net.Http.Headers;
using System.Text.Json;
var baseUrl = Environment.GetEnvironmentVariable("RMF_API_URL");
var chave = Environment.GetEnvironmentVariable("RMF_API_KEY");
if (string.IsNullOrEmpty(baseUrl) || string.IsNullOrEmpty(chave))
{
Console.Error.WriteLine("defina RMF_API_URL (sandbox: https://sandbox.api.rmfestrategia.com/v1) e RMF_API_KEY");
return 2;
}
var http = new HttpClient { BaseAddress = new Uri(baseUrl.TrimEnd('/') + "/") };
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", chave);
var p = await ConsultarAliquota("85414121", "SP", "2026-10");
Console.WriteLine($"aliquota_interna={p.GetProperty("aliquota_interna")} aliquota_st={p.GetProperty("aliquota_st")} confianca={p.GetProperty("confianca").GetString()}");
return 0;
async Task<JsonElement> ConsultarAliquota(string ncm, string uf, string competencia)
{
var caminho = $"icms/aliquotas?ncm={ncm}&uf={uf}&inicio={competencia}&fim={competencia}";
for (var tentativa = 1; tentativa <= 5; tentativa++)
{
// RMF_SIMULAR_429=1 pede ao sandbox um 429 na primeira tentativa (X-Sandbox-Simular: limite)
using var req = new HttpRequestMessage(HttpMethod.Get, caminho);
if (tentativa == 1 && Environment.GetEnvironmentVariable("RMF_SIMULAR_429") == "1")
req.Headers.Add("X-Sandbox-Simular", "limite");
using var resp = await http.SendAsync(req);
if (resp.StatusCode == HttpStatusCode.TooManyRequests)
{
var espera = resp.Headers.RetryAfter?.Delta ?? TimeSpan.FromSeconds(1);
Console.Error.WriteLine($"429: aguardando {espera.TotalSeconds}s (tentativa {tentativa})");
await Task.Delay(espera);
continue;
}
using var doc = JsonDocument.Parse(await resp.Content.ReadAsStringAsync());
if (!resp.IsSuccessStatusCode) // application/problem+json
{
var e = doc.RootElement;
throw new Exception($"{e.GetProperty("codigo")} ({e.GetProperty("request_id")}): {e.GetProperty("detail")}");
}
return doc.RootElement.GetProperty("periodos")[0].Clone();
}
throw new Exception("limite_excedido: tente mais tarde");
}
// Consulta a alíquota de ICMS de um NCM e trata o 429 (espera o Retry-After).
// Java 17 ou mais novo, com Jackson (com.fasterxml.jackson.core:jackson-databind).
// Precisa de RMF_API_URL e RMF_API_KEY no ambiente.
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class Consulta {
static final String BASE = System.getenv("RMF_API_URL");
static final String CHAVE = System.getenv("RMF_API_KEY");
static final HttpClient HTTP = HttpClient.newHttpClient();
static final ObjectMapper JSON = new ObjectMapper();
static JsonNode consultarAliquota(String ncm, String uf, String competencia) throws Exception {
var uri = URI.create(BASE + "/icms/aliquotas?ncm=" + ncm + "&uf=" + uf
+ "&inicio=" + competencia + "&fim=" + competencia);
for (int tentativa = 1; tentativa <= 5; tentativa++) {
var pedido = HttpRequest.newBuilder(uri)
.header("Authorization", "Bearer " + CHAVE)
.header("Accept", "application/json");
// RMF_SIMULAR_429=1 pede ao sandbox um 429 na primeira tentativa (X-Sandbox-Simular: limite)
if (tentativa == 1 && "1".equals(System.getenv("RMF_SIMULAR_429"))) {
pedido.header("X-Sandbox-Simular", "limite");
}
var resp = HTTP.send(pedido.GET().build(), HttpResponse.BodyHandlers.ofString());
if (resp.statusCode() == 429) {
long espera = Long.parseLong(resp.headers().firstValue("Retry-After").orElse("1"));
System.err.println("429: aguardando " + espera + "s (tentativa " + tentativa + ")");
Thread.sleep(espera * 1000);
continue;
}
var corpo = JSON.readTree(resp.body());
if (resp.statusCode() >= 400) { // application/problem+json
throw new RuntimeException(corpo.path("codigo").asText() + " ("
+ corpo.path("request_id").asText() + "): " + corpo.path("detail").asText());
}
return corpo.path("periodos").get(0);
}
throw new RuntimeException("limite_excedido: tente mais tarde");
}
public static void main(String[] args) throws Exception {
if (BASE == null || CHAVE == null) {
System.err.println("defina RMF_API_URL (sandbox: https://sandbox.api.rmfestrategia.com/v1) e RMF_API_KEY");
System.exit(2);
}
var p = consultarAliquota("85414121", "SP", "2026-10");
System.out.println("aliquota_interna=" + p.get("aliquota_interna") + " aliquota_st=" + p.get("aliquota_st")
+ " confianca=" + p.get("confianca").asText());
}
}
program Consulta;
// Consulta a alíquota de ICMS de um NCM e trata o 429 (espera o Retry-After).
// Delphi 10.3 ou mais novo. Precisa de RMF_API_URL e RMF_API_KEY no ambiente.
// Exemplo escrito e revisado à mão: não roda no CI (não existe compilador Delphi em container).
{$APPTYPE CONSOLE}
uses
System.SysUtils, System.JSON, System.Net.HttpClient, System.Net.URLClient;
function ConsultarAliquota(const ABase, AChave, ANcm, AUf, ACompetencia: string): TJSONObject;
var
Http: THTTPClient;
Resp: IHTTPResponse;
Corpo: TJSONObject;
Tentativa, Espera: Integer;
begin
Http := THTTPClient.Create;
try
Http.CustomHeaders['Authorization'] := 'Bearer ' + AChave;
Http.CustomHeaders['Accept'] := 'application/json';
for Tentativa := 1 to 5 do
begin
// RMF_SIMULAR_429=1 pede ao sandbox um 429 na primeira tentativa (X-Sandbox-Simular: limite)
if (Tentativa = 1) and (GetEnvironmentVariable('RMF_SIMULAR_429') = '1') then
Http.CustomHeaders['X-Sandbox-Simular'] := 'limite'
else
Http.CustomHeaders['X-Sandbox-Simular'] := '';
Resp := Http.Get(Format('%s/icms/aliquotas?ncm=%s&uf=%s&inicio=%s&fim=%s',
[ABase, ANcm, AUf, ACompetencia, ACompetencia]));
if Resp.StatusCode = 429 then
begin
Espera := StrToIntDef(Resp.HeaderValue['Retry-After'], 1);
Writeln(ErrOutput, Format('429: aguardando %ds (tentativa %d)', [Espera, Tentativa]));
Sleep(Espera * 1000);
Continue;
end;
Corpo := TJSONObject.ParseJSONValue(Resp.ContentAsString(TEncoding.UTF8)) as TJSONObject;
if Resp.StatusCode >= 400 then // application/problem+json
try
raise Exception.CreateFmt('%s (%s): %s', [Corpo.GetValue<string>('codigo'),
Corpo.GetValue<string>('request_id'), Corpo.GetValue<string>('detail')]);
finally
Corpo.Free;
end;
Exit(Corpo);
end;
raise Exception.Create('limite_excedido: tente mais tarde');
finally
Http.Free;
end;
end;
var
Base, Chave: string;
Resposta, Periodo: TJSONObject;
begin
Base := GetEnvironmentVariable('RMF_API_URL');
Chave := GetEnvironmentVariable('RMF_API_KEY');
if (Base = '') or (Chave = '') then
begin
Writeln(ErrOutput, 'defina RMF_API_URL (sandbox: https://sandbox.api.rmfestrategia.com/v1) e RMF_API_KEY');
Halt(2);
end;
Resposta := ConsultarAliquota(Base, Chave, '85414121', 'SP', '2026-10');
try
Periodo := Resposta.GetValue<TJSONArray>('periodos').Items[0] as TJSONObject;
Writeln('aliquota_interna=', Periodo.GetValue('aliquota_interna').ToString,
' aliquota_st=', Periodo.GetValue('aliquota_st').ToString,
' confianca=', Periodo.GetValue<string>('confianca'));
finally
Resposta.Free;
end;
end.
2. Vários itens de uma vez
POST /v1/icms/aliquotas/consultas aceita até 500 pares de NCM, UF e competência por chamada. Um item inválido não derruba o lote: ele volta com o erro do item (por exemplo, ncm_invalido, com a mensagem "Use 8 dígitos, sem ponto.") e os demais seguem. Acima de 500 itens a resposta é 413 lote_grande_demais.
3. Decida pela confiança
confianca | O que fazer no seu sistema |
|---|---|
confirmada | Use o valor |
provavel | Use e deixe o item visível para conferência |
a_confirmar | Mostre um aviso ao usuário e confirme antes de usar em apuração |
Guarde a versao_base junto do resultado: é o que permite reproduzir a consulta depois.
4. Veja o que a base cobre
GET /v1/icms/cobertura mostra, por UF, o que a base cobre. Hoje só SP tem mapa da lei; as demais UFs usam a alíquota padrão do estado e, por isso, toda alíquota fora de SP sai a_confirmar, com na_st e aliquota_st nulos. A cobertura cresce com a base: consulte o endpoint em vez de fixar a lista no seu código.
Próximos passos
- Referência de
consultarAliquotaIcmspara todos os parâmetros e respostas. - Limites e cotas se você vai consultar em volume.
Esta página em Markdown: https://docs.rmfestrategia.com/guias/conferir-aliquota-icms.md