Enviar o SPED e receber o relatório de ICMS-ST
O caminho completo, do arquivo ao resumo em JSON: cadastrar a empresa, enviar o SPED, esperar a leitura, pedir o relatório e ler o resultado.
1. Cadastre a empresa
O CNPJ do SPED precisa ser de uma empresa da conta; senão o envio responde 422 empresa_nao_cadastrada.
{ "cnpj": "11222333000181", "nome": "Empresa Exemplo Comércio Ltda" }
CNPJ que já está na conta responde 409 empresa_ja_cadastrada. Conta de parceiro informa também o rede_id da carteira.
2. Envie o arquivo e espere a leitura
POST /v1/arquivos, multipart/form-data, campo arquivo, um arquivo por chamada, com Idempotency-Key. Tipo, CNPJ e período saem do próprio arquivo. A resposta é 202 com o arquivo e o Location do job de leitura; acompanhe com GET /v1/jobs/{id}, respeitando o Retry-After, ou receba o webhook.
- cURL
- TypeScript
- Python
- PHP
- C#
- Java
- Delphi
#!/bin/sh
# Envia um SPED, espera o job de leitura (respeitando o Retry-After) e mostra o estado do arquivo.
# 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)}"
ARQUIVO=${RMF_ARQUIVO:-efd-contribuicoes-11222333000181-2025-01.txt}
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
# Um UUID por operação lógica; o MESMO UUID em todas as tentativas do envio.
chave=$(uuidgen 2>/dev/null || cat /proc/sys/kernel/random/uuid)
espera() { awk -F': ' 'tolower($1)=="retry-after" {print int($2)}' "$tmp/cabecalhos"; }
get() { # get <caminho>: grava o corpo em $tmp/corpo e devolve o status; em 429, espera o Retry-After e repete
for _ in 1 2 3 4 5; do
s=$(curl -sS "$RMF_API_URL$1" -H "Authorization: Bearer $RMF_API_KEY" \
-D "$tmp/cabecalhos" -o "$tmp/corpo" -w '%{http_code}')
[ "$s" = 429 ] || break
pausa=$(espera)
sleep "${pausa:-1}"
done
echo "$s"
}
falha() { jq -r '"\(.codigo) (\(.request_id)): \(.detail)"' "$tmp/corpo" >&2; exit 1; }
status=0
for tentativa in 1 2 3 4 5; do
status=$(curl -sS "$RMF_API_URL/arquivos" -H "Authorization: Bearer $RMF_API_KEY" \
-H "Idempotency-Key: $chave" -F "arquivo=@$ARQUIVO" \
-D "$tmp/cabecalhos" -o "$tmp/corpo" -w '%{http_code}')
[ "$status" = 429 ] || break
pausa=$(espera)
sleep "${pausa:-1}"
done
[ "$status" -lt 400 ] || falha
arquivo_id=$(jq -r .id "$tmp/corpo")
# 202: a leitura segue em segundo plano e o job vem em Location. 200: o mesmo conteúdo já foi enviado.
job_id=$(awk -F': ' 'tolower($1)=="location" {sub(/\r$/, "", $2); n=split($2, p, "/"); print p[n]}' "$tmp/cabecalhos")
estado_job=-
if [ -n "$job_id" ]; then
for _ in $(seq 1 90); do
[ "$(get "/jobs/$job_id")" -lt 400 ] || falha
estado_job=$(jq -r .estado "$tmp/corpo")
case "$estado_job" in
concluido | concluido_sem_dados | cancelado) break ;;
falhou) jq -r '"falhou: \(.erro.codigo)"' "$tmp/corpo" >&2; exit 1 ;;
esac
pausa=$(espera)
sleep "${pausa:-2}"
done
fi
[ "$(get "/arquivos/$arquivo_id")" -lt 400 ] || falha
echo "arquivo=$arquivo_id estado=$(jq -r .estado "$tmp/corpo") job=$estado_job"
// Envia um SPED, espera o job de leitura (respeitando o Retry-After) e mostra o estado do arquivo.
// Node 22.18 ou mais novo. Precisa de RMF_API_URL e RMF_API_KEY.
import { randomUUID } from "node:crypto";
import { readFile } from "node:fs/promises";
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);
}
const ARQUIVO = process.env.RMF_ARQUIVO ?? "efd-contribuicoes-11222333000181-2025-01.txt";
const TERMINAIS = ["concluido", "concluido_sem_dados", "falhou", "cancelado"];
const dormir = (s: number) => new Promise((r) => setTimeout(r, s * 1000));
async function chamar(caminho: string, init: RequestInit = {}): Promise<Response> {
for (let tentativa = 1; ; tentativa++) {
const resp = await fetch(`${BASE}${caminho}`, {
...init,
headers: { Authorization: `Bearer ${CHAVE}`, ...init.headers },
});
// Nos GET, 429 espera o Retry-After e repete (o envio tem o seu laço, com a mesma Idempotency-Key).
if (resp.status === 429 && !init.body && tentativa < 5) {
await dormir(Number(resp.headers.get("Retry-After") ?? "1"));
continue;
}
if (resp.status >= 400 && resp.status !== 429) {
const erro = await resp.json(); // application/problem+json
throw new Error(`${erro.codigo} (${erro.request_id}): ${erro.detail}`);
}
return resp;
}
}
async function enviar(caminho: string): Promise<Response> {
// Um UUID por operação lógica; o MESMO UUID em todas as tentativas do envio.
const chave = randomUUID();
const conteudo = await readFile(caminho);
for (let tentativa = 1; tentativa <= 5; tentativa++) {
const form = new FormData();
form.append("arquivo", new Blob([conteudo]), caminho.split("/").pop());
const resp = await chamar("/arquivos", { method: "POST", headers: { "Idempotency-Key": chave }, body: form });
if (resp.status !== 429) return resp;
await dormir(Number(resp.headers.get("Retry-After") ?? "1"));
}
throw new Error("limite_excedido: tente mais tarde");
}
const resp = await enviar(ARQUIVO);
const arquivo = await resp.json();
// 202: a leitura segue em segundo plano e o job vem em Location. 200: o mesmo conteúdo já foi enviado.
const location = resp.headers.get("Location");
let estadoJob = "-";
if (location) {
for (let i = 0; i < 90; i++) {
const job = await (await chamar(location.replace(/^\/v1/, ""))).json();
estadoJob = job.estado;
if (job.estado === "falhou") throw new Error(`falhou: ${job.erro?.codigo}`);
if (TERMINAIS.includes(job.estado)) break;
await dormir(Number(resp.headers.get("Retry-After") ?? "2"));
}
}
const atual = await (await chamar(`/arquivos/${arquivo.id}`)).json();
console.log(`arquivo=${arquivo.id} estado=${atual.estado} job=${estadoJob}`);
"""Envia um SPED, espera o job de leitura (respeitando o Retry-After) e mostra o estado do arquivo.
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 uuid
from pathlib import Path
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")
ARQUIVO = Path(os.environ.get("RMF_ARQUIVO") or "efd-contribuicoes-11222333000181-2025-01.txt")
TERMINAIS = {"concluido", "concluido_sem_dados", "falhou", "cancelado"}
HEADERS = {"Authorization": f"Bearer {CHAVE}"}
def conferir(resp: requests.Response) -> requests.Response:
if resp.status_code >= 400:
erro = resp.json() # application/problem+json
raise RuntimeError(f"{erro['codigo']} ({erro['request_id']}): {erro.get('detail')}")
return resp
def obter(caminho: str) -> requests.Response:
# GET com 429: espera o Retry-After e repete.
for _ in range(5):
resp = requests.get(BASE + caminho, headers=HEADERS, timeout=15)
if resp.status_code != 429:
return conferir(resp)
time.sleep(int(resp.headers.get("Retry-After", "1")))
raise RuntimeError("limite_excedido: tente mais tarde")
def enviar(caminho: Path) -> requests.Response:
# Um UUID por operação lógica; o MESMO UUID em todas as tentativas do envio.
chave = str(uuid.uuid4())
for _ in range(5):
with caminho.open("rb") as f:
resp = requests.post(
f"{BASE}/arquivos",
headers={**HEADERS, "Idempotency-Key": chave},
files={"arquivo": (caminho.name, f)},
timeout=60,
)
if resp.status_code != 429:
return conferir(resp)
time.sleep(int(resp.headers.get("Retry-After", "1")))
raise RuntimeError("limite_excedido: tente mais tarde")
resp = enviar(ARQUIVO)
arquivo = resp.json()
# 202: a leitura segue em segundo plano e o job vem em Location. 200: o mesmo conteúdo já foi enviado.
location = resp.headers.get("Location")
estado_job = "-"
if location:
for _ in range(90):
r = obter(location.removeprefix("/v1"))
job = r.json()
estado_job = job["estado"]
if estado_job == "falhou":
raise RuntimeError(f"falhou: {(job.get('erro') or {}).get('codigo')}")
if estado_job in TERMINAIS:
break
time.sleep(int(r.headers.get("Retry-After", "2")))
atual = obter(f"/arquivos/{arquivo['id']}").json()
print(f"arquivo={arquivo['id']} estado={atual['estado']} job={estado_job}")
<?php
// Envia um SPED, espera o job de leitura (respeitando o Retry-After) e mostra o estado do arquivo.
// 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);
}
$arquivo = getenv('RMF_ARQUIVO') ?: 'efd-contribuicoes-11222333000181-2025-01.txt';
const TERMINAIS = ['concluido', 'concluido_sem_dados', 'falhou', 'cancelado'];
/** GET com 429: espera o Retry-After e repete. Os outros métodos (o envio tem o seu laço) seguem direto. */
function chamar(string $base, string $chave, string $metodo, string $caminho, array $extra = [], $corpo = null): array
{
for ($tentativa = 1; ; $tentativa++) {
$r = chamarUma($base, $chave, $metodo, $caminho, $extra, $corpo);
if ($r[0] !== 429 || $metodo !== 'GET' || $tentativa >= 5) {
return $r;
}
sleep((int) ($r[2]['retry-after'] ?? 1));
}
}
/** Faz a chamada e devolve [status, corpo decodificado, cabeçalhos em minúsculas]. */
function chamarUma(string $base, string $chave, string $metodo, string $caminho, array $extra = [], $corpo = null): array
{
$cab = [];
$ch = curl_init($base . $caminho);
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => $metodo,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => array_merge(["Authorization: Bearer $chave"], $extra),
CURLOPT_HEADERFUNCTION => function ($ch, $linha) use (&$cab) {
if (str_contains($linha, ':')) {
[$nome, $valor] = explode(':', $linha, 2);
$cab[strtolower(trim($nome))] = trim($valor);
}
return strlen($linha);
},
]);
if ($corpo !== null) {
curl_setopt($ch, CURLOPT_POSTFIELDS, $corpo);
}
$texto = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
$json = json_decode($texto, true);
if ($status >= 400 && $status !== 429) { // application/problem+json
throw new RuntimeException("{$json['codigo']} ({$json['request_id']}): {$json['detail']}");
}
return [$status, $json, $cab];
}
// Um UUID por operação lógica; o MESMO UUID em todas as tentativas do envio.
$b = random_bytes(16);
$b[6] = chr((ord($b[6]) & 0x0f) | 0x40);
$b[8] = chr((ord($b[8]) & 0x3f) | 0x80);
$idempotencia = vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($b), 4));
for ($tentativa = 1; $tentativa <= 5; $tentativa++) {
[$status, $arq, $cab] = chamar($base, $chave, 'POST', '/arquivos', ["Idempotency-Key: $idempotencia"],
['arquivo' => new CURLFile($arquivo, 'text/plain', basename($arquivo))]);
if ($status !== 429) {
break;
}
sleep((int) ($cab['retry-after'] ?? 1));
}
if ($status === 429) {
throw new RuntimeException('limite_excedido: tente mais tarde');
}
// 202: a leitura segue em segundo plano e o job vem em Location. 200: o mesmo conteúdo já foi enviado.
$estadoJob = '-';
if (isset($cab['location'])) {
for ($i = 0; $i < 90; $i++) {
[, $job, $cabJob] = chamar($base, $chave, 'GET', preg_replace('#^/v1#', '', $cab['location']));
$estadoJob = $job['estado'];
if ($estadoJob === 'falhou') {
throw new RuntimeException('falhou: ' . ($job['erro']['codigo'] ?? ''));
}
if (in_array($estadoJob, TERMINAIS, true)) {
break;
}
sleep((int) ($cabJob['retry-after'] ?? 2));
}
}
[, $atual] = chamar($base, $chave, 'GET', '/arquivos/' . $arq['id']);
echo "arquivo={$arq['id']} estado={$atual['estado']} job=$estadoJob", PHP_EOL;
// Envia um SPED, espera o job de leitura (respeitando o Retry-After) e mostra o estado do arquivo.
// .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 arquivo = Environment.GetEnvironmentVariable("RMF_ARQUIVO") ?? "efd-contribuicoes-11222333000181-2025-01.txt";
string[] terminais = { "concluido", "concluido_sem_dados", "falhou", "cancelado" };
var http = new HttpClient { BaseAddress = new Uri(baseUrl.TrimEnd('/') + "/") };
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", chave);
// Um UUID por operação lógica; o MESMO UUID em todas as tentativas do envio.
var idempotencia = Guid.NewGuid().ToString();
HttpResponseMessage resp = null!;
for (var tentativa = 1; tentativa <= 5; tentativa++)
{
using var form = new MultipartFormDataContent();
form.Add(new ByteArrayContent(await File.ReadAllBytesAsync(arquivo)), "arquivo", Path.GetFileName(arquivo));
using var req = new HttpRequestMessage(HttpMethod.Post, "arquivos") { Content = form };
req.Headers.Add("Idempotency-Key", idempotencia);
resp = await http.SendAsync(req);
if (resp.StatusCode != HttpStatusCode.TooManyRequests) break;
await Task.Delay(resp.Headers.RetryAfter?.Delta ?? TimeSpan.FromSeconds(1));
}
var arq = await LerJson(resp);
// 202: a leitura segue em segundo plano e o job vem em Location. 200: o mesmo conteúdo já foi enviado.
var estadoJob = "-";
if (resp.Headers.Location is { } location)
{
for (var i = 0; i < 90; i++)
{
using var r = await Obter(location.ToString().Replace("/v1/", ""));
var job = await LerJson(r);
estadoJob = job.GetProperty("estado").GetString();
if (estadoJob == "falhou") throw new Exception($"falhou: {job.GetProperty("erro").GetProperty("codigo")}");
if (terminais.Contains(estadoJob)) break;
await Task.Delay(r.Headers.RetryAfter?.Delta ?? TimeSpan.FromSeconds(2));
}
}
using var ra = await Obter($"arquivos/{arq.GetProperty("id").GetString()}");
var atual = await LerJson(ra);
Console.WriteLine($"arquivo={arq.GetProperty("id").GetString()} estado={atual.GetProperty("estado").GetString()} job={estadoJob}");
return 0;
async Task<HttpResponseMessage> Obter(string caminho) // GET com 429: espera o Retry-After e repete
{
for (var tentativa = 1; tentativa <= 5; tentativa++)
{
var r = await http.GetAsync(caminho);
if (r.StatusCode != HttpStatusCode.TooManyRequests) return r;
await Task.Delay(r.Headers.RetryAfter?.Delta ?? TimeSpan.FromSeconds(1));
r.Dispose();
}
throw new Exception("limite_excedido: tente mais tarde");
}
async Task<JsonElement> LerJson(HttpResponseMessage r)
{
using var doc = JsonDocument.Parse(await r.Content.ReadAsStringAsync());
var e = doc.RootElement.Clone();
if ((int)r.StatusCode >= 400) // application/problem+json
throw new Exception($"{e.GetProperty("codigo")} ({e.GetProperty("request_id")}): {e.GetProperty("detail")}");
return e;
}
// Envia um SPED, espera o job de leitura (respeitando o Retry-After) e mostra o estado do arquivo.
// 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.io.ByteArrayOutputStream;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.UUID;
public class Envio {
static final String BASE = System.getenv("RMF_API_URL");
static final String CHAVE = System.getenv("RMF_API_KEY");
static final List<String> TERMINAIS = List.of("concluido", "concluido_sem_dados", "falhou", "cancelado");
static final HttpClient HTTP = HttpClient.newHttpClient();
static final ObjectMapper JSON = new ObjectMapper();
static HttpResponse<String> conferir(HttpResponse<String> resp) throws Exception {
if (resp.statusCode() >= 400) { // application/problem+json
var e = JSON.readTree(resp.body());
throw new RuntimeException(e.path("codigo").asText() + " (" + e.path("request_id").asText()
+ "): " + e.path("detail").asText());
}
return resp;
}
static HttpResponse<String> get(String caminho) throws Exception {
var req = HttpRequest.newBuilder(URI.create(BASE + caminho))
.header("Authorization", "Bearer " + CHAVE).GET().build();
for (int tentativa = 1; tentativa <= 5; tentativa++) {
var resp = HTTP.send(req, HttpResponse.BodyHandlers.ofString());
if (resp.statusCode() != 429) return conferir(resp);
Thread.sleep(espera(resp, 1) * 1000); // 429: espera o Retry-After e repete
}
throw new RuntimeException("limite_excedido: tente mais tarde");
}
static long espera(HttpResponse<?> resp, long padrao) {
return Long.parseLong(resp.headers().firstValue("Retry-After").orElse(String.valueOf(padrao)));
}
static HttpResponse<String> enviar(Path arquivo) throws Exception {
// Um UUID por operação lógica; o MESMO UUID em todas as tentativas do envio.
var idempotencia = UUID.randomUUID().toString();
var limite = "rmf" + UUID.randomUUID().toString().replace("-", "");
var corpo = new ByteArrayOutputStream();
corpo.write(("--" + limite + "\r\nContent-Disposition: form-data; name=\"arquivo\"; filename=\""
+ arquivo.getFileName() + "\"\r\nContent-Type: text/plain\r\n\r\n").getBytes(StandardCharsets.UTF_8));
corpo.write(Files.readAllBytes(arquivo));
corpo.write(("\r\n--" + limite + "--\r\n").getBytes(StandardCharsets.UTF_8));
for (int tentativa = 1; tentativa <= 5; tentativa++) {
var req = HttpRequest.newBuilder(URI.create(BASE + "/arquivos"))
.header("Authorization", "Bearer " + CHAVE)
.header("Idempotency-Key", idempotencia)
.header("Content-Type", "multipart/form-data; boundary=" + limite)
.POST(HttpRequest.BodyPublishers.ofByteArray(corpo.toByteArray())).build();
var resp = HTTP.send(req, HttpResponse.BodyHandlers.ofString());
if (resp.statusCode() != 429) return conferir(resp);
Thread.sleep(espera(resp, 1) * 1000);
}
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 caminho = Path.of(System.getenv().getOrDefault("RMF_ARQUIVO", "efd-contribuicoes-11222333000181-2025-01.txt"));
var resp = enviar(caminho);
JsonNode arquivo = JSON.readTree(resp.body());
// 202: a leitura segue em segundo plano e o job vem em Location. 200: o mesmo conteúdo já foi enviado.
var estadoJob = "-";
var location = resp.headers().firstValue("Location");
if (location.isPresent()) {
for (int i = 0; i < 90; i++) {
var r = get(location.get().replaceFirst("^/v1", ""));
var job = JSON.readTree(r.body());
estadoJob = job.path("estado").asText();
if (estadoJob.equals("falhou")) {
throw new RuntimeException("falhou: " + job.path("erro").path("codigo").asText());
}
if (TERMINAIS.contains(estadoJob)) break;
Thread.sleep(espera(r, 2) * 1000);
}
}
var atual = JSON.readTree(get("/arquivos/" + arquivo.path("id").asText()).body());
System.out.println("arquivo=" + arquivo.path("id").asText() + " estado=" + atual.path("estado").asText()
+ " job=" + estadoJob);
}
}
program Envio;
// Envia um SPED, espera o job de leitura (respeitando o Retry-After) e mostra o estado do arquivo.
// 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.Classes, System.JSON, System.Net.HttpClient, System.Net.URLClient,
System.Net.Mime;
var
Base, Chave, Caminho: string;
Http: THTTPClient;
procedure Conferir(const AResp: IHTTPResponse);
var
Erro: TJSONObject;
begin
if AResp.StatusCode < 400 then Exit;
Erro := TJSONObject.ParseJSONValue(AResp.ContentAsString(TEncoding.UTF8)) as TJSONObject; // problem+json
try
raise Exception.CreateFmt('%s (%s): %s', [Erro.GetValue<string>('codigo'),
Erro.GetValue<string>('request_id'), Erro.GetValue<string>('detail')]);
finally
Erro.Free;
end;
end;
function Enviar(const ACaminho: string): IHTTPResponse;
var
Idempotencia: string;
Form: TMultipartFormData;
Tentativa: Integer;
begin
// Um UUID por operação lógica; o MESMO UUID em todas as tentativas do envio.
Idempotencia := GUIDToString(TGUID.NewGuid).Trim(['{', '}']).ToLower;
for Tentativa := 1 to 5 do
begin
Form := TMultipartFormData.Create;
try
Form.AddFile('arquivo', ACaminho, 'text/plain');
Result := Http.Post(Base + '/arquivos', Form, nil,
[TNetHeader.Create('Idempotency-Key', Idempotencia)]);
finally
Form.Free;
end;
if Result.StatusCode <> 429 then
begin
Conferir(Result);
Exit;
end;
Sleep(StrToIntDef(Result.HeaderValue['Retry-After'], 1) * 1000);
end;
raise Exception.Create('limite_excedido: tente mais tarde');
end;
function Obter(const ACaminho: string; out AResp: IHTTPResponse): TJSONObject;
var
Tentativa: Integer;
begin
for Tentativa := 1 to 5 do
begin
AResp := Http.Get(Base + ACaminho);
if AResp.StatusCode <> 429 then Break;
Sleep(StrToIntDef(AResp.HeaderValue['Retry-After'], 1) * 1000); // 429: espera o Retry-After e repete
end;
Conferir(AResp);
Result := TJSONObject.ParseJSONValue(AResp.ContentAsString(TEncoding.UTF8)) as TJSONObject;
end;
var
Resp, RJob: IHTTPResponse;
Arquivo, Job, Atual: TJSONObject;
Location, EstadoJob, ArquivoId: string;
I: Integer;
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;
Caminho := GetEnvironmentVariable('RMF_ARQUIVO');
if Caminho = '' then Caminho := 'efd-contribuicoes-11222333000181-2025-01.txt';
Http := THTTPClient.Create;
try
Http.CustomHeaders['Authorization'] := 'Bearer ' + Chave;
Resp := Enviar(Caminho);
Arquivo := TJSONObject.ParseJSONValue(Resp.ContentAsString(TEncoding.UTF8)) as TJSONObject;
try
ArquivoId := Arquivo.GetValue<string>('id');
// 202: a leitura segue em segundo plano e o job vem em Location. 200: o mesmo conteúdo já foi enviado.
Location := Resp.HeaderValue['Location'];
EstadoJob := '-';
if Location <> '' then
for I := 1 to 90 do
begin
Job := Obter(Location.Replace('/v1', '', []), RJob);
try
EstadoJob := Job.GetValue<string>('estado');
if EstadoJob = 'falhou' then
raise Exception.Create('falhou: ' + Job.GetValue<TJSONObject>('erro').GetValue<string>('codigo'));
finally
Job.Free;
end;
if (EstadoJob = 'concluido') or (EstadoJob = 'concluido_sem_dados') or (EstadoJob = 'cancelado') then
Break;
Sleep(StrToIntDef(RJob.HeaderValue['Retry-After'], 2) * 1000);
end;
Atual := Obter('/arquivos/' + ArquivoId, Resp);
try
Writeln('arquivo=', ArquivoId, ' estado=', Atual.GetValue<string>('estado'), ' job=', EstadoJob);
finally
Atual.Free;
end;
finally
Arquivo.Free;
end;
finally
Http.Free;
end;
end.
- Mesmo conteúdo enviado de novo pela mesma conta:
200com o arquivo existente eduplicado: true. - Período já lido para o mesmo CNPJ: o arquivo termina em
periodo_ja_lidoe não é usado de novo. - Uma retificadora substitui o arquivo anterior, que passa a
substituido; o camposubstituido arquivo novo diz qual. - Estados do arquivo:
recebido,na_fila,lendo,lido,periodo_ja_lido,substituido,rejeitado(com omotivo).
No sandbox, o limite do arquivo é 20 MB e os resultados são fixos para os arquivos de exemplo. Baixe o efd-contribuicoes-11222333000181-2025-01.txt sintético (a lista completa está em Sandbox).
3. Peça o relatório
Com o arquivo em lido, POST /v1/relatorios com Idempotency-Key:
{
"tipo": "icms_st",
"cnpj": "11222333000181",
"periodo": { "inicio": "2025-01", "fim": "2025-01" },
"opcoes": { "periodicidade_saida": "mensal" }
}
Os tipos disponíveis para a sua conta estão em GET /v1/relatorios/tipos; consulte-o em vez de fixar a lista no código. Se faltar o arquivo de uma competência, o relatório falha com arquivo_faltando. Neste lançamento os relatórios aceitam só empresa com CNPJ numérico (14 dígitos): um CNPJ alfanumérico responde 422 parametro_invalido. O relatório planilha_sped gera uma planilha só, do primeiro tipo de SPED com arquivo lido no período.
4. Leia o resumo
Espere o job (ou o evento relatorio.concluido) e leia GET /v1/relatorios/{id}. O resumo em JSON vem sempre; a planilha de resultado só aparece em arquivos quando o plano da conta a inclui, e se baixa com GET /v1/relatorios/{id}/arquivos/{arquivo_id}.
{
"id": "rel_01JC8F6A9V1T4B7C2D5E8FGHJK",
"tipo": "icms_st",
"estado": "concluido",
"resumo": {
"total_credito": "18234.57",
"por_competencia": [{ "competencia": "2026-01", "total_credito": "2011.40" }],
"versao_base_aliquotas": "e964db7cb0e8"
},
"avisos": ["Há NCMs com alíquota de ICMS a confirmar no período. Confira esses NCMs em GET /icms/aliquotas e fale com a equipe da RMF antes de usar o crédito em apuração."]
}
Mostre os avisos ao usuário. Estados do relatório: na_fila, processando, concluido, concluido_sem_dados, falhou (com erro.codigo) e cancelado.
Esta página em Markdown: https://docs.rmfestrategia.com/guias/enviar-sped.md