AlchemyCode API docs
SAT · sat.articulo69b

Consulta Artículo 69-B

Cruza uno o varios RFC contra las listas definitivas y presuntas del SAT (Art. 69, 69-B y 69-B Bis). Devuelve situación, nivel de riesgo y motivo de facturación.

Síncrono · 200 1 unidad por consulta Sin costo en beta · consumo medido Requiere: Ninguno

Qué hace

Consulta síncrona contra las listas definitivas y presuntas del SAT (artículos 69, 69-B y 69-B Bis). Acepta un RFC o un lote de hasta 5,000 y devuelve situación, nivel de riesgo y si puede facturar. No crea job: responde en la misma petición.

Qué obtienes:

  • Situación de cada RFC en las listas del SAT.
  • Nivel de riesgo y motivo de facturación.
  • Versión de la lista consultada para trazabilidad.

Requisitos

Este servicio no requiere credenciales de empresa: puedes consultarlo con tu API key y, si envías company_id, el consumo se atribuye a esa empresa.
La latencia declarada en esta página es orientativa: 1 – 8 s típico (lotes grandes: hasta 30 s). El portal de origen puede tardar más en horas pico; tu integración debe tolerar reintentos y jobs lentos.

Petición

POST/v1/sat/articulo-69bAPI key con scope services:run
CampoTipoDescripción
company_idUUIDopcionalOpcional. Si se envía, el consumo se atribuye a esa empresa.
rfcstringopcionalUn solo RFC. Mutuamente excluyente con rfcs.
rfcsarray[string]opcionalLote de hasta 5,000 RFC. Mutuamente excluyente con rfc.
Cada consulta consume 1 unidad de tu cuota mensual, se cobre o no. El detalle del cálculo está en Límites y cuotas.

Respuesta

La respuesta es 200 OK y trae el resultado en data. No se crea job, no hay polling y no aplica wait.

JSON
{
  "object": "result",
  "service": "sat.articulo69b",
  "data": {
    "consultados": 3,
    "criticos": 1,
    "resultados": [
      {
        "rfc": "OMX2503255F9",
        "razon_social": "OMNICONMX, S.A. de C.V.",
        "en_69b": false,
        "situacion": "No listado",
        "nivel_riesgo": "ninguno",
        "puede_facturar": true,
        "motivo": "Sin coincidencias en listas definitivas",
        "listas": [],
        "ultima_actualizacion": "2026-10-30"
      },
      {
        "rfc": "EKU9003173C9",
        "razon_social": null,
        "en_69b": true,
        "situacion": "Presunto no localizado",
        "nivel_riesgo": "alto",
        "puede_facturar": false,
        "motivo": "Operaciones simuladas",
        "listas": [
          "69-B"
        ],
        "ultima_actualizacion": "2026-10-30"
      },
      {
        "rfc": "XAXX010101000",
        "razon_social": null,
        "en_69b": false,
        "situacion": "No listado",
        "nivel_riesgo": "ninguno",
        "puede_facturar": true,
        "motivo": "RFC genérico sin obligaciones de facturación",
        "listas": [],
        "ultima_actualizacion": "2026-10-30"
      }
    ],
    "lista_version": "2026-10-30"
  },
  "warnings": [],
  "sandbox": false,
  "elapsed_ms": 3421
}

Los servicios síncronos también se miden: cada petición consume las unidades del catálogo y se registra en /v1/usage con su latencia.

Síncrono por diseño

El servicio responde en la misma petición; no existe job, wait ni webhook de resultado. Esto simplifica la integración, pero exige cuidar el timeout de tu cliente.

AspectoComportamiento
Respuesta200 OK con object: "result".
Timeout recomendado60 s de lectura; lotes grandes pueden acercarse a 30 s.
IdempotenciaNo aplica Idempotency-Key: si reintentas, se ejecuta otra consulta.
ReintentosSólo ante 429, 502, 503 o 504, con backoff.
Límite de loterfcs hasta 5,000 por petición.
Para volúmenes grandes divide en lotes y procesa en paralelo con un límite de concurrencia. La cuota se evalúa antes de ejecutar: si no hay unidades, la respuesta es 429 QUOTA_EXCEEDED sin consumir.

Errores propios del servicio

CódigoHTTPCuándo ocurreReintentable
VALIDATION_ERROR422Enviaste rfc y rfcs, ninguno, o un lote mayor a 5,000.No
QUOTA_EXCEEDED429Sin unidades disponibles en el periodo.No
PROVIDER_ERROR502El proveedor de listas devolvió un error.Sí
PROVIDER_TIMEOUT504La consulta excedió el tiempo máximo; reintenta con backoff.Sí

Además aplican los errores de plataforma (AUTH_REQUIRED, SCOPE_FORBIDDEN, QUOTA_EXCEEDED, RATE_LIMITED, COMPANY_NOT_FOUND, JOB_*). El catálogo completo está en Errores.

Ejemplos completos

Los ocho ejemplos incluyen autenticación, Content-Type, lectura de errores problem+json y el flujo completo del trabajo. Reemplaza $ALCHEMY_API_KEY por una llave real y ajusta el company_id.

CURL
# Consulta Artículo 69-B · AlchemyCode API
curl -i -sS -X POST "https://api.434alchemycode.com/v1/sat/articulo-69b" \
  -H "Authorization: Bearer $ALCHEMY_API_KEY" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  --data-binary @- <<'JSON'
{
  "rfcs": [
    "OMX2503255F9",
    "EKU9003173C9",
    "XAXX010101000"
  ]
}
JSON

# Respuesta síncrona 200: el resultado viene en .data, sin job ni polling.
# Si hay error, el cuerpo es application/problem+json con .code estable.
PHP
<?php
declare(strict_types=1);

/**
 * Consulta Artículo 69-B · AlchemyCode API
 * Requiere PHP 8.1+ con ext-curl y ext-json. Sin dependencias externas.
 */

const ALCHEMY_BASE = 'https://api.434alchemycode.com/v1';

final class AlchemyError extends RuntimeException
{
    public function __construct(
        public readonly string $errorCode,
        public readonly int $status,
        public readonly bool $retryable,
        public readonly string $requestId,
        string $message,
    ) {
        parent::__construct($message, $status);
    }
}

function alchemy(string $method, string $path, ?array $payload = null): array
{
    $key = getenv('ALCHEMY_API_KEY');
    if ($key === false || $key === '') {
        throw new RuntimeException('Falta la variable de entorno ALCHEMY_API_KEY.');
    }

    $ch = curl_init(ALCHEMY_BASE . $path);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_CUSTOMREQUEST  => $method,
        CURLOPT_HTTPHEADER     => [
            'Authorization: Bearer ' . $key,
            'Accept: application/json',
            'Content-Type: application/json',
            'User-Agent: OmnicomERP/2.4 (+https://omnicom.mx)',
        ],
        CURLOPT_CONNECTTIMEOUT => 10,
        CURLOPT_TIMEOUT        => 45,
    ]);
    if ($payload !== null) {
        curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR));
    }

    $raw    = curl_exec($ch);
    $status = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
    $errno  = curl_errno($ch);
    $error  = curl_error($ch);
    curl_close($ch);

    if ($errno !== 0 || $raw === false) {
        // Fallo de red (sin respuesta HTTP): reintenta con backoff exponencial.
        throw new RuntimeException("Error de red cURL {$errno}: {$error}", $errno);
    }

    $body = json_decode((string) $raw, true);
    if (!is_array($body)) {
        throw new RuntimeException("Respuesta no JSON (HTTP {$status}).");
    }
    if ($status >= 400) {
        // application/problem+json (RFC 9457): `code` es estable, `detail` explicativo.
        throw new AlchemyError(
            (string) ($body['code'] ?? 'UNKNOWN'),
            $status,
            (bool) ($body['retryable'] ?? false),
            (string) ($body['request_id'] ?? 'n/a'),
            sprintf('[%s] %s', $body['code'] ?? 'UNKNOWN', $body['detail'] ?? $body['title'] ?? 'Error'),
        );
    }
    return $body;
}

$payload = json_decode(<<<'JSON'
{
  "rfcs": [
    "OMX2503255F9",
    "EKU9003173C9",
    "XAXX010101000"
  ]
}
JSON, true, 512, JSON_THROW_ON_ERROR);

try {
    $result = alchemy('POST', '/sat/articulo-69b', $payload);
    // 200 OK: el resultado viene en .data (servicio síncrono).
    echo json_encode($result['data'], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE), PHP_EOL;
} catch (AlchemyError $e) {
    fwrite(STDERR, "{$e->getMessage()} (HTTP {$e->status}, request_id {$e->requestId})\n");
    if ($e->retryable) {
        // 429, 502, 503 o 504: reintenta con backoff (1 s, 2 s, 4 s…).
    }
    exit(1);
}
Node.js
// Consulta Artículo 69-B · AlchemyCode API
// Requiere Node.js 18+ (fetch nativo). Ejecuta: node consulta.js

const BASE = 'https://api.434alchemycode.com/v1';
const API_KEY = process.env.ALCHEMY_API_KEY;
if (!API_KEY) throw new Error('Falta la variable de entorno ALCHEMY_API_KEY.');

async function alchemy(method, path, body) {
  const response = await fetch(BASE + path, {
    method,
    headers: {
      Authorization: `Bearer ${API_KEY}`,
      Accept: 'application/json',
      'Content-Type': 'application/json',
    },
    body: body === undefined ? undefined : JSON.stringify(body),
  });
  const text = await response.text();
  const data = text ? JSON.parse(text) : null;
  if (!response.ok) {
    const problem = data ?? {};
    const error = new Error(`[${problem.code ?? 'UNKNOWN'}] ${problem.detail ?? response.statusText}`);
    Object.assign(error, {
      status: response.status,
      code: problem.code,
      retryable: problem.retryable ?? false,
      requestId: problem.request_id,
    });
    throw error;
  }
  return data;
}

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

const payload = {
  "rfcs": [
    "OMX2503255F9",
    "EKU9003173C9",
    "XAXX010101000"
  ]
};

async function main() {
  try {
    const result = await alchemy('POST', '/sat/articulo-69b', payload);
    console.log(JSON.stringify(result.data, null, 2)); // 200 OK, síncrono
  } catch (error) {
    console.error(error.message, {
      status: error.status,
      code: error.code,
      retryable: error.retryable,
      requestId: error.requestId,
    });
    process.exitCode = error.retryable ? 2 : 1;
  }
}

main();
Python
"""Consulta Artículo 69-B · AlchemyCode API

Requiere Python 3.10+ y requests (pip install requests).
"""

from __future__ import annotations

import json
import os
import sys
import time

import requests

BASE = "https://api.434alchemycode.com/v1"
API_KEY = os.environ["ALCHEMY_API_KEY"]

session = requests.Session()
session.headers.update(
    {
        "Authorization": f"Bearer {API_KEY}",
        "Accept": "application/json",
        "Content-Type": "application/json",
        "User-Agent": "OmnicomERP/2.4 (+https://omnicom.mx)",
    }
)


class AlchemyError(RuntimeError):
    def __init__(self, status: int, problem: dict) -> None:
        self.status = status
        self.code = problem.get("code", "UNKNOWN")
        self.retryable = bool(problem.get("retryable"))
        self.request_id = problem.get("request_id")
        super().__init__(f"[{self.code}] {problem.get('detail') or problem.get('title')}")


def alchemy(method: str, path: str, payload: dict | None = None) -> dict:
    response = session.request(method, BASE + path, json=payload, timeout=(10, 45))
    if response.status_code >= 400:
        try:
            problem = response.json()
        except ValueError:
            problem = {"code": "UNKNOWN", "detail": response.text[:200]}
        raise AlchemyError(response.status_code, problem)
    return response.json()

payload = json.loads(r'''
{
  "rfcs": [
    "OMX2503255F9",
    "EKU9003173C9",
    "XAXX010101000"
  ]
}
''')


def main() -> None:
    try:
        result = alchemy("POST", "/sat/articulo-69b", payload)
        print(json.dumps(result["data"], indent=2, ensure_ascii=False))  # 200 OK
    except AlchemyError as exc:
        print(f"{exc} (HTTP {exc.status}, request_id {exc.request_id})", file=sys.stderr)
        raise SystemExit(2 if exc.retryable else 1)


if __name__ == "__main__":
    main()
Go
// Consulta Artículo 69-B · AlchemyCode API
// Requiere Go 1.21+. Ejecuta: go run consulta.go
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"time"
)

const baseURL = "https://api.434alchemycode.com/v1"

type problem struct {
	Code      string `json:"code"`
	Detail    string `json:"detail"`
	Title     string `json:"title"`
	RequestID string `json:"request_id"`
	Retryable bool   `json:"retryable"`
}

type jobFile struct {
	Name        string `json:"name"`
	ContentType string `json:"content_type"`
	URL         string `json:"url"`
}

func call(method, path string, payload any, out any) error {
	var body io.Reader
	if payload != nil {
		raw, err := json.Marshal(payload)
		if err != nil {
			return err
		}
		body = bytes.NewReader(raw)
	}

	req, err := http.NewRequest(method, baseURL+path, body)
	if err != nil {
		return err
	}
	req.Header.Set("Authorization", "Bearer "+os.Getenv("ALCHEMY_API_KEY"))
	req.Header.Set("Accept", "application/json")
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("User-Agent", "OmnicomERP/2.4 (+https://omnicom.mx)")

	client := &http.Client{Timeout: 45 * time.Second}
	resp, err := client.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	raw, err := io.ReadAll(resp.Body)
	if err != nil {
		return err
	}
	if resp.StatusCode >= 400 {
		var p problem
		_ = json.Unmarshal(raw, &p)
		return fmt.Errorf("[%s] %s (HTTP %d, request_id %s)", p.Code, p.Detail, resp.StatusCode, p.RequestID)
	}
	if out == nil {
		return nil
	}
	return json.Unmarshal(raw, out)
}

const payloadJSON = `{
  "rfcs": [
    "OMX2503255F9",
    "EKU9003173C9",
    "XAXX010101000"
  ]
}`

type syncResult struct {
	Data     map[string]any `json:"data"`
	Warnings []string       `json:"warnings"`
}

func main() {
	var payload map[string]any
	if err := json.Unmarshal([]byte(payloadJSON), &payload); err != nil {
		fmt.Fprintln(os.Stderr, "payload inválido:", err)
		os.Exit(1)
	}
	var result syncResult
	if err := call("POST", "/sat/articulo-69b", payload, &result); err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(1)
	}
	pretty, _ := json.MarshalIndent(result.Data, "", "  ")
	fmt.Println(string(pretty)) // 200 OK, servicio síncrono
}
Java
// Consulta Artículo 69-B · AlchemyCode API
// Requiere Java 17+ y Jackson (com.fasterxml.jackson.core:jackson-databind:2.17.2).
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;
import java.time.Duration;

public final class AlchemyConsulta {

    private static final String BASE = "https://api.434alchemycode.com/v1";
    private static final ObjectMapper JSON = new ObjectMapper();
    private static final HttpClient HTTP = HttpClient.newBuilder()
            .connectTimeout(Duration.ofSeconds(10))
            .build();

    private static JsonNode call(String method, String path, String body) throws Exception {
        String apiKey = System.getenv("ALCHEMY_API_KEY");
        if (apiKey == null || apiKey.isBlank()) {
            throw new IllegalStateException("Falta la variable de entorno ALCHEMY_API_KEY.");
        }

        HttpRequest.Builder builder = HttpRequest.newBuilder(URI.create(BASE + path))
                .timeout(Duration.ofSeconds(45))
                .header("Authorization", "Bearer " + apiKey)
                .header("Accept", "application/json")
                .header("Content-Type", "application/json")
                .header("User-Agent", "OmnicomERP/2.4 (+https://omnicom.mx)");
        builder.method(method, body == null
                ? HttpRequest.BodyPublishers.noBody()
                : HttpRequest.BodyPublishers.ofString(body));

        HttpResponse<String> response = HTTP.send(builder.build(), HttpResponse.BodyHandlers.ofString());
        JsonNode json = JSON.readTree(response.body());
        if (response.statusCode() >= 400) {
            throw new IllegalStateException(String.format(
                    "[%s] %s (HTTP %d, request_id %s)",
                    json.path("code").asText("UNKNOWN"),
                    json.path("detail").asText(json.path("title").asText("Error")),
                    response.statusCode(),
                    json.path("request_id").asText("n/a")));
        }
        return json;
    }

    public static void main(String[] args) throws Exception {
        String body = """
                {
                  "rfcs": [
                    "OMX2503255F9",
                    "EKU9003173C9",
                    "XAXX010101000"
                  ]
                }
                """;

        try {
            JsonNode result = call("POST", "/sat/articulo-69b", body);
            System.out.println(JSON.writerWithDefaultPrettyPrinter()
                    .writeValueAsString(result.path("data")));
        } catch (IllegalStateException error) {
            System.err.println(error.getMessage());
            System.exit(1);
        }
    }
}
DOTNET
// Consulta Artículo 69-B · AlchemyCode API
// Requiere .NET 8+. Ejecuta: dotnet run
using System.Net.Http.Json;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;

const string BaseUrl = "https://api.434alchemycode.com/v1";
var apiKey = Environment.GetEnvironmentVariable("ALCHEMY_API_KEY")
    ?? throw new InvalidOperationException("Falta la variable ALCHEMY_API_KEY.");

using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(45) };
http.DefaultRequestHeaders.Authorization = new("Bearer", apiKey);
http.DefaultRequestHeaders.Accept.ParseAdd("application/json");

async Task<JsonNode> CallAsync(string method, string path, string? body = null)
{
    using var request = new HttpRequestMessage(new HttpMethod(method), BaseUrl + path);
    if (body is not null)
    {
        request.Content = new StringContent(body, Encoding.UTF8, "application/json");
    }

    using var response = await http.SendAsync(request);
    var text = await response.Content.ReadAsStringAsync();
    var json = JsonNode.Parse(text) ?? new JsonObject();
    if (!response.IsSuccessStatusCode)
    {
        throw new HttpRequestException(
            $"[{json["code"]}] {json["detail"]} (HTTP {(int)response.StatusCode}, request_id {json["request_id"]})");
    }
    return json;
}

var payload = """
    {
      "rfcs": [
        "OMX2503255F9",
        "EKU9003173C9",
        "XAXX010101000"
      ]
    }
    """;

try
{
    var result = await CallAsync("POST", "/sat/articulo-69b", payload);
    Console.WriteLine(result["data"]?.ToJsonString(new JsonSerializerOptions { WriteIndented = true }));
}
catch (HttpRequestException error)
{
    Console.Error.WriteLine(error.Message);
    return 1;
}

return 0;
Ruby
# Consulta Artículo 69-B · AlchemyCode API
# Requiere Ruby 3.1+ (net/http y json son biblioteca estándar).
require 'json'
require 'net/http'
require 'uri'

BASE = 'https://api.434alchemycode.com/v1'
API_KEY = ENV.fetch('ALCHEMY_API_KEY')

class AlchemyError < StandardError
  attr_reader :code, :status, :retryable, :request_id

  def initialize(status, problem)
    @status = status
    @code = problem['code'] || 'UNKNOWN'
    @retryable = problem['retryable'] || false
    @request_id = problem['request_id']
    super("[#{@code}] #{problem['detail'] || problem['title']}")
  end
end

def alchemy(method, path, payload = nil)
  uri = URI("#{BASE}#{path}")
  request = method == 'GET' ? Net::HTTP::Get.new(uri) : Net::HTTP::Post.new(uri)
  request['Authorization'] = "Bearer #{API_KEY}"
  request['Accept'] = 'application/json'
  request['Content-Type'] = 'application/json'
  request['User-Agent'] = 'OmnicomERP/2.4 (+https://omnicom.mx)'
  request.body = JSON.generate(payload) if payload

  response = Net::HTTP.start(
    uri.host, uri.port, use_ssl: uri.scheme == 'https', open_timeout: 10, read_timeout: 45
  ) { |http| http.request(request) }

  data = response.body.to_s.empty? ? {} : JSON.parse(response.body)
  raise AlchemyError.new(response.code.to_i, data) if response.code.to_i >= 400

  data
end

payload = JSON.parse(<<~JSON)
{
  "rfcs": [
    "OMX2503255F9",
    "EKU9003173C9",
    "XAXX010101000"
  ]
}
JSON

begin
  result = alchemy('POST', '/sat/articulo-69b', payload)
  puts JSON.pretty_generate(result['data']) # 200 OK, síncrono
rescue AlchemyError => e
  warn "#{e.message} (HTTP #{e.status}, request_id #{e.request_id})"
  exit(e.retryable ? 2 : 1)
end
El company_id, el job_id y los archivos son de ejemplo. No los reutilices: cada cuenta sólo puede ver sus propias empresas y trabajos.

Continúa