Referencia técnica de la API Enterprise: autenticación, ambiente de pruebas, permisos, errores y el detalle de cada API.
Dirigido a: equipos técnicos de las empresas cliente. Los términos (asociación, colaborador, RFC de asociación, ticket de asociación, tipo multi RFC…) se definen en Conceptos y los valores de cada catálogo están en Catálogos. En la API, a los colaboradores se les llama usuarios (userId).
Uso general
Autenticación
Todas las peticiones llevan el token de API del usuario B2B:
Authorization: Bearer <token>
| Ambiente | URL base | Token |
|---|---|---|
| Producción | https://api.fotofacturas.ai/B2B/<Controller>/<API> | Web app business, sección API y plan. |
| Pruebas | https://api-staging.fotofacturas.ai/B2B/<Controller>/<API> | Web app business de pruebas, sección API y plan. |
El ambiente de pruebas funciona igual que producción, pero los tickets no se facturan de verdad: un simulador los mueve de estatus según los últimos 4 dígitos de la tarjeta (ver Simulación de tickets).
La API solo permite ver y modificar la información del árbol de asociaciones de tu cuenta: cualquier asociación, usuario, RFC o ticket fuera de ese árbol se rechaza.
Permisos de API
Cada API requiere un permiso habilitado en tu cuenta. Fotofacturas los habilita según lo contratado; para habilitar uno, pídelo indicando su nombre. Sin el permiso, la API responde No estas autorizado para usar esta API. Ping no requiere permiso.
Respuesta
Todas las APIs regresan este objeto (JSON en camelCase):
| Campo | Tipo | Descripción |
|---|---|---|
isSuccess | boolean | Si la operación fue exitosa. |
message | string | null | Mensaje del resultado; en errores, el motivo. |
data | object | null | Información solicitada (solo en las APIs que regresan datos). |
Valida siempre isSuccess: los errores de negocio se responden con HTTP 200.
{
"isSuccess": false,
"message": "El RFC XAXX010101000 no está registrado",
"data": null
}
Errores
| HTTP | Cuándo | message |
|---|---|---|
| 200 | Éxito o error de validación o de negocio (dato inválido, recurso fuera de tu árbol, límite del plan, API sin permiso…). | El motivo del error. |
| 400 | JSON mal formado, tipo de dato incorrecto o falta un campo requerido. | Datos inválidos en: userId, rfc o Datos inválidos en la petición. |
| 401 | Problema con el token (ver tabla siguiente). | El motivo. |
| 429 | Más de 500 peticiones por minuto desde la misma IP. | Demasiadas peticiones. Por favor, intenta más tarde. |
| 500 | Error inesperado. | Error inesperado en el sistema |
Errores de autenticación (HTTP 401):
| Mensaje | Causa |
|---|---|
Falta header de autorización | No se mandó Authorization. |
Formato inválido del header de autorización | El header no empieza con Bearer . |
Falta el token en el header de autorización | No hay token después de Bearer. |
Token no válido | El token no existe o la cuenta está inactiva. |
Usuario no autorizado para usar este servicio | El token no es de un usuario B2B. |
Servicio no disponible, reintente en un minuto | El servicio está iniciando. |
Errores frecuentes (HTTP 200, isSuccess: false):
| Mensaje | Causa |
|---|---|
No estas autorizado para usar esta API | Tu cuenta no tiene el permiso de esa API. |
Por favor reintente en unos momentos | El servicio está iniciando. |
La asociación no le pertenece / La asociación no te pertenece | La asociación no está en tu árbol. |
El usuario no pertenece a tus asociaciones / Este usuario no pertenece a su asociación | El usuario no está en tu árbol. |
El usuario no existe / La asociación no existe / El ticket no existe | El id no existe. |
El usuario no está activo | Se intentó subir un ticket para un usuario inactivo. |
El RFC es obligatorio / El RFC {rfc} no es válido | Falta el RFC o su formato no es válido. |
El RFC {rfc} no está registrado | El RFC no está entre los que puede usar el usuario o la asociación. |
El RFC {rfc} está deshabilitado | El RFC está deshabilitado. |
El plan solo permite {n} RFCs en la asociación / El plan solo permite {n} RFCs por usuario | Se alcanzó el límite de RFC del plan. |
Has alcanzado tu límite mensual de {n} tickets... / Has alcanzado tu límite personal mensual de {n} tickets... | Se alcanzó el límite mensual de tickets del grupo o del usuario. |
La fecha de inicio es obligatoria / La fecha de fin es obligatoria / La fecha de inicio no puede ser mayor a la fecha de fin | Rango de fechas faltante o inválido. |
El mes debe estar entre 1 y 12 / Año inválido / No se puede consultar un mes futuro | Mes o año inválido. |
Formatos
- Entrada:
GET: query params.POST/PUTsin archivos: JSON.- APIs con archivos (CSF, logos, imagen del ticket):
multipart/form-data. Para varios archivos en un campo se repite la llave (por ejemploCSFs); los campos anidados usan punto (por ejemploConfig.CompanyName).
- Fechas de entrada (
startDate,endDate):yyyy-MM-dd, en fecha de México, e incluyen el día completo. - Fechas de respuesta: hora de México, sin zona horaria.
- Reportes por mes (
month,year): mes calendario completo en hora de México.
Association
GetAssociationList
GET /B2B/AssociationV1/GetAssociationList · Sin parámetros
Lista plana de todas las asociaciones de tu árbol, ordenada por id, con sus RFC. parentId es null en la asociación raíz.
{
"data": [
{ "associationId": 41, "name": "FOTOFACTURAS", "parentId": null, "rfcs": [{ "rfc": "FFA200101AB1", "isActive": true }] },
{ "associationId": 42, "name": "Sucursal Norte", "parentId": 41, "rfcs": [] }
],
"isSuccess": true,
"message": null
}
GetAssociationTree
GET /B2B/AssociationV1/GetAssociationTree · Sin parámetros
Árbol de asociaciones de tu cuenta, con sus RFC.
{
"data": {
"associationId": 41,
"name": "FOTOFACTURAS",
"rfcs": [{ "rfc": "FFA200101AB1", "isActive": true }],
"children": [
{ "associationId": 42, "name": "Sucursal Norte", "rfcs": [], "children": [] }
]
},
"isSuccess": true,
"message": null
}
GetAssociationById
GET /B2B/AssociationV1/GetAssociationById · int associationId
Detalle de una asociación: datos generales, usuarios, RFC y configuración.
{
"data": {
"associationId": 42,
"name": "Sucursal Norte",
"parentId": 41,
"createDate": "2026-10-01T10:15:00",
"updateDate": "2026-10-05T12:00:00",
"users": [ ... ],
"rfcs": [
{
"userTaxInfoId": 10423,
"rfc": "FFA200101AB1",
"name": "FOTOFACTURAS SA DE CV",
"csfFile": "https://...",
"isVerified": true,
"isActive": true,
"createDate": "2026-10-01T10:20:00",
"updateDate": "2026-10-01T10:20:00"
}
],
"config": {
"logoSquare": "https://...",
"logoRectangle": null,
"companyName": "Sucursal Norte",
"defaultRegimenFiscalId": 601,
"defaultUsoCfdiId": "G03",
"defaultTicketPaymentMethodId": "01",
"canChangeRegimenFiscal": true,
"canChangeUsoCfdi": true,
"canChangePaymentMethod": true,
"styleRoundness": 8,
"styleSidebarColor": "#1E293B",
"styleAccentColor": "#3B82F6",
"styleAccentStrongColor": "#1D4ED8"
}
},
"isSuccess": true,
"message": null
}
users: mismo formato queGetUsersByAssociation.rfcs: RFC de la asociación, activos y deshabilitados.csfFilees una URL temporal.config: siempre viene; los campos no configurados regresan su valor por defecto. Los logos son URLs públicas onull.
CreateAssociation
POST (multipart/form-data) /B2B/AssociationV1/CreateAssociation
Crea una asociación hija. Opcionalmente recibe su configuración y una o varias CSF para registrar sus RFC. Si alguna CSF es inválida, no se crea nada.
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
Name | string | Sí | Nombre de la asociación. |
ParentId | int | Sí | Asociación padre (de tu árbol). |
CSFs | archivo(s) PDF | No | CSF de la asociación. Repite la llave para varias. Sin RFC repetidos y sin exceder multiRfcPerAssociation. |
Config.* | No | Campos de UpdateAssociationConfig con prefijo Config. (por ejemplo Config.CompanyName). |
{ "data": 43, "isSuccess": true, "message": "Guardada correctamente" }
data es el associationId creado.
UpdateAssociation
PUT (JSON) /B2B/AssociationV1/UpdateAssociation
Cambia el nombre y, opcionalmente, el padre de una asociación.
{ "associationId": 42, "name": "Sucursal Norte", "parentId": 41 }
parentIdes opcional; si no se manda o es el mismo, la asociación no se mueve.- La asociación raíz no puede cambiar de padre.
- El nuevo padre debe ser de tu árbol y no puede ser la misma asociación ni una de sus descendientes.
Respuesta: message Actualizada correctamente.
UpdateAssociationConfig
PUT (multipart/form-data) /B2B/AssociationV1/UpdateAssociationConfig
Actualiza la configuración de una asociación. Solo cambian los campos que se mandan.
| Campo | Tipo | Descripción |
|---|---|---|
AssociationId | int | Requerido. |
LogoSquare / LogoRectangle | archivo JPG o PNG | Logo cuadrado / rectangular (máx. 4 MB). Reemplaza el anterior. |
CompanyName | string | Razón social a mostrar (máx. 150 caracteres). |
DefaultRegimenFiscalId | int | Régimen fiscal por defecto. |
DefaultUsoCfdiId | string | Uso de CFDI por defecto. Requiere régimen fiscal (el que se manda o el guardado) y debe ser válido para él. |
DefaultTicketPaymentMethodId | string | Forma de pago por defecto. |
CanChangeRegimenFiscal / CanChangeUsoCfdi / CanChangePaymentMethod | bool | Si el usuario puede cambiar ese valor al subir un ticket. |
StyleRoundness | int | Redondeo de la interfaz (no negativo). |
StyleSidebarColor / StyleAccentColor / StyleAccentStrongColor | string | Color #RRGGBB o #RRGGBBAA. |
Respuesta: message Configuración actualizada.
SaveAssociationCSF
POST (multipart/form-data) /B2B/AssociationV1/SaveAssociationCSF
Registra o actualiza un RFC de la asociación subiendo su CSF. Si el RFC ya existe en la asociación, se actualiza y se reactiva; si no, se crea. Crear o reactivar respeta multiRfcPerAssociation.
| Campo | Tipo | Requerido |
|---|---|---|
AssociationId | int | Sí |
CSF | archivo PDF | Sí |
Respuesta: message CSF creada o CSF actualizada.
ChangeAssociationCSFStatus
PUT (JSON) /B2B/AssociationV1/ChangeAssociationCSFStatus
Activa o deshabilita un RFC de la asociación. Activar respeta multiRfcPerAssociation.
{ "associationId": 42, "rfc": "FFA200101AB1", "isActive": false }
Respuesta: message CSF deshabilitada (o activada).
Miscellaneous
Ping
GET /B2B/MiscellaneousV1/Ping · Sin parámetros · No requiere permiso
Confirma que la API responde y que el token es válido. Respuesta: message Pong.
GetAccountInfo
GET /B2B/MiscellaneousV1/GetAccountInfo · Sin parámetros
Datos del usuario B2B, de su plan y del consumo del mes actual.
{
"data": {
"username": "foto.facturas",
"name": "Laura Méndez",
"email": "fotofacturas@fotofacturas.ai",
"phone": "5512345678",
"isActive": true,
"createDate": "2026-01-10T09:00:00",
"updateDate": "2026-01-10T09:00:00",
"associationAndPlan": {
"associationId": 41,
"name": "FOTOFACTURAS",
"createDate": "2026-01-10T09:00:00",
"updateDate": "2026-01-10T09:00:00",
"ticketsMonthlyLimit": 1000,
"ticketsMonthlyLimitPerUser": 50,
"canPictureInvoice": true,
"multiRfcPerUser": 5,
"multiRfcPerAssociation": 3,
"multiRfcType": "M",
"ticketsUsed": 21,
"associationsCount": 4,
"associationActiveTaxInfosCount": 3,
"associationInactiveTaxInfosCount": 1,
"userActiveTaxInfosCount": 12,
"userInactiveTaxInfosCount": 2
}
},
"isSuccess": true,
"message": null
}
| Campo | Descripción |
|---|---|
ticketsMonthlyLimit, ticketsMonthlyLimitPerUser, canPictureInvoice, multiRfcPerUser, multiRfcPerAssociation, multiRfcType | Límites del plan (ver Conceptos › Plan y límites). |
ticketsUsed | Tickets del mes actual de todo el árbol, sin contar descartados. |
associationsCount | Asociaciones del árbol, incluida la raíz. |
associationActiveTaxInfosCount / associationInactiveTaxInfosCount | RFC de asociación activos / deshabilitados. |
userActiveTaxInfosCount / userInactiveTaxInfosCount | RFC de usuarios activos / deshabilitados. |
associationAndPlan es null si la cuenta no tiene asociación asignada.
GetTicketPaymentMethods
GET /B2B/MiscellaneousV1/GetTicketPaymentMethods · Sin parámetros
Catálogo de formas de pago habilitadas.
{
"data": [
{ "ticketPaymentMethodId": "01", "name": "Efectivo" },
{ "ticketPaymentMethodId": "28", "name": "Tarjeta de débito" }
],
"isSuccess": true,
"message": null
}
GetRegimenFiscalAndCFDI
GET /B2B/MiscellaneousV1/GetRegimenFiscalAndCFDI · Sin parámetros
Catálogo de regímenes fiscales con los usos de CFDI válidos para cada uno.
{
"data": [
{
"regimenFiscalId": 626,
"name": "Régimen Simplificado de Confianza",
"isFisica": true,
"isMoral": true,
"cfdi": [{ "usoCFDIId": "G03", "name": "Gastos en general" }]
}
],
"isSuccess": true,
"message": null
}
GetTicketCodes
GET /B2B/MiscellaneousV1/GetTicketCodes · Sin parámetros
Catálogo de códigos del ticket (resultado o motivo de rechazo).
{
"data": [
{ "ticketCodeId": 1, "category": "Ticket", "description": "El ticket fue creado.", "userMessage": null, "isSuccessCode": null }
],
"isSuccess": true,
"message": null
}
description: descripción del código.userMessage: mensaje para el usuario final; puede sernull.isSuccessCode: si el código es un resultado de éxito; puede sernull.
Productos y servicios del SAT
Catálogo jerárquico División → Familia → Clase → Producto. Las claves son prefijos (división 50 → familia 5010 → clase 501015 → productos 501015xx). URL: /B2B/MiscellaneousV1/<API>.
| API | Método | Parámetros (opcionales) | Respuesta (data) |
|---|---|---|---|
GetSatProductDivisions | GET | Ninguno | [{ "satProductDivisionId", "description" }] |
GetSatProductFamilies | GET | satProductDivisionId | [{ "satProductFamilyId", "description", "satProductDivisionId" }] |
GetSatProductClasses | GET | satProductFamilyId | [{ "satProductClassId", "description", "satProductFamilyId" }] |
GetSatProducts | GET | satProductClassId, satProductFamilyId, satProductDivisionId, satProductId | [{ "satProductId", "description", "similares", "satProductClassId" }] |
En GetSatProducts manda al menos un filtro: sin filtros regresa el catálogo completo.
Ticket
GetTicketById
GET /B2B/TicketV1/GetTicketById · int userTicketId
Detalle de un ticket, con los datos de la factura si ya se facturó.
{
"data": {
"userTicketId": 88254,
"userId": 3,
"ticketUrl": "https://...",
"pdfUrl": "https://...",
"xmlUrl": "https://...",
"ticketDate": "2026-10-04T00:00:00",
"ticketAmount": 350.50,
"ticketPaymentMethodId": "28",
"ticketPaymentMethod": "Tarjeta de débito",
"ticketStatusId": 11,
"ticketStatus": "SIGNED",
"taxIdentifier": "XXXX007766JH7",
"taxName": "MARIO DIAZ",
"clientName": "Mario Díaz",
"clientEmail": "fotofacturas@fotofacturas.ai",
"ticketCodeId": 2,
"invoiceAttemptsCount": 1,
"invoiceMethod": 2,
"ticketOriginId": 4,
"ticketOrigin": "API Enterprise",
"associationId": 42,
"xmlData": { ... },
"createDate": "2026-10-04T20:10:02",
"updateDate": "2026-10-05T09:30:00"
},
"isSuccess": true,
"message": null
}
ticketUrl,pdfUrlyxmlUrlson URLs temporales.pdfUrlyxmlUrlsonnullsi no hay factura.xmlData: datos leídos del XML de la factura (nullsi no hay XML).taxIdentifier/taxName: RFC y nombre a los que se factura.invoiceMethod: cómo se obtuvo la factura (nullsi no está facturado).ticketOriginId/ticketOrigin: canal por el que se subió el ticket.associationId: asociación del ticket (la del usuario o, en un ticket de asociación, la propia asociación).
Los valores de estatus, código, método de facturación y origen están en Catálogos.
CreateUserTicket
POST (multipart/form-data) /B2B/TicketV1/CreateUserTicket
Sube un ticket a nombre de un usuario. El RFC debe ser uno que el usuario pueda usar según el tipo multi RFC del plan.
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
UserId | int | Sí | Usuario activo de tu árbol. |
Image | archivo | Sí | Foto del ticket: JPG, PNG, WEBP, HEIC o PDF (máx. 10 MB). |
Rfc | string | Sí | RFC al que se factura. Debe estar registrado y activo. |
TicketPaymentMethodId | string | Sí | Catálogo GetTicketPaymentMethods. |
RegimenFiscalId | int | Sí | Catálogo GetRegimenFiscalAndCFDI. |
UsoCfdiId | string | Sí | Debe ser válido para el régimen fiscal. |
CardLastFour | string | Condicional | Últimos 4 dígitos de la tarjeta. Obligatorio con 04 y 28. En pruebas elige el escenario del simulador. |
{ "data": 88255, "isSuccess": true, "message": "Ticket creado" }
data es el userTicketId creado. Se rechaza si el usuario o el grupo alcanzaron su límite mensual o si el plan no permite subir tickets.
CreateAssociationTicket
POST (multipart/form-data) /B2B/TicketV1/CreateAssociationTicket
Sube un ticket a nombre de una asociación. El RFC debe ser uno de los RFC de esa asociación.
| Campo | Tipo | Requerido |
|---|---|---|
AssociationId | int | Sí |
Image, Rfc, TicketPaymentMethodId, RegimenFiscalId, UsoCfdiId, CardLastFour | Igual que en CreateUserTicket. |
{ "data": 88256, "isSuccess": true, "message": "Ticket creado" }
Report
Los reportes cubren los tickets de usuarios y de asociaciones y excluyen los tickets descartados.
Filtro associationId (opcional) en GetTicketCountByDay, GetPeriodStats, GetTicketFamilyStats, GetTicketStatuses y GetTicketsByMonth:
- Sin él, el reporte cubre todo el árbol de tu cuenta.
- Con él, cubre esa asociación y sus descendientes. Si no es de tu árbol, responde
La asociación no le pertenece. - En
GetPeriodStats,ticketsMonthlyLimitsiempre es el límite del plan de la cuenta.
GetTicketCountByDay
GET /B2B/ReportV1/GetTicketCountByDay · startDate, endDate (requeridos) · associationId (opcional)
Tickets por día en un rango, incluidos los días sin tickets. average = total de tickets / días del rango.
{
"data": {
"days": [
{ "date": "2026-10-01", "ticketCount": 0 },
{ "date": "2026-10-02", "ticketCount": 3 },
{ "date": "2026-10-03", "ticketCount": 1 }
],
"average": 1.3333333333333333
},
"isSuccess": true,
"message": null
}
GetPeriodStats
GET /B2B/ReportV1/GetPeriodStats · month (1-12), year (requeridos; no se permiten meses futuros) · associationId (opcional)
Estadísticas de un mes completo comparadas con el mes anterior.
{
"data": {
"currentPeriod": {
"startDate": "2026-10-01T00:00:00",
"endDate": "2026-10-31T23:59:59.999",
"processedTicketCount": 8,
"waitingTicketCount": 1,
"cancelledTicketCount": 12,
"totalInvoiced": 2983.76,
"totalIVATaxInvoiced": 477.40,
"totalTicketsUsed": 21,
"ticketsMonthlyLimit": 1000
},
"previousPeriod": { ... }
},
"isSuccess": true,
"message": null
}
| Campo | Descripción |
|---|---|
processedTicketCount | Tickets facturados (11). |
cancelledTicketCount | Tickets no facturables (10). |
waitingTicketCount | Tickets en proceso (cualquier otro estatus). |
totalTicketsUsed | Suma de los tres anteriores. |
totalInvoiced | Suma del monto de los tickets facturados. |
totalIVATaxInvoiced | totalInvoiced × 16 %. |
ticketsMonthlyLimit | Límite mensual de tickets del plan. |
GetTicketFamilyStats
GET /B2B/ReportV1/GetTicketFamilyStats · startDate, endDate (requeridos) · associationId (opcional)
Productos facturados agrupados por familia del catálogo del SAT.
{
"data": [
{ "familyId": "1510", "description": "Combustibles", "count": 42 },
{ "familyId": "5019", "description": "Alimentos preparados", "count": 17 }
],
"isSuccess": true,
"message": null
}
familyId es la clave de la familia (satProductFamilyId en GetSatProductFamilies).
GetTicketStatuses
GET /B2B/ReportV1/GetTicketStatuses · startDate, endDate (requeridos) · associationId (opcional)
Tickets de un rango con su estatus y su código.
{
"data": [
{
"userTicketId": 88254,
"createDate": "2026-10-04T20:10:02",
"ticketStatus": { "ticketStatusId": 5, "name": "WAITING INVOICE EMAIL" },
"ticketCode": { "ticketCodeId": 1, "name": "El ticket fue creado." }
}
],
"isSuccess": true,
"message": null
}
ticketCode.name es la description del catálogo GetTicketCodes.
GetTicketsByMonth
GET /B2B/ReportV1/GetTicketsByMonth · month (1-12), year (requeridos; no se permiten meses futuros) · associationId (opcional)
Tickets de un mes completo, del más reciente al más antiguo. Mismo formato que GetTicketById, sin xmlData, invoiceMethod, ticketOriginId, ticketOrigin ni associationId.
{
"data": [
{
"userTicketId": 88254,
"userId": 3,
"ticketUrl": "https://...",
"pdfUrl": null,
"xmlUrl": null,
"ticketDate": null,
"ticketAmount": null,
"ticketPaymentMethodId": "28",
"ticketPaymentMethod": "Tarjeta de débito",
"ticketStatusId": 5,
"ticketStatus": "WAITING INVOICE EMAIL",
"taxIdentifier": "XXX980317KJ7",
"taxName": "MARIO DIAZ",
"clientName": "MARIO DIAZ",
"clientEmail": "fotofacturas@fotofacturas.ai",
"ticketCodeId": 1,
"invoiceAttemptsCount": 0,
"createDate": "2026-10-04T20:10:02",
"updateDate": "2026-10-04T20:10:02"
}
],
"isSuccess": true,
"message": null
}
GetTicketsByUser
GET /B2B/ReportV1/GetTicketsByUser · int userId
Todos los tickets de un usuario de tu árbol, del más reciente al más antiguo. Mismo formato que GetTicketsByMonth.
User
Los RFC de asociación se administran con las APIs de Association (SaveAssociationCSF, ChangeAssociationCSFStatus), no con estas.
GetUsersByAssociation
GET /B2B/UserV1/GetUsersByAssociation · int associationId
Usuarios de una asociación, con sus RFC.
{
"data": [
{
"userId": 10619,
"createDate": "2026-01-27T21:06:34",
"updateDate": "2026-01-27T21:06:34",
"isActive": true,
"isDeleted": false,
"name": "Mario",
"pSurname": "Díaz",
"mSurname": null,
"email": "fotofacturas@fotofacturas.ai",
"phoneCode": "52",
"phone": "5512345678",
"isEmailVerified": false,
"isPhoneVerified": false,
"isAssociationRoot": false,
"taxInfos": [
{
"userTaxInfoId": 10423,
"rfc": "XXXX80816UI7",
"name": "Mario Díaz",
"csfFile": "https://...",
"isVerified": true,
"isActive": true,
"createDate": "2026-01-27T21:09:23",
"updateDate": "2026-01-27T21:09:23"
}
]
}
],
"isSuccess": true,
"message": null
}
- El primer registro siempre tiene
isAssociationRoot: true: es el usuario raíz, que representa a la propia asociación. SustaxInfosson los RFC de la asociación y es el titular de los tickets de asociación. No lo edites ni lo desactives con las APIs de User. taxInfos: RFC del usuario, activos y deshabilitados.csfFilees una URL temporal.
CreateUser
POST (JSON) /B2B/UserV1/CreateUser
Crea un usuario en una asociación de tu árbol.
{
"associationId": 42,
"name": "Nombre",
"pSurname": "Apellido Paterno",
"mSurname": "Apellido Materno",
"email": "usuario@empresa.com",
"phone": "5512345678",
"phoneCode": "52"
}
- El correo y el teléfono deben ser únicos en Fotofacturas.
phone(mínimo 8 dígitos) yphoneCode(mínimo 2) se guardan solo con dígitos.mSurnamepuede sernull.
{ "data": 11491, "isSuccess": true, "message": "Usuario creado" }
data es el userId creado.
UpdateUser
PUT (JSON) /B2B/UserV1/UpdateUser
Actualiza los datos de un usuario; también permite moverlo a otra asociación de tu árbol. Si cambia el correo o el teléfono, se pierde su verificación.
{
"userId": 11491,
"associationId": 42,
"name": "Nombre",
"pSurname": "Apellido Paterno",
"mSurname": "Apellido Materno",
"email": "usuario@empresa.com",
"phone": "5512345678",
"phoneCode": "52"
}
Respuesta: message Usuario actualizado.
ChangeUserStatus
PUT (JSON) /B2B/UserV1/ChangeUserStatus
Activa o desactiva un usuario. Un usuario inactivo no puede subir tickets.
{ "userId": 11491, "isActive": false }
Respuesta: message Usuario desactivado (o activado).
AddUserTaxInfo
POST (multipart/form-data) /B2B/UserV1/AddUserTaxInfo
Registra un RFC del usuario subiendo su CSF. Respeta multiRfcPerUser. Se rechaza si el usuario ya tiene ese RFC (para actualizarlo usa UpdateUserTaxInfo).
| Campo | Tipo | Requerido |
|---|---|---|
pdf | archivo PDF (CSF) | Sí |
userId | int | Sí |
Respuesta: message CSF agregada.
UpdateUserTaxInfo
PUT (multipart/form-data) /B2B/UserV1/UpdateUserTaxInfo
Reemplaza la CSF de un RFC del usuario; la nueva CSF debe ser del mismo RFC. Actualiza los datos fiscales y reactiva el RFC (si estaba deshabilitado, respeta multiRfcPerUser).
| Campo | Tipo | Requerido |
|---|---|---|
pdf | archivo PDF (CSF) | Sí |
userTaxInfoId | int | Sí |
Respuesta: message CSF actualizada.
Webhook
Referencia de las APIs de webhooks. Cómo funcionan (eventos, qué webhook se usa para cada ticket, formato del aviso, firma, reintentos y solución de problemas) está en Webhooks. También se configuran y se reenvían eventos desde la pantalla Webhooks de la web app business; el reenvío solo está disponible ahí.
GetWebhooks
GET /B2B/WebhookV1/GetWebhooks · int associationId
Webhooks de una asociación, activos e inactivos. Nunca regresa el secreto.
{
"data": [
{
"webhookSubscriptionId": 7,
"associationId": 41,
"eventType": 1,
"eventName": "ticket.invoiced",
"url": "https://erp.empresa.com/webhooks/fotofacturas",
"isActive": true,
"createDate": "2026-10-13T09:00:00",
"updateDate": "2026-10-13T09:00:00"
}
],
"isSuccess": true,
"message": null
}
SaveWebhook
POST (JSON) /B2B/WebhookV1/SaveWebhook
Crea o actualiza el webhook de un evento en una asociación (uno por evento y asociación).
{ "associationId": 41, "eventType": 1, "url": "https://erp.empresa.com/webhooks/fotofacturas", "isActive": true }
{ "data": { "webhookSubscriptionId": 7, "secret": "whsec_3f8a..." }, "isSuccess": true, "message": "Webhook guardado" }
- La URL debe ser HTTPS en el puerto 443, sin usuario ni contraseña, y resolver a IPs públicas (ver Webhooks › Requisitos de la URL).
- Si el webhook ya existía, se actualizan
urleisActivey el secreto no cambia. secretsolo viene al crear el webhook (al actualizar esnull). Si lo pierdes, usaRegenerateWebhookSecret.
RegenerateWebhookSecret
POST (JSON) /B2B/WebhookV1/RegenerateWebhookSecret
Genera un secreto nuevo; el anterior deja de ser válido de inmediato.
{ "webhookSubscriptionId": 7 }
{ "data": "whsec_9b1c...", "isSuccess": true, "message": "Secreto regenerado" }
TestWebhook
POST (JSON) /B2B/WebhookV1/TestWebhook
Envía un evento ping a la URL del webhook (aunque esté desactivado) y regresa el resultado. No se reintenta ni queda en el historial.
{ "webhookSubscriptionId": 7 }
{
"data": { "isDelivered": true, "statusCode": 200, "responseBody": "{\"ok\":true}", "error": null, "durationMs": 184 },
"isSuccess": true,
"message": null
}
responseBody se limita a 2000 caracteres. Con timeout o error de conexión, statusCode es null y error trae el motivo.
GetWebhookEvents
GET /B2B/WebhookV1/GetWebhookEvents · startDate, endDate (requeridos, máximo 31 días) · associationId (asociación del ticket), userTicketId, status (opcionales)
Historial de eventos de un rango, del más reciente al más antiguo. Regresa como máximo maxResults (5000) eventos; si totalCount es mayor, reduce el rango o filtra.
{
"data": {
"totalCount": 1,
"maxResults": 5000,
"events": [
{
"webhookEventId": 98123,
"webhookSubscriptionId": 7,
"associationId": 42,
"eventType": 1,
"eventName": "ticket.invoiced",
"userTicketId": 88254,
"ticketStatusId": 11,
"previousTicketStatusId": 5,
"status": 2,
"statusName": "DELIVERED",
"attemptCount": 1,
"occurredDate": "2026-10-13T10:15:22",
"nextAttemptDate": null,
"lastAttemptDate": "2026-10-13T10:15:25",
"deliveredDate": "2026-10-13T10:15:25",
"createDate": "2026-10-13T10:15:22"
}
]
},
"isSuccess": true,
"message": null
}
status: 1 PENDING, 2 DELIVERED, 3 FAILED.
GetWebhookEventById
GET /B2B/WebhookV1/GetWebhookEventById · long webhookEventId
Detalle de un evento: mismos campos que GetWebhookEvents, más la URL actual del webhook, el body enviado y cada intento.
{
"url": "https://erp.empresa.com/webhooks/fotofacturas",
"payload": { "eventId": 98123, "eventType": "ticket.invoiced", "occurredAt": "...", "data": { ... } },
"attempts": [
{ "attemptNumber": 1, "statusCode": 500, "responseBody": "Internal Server Error", "error": null, "durationMs": 230, "createDate": "2026-10-13T10:15:25" },
{ "attemptNumber": 2, "statusCode": 200, "responseBody": "{\"ok\":true}", "error": null, "durationMs": 140, "createDate": "2026-10-13T10:16:25" }
]
}
payload es null si el evento aún no se ha intentado enviar.
Los mensajes de error de estas APIs están en Webhooks › Solución de problemas.