Documentation

APIs Enterprise

API for enterprise integrations.

It lets you manage the company structure (associations), its users and its RFCs, upload tickets to be invoiced, and query reports.

Business concepts

B2B account (administrator)

Each client company has a B2B administrator user with a static token. Every request is made with that token, and the API only lets you view and modify the information of your own association tree: any association, user, RFC or ticket outside that tree is rejected.

Association

An association is an organizational unit of the company: the company itself, a branch, a department, a project, etc. Associations form a tree:

  • The root association is the association of the B2B account (the company). It cannot be moved or change its parent.
  • Child associations are created under the root (and they can have children of their own), with no limit on levels.
  • Each association can have RFCs loaded, to be inherited by its users or used by the association itself.
  • An optional configuration: logos, company name, colors and invoicing defaults (tax regime, CFDI use and payment method). Anything not configured uses Fotofacturas defaults.

User

A user is the end person (for example, an employee) who uploads tickets to be invoiced, whether from the Fotofacturas app, the web app, WhatsApp, embedded or through this API. Each user belongs to one association and can have their own RFCs. A user can be activated/deactivated; an inactive user cannot upload tickets or use the Fotofacturas platforms.

RFC and CSF

An RFC is registered by uploading its Constancia de Situación Fiscal (CSF), the tax status certificate, as a PDF. Fotofacturas processes the PDF automatically and extracts the RFC, the legal name, the tax address and the tax regimes. Each registered RFC is called tax information (tax info).

  • An RFC can belong to a user or to an association.
  • If the CSF of an already registered RFC is uploaded again, it is updated (same RFC) and reactivated.
  • An RFC can be active or disabled. Only active RFCs count toward the plan limits and can be used for invoicing.

Which RFCs can be used for invoicing

It is defined by the RFC mode of the contracted plan:

ModeUser ticketAssociation ticket
Child (C)Only the user’s own RFCs.The association’s own RFCs.
Parent (P)The RFCs of the association the user belongs to; if that association has no active RFCs, those of the root association.The association’s own RFCs.
Mixed (M)Their own RFCs + those of their association (or those of the root association if their association has no active RFCs).The association’s own RFCs.

Plan limits

LimitDescription
ticketsMonthlyLimitTickets per month for the whole association tree. null = no group limit.
ticketsMonthlyLimitPerUserTickets per month per user. null = no per-user limit.
canPictureInvoiceWhether the plan allows uploading tickets.
multiRfcPerUserMaximum own active RFCs per user.
multiRfcPerAssociationMaximum active RFCs per association.
multiRfcTypeRFC usage mode (C, P, M), see the table above.

The month used for limits is the calendar month in Mexico time.

Ticket

A ticket is the photo of a purchase receipt that Fotofacturas invoices on behalf of the chosen RFC. When uploading it you specify the RFC, the tax regime, the CFDI use and the payment method. A ticket can be:

  • User ticket: invoiced to an RFC the user is allowed to use.
  • Association ticket: loaded directly to the association and invoiced to one of the association’s own RFCs.

Ticket status (ticketStatusId):

IdStatusMeaning
1CREATEDReceived, pending processing.
3AUTO INVOICINGIn automatic invoicing.
5WAITING INVOICE EMAILWaiting for the invoice by email.
6MANUAL REVIEWIn manual review.
7ADVANCED REVIEWIn advanced review.
8WAITING MERCHANTWaiting for the merchant’s response.
10UNSIGNABLECould not be invoiced (cancelled).
11SIGNEDInvoiced.

The ticket code (ticketCodeId) indicates the final result or the error reason (catalog in GetTicketCodes).

Archived tickets still in CREATED status are considered discarded: they don’t count in reports, lists or plan usage.

Dates

  • All response dates are in Mexico time.
  • Input dates (startDate, endDate) are dates without time in yyyy-MM-dd format, as Mexico dates, and include the whole day (from 00:00 to 23:59:59).
  • Monthly reports (month, year) use the whole calendar month in Mexico time.

General usage

Authentication

Every request carries the static token of the B2B account:

Authorization: Bearer <token>

Base URL: https://api.fotofacturas.ai/B2B/<Controller>/<Action>

If authentication fails, the API returns HTTP 401 with isSuccess: false and one of these messages:

MessageCause
Falta header de autorizaciónThe Authorization header was not sent.
Formato inválido del header de autorizaciónThe header doesn’t start with Bearer .
Falta el token en el header de autorizaciónThe header has no token after Bearer.
Token no válidoThe token doesn’t exist or the account is inactive.
Usuario no autorizado para usar este servicioThe account is not a B2B account.
Servicio no disponible, reintente en un minutoThe service is starting up; retry in a few seconds.

API permissions

Each API requires a permission assigned to the B2B account. Fotofacturas enables permissions per account according to the contract; an account can have access to all APIs or only to some.

  • If the account doesn’t have the permission for the API it calls, the request returns HTTP 200 with isSuccess: false and the message No estas autorizado para usar esta API.
  • To enable an API, contact Fotofacturas with the permission name.
IdPermission
1AssociationV1/GetAssociationList
2AssociationV1/GetAssociationTree
3AssociationV1/GetAssociationById
4AssociationV1/CreateAssociation
5AssociationV1/UpdateAssociation
6AssociationV1/UpdateAssociationConfig
7AssociationV1/SaveAssociationCSF
8AssociationV1/ChangeAssociationCSFStatus
9MiscellaneousV1/Ping
10MiscellaneousV1/GetRegimenFiscalAndCFDI
11MiscellaneousV1/GetSatProductDivisions
12MiscellaneousV1/GetSatProductFamilies
13MiscellaneousV1/GetSatProductClasses
14MiscellaneousV1/GetSatProducts
15MiscellaneousV1/GetTicketPaymentMethods
16MiscellaneousV1/GetTicketCodes
17MiscellaneousV1/GetAccountInfo
18TicketV1/GetTicketById
19TicketV1/CreateUserTicket
20TicketV1/CreateAssociationTicket
21ReportV1/GetTicketCountByDay
22ReportV1/GetPeriodStats
23ReportV1/GetTicketFamilyStats
24ReportV1/GetTicketStatuses
25ReportV1/GetTicketsByMonth
26ReportV1/GetTicketsByUser
27UserV1/GetUsersByAssociation
28UserV1/CreateUser
29UserV1/UpdateUser
30UserV1/ChangeUserStatus
31UserV1/AddUserTaxInfo
32UserV1/UpdateUserTaxInfo
35WebhookV1/GetWebhooks
36WebhookV1/SaveWebhook
37WebhookV1/RegenerateWebhookSecret
38WebhookV1/TestWebhook
39WebhookV1/GetWebhookEvents
40WebhookV1/GetWebhookEventById
41WebhookV1/ResendWebhookEvent

Response

Every API returns the following object (JSON in camelCase):

FieldTypeDescription
isSuccessbooleanIndicates whether the operation succeeded.
messagestring | nullResult message. On errors it contains the reason.
dataobject | nullRequested information (only in the APIs that return data).

Business error example:

{
  "isSuccess": false,
  "message": "El RFC XAXX010101000 no está registrado",
  "data": null
}

Errors

HTTPWhen it happensBody
200Successful request or validation/business error (invalid data, resource outside the account’s tree, plan limit, API without permission, etc.).FFResponse with isSuccess: false and the reason in message.
400The request can’t be read: malformed JSON, wrong data type or a required body field is missing.{ "isSuccess": false, "message": "Datos inválidos en: userId, rfc" } with the fields in error, or Datos inválidos en la petición if the field can’t be identified.
401The Authorization header is missing, isn’t in Bearer <token> format, the token doesn’t exist, the account is inactive or the account is not B2B.{ "isSuccess": false, "message": "..." } with the reason (see the authentication table).
429More than 200 requests per minute from the same IP.{ "isSuccess": false, "message": "Demasiadas peticiones. Por favor, intenta más tarde." }
500Unexpected server error.{ "isSuccess": false, "message": "Error inesperado en el sistema", "data": "Error inesperado en el sistema" }

Always check isSuccess: an HTTP 200 response doesn’t mean the operation was performed.

Common error messages (HTTP 200, isSuccess: false):

MessageCause
No estas autorizado para usar esta APIThe account doesn’t have the API permission (see API permissions).
Por favor reintente en unos momentosThe service is starting up; retry in a few seconds.
La asociación no le pertenece / La asociación no te perteneceThe given association is not in the account’s tree.
El usuario no pertenece a tus asociaciones / Este usuario no pertenece a su asociaciónThe given user is not in the account’s tree.
El usuario no existe / La asociación no existe / El ticket no existeThe given id doesn’t exist.
El usuario no está activoA ticket was uploaded for an inactive user.
El RFC es obligatorio / El RFC {rfc} no es válidoThe RFC is missing or its format is invalid.
El RFC {rfc} no está registradoThe RFC is not among those the user or association can use.
El RFC {rfc} está deshabilitadoThe RFC exists but is disabled.
El plan solo permite {n} RFCs en la asociación / El plan solo permite {n} RFCs por usuarioThe plan’s RFC limit was reached.
Has alcanzado tu límite mensual de {n} tickets... / Has alcanzado tu límite personal mensual de {n} tickets...The group’s or user’s monthly ticket limit was reached.
La fecha de inicio es obligatoria / La fecha de fin es obligatoria / La fecha de inicio no puede ser mayor a la fecha de finMissing or invalid date range in reports.
El mes debe estar entre 1 y 12 / Año inválido / No se puede consultar un mes futuroInvalid month or year in monthly reports.

Input formats

  • Query params: GET APIs.
  • JSON: PUT/POST APIs without files.
  • multipart/form-data: APIs that receive files (CSF, logos or the ticket image). To send several files in the same field, repeat the key (e.g. CSFs). Nested fields are sent with a dot (e.g. Config.CompanyName).

Association

GetAssociationList

Flat list of all associations in the account’s tree, sorted by id, with the RFCs of each one.

MethodGET
URLhttps://api.fotofacturas.ai/B2B/AssociationV1/GetAssociationList
ParametersNone
{
  "data": [
    {
      "associationId": 41,
      "name": "FOTOFACTURAS",
      "parentId": null,
      "rfcs": [
        { "rfc": "FFA200101AB1", "isActive": true }
      ]
    },
    {
      "associationId": 42,
      "name": "Sucursal Norte",
      "parentId": 41,
      "rfcs": []
    }
  ],
  "isSuccess": true,
  "message": null
}

parentId is null for the root association.

GetAssociationTree

Hierarchical tree of the account’s associations, with the RFCs of each one.

MethodGET
URLhttps://api.fotofacturas.ai/B2B/AssociationV1/GetAssociationTree
ParametersNone
{
  "data": {
    "associationId": 41,
    "name": "FOTOFACTURAS",
    "rfcs": [
      { "rfc": "FFA200101AB1", "isActive": true }
    ],
    "children": [
      {
        "associationId": 42,
        "name": "Sucursal Norte",
        "rfcs": [],
        "children": []
      }
    ]
  },
  "isSuccess": true,
  "message": null
}

GetAssociationById

Details of an association: general data, users, RFCs and configuration.

MethodGET
URLhttps://api.fotofacturas.ai/B2B/AssociationV1/GetAssociationById
Parametersint associationId
{
  "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: the association’s users, same format as GetUsersByAssociation.
  • rfcs: the association’s RFCs (active and disabled). csfFile is a temporary URL to download the CSF.
  • config: always present; fields not configured return their default value. Logos are public URLs or null.

CreateAssociation

Creates a child association within the tree. Optionally receives its configuration and one or more CSF PDFs to register its RFCs. If any CSF is invalid, nothing is created.

MethodPOST (multipart/form-data)
URLhttps://api.fotofacturas.ai/B2B/AssociationV1/CreateAssociation
FieldTypeRequiredDescription
NamestringYesAssociation name.
ParentIdintYesParent association (must be in the account’s tree).
CSFsPDF file(s)NoThe association’s CSFs. Repeat the key for several. The same RFC can’t be sent twice and multiRfcPerAssociation can’t be exceeded.
Config.*NoSame fields as UpdateAssociationConfig with the Config. prefix (e.g. Config.CompanyName, Config.LogoSquare).
{
  "data": 43,
  "isSuccess": true,
  "message": "Guardada correctamente"
}

data is the created associationId.

UpdateAssociation

Changes the name and, optionally, the parent of an association. The plan and other data are not modified.

MethodPUT (JSON)
URLhttps://api.fotofacturas.ai/B2B/AssociationV1/UpdateAssociation
{
  "associationId": 42,
  "name": "Sucursal Norte",
  "parentId": 41
}
  • parentId is optional: if it isn’t sent (or is the same), the association doesn’t move.
  • The root association can’t change its parent.
  • The new parent must be in the tree and can’t be the association itself or one of its descendants (circular reference).
{
  "isSuccess": true,
  "message": "Actualizada correctamente"
}

UpdateAssociationConfig

Updates an association’s configuration. Only the fields that are sent are modified; fields not sent keep their value.

MethodPUT (multipart/form-data)
URLhttps://api.fotofacturas.ai/B2B/AssociationV1/UpdateAssociationConfig
FieldTypeDescription
AssociationIdintRequired.
LogoSquareJPG/PNG fileSquare logo (max. 4 MB). Replaces the previous one.
LogoRectangleJPG/PNG fileRectangular logo (max. 4 MB). Replaces the previous one.
CompanyNamestringCompany name to display (max. 150 characters).
DefaultRegimenFiscalIdintDefault tax regime (GetRegimenFiscalAndCFDI catalog).
DefaultUsoCfdiIdstringDefault CFDI use. Requires a tax regime (the one sent or the saved one) and must be valid for that regime.
DefaultTicketPaymentMethodIdstringDefault payment method (GetTicketPaymentMethods catalog).
CanChangeRegimenFiscalboolWhether the user can change the tax regime.
CanChangeUsoCfdiboolWhether the user can change the CFDI use.
CanChangePaymentMethodboolWhether the user can change the payment method.
StyleRoundnessintInterface corner roundness (non-negative).
StyleSidebarColorstringColor #RRGGBB or #RRGGBBAA.
StyleAccentColorstringColor #RRGGBB or #RRGGBBAA.
StyleAccentStrongColorstringColor #RRGGBB or #RRGGBBAA.
{
  "isSuccess": true,
  "message": "Configuración actualizada"
}

SaveAssociationCSF

Registers or updates an association RFC by uploading its CSF. If the RFC in the PDF already exists in the association, it is updated (and reactivated); otherwise it is created. Creating or reactivating an RFC respects the multiRfcPerAssociation limit.

MethodPOST (multipart/form-data)
URLhttps://api.fotofacturas.ai/B2B/AssociationV1/SaveAssociationCSF
FieldTypeRequired
AssociationIdintYes
CSFPDF fileYes
{
  "isSuccess": true,
  "message": "CSF creada"
}

message is CSF creada or CSF actualizada depending on the case.

ChangeAssociationCSFStatus

Activates or disables an association RFC. Activating respects the multiRfcPerAssociation limit.

MethodPUT (JSON)
URLhttps://api.fotofacturas.ai/B2B/AssociationV1/ChangeAssociationCSFStatus
{
  "associationId": 42,
  "rfc": "FFA200101AB1",
  "isActive": false
}
{
  "isSuccess": true,
  "message": "CSF deshabilitada"
}

Miscellaneous

Ping

Confirms that the API is up and the token is valid.

MethodGET
URLhttps://api.fotofacturas.ai/B2B/MiscellaneousV1/Ping
ParametersNone
{
  "isSuccess": true,
  "message": "Pong"
}

GetAccountInfo

Information about the B2B account, its plan and the current month’s usage.

MethodGET
URLhttps://api.fotofacturas.ai/B2B/MiscellaneousV1/GetAccountInfo
ParametersNone
{
  "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
}
FieldDescription
ticketsUsedTickets in the current month across the whole tree (users and associations), excluding discarded ones.
associationsCountAssociations in the tree, including the root.
associationActiveTaxInfosCount / associationInactiveTaxInfosCountActive / disabled association RFCs.
userActiveTaxInfosCount / userInactiveTaxInfosCountActive / disabled user RFCs.

associationAndPlan is null if the account has no association assigned.

GetTicketPaymentMethods

Catalog of payment methods (visible ones only), sorted.

MethodGET
URLhttps://api.fotofacturas.ai/B2B/MiscellaneousV1/GetTicketPaymentMethods
ParametersNone
{
  "data": [
    { "ticketPaymentMethodId": "01", "name": "Efectivo" },
    { "ticketPaymentMethodId": "28", "name": "Tarjeta de débito" }
  ],
  "isSuccess": true,
  "message": null
}

GetRegimenFiscalAndCFDI

Catalog of tax regimes with the valid CFDI uses for each one.

MethodGET
URLhttps://api.fotofacturas.ai/B2B/MiscellaneousV1/GetRegimenFiscalAndCFDI
ParametersNone
{
  "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

Catalog of ticket result codes (success or error reason).

MethodGET
URLhttps://api.fotofacturas.ai/B2B/MiscellaneousV1/GetTicketCodes
ParametersNone
{
  "data": [
    {
      "ticketCodeId": 1,
      "category": "Ticket",
      "description": "El ticket fue creado.",
      "isSuccessCode": null
    }
  ],
  "isSuccess": true,
  "message": null
}

SAT products and services catalog

Hierarchical SAT catalog: Division → Family → Class → Product. Keys are prefixes: division 50 contains family 5010, which contains class 501015, which contains products 501015xx.

APIMethodParameters (optional)Response (data)
GetSatProductDivisionsGETNone[{ "satProductDivisionId", "description" }]
GetSatProductFamiliesGETstring? satProductDivisionId[{ "satProductFamilyId", "description", "satProductDivisionId" }]
GetSatProductClassesGETstring? satProductFamilyId[{ "satProductClassId", "description", "satProductFamilyId" }]
GetSatProductsGETstring? satProductClassId, string? satProductFamilyId, string? satProductDivisionId, string? satProductId[{ "satProductId", "description", "similares", "satProductClassId" }]

URL: https://api.fotofacturas.ai/B2B/MiscellaneousV1/<API>

For GetSatProducts, sending at least one filter is recommended: with no filters it returns the full catalog.


Ticket

GetTicketById

Details of a ticket, including the invoice data if it has already been invoiced.

MethodGET
URLhttps://api.fotofacturas.ai/B2B/TicketV1/GetTicketById
Parametersint userTicketId
{
  "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": 1,
    "invoiceAttemptsCount": 1,
    "invoiceMethod": 1,
    "xmlData": { ... },
    "createDate": "2026-10-04T20:10:02",
    "updateDate": "2026-10-05T09:30:00"
  },
  "isSuccess": true,
  "message": null
}
  • ticketUrl, pdfUrl and xmlUrl are temporary download URLs (pdfUrl/xmlUrl are null if there is no invoice yet).
  • xmlData contains the data read from the invoice XML (null if there is no XML).
  • taxIdentifier/taxName are the RFC and name the ticket is invoiced to.

CreateUserTicket

Uploads a ticket on behalf of a user. The RFC must be one the user can use according to the plan’s RFC mode (see Business concepts).

MethodPOST (multipart/form-data)
URLhttps://api.fotofacturas.ai/B2B/TicketV1/CreateUserTicket
FieldTypeRequiredDescription
UserIdintYesActive user in the tree.
Imageimage fileYesPhoto of the ticket (max. 10 MB).
RfcstringYesRFC to invoice to. Must be registered and active.
TicketPaymentMethodIdstringYesGetTicketPaymentMethods catalog.
RegimenFiscalIdintYesGetRegimenFiscalAndCFDI catalog.
UsoCfdiIdstringYesMust be valid for the tax regime.
CardLastFourstringConditionalLast 4 digits of the card. Required with card payment methods (04, 28).
{
  "data": 88255,
  "isSuccess": true,
  "message": "Ticket creado"
}

data is the created userTicketId. It is rejected if the user or the group reached their monthly ticket limit or if the plan doesn’t allow uploading tickets.

CreateAssociationTicket

Uploads a ticket on behalf of an association. The RFC must be one of the association’s own RFCs.

MethodPOST (multipart/form-data)
URLhttps://api.fotofacturas.ai/B2B/TicketV1/CreateAssociationTicket
FieldTypeRequired
AssociationIdintYes
Image, Rfc, TicketPaymentMethodId, RegimenFiscalId, UsoCfdiId, CardLastFourSame as in CreateUserTicket.
{
  "data": 88256,
  "isSuccess": true,
  "message": "Ticket creado"
}

Report

All reports consider the account’s whole association tree (users and associations) and exclude discarded tickets.

GetTicketCountByDay

Number of tickets per day in a date range. Includes every day in the range (with 0 if there were no tickets).

MethodGET
URLhttps://api.fotofacturas.ai/B2B/ReportV1/GetTicketCountByDay
Parametersdate startDate, date endDate (required, yyyy-MM-dd)
{
  "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
}

average = total tickets / number of days in the range.

GetPeriodStats

Statistics for a whole month compared against the previous month.

MethodGET
URLhttps://api.fotofacturas.ai/B2B/ReportV1/GetPeriodStats
Parametersint month (1-12), int year (required; future months are not allowed)
{
  "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": {
      "startDate": "2026-09-01T00:00:00",
      "endDate": "2026-09-30T23:59:59.999",
      "processedTicketCount": 24,
      "waitingTicketCount": 6,
      "cancelledTicketCount": 9,
      "totalInvoiced": 13222.30,
      "totalIVATaxInvoiced": 2115.57,
      "totalTicketsUsed": 39,
      "ticketsMonthlyLimit": 1000
    }
  },
  "isSuccess": true,
  "message": null
}
FieldDescription
processedTicketCountInvoiced tickets (SIGNED).
cancelledTicketCountTickets that could not be invoiced (UNSIGNABLE).
waitingTicketCountTickets in any other status (in progress).
totalTicketsUsedSum of the three above.
totalInvoicedSum of the amounts of invoiced tickets.
totalIVATaxInvoicedtotalInvoiced × 16%.
ticketsMonthlyLimitMonthly ticket limit of the current plan.

GetTicketFamilyStats

Number of invoiced products grouped by SAT catalog family, in a date range.

MethodGET
URLhttps://api.fotofacturas.ai/B2B/ReportV1/GetTicketFamilyStats
Parametersdate startDate, date endDate (required)
{
  "data": [
    { "description": "Combustibles", "count": 42 },
    { "description": "Alimentos preparados", "count": 17 }
  ],
  "isSuccess": true,
  "message": null
}

GetTicketStatuses

List of tickets in a date range with their final status and result code.

MethodGET
URLhttps://api.fotofacturas.ai/B2B/ReportV1/GetTicketStatuses
Parametersdate startDate, date endDate (required)
{
  "data": [
    {
      "userTicketId": 88254,
      "createDate": "2026-10-04T20:10:02",
      "ticketStatus": { "ticketStatusId": 11, "name": "SIGNED" },
      "ticketCode": { "ticketCodeId": 1, "name": "El ticket fue creado." }
    }
  ],
  "isSuccess": true,
  "message": null
}

ticketCode.name is the same description as in the GetTicketCodes catalog.

GetTicketsByMonth

Tickets for a whole month, from newest to oldest.

MethodGET
URLhttps://api.fotofacturas.ai/B2B/ReportV1/GetTicketsByMonth
Parametersint month (1-12), int year (required; future months are not allowed)
{
  "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
}

Same format as GetTicketById, without xmlData or invoiceMethod.

GetTicketsByUser

All tickets of a user in the tree, from newest to oldest.

MethodGET
URLhttps://api.fotofacturas.ai/B2B/ReportV1/GetTicketsByUser
Parametersint userId

Response: same format as GetTicketsByMonth.


User

Association RFCs are not managed with these APIs but with the Association APIs (SaveAssociationCSF, ChangeAssociationCSFStatus).

GetUsersByAssociation

Users of an association, with their RFCs.

MethodGET
URLhttps://api.fotofacturas.ai/B2B/UserV1/GetUsersByAssociation
Parametersint associationId
{
  "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
}
  • The list includes a record with isAssociationRoot: true that represents the association itself: its taxInfos are the association’s RFCs (the same as rfcs in GetAssociationById) and it is the owner of association tickets. It always comes first. It is not a real user: don’t edit or deactivate it with the User APIs.
  • taxInfos: the user’s RFCs (active and disabled), same format as rfcs in GetAssociationById. csfFile is a temporary URL to download the CSF.

CreateUser

Creates a user in an association of the tree.

MethodPOST (JSON)
URLhttps://api.fotofacturas.ai/B2B/UserV1/CreateUser
{
  "associationId": 42,
  "name": "Nombre",
  "pSurname": "Apellido Paterno",
  "mSurname": "Apellido Materno",
  "email": "usuario@empresa.com",
  "phone": "5512345678",
  "phoneCode": "52"
}
  • Email and phone must be unique in Fotofacturas.
  • phone (at least 8 digits) and phoneCode (at least 2 digits) are stored as digits only. mSurname can be null.
{
  "data": 11491,
  "isSuccess": true,
  "message": "Usuario creado"
}

data is the created userId.

UpdateUser

Updates a user’s data. It also lets you move the user to another association in the tree.

MethodPUT (JSON)
URLhttps://api.fotofacturas.ai/B2B/UserV1/UpdateUser
{
  "userId": 11491,
  "associationId": 42,
  "name": "Nombre",
  "pSurname": "Apellido Paterno",
  "mSurname": "Apellido Materno",
  "email": "usuario@empresa.com",
  "phone": "5512345678",
  "phoneCode": "52"
}

If the email or phone changes, its verification is lost.

{
  "isSuccess": true,
  "message": "Usuario actualizado"
}

ChangeUserStatus

Activates or deactivates a user. An inactive user can’t upload tickets.

MethodPUT (JSON)
URLhttps://api.fotofacturas.ai/B2B/UserV1/ChangeUserStatus
{
  "userId": 11491,
  "isActive": false
}
{
  "isSuccess": true,
  "message": "Usuario desactivado"
}

AddUserTaxInfo

Registers an RFC for the user by uploading its CSF. Respects the multiRfcPerUser limit (own active RFCs).

MethodPOST (multipart/form-data)
URLhttps://api.fotofacturas.ai/B2B/UserV1/AddUserTaxInfo
FieldTypeRequired
pdfPDF file (CSF)Yes
userIdintYes
{
  "isSuccess": true,
  "message": "CSF agregada"
}

It is rejected if the user already has that RFC registered (to update it, use UpdateUserTaxInfo).

UpdateUserTaxInfo

Replaces the CSF of an RFC the user already has registered. The new CSF must be for the same RFC. It updates the tax data and reactivates the RFC (if it was disabled, the multiRfcPerUser limit applies).

MethodPUT (multipart/form-data)
URLhttps://api.fotofacturas.ai/B2B/UserV1/UpdateUserTaxInfo
FieldTypeRequired
pdfPDF file (CSF)Yes
userTaxInfoIdintYes
{
  "isSuccess": true,
  "message": "CSF actualizada"
}

Webhook

Webhooks let you receive a notification (POST) on your own server every time a ticket changes status, without polling the API.

Events

eventType (id)NameWhen it is sent
1ticket.invoicedThe ticket moved to SIGNED (invoiced).
2ticket.failedThe ticket moved to UNSIGNABLE (could not be invoiced).
3ticket.status_changedThe ticket changed status, including its creation (previousTicketStatusId: null → 1 CREATED).

A single change can generate more than one event: when a ticket is invoiced, ticket.status_changed and ticket.invoiced are both sent (each to its own URL, if configured). It applies to every ticket in the tree, regardless of the channel it was uploaded through (API, app, web, WhatsApp, embedded).

Configuration per association

Each association has its own configuration: one URL per event type. For each ticket event, the following is used:

  1. The configuration of the ticket’s association, if it has that event configured and active.
  2. Otherwise, the configuration of the root association.
  3. If neither has it, nothing is sent.

That way, by configuring only the root association you receive the events of the whole tree, and an association can override one or more events with its own URL.

Notification format

POST <url configurada>
Content-Type: application/json
User-Agent: Fotofacturas-Webhooks/1.0
X-FF-Event: ticket.invoiced
X-FF-Event-Id: 98123
X-FF-Timestamp: 1791900922
X-FF-Signature: v1=5d41402abc4b2a76b9719d911017c592...
{
  "eventId": 98123,
  "eventType": "ticket.invoiced",
  "occurredAt": "2026-10-13T10:15:22.123",
  "data": {
    "userTicketId": 88254,
    "userId": 3,
    "associationId": 42,
    "ticketCommentId": 551203,
    "ticketStatusId": 11,
    "ticketStatus": "SIGNED",
    "previousTicketStatusId": 5,
    "ticketCodeId": 1
  }
}
  • userId is null for association tickets.
  • occurredAt is in Mexico time.
  • The payload is intentionally minimal: for the ticket details (invoice, PDF, XML) call GetTicketById.

Verifying the signature

Each webhook has a secret (whsec_...). To validate that the notification comes from Fotofacturas:

  1. Concatenate X-FF-Timestamp + . + the exact body received (without re-serializing).
  2. Compute HMAC-SHA256 with the secret as the key and encode it in hexadecimal (lowercase).
  3. Compare it against the value of X-FF-Signature after v1=, using a constant-time comparison.
  4. Recommended: reject notifications whose X-FF-Timestamp is more than 5 minutes old.
const crypto = require('crypto');
const expected = crypto.createHmac('sha256', secret).update(`${timestamp}.${rawBody}`).digest('hex');
const valid = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature.replace('v1=', '')));

Delivery and retries

  • It is considered delivered when the server responds 2xx in under 10 seconds. Any other response, timeout or connection error counts as a failure.
  • There is an immediate send and up to 4 retries: at 1 minute, 5 minutes, 1 hour and 6 hours. If all fail, the event is marked FAILED and can be resent with ResendWebhookEvent.
  • Redirects are not followed (3xx counts as a failure).
  • Duplicates: an event can arrive more than once (e.g. manual resend). Use eventId to ignore duplicates.
  • Order: because of retries, events can arrive out of order. Use occurredAt / ticketCommentId to order them.
  • If the webhook is deactivated, pending events are marked FAILED without being sent.
  • Event history is kept for 45 days.

Event status (status): 1 PENDING (pending or retrying), 2 DELIVERED, 3 FAILED.

GetWebhooks

Webhooks configured for an association (active and inactive).

MethodGET
URLhttps://api.fotofacturas.ai/B2B/WebhookV1/GetWebhooks
Parametersint associationId
{
  "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
}

The secret is never returned by this API.

SaveWebhook

Creates or updates the webhook for an event in an association (one per event per association).

MethodPOST (JSON)
URLhttps://api.fotofacturas.ai/B2B/WebhookV1/SaveWebhook
{
  "associationId": 41,
  "eventType": 1,
  "url": "https://erp.empresa.com/webhooks/fotofacturas",
  "isActive": true
}
  • The URL must be HTTPS, with no username/password, and its domain must resolve to a public IP.
  • If the webhook already existed, url and isActive are updated and the secret doesn’t change.
{
  "data": {
    "webhookSubscriptionId": 7,
    "secret": "whsec_3f8a..."
  },
  "isSuccess": true,
  "message": "Webhook guardado"
}

secret is only returned when the webhook is created (on updates it is null): store it at that moment. If it is lost, use RegenerateWebhookSecret.

RegenerateWebhookSecret

Generates a new secret for the webhook. The previous one stops being valid immediately.

MethodPOST (JSON)
URLhttps://api.fotofacturas.ai/B2B/WebhookV1/RegenerateWebhookSecret
{ "webhookSubscriptionId": 7 }
{
  "data": "whsec_9b1c...",
  "isSuccess": true,
  "message": "Secreto regenerado"
}

TestWebhook

Immediately sends a ping test event to the webhook URL (even if it’s inactive) and returns the result. It is not stored in the history or retried.

MethodPOST (JSON)
URLhttps://api.fotofacturas.ai/B2B/WebhookV1/TestWebhook
{ "webhookSubscriptionId": 7 }

Payload sent (X-FF-Event: ping, X-FF-Event-Id: ping, signed like the others):

{
  "eventId": null,
  "eventType": "ping",
  "occurredAt": "2026-10-13T10:00:00",
  "data": { "webhookSubscriptionId": 7, "associationId": 41, "subscribedEventType": "ticket.invoiced" }
}

Response:

{
  "data": {
    "isDelivered": true,
    "statusCode": 200,
    "responseBody": "{\"ok\":true}",
    "error": null,
    "durationMs": 184
  },
  "isSuccess": true,
  "message": null
}

responseBody is limited to 2000 characters. On timeout or connection error, statusCode is null and error contains the reason.

GetWebhookEvents

History of events generated in a date range, from newest to oldest.

MethodGET
URLhttps://api.fotofacturas.ai/B2B/WebhookV1/GetWebhookEvents
Parametersdate startDate, date endDate (required, max. 31 days); optional: int associationId (the ticket’s association), int userTicketId, short status
{
  "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
}

At most maxResults events are returned; if totalCount is higher, narrow the range or filter.

GetWebhookEventById

Details of an event: the payload sent, the webhook’s current URL and every attempt with the server’s response.

MethodGET
URLhttps://api.fotofacturas.ai/B2B/WebhookV1/GetWebhookEventById
Parameterslong webhookEventId

Same fields as GetWebhookEvents, plus:

{
  "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 is null if the event hasn’t been sent yet.

ResendWebhookEvent

Re-queues a delivered or failed event to send it again (with its full retry cycle). It is sent to the webhook’s current URL with the same eventId and payload.

MethodPOST (JSON)
URLhttps://api.fotofacturas.ai/B2B/WebhookV1/ResendWebhookEvent
{ "webhookEventId": 98123 }
{
  "isSuccess": true,
  "message": "Evento reenviado"
}

Webhook API error messages: El tipo de evento no es válido, La URL no es válida, La URL debe usar HTTPS, La URL no puede incluir usuario ni contraseña, No se pudo resolver el dominio de la URL, La URL apunta a una dirección no permitida, La URL no puede tener más de 1000 caracteres, El webhook no existe, El evento no existe, El evento ya está pendiente de envío, El rango no puede ser mayor a 31 días.