Estatus de Domicilio Fiscal
Consulta en el portal del SAT la pestaña de ubicación fiscal y devuelve el estatus del domicilio, la fecha de la consulta y la evidencia de pantalla. No genera PDF oficial.
Qué hace
Verifica que el domicilio fiscal declarado siga vigente en el padrón. Este servicio no produce un PDF oficial: devuelve el estatus y una evidencia de pantalla, útil como señal de riesgo en expedientes de crédito o arrendamiento.
Qué obtienes:
- Estatus del domicilio y código interno de estatus.
- Domicilio declarado según el portal del SAT.
- Fecha de la última verificación y evidencia en imagen.
Requisitos
| Requisito | Qué necesita | Dónde se configura |
|---|---|---|
fiel | e.firma vigente del RFC de la empresa. | Se carga en POST /v1/companies/{id}/credentials/sat. |
CREDENTIAL_MISSING y el detalle indica exactamente qué cargar. Consulta Empresas y credenciales.COMPANY_SUSPENDED antes de crear el job.Petición
/v1/sat/domiciliosAPI key con scope services:run| Campo | Tipo | Descripción |
|---|---|---|
company_id | UUIDrequerido | Empresa con e.firma vigente. |
wait | booleanopcional | Espera hasta 25 s el resultado. Por omisión false. |
Este endpoint acepta Idempotency-Key: si reintentas con la misma clave y el mismo cuerpo, recibes el mismo job en lugar de duplicar la consulta. La clave se conserva 24 horas.
Respuesta
Si wait es false (por omisión), la respuesta es 202 Accepted con el job en cola. El resultado se lee después con GET /v1/jobs/{id} o llega por webhook.
{
"object": "job",
"id": "4d1f8c0a-6b2e-4c93-8f51-2a7d9e3b6c08",
"service": "sat.domicilio",
"status": "queued",
"company_id": "9b2f5c1e-7a3d-4f8b-9c21-6d4e8a0b5f17",
"created_at": "2026-10-31T16:42:07.318920+00:00",
"started_at": null,
"finished_at": null,
"units": 0,
"result": null,
"files": [],
"error": null,
"warnings": [],
"sandbox": false,
"links": {
"self": "/v1/jobs/4d1f8c0a-6b2e-4c93-8f51-2a7d9e3b6c08"
}
}Cuando el job termina, el objeto cambia a succeeded y trae result y files. Este es un ejemplo realista:
{
"object": "job",
"id": "4d1f8c0a-6b2e-4c93-8f51-2a7d9e3b6c08",
"service": "sat.domicilio",
"status": "succeeded",
"company_id": "9b2f5c1e-7a3d-4f8b-9c21-6d4e8a0b5f17",
"created_at": "2026-10-31T16:42:07.318920+00:00",
"started_at": "2026-10-31T16:42:08.041228+00:00",
"finished_at": "2026-10-31T16:44:12.882014+00:00",
"units": 1,
"result": {
"rfc": "OMX2503255F9",
"razon_social": "OMNICONMX, S.A. de C.V.",
"estatus": "Verificado",
"codigo_estatus": "DOM_VERIFICADO",
"domicilio_declarado": {
"calle": "Av. Insurgentes Sur",
"colonia": "Crédito Constructor",
"codigo_postal": "03940",
"municipio": "Benito Juárez",
"estado": "Ciudad de México"
},
"ultima_verificacion": "2026-10-18",
"advertencia": null
},
"files": [
{
"id": "a91c4f7e2b6d48c05e3a1f9b7d2c6084",
"name": "evidencia-domicilio.png",
"content_type": "image/png",
"kind": "evidence",
"size": 96256,
"sha256": "4a7d2c9e1f6b8305d2c4a7e9f1b3d5c8a0e2f4b6d8c1a3e5f7b9d0c2e4a6f8b1",
"url": "/v1/files/a91c4f7e2b6d48c05e3a1f9b7d2c6084?p=OWIyZjVjMWUtN2EzZC00ZjhiLTljMjEtNmQ0ZThhMGI1ZjE3&exp=1793410512&sig=3f9c1d7a2b6e4c8d0f1a5b9e3c7d2f6a8b0c4e2d"
}
],
"error": null,
"warnings": [],
"sandbox": false,
"links": {
"self": "/v1/jobs/4d1f8c0a-6b2e-4c93-8f51-2a7d9e3b6c08"
}
}Las URLs de files[] son firmadas y expiran a los 15 minutos. Vuelve a consultar el job para obtener URLs nuevas mientras el trabajo no expire (72 h).
wait=true o job asíncrono
wait=true hace que la petición espere hasta 25 segundos a que el job termine. Si termina a tiempo, responde 200 con el job en succeeded; si no, responde 202 con el job en curso y debes seguir consultándolo.
| Aspecto | wait=true | Job asíncrono (wait=false) |
|---|---|---|
| Respuesta | 200 si termina en ≤ 25 s; 202 si no. | Siempre 202. |
| Latencia de tu request | Hasta 25 s (bloquea el hilo). | Inmediata (~100–300 ms). |
| Ideal para | Formularios interactivos, validaciones puntuales. | Procesos masivos, colas, cualquier operación real. |
| Riesgo | Timeouts en gateways con límite de 30 s. | Requiere polling o webhook. |
| Recomendación | Úsalo sólo si el usuario está esperando en pantalla. | Predeterminado para backend. |
curl -sS -X POST "https://api.434alchemycode.com/v1/sat/domicilios" \
-H "Authorization: Bearer $ALCHEMY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"company_id": "9b2f5c1e-7a3d-4f8b-9c21-6d4e8a0b5f17", "wait": true}'
Con wait=true el cuerpo cambia de 202 a 200, pero el objeto job conserva la misma forma. No vuelvas a enviar la consulta si recibes 202: ya está en curso.
Errores propios del servicio
| Código | HTTP | Cuándo ocurre | Reintentable |
|---|---|---|---|
CREDENTIAL_MISSING | 422 | Falta la e.firma de la empresa. | No |
FIEL_EXPIRED | 422 | La e.firma está vencida. | No |
PORTAL_UNAVAILABLE | 503 | El portal del SAT no respondió. | Sí |
CREDENTIAL_INVALID | 422 | Credenciales rechazadas por el SAT. | No |
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.
# Estatus de Domicilio Fiscal · AlchemyCode API
curl -i -sS -X POST "https://api.434alchemycode.com/v1/sat/domicilios" \
-H "Authorization: Bearer $ALCHEMY_API_KEY" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 6f1c2c7a-3d0e-4a51-9b0e-5c2a7f9d1e44" \
--data-binary @- <<'JSON'
{
"company_id": "9b2f5c1e-7a3d-4f8b-9c21-6d4e8a0b5f17",
"wait": false
}
JSON
# 1) La respuesta es HTTP/1.1 202 Accepted con el job en estado "queued".
# 2) Consulta el job cada 2 s hasta que status sea "succeeded":
curl -sS "https://api.434alchemycode.com/v1/jobs/4d1f8c0a-6b2e-4c93-8f51-2a7d9e3b6c08" \
-H "Authorization: Bearer $ALCHEMY_API_KEY"
# 3) El resultado viene en .result y los archivos en .files[].url
# (URLs firmadas que expiran a los 15 minutos).
<?php
declare(strict_types=1);
/**
* Estatus de Domicilio Fiscal · 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'
{
"company_id": "9b2f5c1e-7a3d-4f8b-9c21-6d4e8a0b5f17",
"wait": false
}
JSON, true, 512, JSON_THROW_ON_ERROR);
try {
$job = alchemy('POST', '/sat/domicilios', $payload);
// 202 Accepted: el trabajo quedó en cola.
printf("Trabajo %s en estado %s\n", $job['id'], $job['status']);
$deadline = microtime(true) + 120;
while (in_array($job['status'], ['queued', 'running'], true) && microtime(true) < $deadline) {
sleep(2);
$job = alchemy('GET', '/jobs/' . $job['id']);
}
if ($job['status'] !== 'succeeded') {
fwrite(STDERR, "El trabajo terminó en estado {$job['status']}\n");
exit(1);
}
echo json_encode($job['result'], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE), PHP_EOL;
foreach ($job['files'] as $file) {
printf("Archivo: %s (%s)\n", $file['url'], $file['content_type']);
}
} 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);
}
// Estatus de Domicilio Fiscal · 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 = {
"company_id": "9b2f5c1e-7a3d-4f8b-9c21-6d4e8a0b5f17",
"wait": false
};
async function main() {
try {
let job = await alchemy('POST', '/sat/domicilios', payload);
console.log(`Trabajo ${job.id}: ${job.status}`); // 202 Accepted
const deadline = Date.now() + 120_000;
while (['queued', 'running'].includes(job.status) && Date.now() < deadline) {
await sleep(2000);
job = await alchemy('GET', `/jobs/${job.id}`);
}
if (job.status !== 'succeeded') {
throw new Error(`El trabajo ${job.id} terminó en estado ${job.status}`);
}
console.log(JSON.stringify(job.result, null, 2));
for (const file of job.files) {
console.log(`${file.name}: ${file.url}`);
}
} 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();
"""Estatus de Domicilio Fiscal · 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'''
{
"company_id": "9b2f5c1e-7a3d-4f8b-9c21-6d4e8a0b5f17",
"wait": false
}
''')
def main() -> None:
try:
job = alchemy("POST", "/sat/domicilios", payload)
print("Trabajo", job["id"], job["status"]) # 202 Accepted
deadline = time.monotonic() + 120
while job["status"] in ("queued", "running") and time.monotonic() < deadline:
time.sleep(2)
job = alchemy("GET", f"/jobs/{job['id']}")
if job["status"] != "succeeded":
raise SystemExit(f"El trabajo terminó en estado {job['status']}")
print(json.dumps(job["result"], indent=2, ensure_ascii=False))
for file in job["files"]:
print(f"{file['name']}: {file['url']}")
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()
// Estatus de Domicilio Fiscal · 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 = `{
"company_id": "9b2f5c1e-7a3d-4f8b-9c21-6d4e8a0b5f17",
"wait": false
}`
type job struct {
ID string `json:"id"`
Status string `json:"status"`
Result json.RawMessage `json:"result"`
Files []jobFile `json:"files"`
}
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 current job
if err := call("POST", "/sat/domicilios", payload, ¤t); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
fmt.Printf("Trabajo %s: %s\n", current.ID, current.Status) // 202 Accepted
deadline := time.Now().Add(2 * time.Minute)
for (current.Status == "queued" || current.Status == "running") && time.Now().Before(deadline) {
time.Sleep(2 * time.Second)
if err := call("GET", "/jobs/"+current.ID, nil, ¤t); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
if current.Status != "succeeded" {
fmt.Fprintf(os.Stderr, "El trabajo terminó en estado %s\n", current.Status)
os.Exit(1)
}
var result map[string]any
_ = json.Unmarshal(current.Result, &result)
pretty, _ := json.MarshalIndent(result, "", " ")
fmt.Println(string(pretty))
for _, f := range current.Files {
fmt.Printf("%s: %s\n", f.Name, f.URL)
}
}
// Estatus de Domicilio Fiscal · 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 = """
{
"company_id": "9b2f5c1e-7a3d-4f8b-9c21-6d4e8a0b5f17",
"wait": false
}
""";
try {
JsonNode job = call("POST", "/sat/domicilios", body);
System.out.println("Trabajo " + job.path("id").asText() + ": " + job.path("status").asText());
long deadline = System.currentTimeMillis() + 120_000;
while (("queued".equals(job.path("status").asText())
|| "running".equals(job.path("status").asText()))
&& System.currentTimeMillis() < deadline) {
Thread.sleep(2_000);
job = call("GET", "/jobs/" + job.path("id").asText(), null);
}
if (!"succeeded".equals(job.path("status").asText())) {
System.err.println("El trabajo terminó en estado " + job.path("status").asText());
System.exit(1);
}
System.out.println(JSON.writerWithDefaultPrettyPrinter()
.writeValueAsString(job.path("result")));
for (JsonNode file : job.path("files")) {
System.out.println(file.path("name").asText() + ": " + file.path("url").asText());
}
} catch (IllegalStateException error) {
System.err.println(error.getMessage());
System.exit(1);
}
}
}
// Estatus de Domicilio Fiscal · 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 = """
{
"company_id": "9b2f5c1e-7a3d-4f8b-9c21-6d4e8a0b5f17",
"wait": false
}
""";
try
{
var job = await CallAsync("POST", "/sat/domicilios", payload);
Console.WriteLine($"Trabajo {job["id"]}: {job["status"]}"); // 202 Accepted
var deadline = DateTime.UtcNow.AddMinutes(2);
while (((string?)job["status"] is "queued" or "running") && DateTime.UtcNow < deadline)
{
await Task.Delay(2000);
job = await CallAsync("GET", $"/jobs/{job["id"]}");
}
if ((string?)job["status"] != "succeeded")
{
Console.Error.WriteLine($"El trabajo terminó en estado {job["status"]}");
return 1;
}
Console.WriteLine(job["result"]?.ToJsonString(new JsonSerializerOptions { WriteIndented = true }));
foreach (var file in job["files"]!.AsArray())
{
Console.WriteLine($"{file!["name"]}: {file["url"]}");
}
}
catch (HttpRequestException error)
{
Console.Error.WriteLine(error.Message);
return 1;
}
return 0;
# Estatus de Domicilio Fiscal · 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)
{
"company_id": "9b2f5c1e-7a3d-4f8b-9c21-6d4e8a0b5f17",
"wait": false
}
JSON
begin
job = alchemy('POST', '/sat/domicilios', payload)
puts "Trabajo #{job['id']}: #{job['status']}" # 202 Accepted
deadline = Time.now + 120
while %w[queued running].include?(job['status']) && Time.now < deadline
sleep 2
job = alchemy('GET', "/jobs/#{job['id']}")
end
raise "El trabajo terminó en estado #{job['status']}" unless job['status'] == 'succeeded'
puts JSON.pretty_generate(job['result'])
job['files'].each { |file| puts "#{file['name']}: #{file['url']}" }
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.