Skip to main content
POST
/
api
/
upload-image
Upload Image API
curl --request POST \
  --url https://api.example.com/api/upload-image \
  --header 'Content-Type: application/json' \
  --data '{}'
{
  "success": true,
  "url": "<string>",
  "analysis": "<string>",
  "data": {
    "es_ticket": true,
    "monto": 123,
    "comercio": "<string>",
    "categoria_sugerida": "<string>",
    "items": [
      {}
    ],
    "fecha": "<string>",
    "descripcion": "<string>",
    "razon": "<string>",
    "sugerencia": "<string>"
  }
}

Overview

The Upload Image API processes receipt and invoice images using AI-powered OCR (Optical Character Recognition). It uploads images to Supabase Storage and analyzes them with Google’s Gemini 2.5 Flash multimodal model to extract transaction details.

Authentication

Requires:
  • Supabase authentication
  • Valid OpenRouter API key configured in environment variables
  • Access to Supabase Storage bucket facturas

Endpoint

POST /api/upload-image

Request

image
File
required
Image file in multipart/form-data format.Supported formats: JPG, PNG, WEBPRecommended: Clear photos of receipts, invoices, or purchase tickets

Content-Type

multipart/form-data

Response

success
boolean
Indicates if the upload and analysis completed successfully
url
string
Public URL of the uploaded image in Supabase Storage
analysis
string
Formatted markdown text with OCR analysis results for display
data
object
Structured OCR data for programmatic use
es_ticket
boolean
Whether the image is recognized as a valid receipt/invoice
monto
number
Extracted total amount (only if es_ticket: true)
comercio
string
Extracted business/merchant name
categoria_sugerida
string
AI-suggested category from valid system categories
items
array
List of items/products found on the receipt
fecha
string
Extracted date in YYYY-MM-DD format
descripcion
string
Brief description of the transaction
razon
string
Reason if image is not recognized as a ticket (es_ticket: false)
sugerencia
string
Suggestion for user if image is invalid

Valid Categories

The AI suggests one of these predefined categories: Expenses (Gastos):
  • Alimentación
  • Transporte
  • Vivienda
  • Salud
  • Entretenimiento
  • Educación
  • Otros Gastos
Income (Ingresos):
  • Salario
  • Ventas
  • Servicios
  • Inversiones
  • Otros Ingresos

Examples

const formData = new FormData();
const fileInput = document.querySelector('input[type="file"]');
formData.append('image', fileInput.files[0]);

const response = await fetch('/api/upload-image', {
  method: 'POST',
  body: formData
});

const result = await response.json();
console.log('Analysis:', result.analysis);
console.log('Amount:', result.data.monto);

Response Examples

Valid Receipt (Gas Station)

{
  "success": true,
  "url": "https://example.supabase.co/storage/v1/object/public/facturas/ticket_1709740800000_receipt.jpg",
  "analysis": "📸 **TICKET ANALIZADO:**\n\n💰 **Monto:** $450.50 MXN\n🏪 **Comercio:** Pemex\n📁 **Categoría sugerida:** Transporte\n📋 **Items:** Magna Premium 30L, Total\n📅 **Fecha:** 2024-03-06\n\n📝 **Descripción:** Llenado de combustible en Pemex",
  "data": {
    "es_ticket": true,
    "monto": 450.50,
    "comercio": "Pemex",
    "categoria_sugerida": "Transporte",
    "items": ["Magna Premium 30L", "Total"],
    "fecha": "2024-03-06",
    "descripcion": "Llenado de combustible en Pemex"
  }
}

Valid Receipt (Supermarket)

{
  "success": true,
  "url": "https://example.supabase.co/storage/v1/object/public/facturas/ticket_1709827200000_walmart.jpg",
  "analysis": "📸 **TICKET ANALIZADO:**\n\n💰 **Monto:** $350 MXN\n🏪 **Comercio:** Walmart\n📁 **Categoría sugerida:** Alimentación\n📋 **Items:** Leche, Pan, Huevos, Verduras\n📅 **Fecha:** 2024-03-09\n\n📝 **Descripción:** Compra de despensa en Walmart",
  "data": {
    "es_ticket": true,
    "monto": 350.00,
    "comercio": "Walmart",
    "categoria_sugerida": "Alimentación",
    "items": ["Leche", "Pan", "Huevos", "Verduras"],
    "fecha": "2024-03-09",
    "descripcion": "Compra de despensa en Walmart"
  }
}

Invalid Image (Not a Receipt)

{
  "success": true,
  "url": "https://example.supabase.co/storage/v1/object/public/facturas/ticket_1709913600000_screenshot.jpg",
  "analysis": "⚠️ **IMAGEN NO RECONOCIDA COMO TICKET**\n\nEsta es una captura de pantalla de una conversación de texto, no un ticket o factura.\n\n💡 **Sugerencia:** Por favor sube una foto de un ticket, factura o recibo de compra para que pueda analizarlo.\n\nSi quieres registrar algo manualmente, dime:\n- ¿Es gasto o ingreso?\n- Monto\n- Comercio/Proveedor\n- Categoría",
  "data": {
    "es_ticket": false,
    "razon": "Esta es una captura de pantalla de una conversación de texto, no un ticket o factura",
    "sugerencia": "Por favor sube una foto de un ticket, factura o recibo de compra para que pueda analizarlo"
  }
}

Error Response

{
  "error": "No image provided"
}

Workflow

  1. Upload Image to Supabase Storage bucket facturas
    • Filename format: ticket_{timestamp}_{original_name}
    • Content-Type preserved from original file
  2. Get Public URL from Supabase Storage
  3. Analyze with Gemini 2.5 Flash
    • Multimodal vision capabilities
    • OCR with structured JSON output
    • Category suggestion from valid list
  4. Validate Response
    • Determines if image is a valid receipt (es_ticket)
    • Extracts structured data (amount, merchant, items, date)
  5. Format Response
    • User-friendly markdown text (analysis)
    • Structured JSON for programmatic use (data)

AI Vision Capabilities

Gemini 2.5 Flash is specifically prompted to:
  • Validate if the image is actually a receipt/invoice
  • Extract total amount, merchant name, date
  • Identify purchased items
  • Suggest appropriate category from the valid list
  • Reject screenshots, random photos, or non-receipt images
  • Provide helpful suggestions when image is invalid
The AI uses response_format: { type: 'json_object' } to ensure structured JSON output. Temperature is set to 0.1 for consistent, accurate extractions.

Storage Details

  • Bucket: facturas (Supabase Storage)
  • Access: Public read access for uploaded images
  • Naming: ticket_{timestamp}_{original_filename}
  • Persistence: Images remain in storage for future reference

Integration with Chat

The extracted data can be passed to /api/chat/stream for conversational transaction registration:
// 1. Upload image
const uploadResult = await uploadImage(file);

// 2. Send to chat with OCR data
const chatResponse = await fetch('/api/chat/stream', {
  method: 'POST',
  body: JSON.stringify({
    message: 'Registra este gasto',
    images: [uploadResult.url],
    messages: []
  })
});

Build docs developers (and LLMs) love