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.
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
company_id, el consumo se atribuye a esa empresa.Petición
/v1/sat/articulo-69bAPI key con scope services:run| Campo | Tipo | Descripción |
|---|---|---|
company_id | UUIDopcional | Opcional. Si se envía, el consumo se atribuye a esa empresa. |
rfc | stringopcional | Un solo RFC. Mutuamente excluyente con rfcs. |
rfcs | array[string]opcional | Lote de hasta 5,000 RFC. Mutuamente excluyente con rfc. |
Respuesta
La respuesta es 200 OK y trae el resultado en data. No se crea job, no hay polling y no aplica wait.
{
"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.
| Aspecto | Comportamiento |
|---|---|
| Respuesta | 200 OK con object: "result". |
| Timeout recomendado | 60 s de lectura; lotes grandes pueden acercarse a 30 s. |
| Idempotencia | No aplica Idempotency-Key: si reintentas, se ejecuta otra consulta. |
| Reintentos | Sólo ante 429, 502, 503 o 504, con backoff. |
| Límite de lote | rfcs hasta 5,000 por petición. |
429 QUOTA_EXCEEDED sin consumir.Errores propios del servicio
| Código | HTTP | Cuándo ocurre | Reintentable |
|---|---|---|---|
VALIDATION_ERROR | 422 | Enviaste rfc y rfcs, ninguno, o un lote mayor a 5,000. | No |
QUOTA_EXCEEDED | 429 | Sin unidades disponibles en el periodo. | No |
PROVIDER_ERROR | 502 | El proveedor de listas devolvió un error. | Sí |
PROVIDER_TIMEOUT | 504 | La 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.
# 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
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);
}
// 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();
"""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()
// 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
}
// 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);
}
}
}
// 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;
# 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
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.