Files
balam/bind-api-sandbox/VALIDACION-API.md
T
JohannVelazquez 9ccfbe262e Validación técnica de la API de BIND contra producción (GET-only)
Plan A del saldo CONFIRMADO (Total − Payments − CreditNotes, misma fila);
flujo MVP sostenible 100% en lectura; PDF del CFDI por API. Hallazgo duro:
sin recurso de pagos individuales ni REP (escalar a Pedro). Cliente extendido
(Quotes, Currencies, Locations, pdf/xml, GET por ID estilo REST) y tipos
reales en types.real.ts. Detalle en bind-api-sandbox/VALIDACION-API.md;
reporte crudo y token gitignoreados.
2026-07-06 17:17:27 -06:00

26 KiB
Raw Blame History

Validación técnica de la API de BIND ERP — cuenta real (Balam)

Actividad: "Validación técnica de la API de BIND" · Etapa 0 (Discovery) Fecha de ejecución: 6-jul-2026 · Base URL: https://api.bind.com.mx Token: entregado por Pedro el 6-jul (correo, REGISTRO #29) — generado con el usuario de Arturo Rosas, conforme al acuerdo del kickoff (REGISTRO #22). Vive en bind-api-sandbox/.env (BIND_API_TOKEN), fuera de git. Método: script src/validate-real-api.tsexclusivamente GET (no existe código de escritura en el script), 117 de 120 peticiones presupuestadas (límite real: 20K/día). Reporte crudo sanitizado en validation-output/report.json (gitignoreado). Política de datos: este documento contiene solo estructura — nombres de campos, tipos, formatos, conteos y códigos de estatus. Ningún valor real de Balam (nombres, RFCs, montos, folios, correos). Los ejemplos son inventados con el mismo shape.


Resumen ejecutivo

Pregunta Veredicto
¿El token autentica? Sí — Authorization: Bearer como único header. Sin subscription key.
¿Alcance del token? Cubre todos los recursos existentes que probamos (ningún 401/403 por recurso) — coherente con "cuenta mayor, todos los permisos" del kickoff.
¿Se distingue prefactura de factura timbrada? Sí — UUID eq null / IsFiscalInvoice eq false devuelven filas.
¿PPD/PUE visible? Sí, en el detalle por factura (CFDIPaymentTerm) — no en la lista.
¿Saldo abierto por factura? Plan A operativo: Total Payments CreditNotes, todos campos de la misma fila de /api/Invoices. Verificado aritméticamente.
¿Pagos individuales listables? No — no existe recurso de pagos consultable (19 nombres probados → 404). El acumulado sí (Invoices.Payments).
¿Cotizaciones consultables? Sí (/api/Quotes + detalle) — pero sin relación visible cotización→factura.
¿OData? ⚠️ Parcial — $filter/$top/$skip/$orderby sí (sintaxis v3); $select y conteo total no.
¿Multi-empresa? Acotado a una empresa (la del usuario del token). Sin recurso Companies ni campo de empresa.
¿PDF del CFDI por API? GET /api/Invoices/{id}/pdfapplication/pdf.

1 · Autenticación

Esquema confirmado: un solo header.

GET https://api.bind.com.mx/api/{Recurso}
Authorization: Bearer <token>
Accept: application/json
  • El Ocp-Apim-Subscription-Key que el sandbox contemplaba como posible no es necesario — no se envió en ninguna petición y todo funcionó.
Escenario Estatus Cuerpo (estructura)
Token válido 200 Colección OData { value: [...] }
Sin header Authorization 401 { "Message": "Authorization has been denied for this request." }
Token corrupto/inválido ⚠️ 500 { "message": "API Key es inválida. | Your API Key is invalid.", "code": "0" }

Hallazgo importante: un token inválido responde 500, no 401/403. El monitoreo de la plataforma no puede fiarse del código HTTP para distinguir "token revocado" de "error del servidor de BIND" — hay que inspeccionar el mensaje del body (API Key es inválida).


2 · Cobertura de endpoints (inventario)

Todos con GET {recurso}?$top=1. No apareció ningún 401/403 por recurso: con este token todo existe (200) o no existe (404) — no hay recursos "prohibidos" visibles.

Responden 200

Recurso Método probado Estatus Notas
Invoices GET lista / GET /{id} / GET /{id}/pdf / GET /{id}/xml 200 Colección OData. Detalle trae más campos que la lista (50 vs 32).
Clients GET lista / GET /{id} 200 Así se llaman los clientes (no Customers). Detalle 28 campos vs 10 de lista.
Quotes GET lista / GET /{id} 200 Cotizaciones. Detalle 45 campos con partidas Items[].
Products GET lista 200 30 campos. Incluye ChargeVAT, CurrencyCode, unidad.
Currencies GET lista 200 Catálogo: ID, Name, Code (3 letras), ExchangeRate.
Warehouses GET lista 200 ID, Name, LocationID, AvailableInOtherLoc. 1 fila (Matriz).
Locations GET lista 200 Sucursales/domicilios: Name, Street, ZipCode, City, State… 1 fila.
Activities GET lista 200 Devuelve colección vacía en esta cuenta (0 filas).
PriceLists GET lista 200 Listas de precios.
Orders GET lista 200 Pedidos (no se profundizó — fuera del flujo MVP).
Providers GET lista 200 Proveedores (fuera del flujo MVP).
Banks GET lista 200 Catálogo bancario (relevante futuro: conciliación).
BankAccounts GET lista 200 Cuentas bancarias de la empresa (ídem).
Users GET lista 200 Usuarios BIND. No se analizó su shape (contiene datos personales, no prioritario).

Responden 404 (no existen con ese nombre)

Grupo Nombres probados → 404
Clientes (alias) Customers
Pagos Payments, Payment, ClientPayments, CustomerPayments, Incomes, Income, Deposits, Collections, PaymentComplements, Complements, CashReceipts, AccountsReceivable, Receivables, Cobros, Pagos, InvoicePayments, PaymentsReceived
Pagos (sub-recurso) Invoices/{id}/Payments, Invoices/{id}/payments, Invoices/{id}/CreditNotes
Cotizaciones (alias) Quotations, Cotizaciones
Series Series, InvoiceSeries, Folios, DocumentSeries (la serie es campo de la factura, no recurso)
Sucursales (alias) Branches, Sucursales (lo real es Locations)
Otros CreditNotes, Taxes, Prices, SalesOrders, PurchaseOrders, Suppliers, Sellers, Employees, Companies, Expenses, Inventory, CFDI, CFDIs

3 · Inventario de campos por recurso

Solo nombres, tipos y formatos observados (muestras de $top=5). (≈corto/medio/largo) = longitud aproximada del string; los valores reales nunca se persistieron.

3.1 Invoices — lista (32 campos)

Campo Tipo/formato Nota
ID guid Clave para GET /api/Invoices/{id}.
Serie string corto (a veces vacío) ⚠️ En el detalle se llama Series (inconsistencia del API).
Number integer Folio interno.
UUID guid Folio fiscal del CFDI. null en prefacturas.
Date datetime ISO Fecha del documento.
ExpirationDate datetime ISO Fecha de vencimiento (esto alimenta el aging). No existe campo DueDate.
ClientID / ClientName guid / string Denormalizado en la propia fila.
RFC string formato RFC Del receptor.
Cost, Subtotal, Discount, Total decimal
VAT, IEPS, ISRRet, VATRet decimal Impuestos y retenciones.
VATRate, VATRetRate decimal Tasas (p. ej. 0.16).
Payments decimal Acumulado pagado de la factura (ver §4).
CreditNotes decimal Acumulado de notas de crédito aplicadas.
CurrencyID guid FK a Currencies (la lista no trae el código — el detalle sí).
ExchangeRate decimal TC fijado al emitir.
LocationID, WarehouseID, PriceListID guid
CFDIUse integer ⚠️ Código interno (se observaron 3, 23), no la clave SAT (G03…). Falta tabla de mapeo.
Comments string Aquí ponen hoy el nº de ticket Jira (Discovery #27).
PurchaseOrder string Orden de compra.
IsFiscalInvoice boolean false = prefactura (sin timbrar).
ShowIEPS boolean
Status integer Ver semántica abajo.

Semántica de Status (mapeada contra el propio API, lista→detalle):

Código Etiqueta (campo Status del detalle)
0 Activa
1 Pagada
2 Cancelada

Se probaron códigos 35: sin filas (o no existen o no hay ejemplares). ⚠️ Status no distingue prefactura de timbrada — el discriminador fiable es UUID eq null / IsFiscalInvoice eq false (ambos filtros devuelven filas: las prefacturas sí son visibles por API).

3.2 Invoices/{id} — detalle (50 campos; los adicionales)

Campo Tipo/formato Nota
Series string La lista lo llama Serie.
Status / StatusCode string / integer Etiqueta + código (p. ej. "Pagada" / 1).
PaymentTerms integer Días de crédito de la factura. Solo en detalle.
CFDIPaymentTerm string ⚠️ El método de pago SAT (PPD/PUE) — se observó el literal "PAGO EN UNA SOLA EXHIBICIÓN" (=PUE). Puede venir vacío. Solo en detalle.
CFDIPaymentMethod string ⚠️ La forma de pago SAT (se observaron "Transferencia Electrónica de Fondos", "Por Definir"). Nomenclatura invertida respecto al SAT — ver hallazgos.
CFDIAccountNumber string Nº de cuenta (últimos dígitos), puede venir vacío.
CurrencyName string 3 letras Código de moneda (MXN/USD) — en el detalle es el código, no el nombre.
ClientPhoneNumber, ClientContact string | null
CreatedByID / CreatedByName guid / string Quién creó el documento (auditoría).
CreationDate / ApplicationDate datetime ISO
PriceListName, LocationName, WarehouseName string Denormalizados.
FiscalID guid
Address string largo Dirección fiscal del receptor.
Products array Partidas de productos (vacío en la muestra — Balam factura servicios).
Services array Partidas de servicios.
Services[].ID, Services[].ServiceID guid
Services[].IndexNumber integer Orden de la partida.
Services[].Name, Services[].Code string Concepto (p. ej. el 029 "consultoría y servicios" del Discovery).
Services[].Qty, Services[].Price decimal
Services[].VATRate decimal Tasa de IVA por partida — habilita la validación 16 % / 0 %.
Services[].Discount decimal

El detalle no trae CFDIUse (solo la lista) ni un campo de saldo precalculado.

3.3 Quotes — lista (11 campos) y detalle (45)

Lista: ID (guid), Number (string), CreationDate (datetime), ClientName, Locations (string), Comments, TotalOriginalCurrency (decimal), Currency (nombre, p. ej. "Peso mexicano"), Total (decimal), Status (integer), StatusText (string).

Semántica de Quotes.Status (mapeada vía $filter + StatusText de la misma fila):

Código StatusText
0 Activa
1 Cancelada
2 Surtida

Detalle Quotes/{id} agrega: QuoteNumber, ClientID/ClientContact/ClientPhone, LocationName/ID, PriceListName/ID, EmployeeName/ID (comercial que cotizó), CurrencyCode (3 letras), ExchangeRate, Subtotal, Discount, IEPS, VAT/VATRate, ISR/ISRRate, VatRet, Total, BaseCurrency (bool), OriginalCurrencySubtotal, OriginalCurrencyDiscountAmount, IsPercentage (bool), ContactEmails, ExternalIDType (int), Comments, y partidas Items[]: ID, Code, ProductID, ProductName, Unit, Qty, Price, Amount, IEPS, VAT, IndexNumber.

⚠️ No hay campo que ligue la cotización con la factura generada (ni InvoiceID en Quote, ni QuoteID en Invoice). "Surtida" dice que se convirtió, pero no a qué factura. Ver implicaciones (§9.2).

3.4 Clients — lista (10 campos) y detalle (28)

Lista: ID (guid), Number (int), ClientName, LegalName, RFC, Email, Phone, NextContactDate, LocationID (guid), RegimenFiscal (string).

Detalle Clients/{id} agrega:

Campo Tipo Nota
CommercialName string
CreditDays integer Días de crédito default del cliente (los 30/45/90 del Discovery).
CreditAmount decimal Límite de crédito.
PaymentMethod string Forma de pago default (se observó "Efectivo").
PaymentTermType string Puede venir vacío.
Status string "Activo"/…
SalesContact / CreditContact string Contactos comercial y de cobranza.
Loctaion / LoctaionID string / guid ⚠️ Typo real del API ("Loctaion", sic) — el cliente tipado debe usar el nombre con typo.
PriceList / PriceListID string / guid
Email, Telephones string | null Correos configurados (los que usa el botón "enviar email" de BIND).
AccountNumber, DefaultDiscount, ClientSource, Account varios
City, State, Addresses[] string / array
RegimenFiscal string Régimen fiscal SAT.
CreationDate datetime

No se observó un campo "uso CFDI default por cliente" — el CFDIUse vive en la factura. La regla "gastos en general vs sin efectos fiscales" tendrá que derivarse de otra señal (p. ej. RFC extranjero/XEXX010101000, país, o configuración en la plataforma).

3.5 Catálogos

  • Currencies: ID (guid), Name, Code (3 letras), ExchangeRate (decimal). 4 filas en la cuenta.
  • Warehouses: ID, Name ("Matriz"), LocationID, AvailableInOtherLoc (bool). 1 fila — confirma el "hoy solo matriz" del Discovery.
  • Locations: ID, Name, Street, ExtNumber, IntNumber, ZipCode, Colonia, City, State. 1 fila.
  • Products: 30 campos, incl. Code, Title, Cost, CostType(+Text), CurrentInventory, ChargeVAT (bool), Unit, CurrencyID/Code, PricingType(+Text), PurchaseType(+Text), IEPSRate, Type(+Text), SKU, categorías.

3.6 Documentos del CFDI

Endpoint Estatus Content-Type Nota
GET /api/Invoices/{id}/pdf 200 application/pdf PDF real descargable por API — insumo directo del módulo de envío.
GET /api/Invoices/{id}/xml 200 application/json Responde 200 pero como JSON — probablemente envuelve el XML o una URL. El contenido se descartó por política de no persistir datos; shape pendiente (§10).

4 · La pregunta del saldo — veredicto

Plan A (operativo). No se necesita Plan B ni Plan C.

  • No existe un campo literal Balance/Saldo, pero cada fila de /api/Invoices trae Total, Payments (acumulado pagado) y CreditNotes (acumulado de notas de crédito):
\text{SaldoPorFactura} = \text{Total} - \text{Payments} - \text{CreditNotes}
  • Verificación aritmética (en memoria, sin persistir montos): en 5/5 facturas con Status=1 (Pagada), Payments + CreditNotes ≈ Total (diferencia < 0.01); en 5/5 con Status=0 (Activa), el residual es positivo. La fórmula cuadra en ambas poblaciones.
  • Es "Plan A" en el sentido operativo del riesgo de la propuesta: una sola llamada a /api/Invoices basta para calcular saldo y aging de toda la cartera — no hay que correlacionar una colección de pagos (Plan B) ni capturar nada a mano (Plan C).
  • Matiz honesto: BIND no expone el número ya restado; la resta la hace la plataforma. El costo es cero (mismos campos, misma fila).

5 · Payments — el hallazgo duro

No existe recurso consultable de pagos individuales. Se probaron 17 nombres de colección y 3 sub-recursos (§2) — todos 404.

Lo que hay:

Necesidad del MVP ¿Cubierta? Cómo
Saldo por factura Total Payments CreditNotes (§4).
¿Factura pagada? Status = 1 y/o residual ≈ 0.
Aging / vencimiento ExpirationDate + saldo.
Fecha y monto de cada abono individual No visible por API con este token.
Complementos de pago (REP) de facturas PPD Ningún recurso visible (PaymentComplements, Complements → 404).

Implicación: el motor de cobranza puede detectar que una factura se pagó (transición de Status/residual entre sincronizaciones) y registrar el timestamp de detección en la plataforma, pero no la fecha valor del pago según BIND. Preguntar a Pedro/soporte BIND si existe un endpoint de pagos/REP no descubierto (la doc completa está tras login en developers.bind.com.mx) — ver §10.


6 · OData y paginación

Mecanismo ¿Funciona? Evidencia
$top con tope $top=100 → 200; $top=101 → 500.
$skip $top=1&$skip=1 devuelve la fila siguiente (verificado por ID).
$orderby Date asc → orden ascendente verificado.
$filter eq (int) Status eq 1, CFDIUse eq 3 → 200.
$filter eq null UUID eq null → 200 con filas.
$filter ge + fecha sintaxis v3 Date ge datetime'2020-01-01T00:00:00' → 200.
$select 500. No se pueden proyectar columnas; el payload siempre viene completo.
$inlinecount=allpages (v3) → 500.
$count=true (v4) ⚠️ → 200 pero ignorado: no devuelve conteo.
odata.nextLink Nunca apareció.

Paginación: no hay nextLink ni conteo total ⇒ paginación manual con $top=100&$skip=N hasta recibir página corta. ⚠️ GET sin $top devuelve la colección completa en una respuesta (se observó con una colección de 41 filas) — con colecciones grandes es un riesgo de payload; siempre paginar. Sondeo por $skip (sin descargar): la colección histórica de Invoices supera las 1,000 filas.

Rate limit: no se observó ningún header de cuota (X-RateLimit-*, Retry-After en 200s) — el límite de 20K/día no es observable por request; hay que llevarlo con contador local (como ya hace BindClient).

Estabilidad: ~3 respuestas 500 transitorias en 117 peticiones (resueltas al primer retry). El retry con backoff no es opcional en producción. Nota: BIND usa 500 también para errores de sintaxis OData y token inválido — distinguir por body/contexto antes de reintentar a ciegas.

GET por ID: estilo RESTGET /api/Invoices/{id} → 200; el estilo OData Invoices(guid'...')404. (El cliente del sandbox asumía el estilo OData; ya se corrigió.)


7 · Multi-empresa

  • Companies → 404; ningún recurso expone campo Company/Empresa.
  • Locations y Warehouses devuelven 1 fila (Matriz).
  • Conclusión: el token está acotado a la empresa del usuario que lo generó (Arturo → Balam). La distinción multi-empresa del portal Jira (Balam/Regiotour/Elmstone, Discovery #27) no viaja a BIND por este token: para facturar otras empresas se necesitaría una cuenta BIND distinta con su propio token. Anotado para el roadmap — coherente con dejar multi-empresa fuera del MVP.

8 · Hallazgos inesperados

  1. Token inválido → 500 (no 401/403), con mensaje "API Key es inválida" en el body. El 401 solo aparece cuando falta el header.
  2. $select no funciona (500): no se puede reducir payload por columnas.
  3. Conteo total inaccesible: $inlinecount truena (500) y $count=true se ignora — el total solo se conoce paginando hasta el final.
  4. No hay recurso de pagos (17 nombres → 404) — el acumulado vive dentro de la factura (§5).
  5. Nomenclatura CFDI invertida respecto al SAT: CFDIPaymentTerm = Método de pago SAT (PPD/PUE); CFDIPaymentMethod = Forma de pago SAT (transferencia, efectivo…). Cablearlo al revés rompería la validación de oro PPD/PUE.
  6. CFDIUse es un código interno (enteros 3, 23), no la clave SAT (G03, S01…). Se necesita la tabla de mapeo (pedir a Pedro o doc tras login).
  7. Typo real en el API: el detalle de Clients trae Loctaion/LoctaionID (sic).
  8. Inconsistencias lista vs detalle: Serie (lista) vs Series (detalle); Status int (lista) vs Status string + StatusCode int (detalle); CurrencyID (lista) vs CurrencyName con el código (detalle); PPD/PUE y días de crédito solo en el detalle.
  9. Prefacturas visibles en la misma colección Invoices (UUID null / IsFiscalInvoice false) — no hay recurso separado.
  10. Activities existe pero está vacío en esta cuenta (0 filas) — el recurso que la doc pública usa de ejemplo no tiene datos aquí.
  11. 500 transitorios ocasionales que se resuelven con retry inmediato.
  12. Higiene de secretos: el .txt del token estaba en la raíz del repo sin gitignorear (no trackeado aún) — se agregó bind_token_api.txt y *.env al .gitignore raíz. Recomendación vigente: moverlo a un gestor de secretos y borrarlo del correo/disco.

9 · Implicaciones para el MVP

Cruce contra el flujo objetivo del Discovery (#27): Jira → cotización BIND → prefactura → validación humana → CFDI → envío.

9.1 Lo que la API ya sostiene (solo lectura, hoy)

Paso del flujo Soporte confirmado
Cotización Quotes legible con partidas, comercial (EmployeeName), moneda/TC y estatus (Activa/Cancelada/Surtida). La plataforma puede detectar cotizaciones nuevas y validar el prerequisito "cotización obligatoria" de Ara.
Prefactura Listable vía UUID eq null / IsFiscalInvoice eq false → el dashboard "prefacturas pendientes de validación" es viable 100 % lectura.
CFDI UUID, Series+Number, RFC, moneda, ExchangeRate, impuestos por partida (Services[].VATRate), uso CFDI (código), PPD/PUE (CFDIPaymentTerm en detalle), creador y fechas.
Validaciones de oro PPD/PUE: auditable por factura (detalle). La plataforma puede alertar "PUE detectado — ¿fue consciente?" apenas aparezca. • IVA 16 %/0 %: VATRate por partida + moneda + RFC → la regla "extranjero con IVA ≠ 0" (el error que Ara señaló en vivo) es detectable automáticamente. • Días de crédito: Clients.CreditDays (default) vs PaymentTerms (factura) — discrepancias detectables.
Cobranza / aging ExpirationDate + saldo derivado (§4) + Status → aging y alertas internas sin recurso de pagos.
Envío PDF real por API (/{id}/pdf) + correos del cliente (Clients.Email, ContactEmails) + las particularidades por cliente (Excel de Ara/Arturo) viven en la plataforma.

9.2 Restricciones de diseño que impone lo encontrado

  1. Sync incremental obligatorio. PPD/PUE y días de crédito viven en el detalle ⇒ 1 llamada por factura. Con ~55 facturas/mes es trivial, pero el histórico (>1,000) exige sincronizar por delta (Date ge la última corrida) y guardar en Postgres — nunca re-barrer todo el detalle.
  2. Trazabilidad cotización→factura la lleva la plataforma. BIND no expone el vínculo; al orquestar la conversión (Etapa 2) la plataforma debe registrar el par QuoteID→InvoiceID en su propia BD (y/o convención en Comments, como hoy hacen con el ticket Jira).
  3. "Fecha de pago" = fecha de detección. Sin pagos individuales, la plataforma registra cuándo observó el cambio a Pagada — suficiente para cobranza operativa; insuficiente para conciliación contable fina (que de todos modos es fase posterior).
  4. Catálogos internos a mapear: CFDIUse (int→clave SAT) y códigos de Status no observados (3+). Confirmar con Pedro.
  5. Cliente HTTP: paginar siempre ($top=100), retry en 500 transitorio, no usar $select, IDs estilo REST, contador local de cuota (sin headers de rate limit).
  6. Los complementos de pago (REP) no son visibles — riesgo para el flujo PPD completo; escalar a Pedro (§10).

9.3 Presupuesto de peticiones (viabilidad del sync)

Escenario conservador: lista de facturas delta (12 req) + detalle solo de facturas nuevas/cambiadas (~3/día) + cotizaciones delta (1 req) + clientes delta (1 req) ⇒ < 10 req por ciclo. Con polling cada 15 min ≈ ~1,000 req/día, 5 % del límite de 20K. Holgado.


10 · Lo que quedó SIN validar y por qué

Pendiente Por qué no se validó Cómo cerrarlo
Escritura (crear cotización/prefactura, convertir, emitir CFDI, cancelar) Prohibido en esta actividad: cuenta de producción, sin sandbox, efecto fiscal. Regla dura de solo-GET. Doc detallada tras login (developers.bind.com.mx) con Pedro; luego dry-run + confirmación humana en Etapa 2, empezando por un documento de prueba interno coordinado con Arturo.
Si la factura creada desde una cotización hereda alguna referencia a ésta Requiere ejecutar la conversión (= escritura). Mismo camino que el punto anterior; o preguntar a Arturo si la UI muestra el vínculo.
Shape real del /{id}/xml (¿XML embebido? ¿URL?) El body se descartó por política de no persistir datos reales en esta corrida. 1 GET dirigido leyendo solo las claves del JSON (sin valores), en la próxima sesión técnica.
Mapa completo CFDIUse interno → clave SAT No hay catálogo expuesto; solo se observaron códigos 3 y 23. Pedir tabla a Pedro o doc tras login.
Códigos de Status > 2 (¿parciales, vencidas?) Los filtros 35 no devolvieron filas: o no existen o no hay ejemplares en la cuenta. Doc tras login; observar en operación.
Complementos de pago (REP) para PPD Ningún recurso visible con los nombres probados. Crítico — preguntar a Pedro/soporte BIND; el flujo PPD del MVP lo necesita al menos en lectura.
Rate limit real (20K/día) y comportamiento al agotarlo No hay headers de cuota y agotar el límite adrede sería irresponsable en producción. Aceptar el dato de Noe (20K) y llevar contador local.
Shape de Users, Orders, Providers, Banks, BankAccounts, PriceLists Fuera del flujo del MVP; Users además contiene datos personales. Cuando conciliación (Anexo B) lo requiera.
Webhooks / eventos push No documentados públicamente; no sondeables por GET. Preguntar a Pedro; mientras, polling incremental.

Anexo · Reproducir la validación

cd bind-api-sandbox
# .env debe tener BIND_API_TOKEN (nunca se versiona; .gitignore lo cubre)
npm run validate:real              # ronda 1: auth + inventario + shapes + OData + byId
npx tsx src/validate-real-api.ts --round2   # pagos, estatus, prefactura, detalles, paginación
npx tsx src/validate-real-api.ts --round3   # tope $top, literales CFDI, pdf/xml
npx tsx src/validate-real-api.ts --round4   # aritmética del saldo + xml
  • Presupuesto acumulado entre rondas (VALIDATION_BUDGET, default 120). El script aborta al agotarlo.
  • El reporte validation-output/report.json está sanitizado (solo estructura) y además gitignoreado por defensa en profundidad.
  • El script es GET-only por construcción: no contiene ningún código capaz de emitir escrituras.