Rails Claude Vision API: gestructureerde data uit PDFs, bonnen en screenshots halen met Anthropic
Een boekhoudkantoor belde mij in maart omdat hun drie accountants elke vrijdagmiddag bezig waren een gedeelde Dropbox-map met bonnen om te zetten in rijen in een Postgres-database. Achtduizend onkostendeclaraties per maand. Foto’s van verfrommelde tankbonnen, gescande PDFs van hotelfacturen, screenshots van e-mailbevestigingen, en af en toe een iPhone-foto van een whiteboard. Hun vorige poging was een Tesseract-pipeline die prachtig werkte voor de elf procent van de bonnen die plat, goed verlicht en in een normaal lettertype waren afgedrukt. Voor de andere negenentachtig procent moest een mens alsnog de totalen overtypen. Ze wilden weten of Claude het beter kon.
Na negentien jaar Rails heb ik veel OCR-pipelines gebouwd, en het antwoord was ja — maar niet op de manier die ze verwachtten. De Rails Claude Vision API is geen vervanging voor OCR. Het is een vervanging voor de mens die naar een rommelig document kijkt en zegt “het totaal is zevenenveertig euro en de leverancier is het tankstation bij kantoor”. Deze post is het draaiboek dat ik dat team gaf: hoe je Claudes vision-mogelijkheden in een Rails-app integreert, gestructureerde data uit PDFs en afbeeldingen haalt, de output valideert, en productie overleeft.
Wat de Rails Claude Vision API daadwerkelijk doet
Elk Claude 3- en 4-model is multimodaal. Je stuurt een afbeelding (of een PDF) mee in een bericht, je stelt er een vraag over, en je krijgt tekst terug. Er is geen aparte vision-endpoint. Dezelfde messages API die tekst afhandelt, handelt documenten en afbeeldingen af, en hetzelfde model dat een Rails-migratie kan schrijven kan een Duitse btw-bon lezen en je de regels noemen.
Wat dit in de praktijk betekent voor een Rails-team:
- PDFs werken direct. Je hoeft een PDF niet langer naar PNGs te rasteriseren en elke pagina apart te sturen. Claude accepteert PDFs tot 32MB en 100 pagina’s en verwerkt zowel de tekst als de gerenderde visuals.
- Beeldkwaliteit doet er minder toe dan je denkt. Een wazige telefoonfoto van een bon waar Tesseract niets mee kan, parseert vaak schoon door Claude.
- Je schrijft een prompt, geen parser. Er is geen template. Je beschrijft wat je wilt en valideert de JSON die terugkomt.
De Rails Claude Vision API is het juiste gereedschap wanneer het document variabel, semi-gestructureerd of rommelig is. Het is het verkeerde gereedschap wanneer je een miljoen identieke formulieren hebt — daarvoor wil je een vaste extractie-pipeline. Voor alles daartussenin, wat de meeste echte bedrijven hebben, is het de goedkoopste senior accountant die je ooit zult inhuren.
De Anthropic Ruby-client opzetten
De officiële anthropic gem is de schoonste ingang. Voeg hem toe en configureer je client:
# Gemfile
gem "anthropic", "~> 1.0"
# config/initializers/anthropic.rb
require "anthropic"
ANTHROPIC = Anthropic::Client.new(
api_key: Rails.application.credentials.dig(:anthropic, :api_key)
)
Sla de sleutel op in encrypted credentials, nooit in een .env die in git staat. De post over Rails credentials en secrets management behandelt de volledige setup als je nog niet van dotenv bent afgestapt.
Een minimale vision-aanroep tegen een afbeelding ziet er zo uit:
class ReceiptParser
MODEL = "claude-sonnet-4-6"
def initialize(image_path)
@image_path = image_path
end
def call
base64 = Base64.strict_encode64(File.binread(@image_path))
media_type = Marcel::MimeType.for(Pathname.new(@image_path))
ANTHROPIC.messages.create(
model: MODEL,
max_tokens: 1024,
messages: [{
role: "user",
content: [
{
type: "image",
source: {
type: "base64",
media_type: media_type,
data: base64
}
},
{
type: "text",
text: "Extract the merchant, date, total amount and currency from this receipt. Reply with JSON only."
}
]
}]
)
end
end
Drie dingen om op te merken. Ten eerste: image content blocks staan naast text blocks in dezelfde content-array — volgorde is belangrijk, en het plaatsen van de afbeelding vóór de vraag geeft het model eerst de volledige visuele context. Ten tweede: media_type moet overeenkomen met het werkelijke bestand: stuur een PNG met image/jpeg en je krijgt een 400. Ten derde: “JSON only” in de prompt is een verzoek, geen garantie — dat lossen we in de volgende sectie op.
PDFs zonder rasterisatie
Dit is het deel dat Rails-teams verrast die alleen oudere vision-API’s hebben gebruikt. Claude leest PDFs natively:
class InvoiceExtractor
MODEL = "claude-sonnet-4-6"
def call(pdf_path)
base64 = Base64.strict_encode64(File.binread(pdf_path))
ANTHROPIC.messages.create(
model: MODEL,
max_tokens: 4096,
messages: [{
role: "user",
content: [
{
type: "document",
source: {
type: "base64",
media_type: "application/pdf",
data: base64
}
},
{
type: "text",
text: <<~PROMPT
Extract the invoice header and line items.
Return strict JSON matching this shape:
{
"invoice_number": string,
"issue_date": "YYYY-MM-DD",
"due_date": "YYYY-MM-DD" | null,
"vendor": { "name": string, "vat_id": string | null },
"currency": "EUR" | "USD" | "GBP" | string,
"subtotal_cents": integer,
"tax_cents": integer,
"total_cents": integer,
"line_items": [
{ "description": string, "quantity": number,
"unit_price_cents": integer, "total_cents": integer }
]
}
All money values must be integer cents. No prose, no markdown.
PROMPT
}
]
}]
)
end
end
Een paar productie-relevante feiten. PDFs worden gefactureerd voor zowel hun tekstinhoud (goedkoop) als een gerenderde afbeelding van elke pagina (het dure deel — ongeveer 1500 tot 3000 tokens per pagina afhankelijk van dichtheid). Een factuur van tien pagina’s kost je tussen de vijftien- en dertigduizend input tokens. Vermenigvuldig met je volume voordat je gaat juichen.
Voor PDFs boven 32MB, of boven 100 pagina’s, moet je zelf splitsen. De combine_pdf gem of Ghostscript doen dat allebei. Behandel elke chunk als een aparte extractie en voeg de resultaten in Ruby samen — probeer niet slim te zijn met multi-message conversaties om eerdere pagina’s te “onthouden”.
Gestructureerde JSON output afdwingen
De meest voorkomende productie-faalmodus van de Rails Claude Vision API is dat het model proza zoals “Sure! Here is the invoice data:” voor de JSON terugstuurt. Je wilt geen regex schrijven om dat op te ruimen. Je wilt dat de API je JSON geeft.
Het schoonste patroon is de tool_use API. Je definieert een tool wiens enige taak het is gestructureerde data te ontvangen, je zet tool_choice om het gebruik te forceren, en Claude geeft een gestructureerd tool_use-blok terug in plaats van ruwe tekst:
INVOICE_TOOL = {
name: "record_invoice",
description: "Record the structured invoice data extracted from the document.",
input_schema: {
type: "object",
properties: {
invoice_number: { type: "string" },
issue_date: { type: "string", pattern: "^\\d{4}-\\d{2}-\\d{2}$" },
due_date: { type: ["string", "null"], pattern: "^\\d{4}-\\d{2}-\\d{2}$" },
currency: { type: "string", minLength: 3, maxLength: 3 },
total_cents: { type: "integer", minimum: 0 },
line_items: {
type: "array",
items: {
type: "object",
properties: {
description: { type: "string" },
quantity: { type: "number" },
unit_price_cents: { type: "integer" },
total_cents: { type: "integer" }
},
required: ["description", "total_cents"]
}
}
},
required: ["invoice_number", "issue_date", "currency", "total_cents", "line_items"]
}
}
response = ANTHROPIC.messages.create(
model: "claude-sonnet-4-6",
max_tokens: 4096,
tools: [INVOICE_TOOL],
tool_choice: { type: "tool", name: "record_invoice" },
messages: [{
role: "user",
content: [
{ type: "document", source: { type: "base64", media_type: "application/pdf", data: pdf_base64 } },
{ type: "text", text: "Extract the invoice using the record_invoice tool." }
]
}]
)
invoice_data = response.content.find { |b| b.type == "tool_use" }.input
Het schema doet dubbel werk: het vertelt het model welke vorm te produceren en geeft jou een contract om aan jouw kant tegen te valideren. Combineer het met de json-schema gem en weiger elke extractie die niet matcht — vertrouw modeloutput nooit blind in je database.
De post over LLM function calling in Rails gaat dieper in op de tool-use API voor gevallen buiten extractie.
Kostenoptimalisatie die er echt toe doet
Vision-tokens zijn niet goedkoop, en een kleine Rails-app die bonnen verwerkt kan een rekening van duizend dollar oplopen als je niet oplet. Drie patronen die ik altijd inzet.
Resize voordat je verstuurt. Anthropics aanbeveling is geen zijde langer dan 1568px. Een moderne telefoonfoto is 4032x3024. Die ruw versturen kost je vier keer de tokens van een verkleinde afbeelding zonder kwaliteitsverlies bij de extractie:
require "image_processing/vips"
def prepare_image(path)
ImageProcessing::Vips
.source(path)
.resize_to_limit(1568, 1568)
.convert("jpeg")
.saver(quality: 85)
.call
end
Cache de system prompt. Als je dezelfde extractie over duizenden documenten draait, zijn de instructies elke keer identiek. Anthropics prompt caching hergebruikt die prefix tegen een tiende van de kosten. De post over Anthropic prompt caching in Rails toont de volledige setup; de korte versie is om het system block te markeren met cache_control: { type: "ephemeral" }.
Batch het niet-urgente werk. De Anthropic Message Batches API geeft je vijftig procent korting in ruil voor een resultaat-binnen-24-uur SLA. Alles wat niet user-facing is — overnight ingestion, maandelijkse compliance scans, historische backfills — moet via batches lopen. Zie Anthropic message batches in Rails voor het integratiepatroon.
Voor de boekhoudklant brachten die drie wijzigingen de maandelijkse kosten van $4.200 naar $610 zonder kwaliteitsverlies.
Inbouwen in een Active Job pipeline
Een echte productie-pipeline roept de Rails Claude Vision API vrijwel nooit synchroon aan vanuit een controller. De gebruiker uploadt een bon, je enqueued een job, de job extraheert, valideert en schrijft — en de UI werkt zichzelf bij via Turbo Streams wanneer de rij verschijnt. Hier is het skelet:
class Receipts::ExtractJob < ApplicationJob
queue_as :ai
retry_on Anthropic::Errors::OverloadedError,
Anthropic::Errors::RateLimitError,
wait: :polynomially_longer,
attempts: 8
discard_on ActiveJob::DeserializationError
def perform(receipt_id)
receipt = Receipt.find(receipt_id)
return if receipt.extracted?
image_blob = receipt.image.download
Tempfile.create(["receipt", ".jpg"], binmode: true) do |f|
f.write(image_blob)
f.flush
data = ReceiptParser.new(prepare_image(f.path)).call
validate!(data)
receipt.update!(
merchant: data["merchant"],
purchased_at: data["purchased_at"],
total_cents: data["total_cents"],
currency: data["currency"],
extracted_at: Time.current,
extraction_model: ReceiptParser::MODEL
)
end
Turbo::StreamsChannel.broadcast_replace_to(
"user_#{receipt.user_id}_receipts",
target: dom_id(receipt),
partial: "receipts/receipt",
locals: { receipt: receipt }
)
end
private
def validate!(data)
schema = ReceiptParser::SCHEMA
errors = JSON::Validator.fully_validate(schema, data)
raise ExtractionError, errors.join("; ") if errors.any?
end
end
Drie dingen die deze job doet die in voorbeeldcode vaak ontbreken. Hij retried met exponential backoff op tijdelijke API-fouten — de post over Rails Active Job retries met exponential backoff legt uit waarom polynomially_longer de juiste default is. Hij legt extraction_model vast zodat je later, wanneer je upgrade naar een nieuwere Claude-versie, drift in je data kunt opmerken. En hij broadcast het resultaat over Turbo Streams zodat de gebruiker niet hoeft te verversen.
Documenten die niet verstuurd horen te worden
Niet elk geüpload bestand mag bij Anthropic terechtkomen. Drie checks horen vóór de API-aanroep:
- Grootte. Wijs alles boven 32MB voor PDFs of 5MB voor afbeeldingen af op de controller-laag, met een duidelijke foutmelding.
- Mime type. Gebruik Marcel om de werkelijke inhoud te verifiëren, niet de bestandsnaam. Gebruikers uploaden
.pdf-bestanden die in werkelijkheid JPEGs, ZIPs of Word-documenten zijn. - Gevoeligheid. Een bon-extractie-app is geen plek om een paspoortscan naartoe te sturen. Als je gebruikers identiteitsdocumenten of medische gegevens kunnen uploaden, draai dan eerst een kleine classifier — Claude kan dat zelf in een 50-token aanroep — en weiger extractie met een duidelijke melding in plaats van stilletjes PII naar een derde partij te sturen.
Je wilt ook een kill switch. Een feature flag waarmee je Claude-aanroepen in vijf seconden kunt pauzeren wanneer Anthropic een incident heeft, is goud waard. De post over Rails feature flags met Flipper behandelt de patronen.
Wanneer je de Rails Claude Vision API niet moet gebruiken
Ik zou je tekortdoen als ik de gevallen niet noem waarin dit het verkeerde gereedschap is.
- Hoog volume, identieke formulieren. Een miljoen belastingformulieren met dezelfde lay-out? Een gespecialiseerde OCR-service of een fine-tuned layout-model is op die schaal tien keer goedkoper.
- Realtime, sub-seconde latency. Vision-aanroepen duren twee tot tien seconden. Gebruik ze in async jobs, niet op het request-pad.
- Gereguleerde extractie met audit-eisen. Sommige compliance-regimes vereisen deterministische extractie met verklaarbare provenance. Claude kan onderdeel zijn van de workflow, maar kan niet de enige bewijslijn zijn.
- Mini-budgetten op schaal. Verwerk je honderdduizend documenten per dag op een budget van vijftig dollar, dan klopt de rekensom niet. Resize, cache, batch — maar bij een bepaald volume heb je een ander gereedschap nodig.
Voor de boekhoudklant gold geen van die punten, en we hebben de integratie in twee weken live gezet. Hun accountants besteden vrijdagmiddag nu aan het reviewen van gemarkeerde extracties in plaats van het overtypen van elke regel. De bonnen lopen via Active Storage, een Active Job draait de extractie, en een kleine Stimulus-controller laat de mens goedkeuren, bewerken of weigeren. De Vision API is de motor; de pipeline eromheen maakt het productiewaardig.
FAQ
Kan de Rails Claude Vision API handgeschreven tekst extraheren?
Ja, en veel beter dan traditionele OCR. Claude verwerkt cursief, gemengd schrift en handgeschreven tekst, meertalige notities, en zelfs gedeeltelijke verstoring. De kwaliteit zakt op extreem slecht verlichte foto’s en zeer klein handschrift, dus resize verstandig en overweeg een hername-prompt voor gebruikers wanneer het model velden met lage confidence teruggeeft. Voor pure handschriftherkenning op extreme schaal kan een gespecialiseerde HTR-service nog steeds goedkoper zijn.
Hoe extraheer ik data uit een meerpagina-PDF in Rails?
Stuur de hele PDF als een document content block — Claude leest tot 100 pagina’s en 32MB in één request. Voor grotere documenten splits met combine_pdf of Ghostscript, draai extracties parallel via Active Job, en voeg samen in Ruby. Valideer altijd per chunk de output tegen een JSON schema voor je samenvoegt, want fouten per pagina stapelen snel op.
Is de Anthropic Vision API GDPR-veilig voor Europese klanten?
Anthropic biedt een EU-data-residency-optie en een zero-data-retention-overeenkomst op enterprise plannen. Voor een Rails-app die PII van EU-klanten verwerkt wil je vrijwel zeker beide. Praat met je FG voordat je live gaat en documenteer de data flow in je verwerkingsregister. Ga er niet vanuit dat het standaardplan voldoende is.
Hoe nauwkeurig is Claude op bonnen vergeleken met Tesseract?
In mijn benchmarks op duizend echte bonnen (mix van telefoonfoto’s, scans en PDFs) extraheerde Claude Sonnet bij de eerste poging op 94% van de inputs correct merchant, datum en totaal. Tesseract haalde op dezelfde set 41%. Het gat sluit op platte, hoge-kwaliteit scans en groeit op foto’s. Voor gemengde echte input wint Claude overtuigend, maar combineer het met confidence checks en menselijke review voor alles wat een financiële boeking aanstuurt.
Hulp nodig om Claude in je Rails-app te integreren — vision, RAG, agents of iets anders? TTB Software bouwt productiewaardige AI-integraties op Rails. We doen Rails al negentien jaar en AI-integraties sinds de API een research preview was.
About the Author
Roger Heykoop is een senior Ruby on Rails ontwikkelaar met 19+ jaar Rails ervaring en 35+ jaar ervaring in softwareontwikkeling. Hij is gespecialiseerd in Rails modernisering, performance optimalisatie, en AI-ondersteunde ontwikkeling.
Get in TouchRelated Articles
Need Expert Rails Development?
Let's discuss how we can help you build or modernize your Rails application with 19+ years of expertise
Schedule a Free Consultation