OCR Platform
Admin

URL base API

https://ocr.789.com.mx/v1

Guía de prompts y schemas

Una extracción tiene dos piezas que tú defines:

Pieza Para qué sirve
custom_instruction Qué extraer y cómo interpretarlo: tipo de documento, formatos y reglas de negocio.
response_schema Forma exacta del JSON de salida. Es un JSON Schema (subconjunto) y el resultado se valida contra él.

Esta guía explica cómo escribir las dos, trae plantillas listas para copiar y cómo corregir cada error.

Lo que la plataforma ya hace (no lo repitas)

Cada extracción envía al modelo un prompt de sistema fijo con estas reglas:

  • Devolver solo JSON válido que cumpla exactamente el schema.
  • Sin bloques markdown ni explicaciones.
  • No inventar datos que no estén en el OCR.
  • Usar null cuando un dato falta o no se puede verificar.

Después se envían, en este orden: tu custom_instruction, tu response_schema y el contenido OCR del documento (en el formato de input_result_type).

Si la respuesta no es JSON o no cumple el schema, la plataforma reintenta automáticamente (por defecto hasta 3 intentos) antes de marcar validation_failed.

No escribas "responde en JSON" ni "no inventes datos" en tu instrucción: ya está cubierto. Usa ese espacio para lo que solo tú sabes del documento.

Reglas del response_schema

  1. La raíz debe ser "type": "object".
  2. Cada nodo (cada propiedad y cada items) debe declarar type.
  3. Solo se aceptan estas keywords:
Keyword Uso
type string, number, integer, boolean, object, array, null. Combina tipos con un arreglo: ["string", "null"].
properties Campos de un objeto.
required Claves que deben existir en la respuesta (el valor puede ser null si el tipo lo permite).
items Schema de cada elemento de un array.
enum Lista cerrada de valores. Si el campo acepta null, incluye null en el enum.
additionalProperties false para rechazar claves que no definiste.
description Texto con instrucciones para ese campo (formato, significado, dónde buscarlo).

Cualquier otra keyword (format, pattern, minimum, maxLength, oneOf, anyOf, $ref, default, examples…) responde 422 validation_error. Si necesitas esa regla, escríbela en description o en custom_instruction.

Formato abreviado: no soportado

❌ { "total": "number|null" }
✅ { "type": "object", "properties": { "total": { "type": ["number", "null"] } } }

Patrón recomendado

  • Pon todos los campos en required y hazlos anulables (["string", "null"]). Así la respuesta siempre tiene las mismas claves y lo que no aparece llega como null.
  • Usa "additionalProperties": false para que tu código reciba siempre la misma forma.
  • Usa number para montos; integer solo acepta enteros (116.5 no pasa).
  • Para listas, usa "type": "array" con items. Una lista vacía [] es válida.

description: instrucciones por campo

Es la forma más efectiva de guiar al modelo, porque la regla queda junto al campo:

"fecha_emision": {
  "type": ["string", "null"],
  "description": "Fecha de emisión en formato YYYY-MM-DD."
},
"total": {
  "type": ["number", "null"],
  "description": "Total con impuestos. Número decimal con punto, sin símbolo de moneda ni separador de miles."
}

Con buenas description, la custom_instruction puede quedar corta.

Cómo escribir la custom_instruction

  1. Di qué documento es. "Factura electrónica mexicana (CFDI 4.0)".
  2. Desambigua roles. "Emisor es quien factura; receptor es el cliente".
  3. Fija formatos globales. Fechas YYYY-MM-DD, montos como número decimal, RFC y CURP en mayúsculas sin espacios.
  4. Resuelve casos ambiguos. "Si hay varios totales, usa el total a pagar"; "si hay varias direcciones, usa la del titular".
  5. Explica las listas. "En conceptos incluye cada renglón de la tabla de conceptos, en el mismo orden".
  6. Sé breve. Máximo 8000 caracteres; lo específico de cada campo va en description.
❌ Poco útil ✅ Útil
"Extrae los datos de la factura en JSON y no inventes nada." "Factura CFDI 4.0. Emisor es quien factura. Fechas YYYY-MM-DD. Montos como número decimal sin símbolo. Si hay varios totales, usa el total a pagar."

Plantillas listas para copiar

Sustituye document_id y ajusta campos a tu caso. Todas son válidas tal cual.

Factura CFDI (cfdi_invoice)

{
  "document_id": "doc_01J...",
  "custom_instruction": "Documento: factura electrónica mexicana (CFDI 4.0). Emisor es quien factura; receptor es el cliente. Fechas en formato YYYY-MM-DD. Montos como número decimal con punto, sin símbolo de moneda ni separador de miles. RFC en mayúsculas, sin espacios ni guiones.",
  "response_schema": {
    "type": "object",
    "additionalProperties": false,
    "required": ["uuid", "rfc_emisor", "nombre_emisor", "rfc_receptor", "nombre_receptor", "fecha_emision", "moneda", "subtotal", "iva", "total", "uso_cfdi", "conceptos"],
    "properties": {
      "uuid": { "type": ["string", "null"], "description": "Folio fiscal (UUID) de 36 caracteres." },
      "rfc_emisor": { "type": ["string", "null"] },
      "nombre_emisor": { "type": ["string", "null"], "description": "Nombre o razón social del emisor." },
      "rfc_receptor": { "type": ["string", "null"] },
      "nombre_receptor": { "type": ["string", "null"] },
      "fecha_emision": { "type": ["string", "null"], "description": "Formato YYYY-MM-DD." },
      "moneda": { "type": ["string", "null"], "description": "Código ISO de 3 letras, por ejemplo MXN." },
      "subtotal": { "type": ["number", "null"] },
      "iva": { "type": ["number", "null"], "description": "Total de IVA trasladado." },
      "total": { "type": ["number", "null"], "description": "Total a pagar con impuestos." },
      "uso_cfdi": { "type": ["string", "null"], "description": "Clave de uso CFDI, por ejemplo G03." },
      "conceptos": {
        "type": "array",
        "description": "Un elemento por renglón de la tabla de conceptos. Lista vacía si no hay tabla.",
        "items": {
          "type": "object",
          "additionalProperties": false,
          "required": ["descripcion", "cantidad", "valor_unitario", "importe"],
          "properties": {
            "descripcion": { "type": ["string", "null"] },
            "cantidad": { "type": ["number", "null"] },
            "valor_unitario": { "type": ["number", "null"] },
            "importe": { "type": ["number", "null"] }
          }
        }
      }
    }
  },
  "input_result_type": "markdown"
}

Credencial INE (ine_document)

{
  "document_id": "doc_01J...",
  "custom_instruction": "Documento: credencial para votar INE/IFE de México (frente y, si existe, reverso). Nombres y CURP en mayúsculas, tal como aparecen. Fechas en formato YYYY-MM-DD.",
  "response_schema": {
    "type": "object",
    "additionalProperties": false,
    "required": ["nombre", "apellido_paterno", "apellido_materno", "curp", "clave_elector", "fecha_nacimiento", "sexo", "domicilio", "seccion", "vigencia"],
    "properties": {
      "nombre": { "type": ["string", "null"], "description": "Solo nombre(s), sin apellidos." },
      "apellido_paterno": { "type": ["string", "null"] },
      "apellido_materno": { "type": ["string", "null"] },
      "curp": { "type": ["string", "null"], "description": "18 caracteres." },
      "clave_elector": { "type": ["string", "null"], "description": "18 caracteres." },
      "fecha_nacimiento": { "type": ["string", "null"], "description": "Formato YYYY-MM-DD." },
      "sexo": { "type": ["string", "null"], "enum": ["H", "M", null] },
      "domicilio": { "type": ["string", "null"], "description": "Domicilio completo en una sola línea." },
      "seccion": { "type": ["string", "null"], "description": "Sección electoral de 4 dígitos, como texto." },
      "vigencia": { "type": ["integer", "null"], "description": "Año de vigencia, por ejemplo 2031." }
    }
  },
  "input_result_type": "markdown"
}

Constancia de Situación Fiscal (csf_document)

{
  "document_id": "doc_01J...",
  "custom_instruction": "Documento: Constancia de Situación Fiscal del SAT. RFC en mayúsculas. Fechas en formato YYYY-MM-DD. En regimenes incluye cada régimen listado en la sección de regímenes.",
  "response_schema": {
    "type": "object",
    "additionalProperties": false,
    "required": ["rfc", "nombre_o_razon_social", "codigo_postal", "estatus_padron", "fecha_emision", "regimenes"],
    "properties": {
      "rfc": { "type": ["string", "null"] },
      "nombre_o_razon_social": { "type": ["string", "null"], "description": "Persona física: nombre completo. Persona moral: razón social sin régimen capital." },
      "codigo_postal": { "type": ["string", "null"], "description": "5 dígitos del domicilio fiscal, como texto." },
      "estatus_padron": { "type": ["string", "null"], "description": "Por ejemplo ACTIVO." },
      "fecha_emision": { "type": ["string", "null"], "description": "Fecha en que se emitió la constancia, formato YYYY-MM-DD." },
      "regimenes": {
        "type": "array",
        "items": {
          "type": "object",
          "additionalProperties": false,
          "required": ["descripcion", "fecha_inicio"],
          "properties": {
            "descripcion": { "type": ["string", "null"] },
            "fecha_inicio": { "type": ["string", "null"], "description": "Formato YYYY-MM-DD." }
          }
        }
      }
    }
  },
  "input_result_type": "markdown"
}

Comprobante de domicilio (proof_of_address)

{
  "document_id": "doc_01J...",
  "custom_instruction": "Documento: recibo de servicio usado como comprobante de domicilio (CFE, agua, teléfono, internet). La dirección es la del titular del servicio, no la de la empresa que emite el recibo. Fechas en formato YYYY-MM-DD.",
  "response_schema": {
    "type": "object",
    "additionalProperties": false,
    "required": ["emisor", "titular", "calle_y_numero", "colonia", "municipio", "estado", "codigo_postal", "fecha_emision", "total_a_pagar"],
    "properties": {
      "emisor": { "type": ["string", "null"], "description": "Empresa que emite el recibo, por ejemplo CFE." },
      "titular": { "type": ["string", "null"] },
      "calle_y_numero": { "type": ["string", "null"] },
      "colonia": { "type": ["string", "null"] },
      "municipio": { "type": ["string", "null"], "description": "Municipio o alcaldía." },
      "estado": { "type": ["string", "null"] },
      "codigo_postal": { "type": ["string", "null"], "description": "5 dígitos, como texto." },
      "fecha_emision": { "type": ["string", "null"], "description": "Fecha de emisión o de corte del recibo, formato YYYY-MM-DD." },
      "total_a_pagar": { "type": ["number", "null"] }
    }
  },
  "input_result_type": "markdown"
}

Límites

Valores por defecto (el equipo de plataforma puede ajustarlos):

Límite Valor
custom_instruction 1 a 8000 caracteres
Tamaño del response_schema 64 KB
Profundidad del schema 8 niveles (la raíz cuenta como 1)
Propiedades en todo el schema 100
Nodos items en todo el schema 50
Contenido OCR enviado al modelo 256 KB. Si se excede, la extracción termina failed con extraction_input_unavailable; prueba otro input_result_type (normalmente text es el más compacto).

Errores y cómo corregirlos

Al crear la extracción (422 validation_error)

Si el schema es inválido, error.details.response_schema lista los problemas:

Mensaje Causa Solución
Root schema type must be object. Raíz sin "type": "object" o formato abreviado. Envuelve tus campos en { "type": "object", "properties": { ... } }.
Schema node must define type. Alguna propiedad o items sin type. Agrega type a cada nodo.
Schema contains unsupported keywords or types. Keyword no soportada (format, pattern…), tipo inexistente o description que no es texto. Quita la keyword y mueve la regla a description.
Property names must be non-empty strings. Una propiedad con nombre vacío ("": {...}). Dale un nombre a cada campo.
Property {nombre} must be an object. Un campo definido como texto ("total": "number"). Usa "total": { "type": "number" }.
Schema exceeds maximum allowed ... Supera tamaño, profundidad, propiedades o items. Divide en dos extracciones o simplifica.

Después de procesar (validation_failed)

El modelo respondió, pero su JSON no cumplió el schema en ningún intento. Consulta GET /extractions/{extraction_id}: failure.validation_errors trae los errores del último intento.

{
  "data": {
    "id": "ext_01J...",
    "status": "validation_failed",
    "failure": {
      "code": "schema_validation_failed",
      "message": "The extraction response could not be validated against the requested schema.",
      "validation_errors": [
        "$.total: value does not match expected type.",
        "$.conceptos[0].cantidad: is required."
      ]
    }
  }
}
Error Qué pasó Solución
$.campo: is required. El modelo omitió la clave. Hazla anulable (["string", "null"]) y explica en description dónde está el dato.
$.campo: value does not match expected type. Tipo distinto, por ejemplo "11,600.00" (texto) donde esperabas number. Indica el formato en description o acepta el otro tipo.
$.campo: value is not in enum. Valor fuera de la lista. Amplía el enum, incluye null o explica el mapeo (por ejemplo "Hombre → H").
$.campo: additional property not allowed. El modelo agregó una clave que no definiste. Revisa nombres confusos o quita "additionalProperties": false.
Invalid JSON response from provider. La respuesta no fue JSON. Suele ser un OCR muy largo o ruidoso: prueba otro input_result_type o reduce campos.

Tras ajustar, crea una nueva extracción con una nueva Idempotency-Key.

Iterar rápido

  1. Sube el documento una vez y reutiliza su document_id en cada prueba de extracción.
  2. Lee lo que ve el modelo: GET /documents/{document_id}/result?format=markdown. Si un dato no está en el OCR, ningún prompt lo va a extraer.
  3. Empieza con pocos campos, confirma que salen bien y agrega el resto.
  4. El resultado final está en data.result de GET /extractions/{extraction_id}/result.

Checklist

  • Raíz "type": "object" y type en cada nodo.
  • Solo keywords soportadas; reglas de formato en description.
  • Campos anulables en required y "additionalProperties": false.
  • enum anulables incluyen null.
  • Instrucción con tipo de documento, roles, formatos y casos ambiguos, sin repetir reglas de plataforma.
  • Manejo de validation_failed leyendo failure.validation_errors.