Empresas y credenciales
Una empresa es el RFC sobre el que se ejecutan los servicios. Cada empresa pertenece a una sola cuenta y guarda sus credenciales cifradas: e.firma del SAT y, si aplica, usuario y contraseña de IMSS e Infonavit.
Alta de empresa
/v1/companies201 Created| Campo | Tipo | Descripción |
|---|---|---|
rfc | stringrequerido | RFC con homoclave. 12 caracteres para persona moral, 13 para persona física. Se normaliza a mayúsculas. |
legal_name | stringrequerido | Razón social o nombre, de 2 a 240 caracteres. Se usa en reportes y PDFs. |
person_type | enumopcional | moral o fisica. Si se omite se infiere de la longitud del RFC. |
tax_regime | stringopcional | Clave del régimen fiscal del SAT, por ejemplo 601 o 626. |
address | objectopcional | Domicilio declarado. Se guarda tal cual para tu operación; no sustituye al del SAT. |
external_ref | stringopcional | Identificador en tu sistema (máx. 80). Con el proveedor alchemy guarda el cliente_id interno. |
notes | stringopcional | Notas internas de la empresa. |
curl -sS -X POST "https://api.434alchemycode.com/v1/companies" \
-H "Authorization: Bearer $ALCHEMY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"rfc": "OMX2503255F9",
"legal_name": "OMNICONMX, S.A. de C.V.",
"tax_regime": "601",
"address": {
"calle": "Av. Insurgentes Sur",
"numero_exterior": "1602",
"colonia": "Crédito Constructor",
"codigo_postal": "03940",
"municipio": "Benito Juárez",
"estado": "Ciudad de México"
},
"external_ref": "OMNICOM-001"
}'
{
"id": "9b2f5c1e-7a3d-4f8b-9c21-6d4e8a0b5f17",
"object": "company",
"rfc": "OMX2503255F9",
"legal_name": "OMNICONMX, S.A. de C.V.",
"person_type": "moral",
"tax_regime": "601",
"status": "active",
"external_ref": "OMNICOM-001",
"address": {
"calle": "Av. Insurgentes Sur",
"numero_exterior": "1602",
"numero_interior": "Piso 12",
"colonia": "Crédito Constructor",
"codigo_postal": "03940",
"municipio": "Benito Juárez",
"estado": "Ciudad de México",
"pais": "México"
},
"created_at": "2026-10-31T16:20:11.402331+00:00",
"updated_at": "2026-10-31T16:20:11.402331+00:00",
"credentials": {
"fiel": {
"configured": false,
"status": null,
"rfc": null,
"serial": null,
"valid_to": null,
"verified_at": null
},
"imss": {
"configured": false,
"username": null,
"status": null
},
"infonavit": {
"configured": false,
"username": null,
"status": null
}
},
"usage": {}
}^[A-ZÑ&]{3,4}\d{6}[A-Z0-9]{3}$). Si no cumple, la respuesta es 422 RFC_INVALID con el campo marcado en errors.El alta es idempotente por RFC dentro de tu cuenta: si ya existe, responde 409 COMPANY_DUPLICATE
e incluye el company_id existente para que puedas reutilizarlo.
El plan Invitación · Beta permite hasta 5 empresas activas. Al llegar al límite, archiva una empresa
o solicita ampliación; el error es 409 COMPANY_LIMIT_REACHED.
Carga de e.firma
El SAT exige la e.firma de la empresa para la constancia, la opinión 32-D y el domicilio fiscal. La carga es
multipart/form-data con el certificado, la llave y la contraseña.
/v1/companies/{company_id}/credentials/sat200 OK · multipartcurl -sS -X POST "https://api.434alchemycode.com/v1/companies/9b2f5c1e-7a3d-4f8b-9c21-6d4e8a0b5f17/credentials/sat" \
-H "Authorization: Bearer $ALCHEMY_API_KEY" \
-H "Accept: application/json" \
-F "cer=@./omnicom.cer;type=application/x-x509-ca-cert" \
-F "key=@./omnicom.key;type=application/octet-stream" \
-F "password=$FIEL_PASSWORD"
{
"object": "sat_credential",
"company_id": "9b2f5c1e-7a3d-4f8b-9c21-6d4e8a0b5f17",
"status": "valid",
"rfc": "OMX2503255F9",
"serial": "3A7F19C2B84E",
"subject": "CN=OMNICONMX SA DE CV, x500UniqueIdentifier=OMX2503255F9, O=OMNICONMX, C=MX",
"valid_from": "2024-03-18T00:00:00+00:00",
"valid_to": "2028-03-18T00:00:00+00:00",
"cer_sha256": "b7c1e0a4f8d29c63507a4e1b8d3f2c6a9e0b4d7c1a8f5e2d3b6c9a0f4e7d1c28",
"message": "e.firma validada y cifrada. Lista para consultar el SAT."
}El mismo alta desde PHP, que es el caso más común en integraciones de escritorio y ERP:
<?php
declare(strict_types=1);
$companyId = '9b2f5c1e-7a3d-4f8b-9c21-6d4e8a0b5f17';
$ch = curl_init("https://api.434alchemycode.com/v1/companies/{$companyId}/credentials/sat");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Authorization: Bearer ' . getenv('ALCHEMY_API_KEY'),
'Accept: application/json',
],
// No fijes Content-Type: cURL genera el boundary de multipart por ti.
CURLOPT_POSTFIELDS => [
'cer' => new CURLFile(__DIR__ . '/omnicom.cer', 'application/x-x509-ca-cert', 'omnicom.cer'),
'key' => new CURLFile(__DIR__ . '/omnicom.key', 'application/octet-stream', 'omnicom.key'),
'password' => getenv('FIEL_PASSWORD'),
],
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_TIMEOUT => 60,
]);
$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) {
exit("Error de red cURL {$errno}: {$error}");
}
$body = json_decode((string) $raw, true);
if ($status >= 400) {
exit(sprintf('Error %s: %s', $body['code'] ?? 'UNKNOWN', $body['detail'] ?? 'sin detalle'));
}
printf("e.firma %s, vigente hasta %s\n", $body['status'], $body['valid_to']);
Validaciones que hace la API antes de guardar:
- El
.cerdebe ser un certificado X.509 del SAT legible. - La
.keydebe abrir con la contraseña enviada (si no,FILE_INVALID). - El RFC del certificado debe coincidir con el RFC de la empresa.
- Cada archivo debe pesar menos de 10 MB.
status: "expired" para que puedas ver la vigencia y planear la renovación, pero los servicios del SAT responderán FIEL_EXPIRED hasta que cargues una vigente.Credenciales de IMSS e Infonavit
Se envían como JSON y se usan en el buzón IMSS, la opinión IMSS y la opinión Infonavit. El campo
extra permite guardar datos adicionales que el portal solicite (por ejemplo, el registro patronal).
/v1/companies/{company_id}/credentials/{portal}portal: imss | infonavitcurl -sS -X POST "https://api.434alchemycode.com/v1/companies/9b2f5c1e-7a3d-4f8b-9c21-6d4e8a0b5f17/credentials/imss" \
-H "Authorization: Bearer $ALCHEMY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"username": "B4712XH9071",
"password": "ClavePortalImss#2026",
"extra": {"registro_patronal": "B4712XH9071"}
}'
{
"object": "portal_credential",
"company_id": "9b2f5c1e-7a3d-4f8b-9c21-6d4e8a0b5f17",
"portal": "imss",
"username": "B4712XH9071",
"status": "valid",
"last_verified_at": "2026-10-31T16:26:38+00:00",
"message": "Credenciales de IMSS cifradas y listas para usar."
}Para consultar el estado sin exponer secretos, usa el endpoint de credenciales:
curl -sS "https://api.434alchemycode.com/v1/companies/9b2f5c1e-7a3d-4f8b-9c21-6d4e8a0b5f17/credentials" \
-H "Authorization: Bearer $ALCHEMY_API_KEY"
{
"object": "credentials",
"company_id": "9b2f5c1e-7a3d-4f8b-9c21-6d4e8a0b5f17",
"rfc": "OMX2503255F9",
"fiel": {
"configured": true,
"status": "valid",
"rfc": "OMX2503255F9",
"serial": "3A7F19C2B84E",
"valid_to": "2028-03-18T00:00:00+00:00",
"verified_at": "2026-10-31T16:22:07+00:00"
},
"imss": {"configured": true, "username": "B4712XH9071", "status": "valid"},
"infonavit": {"configured": false, "username": null, "status": null},
"required": [
{"service": "sat.constancia", "needs": ["fiel"]},
{"service": "imss.buzon", "needs": ["imss"]},
{"service": "infonavit.opinion", "needs": ["infonavit"]}
]
}Para eliminar credenciales (por ejemplo al dar de baja a un cliente) usa
DELETE /v1/companies/{company_id}/credentials/{kind} con kind igual a
fiel, imss o infonavit. Responde 204 y no se puede deshacer.
Cómo se guardan los secretos
El material sensible se cifra con envelope encryption antes de tocar la base de datos:
| Capa | Qué es | Dónde vive |
|---|---|---|
| KEK | Llave maestra de 32 bytes (AES vía Fernet) que envuelve las DEK. | Variable ALCHEMY_ENCRYPTION_KEY, fuera de la base de datos. |
| DEK | Llave de datos por cuenta, generada al crear la cuenta y envuelta con la KEK. | Columna accounts.dek_wrapped. |
| Secreto | .cer, .key, contraseña de e.firma y contraseñas de portales, cifrados con la DEK de la cuenta. | Columnas *_ciphertext, con prefijo de versión v1:. |
- El texto plano sólo existe en memoria mientras se ejecuta una consulta; nunca se escribe en disco ni en logs.
- La API nunca devuelve el material: sólo RFC, serial, vigencia, SHA-256 del certificado y estado.
- Rotar la KEK no obliga a re-cifrar cada secreto: se re-envuelven las DEK de las cuentas.
- Si se compromete la base de datos sin la KEK, los secretos no son legibles.
credentials:write puede reemplazar credenciales: trátala como privilegiada y no la compartas con integraciones de sólo lectura.Errores frecuentes de empresas y credenciales
| Código | HTTP | Cuándo ocurre |
|---|---|---|
RFC_INVALID | 422 | El RFC no cumple la estructura del SAT. |
COMPANY_DUPLICATE | 409 | Ya existe una empresa con ese RFC en tu cuenta. |
COMPANY_LIMIT_REACHED | 409 | El plan alcanzó su máximo de empresas. |
COMPANY_NOT_FOUND | 404 | El UUID no existe o pertenece a otra cuenta. |
FILE_REQUIRED | 422 | Falta el .cer o el .key. |
FILE_INVALID | 422 | El certificado o la llave no abren, o el RFC no coincide. |
CREDENTIAL_MISSING | 422 | El servicio requiere credenciales que la empresa no tiene. |
CREDENTIAL_INVALID | 422 | El portal rechazó el usuario o la contraseña. |
FIEL_EXPIRED | 422 | La e.firma está vencida. |
PAYLOAD_TOO_LARGE | 413 | El archivo supera 10 MB o el cuerpo 2 MB. |
Buenas prácticas
- Una empresa por RFC; no reutilices una empresa para otro contribuyente.
- Carga la e.firma desde un backend con TLS; nunca desde el navegador del usuario final.
- No envíes la contraseña de la e.firma por correo, chat ni tickets. Usa un formulario y un gestor de secretos.
- Verifica
valid_toy renueva antes del vencimiento; agenda un recordatorio a 60 días. - Usa
external_refpara mapear tu cliente contra AlchemyCode y evitar duplicados. - Archiva (
DELETE) las empresas que ya no operan: el borrado es lógico y conserva la auditoría. - Elimina credenciales al terminar la relación con un cliente; la baja de la empresa no las borra.