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:
| Mode | User ticket | Association 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
| Limit | Description |
|---|---|
ticketsMonthlyLimit | Tickets per month for the whole association tree. null = no group limit. |
ticketsMonthlyLimitPerUser | Tickets per month per user. null = no per-user limit. |
canPictureInvoice | Whether the plan allows uploading tickets. |
multiRfcPerUser | Maximum own active RFCs per user. |
multiRfcPerAssociation | Maximum active RFCs per association. |
multiRfcType | RFC 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):
| Id | Status | Meaning |
|---|---|---|
| 1 | CREATED | Received, pending processing. |
| 3 | AUTO INVOICING | In automatic invoicing. |
| 5 | WAITING INVOICE EMAIL | Waiting for the invoice by email. |
| 6 | MANUAL REVIEW | In manual review. |
| 7 | ADVANCED REVIEW | In advanced review. |
| 8 | WAITING MERCHANT | Waiting for the merchant’s response. |
| 10 | UNSIGNABLE | Could not be invoiced (cancelled). |
| 11 | SIGNED | Invoiced. |
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 inyyyy-MM-ddformat, 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:
| Message | Cause |
|---|---|
Falta header de autorización | The Authorization header was not sent. |
Formato inválido del header de autorización | The header doesn’t start with Bearer . |
Falta el token en el header de autorización | The header has no token after Bearer. |
Token no válido | The token doesn’t exist or the account is inactive. |
Usuario no autorizado para usar este servicio | The account is not a B2B account. |
Servicio no disponible, reintente en un minuto | The 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: falseand the messageNo estas autorizado para usar esta API. - To enable an API, contact Fotofacturas with the permission name.
| Id | Permission |
|---|---|
| 1 | AssociationV1/GetAssociationList |
| 2 | AssociationV1/GetAssociationTree |
| 3 | AssociationV1/GetAssociationById |
| 4 | AssociationV1/CreateAssociation |
| 5 | AssociationV1/UpdateAssociation |
| 6 | AssociationV1/UpdateAssociationConfig |
| 7 | AssociationV1/SaveAssociationCSF |
| 8 | AssociationV1/ChangeAssociationCSFStatus |
| 9 | MiscellaneousV1/Ping |
| 10 | MiscellaneousV1/GetRegimenFiscalAndCFDI |
| 11 | MiscellaneousV1/GetSatProductDivisions |
| 12 | MiscellaneousV1/GetSatProductFamilies |
| 13 | MiscellaneousV1/GetSatProductClasses |
| 14 | MiscellaneousV1/GetSatProducts |
| 15 | MiscellaneousV1/GetTicketPaymentMethods |
| 16 | MiscellaneousV1/GetTicketCodes |
| 17 | MiscellaneousV1/GetAccountInfo |
| 18 | TicketV1/GetTicketById |
| 19 | TicketV1/CreateUserTicket |
| 20 | TicketV1/CreateAssociationTicket |
| 21 | ReportV1/GetTicketCountByDay |
| 22 | ReportV1/GetPeriodStats |
| 23 | ReportV1/GetTicketFamilyStats |
| 24 | ReportV1/GetTicketStatuses |
| 25 | ReportV1/GetTicketsByMonth |
| 26 | ReportV1/GetTicketsByUser |
| 27 | UserV1/GetUsersByAssociation |
| 28 | UserV1/CreateUser |
| 29 | UserV1/UpdateUser |
| 30 | UserV1/ChangeUserStatus |
| 31 | UserV1/AddUserTaxInfo |
| 32 | UserV1/UpdateUserTaxInfo |
| 35 | WebhookV1/GetWebhooks |
| 36 | WebhookV1/SaveWebhook |
| 37 | WebhookV1/RegenerateWebhookSecret |
| 38 | WebhookV1/TestWebhook |
| 39 | WebhookV1/GetWebhookEvents |
| 40 | WebhookV1/GetWebhookEventById |
| 41 | WebhookV1/ResendWebhookEvent |
Response
Every API returns the following object (JSON in camelCase):
| Field | Type | Description |
|---|---|---|
isSuccess | boolean | Indicates whether the operation succeeded. |
message | string | null | Result message. On errors it contains the reason. |
data | object | null | Requested information (only in the APIs that return data). |
Business error example:
{
"isSuccess": false,
"message": "El RFC XAXX010101000 no está registrado",
"data": null
}
Errors
| HTTP | When it happens | Body |
|---|---|---|
| 200 | Successful 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. |
| 400 | The 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. |
| 401 | The 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). |
| 429 | More than 200 requests per minute from the same IP. | { "isSuccess": false, "message": "Demasiadas peticiones. Por favor, intenta más tarde." } |
| 500 | Unexpected 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):
| Message | Cause |
|---|---|
No estas autorizado para usar esta API | The account doesn’t have the API permission (see API permissions). |
Por favor reintente en unos momentos | The service is starting up; retry in a few seconds. |
La asociación no le pertenece / La asociación no te pertenece | The given association is not in the account’s tree. |
El usuario no pertenece a tus asociaciones / Este usuario no pertenece a su asociación | The given user is not in the account’s tree. |
El usuario no existe / La asociación no existe / El ticket no existe | The given id doesn’t exist. |
El usuario no está activo | A ticket was uploaded for an inactive user. |
El RFC es obligatorio / El RFC {rfc} no es válido | The RFC is missing or its format is invalid. |
El RFC {rfc} no está registrado | The RFC is not among those the user or association can use. |
El RFC {rfc} está deshabilitado | The RFC exists but is disabled. |
El plan solo permite {n} RFCs en la asociación / El plan solo permite {n} RFCs por usuario | The 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 fin | Missing or invalid date range in reports. |
El mes debe estar entre 1 y 12 / Año inválido / No se puede consultar un mes futuro | Invalid month or year in monthly reports. |
Input formats
- Query params:
GETAPIs. - JSON:
PUT/POSTAPIs 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.
| Method | GET |
|---|---|
| URL | https://api.fotofacturas.ai/B2B/AssociationV1/GetAssociationList |
| Parameters | None |
{
"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.
| Method | GET |
|---|---|
| URL | https://api.fotofacturas.ai/B2B/AssociationV1/GetAssociationTree |
| Parameters | None |
{
"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.
| Method | GET |
|---|---|
| URL | https://api.fotofacturas.ai/B2B/AssociationV1/GetAssociationById |
| Parameters | int 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 asGetUsersByAssociation.rfcs: the association’s RFCs (active and disabled).csfFileis a temporary URL to download the CSF.config: always present; fields not configured return their default value. Logos are public URLs ornull.
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.
| Method | POST (multipart/form-data) |
|---|---|
| URL | https://api.fotofacturas.ai/B2B/AssociationV1/CreateAssociation |
| Field | Type | Required | Description |
|---|---|---|---|
Name | string | Yes | Association name. |
ParentId | int | Yes | Parent association (must be in the account’s tree). |
CSFs | PDF file(s) | No | The association’s CSFs. Repeat the key for several. The same RFC can’t be sent twice and multiRfcPerAssociation can’t be exceeded. |
Config.* | No | Same 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.
| Method | PUT (JSON) |
|---|---|
| URL | https://api.fotofacturas.ai/B2B/AssociationV1/UpdateAssociation |
{
"associationId": 42,
"name": "Sucursal Norte",
"parentId": 41
}
parentIdis 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.
| Method | PUT (multipart/form-data) |
|---|---|
| URL | https://api.fotofacturas.ai/B2B/AssociationV1/UpdateAssociationConfig |
| Field | Type | Description |
|---|---|---|
AssociationId | int | Required. |
LogoSquare | JPG/PNG file | Square logo (max. 4 MB). Replaces the previous one. |
LogoRectangle | JPG/PNG file | Rectangular logo (max. 4 MB). Replaces the previous one. |
CompanyName | string | Company name to display (max. 150 characters). |
DefaultRegimenFiscalId | int | Default tax regime (GetRegimenFiscalAndCFDI catalog). |
DefaultUsoCfdiId | string | Default CFDI use. Requires a tax regime (the one sent or the saved one) and must be valid for that regime. |
DefaultTicketPaymentMethodId | string | Default payment method (GetTicketPaymentMethods catalog). |
CanChangeRegimenFiscal | bool | Whether the user can change the tax regime. |
CanChangeUsoCfdi | bool | Whether the user can change the CFDI use. |
CanChangePaymentMethod | bool | Whether the user can change the payment method. |
StyleRoundness | int | Interface corner roundness (non-negative). |
StyleSidebarColor | string | Color #RRGGBB or #RRGGBBAA. |
StyleAccentColor | string | Color #RRGGBB or #RRGGBBAA. |
StyleAccentStrongColor | string | Color #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.
| Method | POST (multipart/form-data) |
|---|---|
| URL | https://api.fotofacturas.ai/B2B/AssociationV1/SaveAssociationCSF |
| Field | Type | Required |
|---|---|---|
AssociationId | int | Yes |
CSF | PDF file | Yes |
{
"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.
| Method | PUT (JSON) |
|---|---|
| URL | https://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.
| Method | GET |
|---|---|
| URL | https://api.fotofacturas.ai/B2B/MiscellaneousV1/Ping |
| Parameters | None |
{
"isSuccess": true,
"message": "Pong"
}
GetAccountInfo
Information about the B2B account, its plan and the current month’s usage.
| Method | GET |
|---|---|
| URL | https://api.fotofacturas.ai/B2B/MiscellaneousV1/GetAccountInfo |
| Parameters | None |
{
"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
}
| Field | Description |
|---|---|
ticketsUsed | Tickets in the current month across the whole tree (users and associations), excluding discarded ones. |
associationsCount | Associations in the tree, including the root. |
associationActiveTaxInfosCount / associationInactiveTaxInfosCount | Active / disabled association RFCs. |
userActiveTaxInfosCount / userInactiveTaxInfosCount | Active / disabled user RFCs. |
associationAndPlan is null if the account has no association assigned.
GetTicketPaymentMethods
Catalog of payment methods (visible ones only), sorted.
| Method | GET |
|---|---|
| URL | https://api.fotofacturas.ai/B2B/MiscellaneousV1/GetTicketPaymentMethods |
| Parameters | None |
{
"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.
| Method | GET |
|---|---|
| URL | https://api.fotofacturas.ai/B2B/MiscellaneousV1/GetRegimenFiscalAndCFDI |
| Parameters | None |
{
"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).
| Method | GET |
|---|---|
| URL | https://api.fotofacturas.ai/B2B/MiscellaneousV1/GetTicketCodes |
| Parameters | None |
{
"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.
| API | Method | Parameters (optional) | Response (data) |
|---|---|---|---|
GetSatProductDivisions | GET | None | [{ "satProductDivisionId", "description" }] |
GetSatProductFamilies | GET | string? satProductDivisionId | [{ "satProductFamilyId", "description", "satProductDivisionId" }] |
GetSatProductClasses | GET | string? satProductFamilyId | [{ "satProductClassId", "description", "satProductFamilyId" }] |
GetSatProducts | GET | string? 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.
| Method | GET |
|---|---|
| URL | https://api.fotofacturas.ai/B2B/TicketV1/GetTicketById |
| Parameters | int 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,pdfUrlandxmlUrlare temporary download URLs (pdfUrl/xmlUrlarenullif there is no invoice yet).xmlDatacontains the data read from the invoice XML (nullif there is no XML).taxIdentifier/taxNameare 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).
| Method | POST (multipart/form-data) |
|---|---|
| URL | https://api.fotofacturas.ai/B2B/TicketV1/CreateUserTicket |
| Field | Type | Required | Description |
|---|---|---|---|
UserId | int | Yes | Active user in the tree. |
Image | image file | Yes | Photo of the ticket (max. 10 MB). |
Rfc | string | Yes | RFC to invoice to. Must be registered and active. |
TicketPaymentMethodId | string | Yes | GetTicketPaymentMethods catalog. |
RegimenFiscalId | int | Yes | GetRegimenFiscalAndCFDI catalog. |
UsoCfdiId | string | Yes | Must be valid for the tax regime. |
CardLastFour | string | Conditional | Last 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.
| Method | POST (multipart/form-data) |
|---|---|
| URL | https://api.fotofacturas.ai/B2B/TicketV1/CreateAssociationTicket |
| Field | Type | Required |
|---|---|---|
AssociationId | int | Yes |
Image, Rfc, TicketPaymentMethodId, RegimenFiscalId, UsoCfdiId, CardLastFour | Same 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).
| Method | GET |
|---|---|
| URL | https://api.fotofacturas.ai/B2B/ReportV1/GetTicketCountByDay |
| Parameters | date 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.
| Method | GET |
|---|---|
| URL | https://api.fotofacturas.ai/B2B/ReportV1/GetPeriodStats |
| Parameters | int 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
}
| Field | Description |
|---|---|
processedTicketCount | Invoiced tickets (SIGNED). |
cancelledTicketCount | Tickets that could not be invoiced (UNSIGNABLE). |
waitingTicketCount | Tickets in any other status (in progress). |
totalTicketsUsed | Sum of the three above. |
totalInvoiced | Sum of the amounts of invoiced tickets. |
totalIVATaxInvoiced | totalInvoiced × 16%. |
ticketsMonthlyLimit | Monthly ticket limit of the current plan. |
GetTicketFamilyStats
Number of invoiced products grouped by SAT catalog family, in a date range.
| Method | GET |
|---|---|
| URL | https://api.fotofacturas.ai/B2B/ReportV1/GetTicketFamilyStats |
| Parameters | date 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.
| Method | GET |
|---|---|
| URL | https://api.fotofacturas.ai/B2B/ReportV1/GetTicketStatuses |
| Parameters | date 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.
| Method | GET |
|---|---|
| URL | https://api.fotofacturas.ai/B2B/ReportV1/GetTicketsByMonth |
| Parameters | int 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.
| Method | GET |
|---|---|
| URL | https://api.fotofacturas.ai/B2B/ReportV1/GetTicketsByUser |
| Parameters | int 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.
| Method | GET |
|---|---|
| URL | https://api.fotofacturas.ai/B2B/UserV1/GetUsersByAssociation |
| Parameters | int 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: truethat represents the association itself: itstaxInfosare the association’s RFCs (the same asrfcsinGetAssociationById) 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 asrfcsinGetAssociationById.csfFileis a temporary URL to download the CSF.
CreateUser
Creates a user in an association of the tree.
| Method | POST (JSON) |
|---|---|
| URL | https://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) andphoneCode(at least 2 digits) are stored as digits only.mSurnamecan benull.
{
"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.
| Method | PUT (JSON) |
|---|---|
| URL | https://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.
| Method | PUT (JSON) |
|---|---|
| URL | https://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).
| Method | POST (multipart/form-data) |
|---|---|
| URL | https://api.fotofacturas.ai/B2B/UserV1/AddUserTaxInfo |
| Field | Type | Required |
|---|---|---|
pdf | PDF file (CSF) | Yes |
userId | int | Yes |
{
"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).
| Method | PUT (multipart/form-data) |
|---|---|
| URL | https://api.fotofacturas.ai/B2B/UserV1/UpdateUserTaxInfo |
| Field | Type | Required |
|---|---|---|
pdf | PDF file (CSF) | Yes |
userTaxInfoId | int | Yes |
{
"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) | Name | When it is sent |
|---|---|---|
| 1 | ticket.invoiced | The ticket moved to SIGNED (invoiced). |
| 2 | ticket.failed | The ticket moved to UNSIGNABLE (could not be invoiced). |
| 3 | ticket.status_changed | The 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:
- The configuration of the ticket’s association, if it has that event configured and active.
- Otherwise, the configuration of the root association.
- 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
}
}
userIdisnullfor association tickets.occurredAtis 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:
- Concatenate
X-FF-Timestamp+.+ the exact body received (without re-serializing). - Compute
HMAC-SHA256with the secret as the key and encode it in hexadecimal (lowercase). - Compare it against the value of
X-FF-Signatureafterv1=, using a constant-time comparison. - Recommended: reject notifications whose
X-FF-Timestampis 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
FAILEDand can be resent withResendWebhookEvent. - Redirects are not followed (3xx counts as a failure).
- Duplicates: an event can arrive more than once (e.g. manual resend). Use
eventIdto ignore duplicates. - Order: because of retries, events can arrive out of order. Use
occurredAt/ticketCommentIdto order them. - If the webhook is deactivated, pending events are marked
FAILEDwithout 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).
| Method | GET |
|---|---|
| URL | https://api.fotofacturas.ai/B2B/WebhookV1/GetWebhooks |
| Parameters | int 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).
| Method | POST (JSON) |
|---|---|
| URL | https://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,
urlandisActiveare 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.
| Method | POST (JSON) |
|---|---|
| URL | https://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.
| Method | POST (JSON) |
|---|---|
| URL | https://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.
| Method | GET |
|---|---|
| URL | https://api.fotofacturas.ai/B2B/WebhookV1/GetWebhookEvents |
| Parameters | date 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.
| Method | GET |
|---|---|
| URL | https://api.fotofacturas.ai/B2B/WebhookV1/GetWebhookEventById |
| Parameters | long 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.
| Method | POST (JSON) |
|---|---|
| URL | https://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.