Integración

API

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>
AmbienteURL baseToken
Producciónhttps://api.fotofacturas.ai/B2B/<Controller>/<API>Web app business, sección API y plan.
Pruebashttps://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):

CampoTipoDescripción
isSuccessbooleanSi la operación fue exitosa.
messagestring | nullMensaje del resultado; en errores, el motivo.
dataobject | nullInformació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

HTTPCuándomessage
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.
400JSON 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.
401Problema con el token (ver tabla siguiente).El motivo.
429Más de 500 peticiones por minuto desde la misma IP.Demasiadas peticiones. Por favor, intenta más tarde.
500Error inesperado.Error inesperado en el sistema

Errores de autenticación (HTTP 401):

MensajeCausa
Falta header de autorizaciónNo se mandó Authorization.
Formato inválido del header de autorizaciónEl header no empieza con Bearer .
Falta el token en el header de autorizaciónNo hay token después de Bearer.
Token no válidoEl token no existe o la cuenta está inactiva.
Usuario no autorizado para usar este servicioEl token no es de un usuario B2B.
Servicio no disponible, reintente en un minutoEl servicio está iniciando.

Errores frecuentes (HTTP 200, isSuccess: false):

MensajeCausa
No estas autorizado para usar esta APITu cuenta no tiene el permiso de esa API.
Por favor reintente en unos momentosEl servicio está iniciando.
La asociación no le pertenece / La asociación no te perteneceLa asociación no está en tu árbol.
El usuario no pertenece a tus asociaciones / Este usuario no pertenece a su asociaciónEl usuario no está en tu árbol.
El usuario no existe / La asociación no existe / El ticket no existeEl id no existe.
El usuario no está activoSe intentó subir un ticket para un usuario inactivo.
El RFC es obligatorio / El RFC {rfc} no es válidoFalta el RFC o su formato no es válido.
El RFC {rfc} no está registradoEl RFC no está entre los que puede usar el usuario o la asociación.
El RFC {rfc} está deshabilitadoEl RFC está deshabilitado.
El plan solo permite {n} RFCs en la asociación / El plan solo permite {n} RFCs por usuarioSe 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 finRango de fechas faltante o inválido.
El mes debe estar entre 1 y 12 / Año inválido / No se puede consultar un mes futuroMes o año inválido.

Formatos

  • Entrada:
    • GET: query params.
    • POST/PUT sin archivos: JSON.
    • APIs con archivos (CSF, logos, imagen del ticket): multipart/form-data. Para varios archivos en un campo se repite la llave (por ejemplo CSFs); los campos anidados usan punto (por ejemplo Config.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 que GetUsersByAssociation.
  • rfcs: RFC de la asociación, activos y deshabilitados. csfFile es una URL temporal.
  • config: siempre viene; los campos no configurados regresan su valor por defecto. Los logos son URLs públicas o null.

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.

CampoTipoRequeridoDescripción
NamestringSíNombre de la asociación.
ParentIdintSíAsociación padre (de tu árbol).
CSFsarchivo(s) PDFNoCSF de la asociación. Repite la llave para varias. Sin RFC repetidos y sin exceder multiRfcPerAssociation.
Config.*NoCampos 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 }
  • parentId es 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.

CampoTipoDescripción
AssociationIdintRequerido.
LogoSquare / LogoRectanglearchivo JPG o PNGLogo cuadrado / rectangular (máx. 4 MB). Reemplaza el anterior.
CompanyNamestringRazón social a mostrar (máx. 150 caracteres).
DefaultRegimenFiscalIdintRégimen fiscal por defecto.
DefaultUsoCfdiIdstringUso de CFDI por defecto. Requiere régimen fiscal (el que se manda o el guardado) y debe ser válido para él.
DefaultTicketPaymentMethodIdstringForma de pago por defecto.
CanChangeRegimenFiscal / CanChangeUsoCfdi / CanChangePaymentMethodboolSi el usuario puede cambiar ese valor al subir un ticket.
StyleRoundnessintRedondeo de la interfaz (no negativo).
StyleSidebarColor / StyleAccentColor / StyleAccentStrongColorstringColor #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.

CampoTipoRequerido
AssociationIdintSí
CSFarchivo PDFSí

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
}
CampoDescripción
ticketsMonthlyLimit, ticketsMonthlyLimitPerUser, canPictureInvoice, multiRfcPerUser, multiRfcPerAssociation, multiRfcTypeLímites del plan (ver Conceptos › Plan y límites).
ticketsUsedTickets del mes actual de todo el árbol, sin contar descartados.
associationsCountAsociaciones del árbol, incluida la raíz.
associationActiveTaxInfosCount / associationInactiveTaxInfosCountRFC de asociación activos / deshabilitados.
userActiveTaxInfosCount / userInactiveTaxInfosCountRFC 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 ser null.
  • isSuccessCode: si el código es un resultado de éxito; puede ser null.

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>.

APIMétodoParámetros (opcionales)Respuesta (data)
GetSatProductDivisionsGETNinguno[{ "satProductDivisionId", "description" }]
GetSatProductFamiliesGETsatProductDivisionId[{ "satProductFamilyId", "description", "satProductDivisionId" }]
GetSatProductClassesGETsatProductFamilyId[{ "satProductClassId", "description", "satProductFamilyId" }]
GetSatProductsGETsatProductClassId, 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, pdfUrl y xmlUrl son URLs temporales. pdfUrl y xmlUrl son null si no hay factura.
  • xmlData: datos leídos del XML de la factura (null si no hay XML).
  • taxIdentifier / taxName: RFC y nombre a los que se factura.
  • invoiceMethod: cómo se obtuvo la factura (null si 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.

CampoTipoRequeridoDescripción
UserIdintSíUsuario activo de tu árbol.
ImagearchivoSíFoto del ticket: JPG, PNG, WEBP, HEIC o PDF (máx. 10 MB).
RfcstringSíRFC al que se factura. Debe estar registrado y activo.
TicketPaymentMethodIdstringSíCatálogo GetTicketPaymentMethods.
RegimenFiscalIdintSíCatálogo GetRegimenFiscalAndCFDI.
UsoCfdiIdstringSíDebe ser válido para el régimen fiscal.
CardLastFourstringCondicionalÚ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.

CampoTipoRequerido
AssociationIdintSí
Image, Rfc, TicketPaymentMethodId, RegimenFiscalId, UsoCfdiId, CardLastFourIgual 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, ticketsMonthlyLimit siempre 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
}
CampoDescripción
processedTicketCountTickets facturados (11).
cancelledTicketCountTickets no facturables (10).
waitingTicketCountTickets en proceso (cualquier otro estatus).
totalTicketsUsedSuma de los tres anteriores.
totalInvoicedSuma del monto de los tickets facturados.
totalIVATaxInvoicedtotalInvoiced × 16 %.
ticketsMonthlyLimitLí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. Sus taxInfos son 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. csfFile es 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) y phoneCode (mínimo 2) se guardan solo con dígitos. mSurname puede ser null.
{ "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).

CampoTipoRequerido
pdfarchivo PDF (CSF)Sí
userIdintSí

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).

CampoTipoRequerido
pdfarchivo PDF (CSF)Sí
userTaxInfoIdintSí

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 url e isActive y el secreto no cambia.
  • secret solo viene al crear el webhook (al actualizar es null). Si lo pierdes, usa RegenerateWebhookSecret.

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.