API Documentation
Everything you need to integrate Gemina's powerful document extraction into your application.
Getting Started#
Extract structured data from documents programmatically. Responses are structured JSON with extracted fields, coordinates, and confidence scores.
There are four ways in. An official SDK is the recommended path — install it and process a document in a single call, then search and chat over your results.
Raw HTTP calls the same REST API from any language. A drop-in chat UI puts the whole experience inside your own app. And an AI agent can connect over MCP — see the MCP server reference.
https://api.gemina.coX-API-Key: your-api-keyOfficial SDKs#
RecommendedInstall one package, then process, search & chat in native code — Document Intelligence included.The fastest path for most integrations.
Process a document in one call#
Authenticate with your API key and extract structured data — the SDK submits the document and polls for the result for you, so there's no upload-then-poll loop to write:
import { readFile } from "node:fs/promises";
import { GeminaClient } from "@gemina/sdk";
const client = new GeminaClient(process.env.GEMINA_API_KEY!);
// Node: wrap a Buffer in a Blob. In the browser, pass a File from an
// <input type="file"> directly.
const buf = await readFile("./invoice.png");
const result = await client.processDocument(
new Blob([buf], { type: "image/png" }),
["invoice_headers"],
);
const values = result.data?.extractions?.[0]?.values;
console.log("supplier:", values?.vendorName?.value);
console.log("total:", values?.totalAmount?.value);
console.log("date:", values?.invoiceDate?.value);Add extraction types to a stored document#
Run more extraction types on a document Gemina already stores — no re-upload. The convenience helper submits the request, polls to a terminal result, and returns the whole document with its existing and new extractions. The generated submit-only API accepts anAddExtractionsInDTO, returns aDocumentAddExtractionsOutDTO, and exposes the poll correlation ID when you want to control polling:
// Reuse a documentId from processDocument, FileTag, or a document lookup.
const updated = await client.addExtractionsAndWait(
documentId,
["invoice_line_items"],
{ includeCoordinates: true },
);
console.log(updated.data?.extractions);
// Submit-only generated surface (poll pollCorrelationId yourself):
// client.documents.addDocumentExtractions({
// documentId,
// addExtractionsInDTO: { extractionTypes: ["invoice_line_items"] },
// }) -> Promise<DocumentAddExtractionsOutDTO>Each new type is billed like an upload and must allow the stored document's page count. A type already present is rejected; a purged source must be uploaded again. Adding a type also resets the shared retention date for the whole document to the account's current retention setting.
Search & analyze your documents#
Query everything you've processed with structured filters, semantic search, or both — and compute exact, database-backed aggregations without re-reading a document:
const { items, meta } = await client.retrieval.retrievalQuery({
retrievalQueryInDTO: {
text: "cleaning services invoices from August",
filters: { totalAmountMin: 1000, currency: "ILS" },
limit: 10,
},
});
for (const item of items ?? []) {
console.log(item.vendorName, item.totalAmount, item.issueDate, item.documentId);
}
console.log(`${meta.count} matches (mode: ${meta.mode})`);
const { rows } = await client.retrieval.retrievalAggregate({
retrievalAggregateInDTO: {
metrics: [{ op: "sum", field: "total_amount" }, { op: "count" }],
groupBy: ["vendor_name"],
},
});
for (const row of rows ?? []) {
console.log(row.group, row.values);
}Chat with your documents#
Ask in natural language and get grounded answers with citations. Follow-up questions keep their conversation context. A conversation's live context expires after roughly 24h of inactivity — reset and resend to continue in a fresh one. The transcript itself is not lost; it stays readable in chat history below:
const reply = await client.chat.chatQuery({
chatQueryInDTO: { message: "How much did we spend on cleaning in 2020?" },
});
console.log(reply.answer);
console.log("confident:", reply.confident);
console.log("citations:", reply.citations);
const chat = client.conversation();
await chat.send("How much did we spend on cleaning in 2020?");
const follow = await chat.send("And which vendor was most expensive?"); // remembers 2020 / cleaning
console.log(follow.answer, "· session:", chat.sessionId);
await chat.delete(); // end it server-side (or chat.reset() to just forget it locally)Chat history#
Past conversations are kept as sessions you can list, reread, and purge. Each one carries an auto-generated title, its turn count, the end-user it was scoped to, and the date your retention window will delete it:
const listing = await client.chat.listChatSessions({ limit: 20 });
for (const session of listing.sessions) {
console.log(session.title, "·", session.turnCount, "turns");
}
const transcript = await client.chat.getChatSession({ sessionId: listing.sessions[0].id });
for (const msg of transcript.messages) {
console.log(`[${msg.role}] ${msg.content}`);
}
await client.chat.purgeChatSession({ sessionId: listing.sessions[0].id });Purging permanently deletes the transcript and the server-side copy of its content — it cannot be undone. Purged sessions vanish from the list; ask for purged records to see their content-free stubs (title cleared, purge timestamp and reason set; timestamps, turn count, and end-user id survive). Transcripts also age out automatically under your account's data-retention setting.
Browser-safe session tokens#
Exchange your API key server-side for a short-lived, scoped token so a browser can search and chat without ever seeing the key. Tokens are read-and-chat scoped: they can query, aggregate, chat, read history, and end a conversation — but they can never purge one, which takes an API key or a console sign-in:
// Server-side (holds the API key)
const session = await client.sessions.mintRetrievalToken({
sessionTokenInDTO: { endUserId: "user-42", ttlSeconds: 900 },
});
// -> { token, expiresAt, expiresIn, tokenType }
// Browser (token only)
import { GeminaClient } from "@gemina/sdk";
const browserClient = GeminaClient.withSessionToken(session.token);
const results = await browserClient.retrieval.retrievalQuery({
retrievalQueryInDTO: { text: "last month's invoices" },
});Human verification#
Put a person in front of an extraction before it reaches your workflow. Mint a token scoped to that one extraction, render the drop-in reviewer (see Embed in your app), and read the result back afterwards. The corrections arrive as verifiedValues — the same shape as values, so switching your pipeline to human-verified data is a one-name change — alongside verifiedDiff, the list of what changed:
// Server-side. Authorize the end-user against this id FIRST — Gemina enforces
// the claim in the signed token, you decide who gets it.
const session = await client.sessions.mintRetrievalToken({
sessionTokenInDTO: {
extractionIds: [extractionId], // pins the token — up to 10
ttlSeconds: 900,
},
});
// Ship session.token to the browser and render <GeminaVerification />.
// The source of truth. The widget's browser callback is best-effort.
const view = await client.documents.getDocumentExtraction({
documentExtractionId: extractionId,
});
if (view.meta.validated) {
for (const change of view.verifiedDiff ?? []) {
// status: "corrected" | "added" | "removed"
console.log(change.status, change.field, change.original, "->", change.verified);
}
useThis(view.verifiedValues); // same shape as values, corrections merged in
} else {
useThis(view.values); // nobody has reviewed it yet
}
// Only if you build your own review UI. One-shot: a second call is a 409.
const summary = await client.documents.validateDocumentExtraction({
targetDocumentExtractionId: extractionId,
extractionValidationInDTO: { data: correctedValues },
});Verification is one-shot per extraction; a second submission is rejected. Reading the extraction back is the source of truth — the widget's browser callback is best-effort, so if the network drops the response the verification is still recorded but the callback never fires.
Error handling#
Typed errors separate a terminal processing failure from a still-processing timeout you can resume:
import { GeminaProcessingError, ResponseError } from "@gemina/sdk";
try {
const result = await client.processDocument(file, ["invoice_headers"]);
} catch (err) {
if (err instanceof GeminaProcessingError) {
console.error("processing failed:", err.result.errors);
} else if (err instanceof ResponseError) {
console.error("HTTP error:", err.response.status);
} else {
throw err;
}
}Process a document in one call#
Authenticate with your API key and extract structured data — the SDK submits the document and polls for the result for you, so there's no upload-then-poll loop to write:
import asyncio
from gemina import GeminaClient, UploadExtractionTypeEnum
async def main():
async with GeminaClient("YOUR_API_KEY") as client:
result = await client.process_document(
"invoice.png", # path, bytes, or a binary file object
[UploadExtractionTypeEnum.INVOICE_HEADERS],
)
values = result.data.extractions[0].values
print("Supplier:", values["vendorName"]["value"])
print("Total: ", values["totalAmount"]["value"], values["currency"]["value"])
print("Date: ", values["invoiceDate"]["value"])
asyncio.run(main())Add extraction types to a stored document#
Run more extraction types on a document Gemina already stores — no re-upload. The convenience helper submits the request, polls to a terminal result, and returns the whole document with its existing and new extractions. The generated submit-only API accepts anAddExtractionsInDTO, returns aDocumentAddExtractionsOutDTO, and exposes the poll correlation ID when you want to control polling:
from gemina import UploadExtractionTypeEnum
# Reuse a document_id from process_document, FileTag, or a document lookup.
updated = await client.add_extractions_and_wait(
document_id,
[UploadExtractionTypeEnum.INVOICE_LINE_ITEMS],
include_coordinates=True,
)
print(updated.data.extractions)
# Submit-only generated surface (poll poll_correlation_id yourself):
# from gemina.generated.models.add_extractions_in_dto import AddExtractionsInDTO
# from gemina.generated.models.document_add_extractions_out_dto import DocumentAddExtractionsOutDTO
# submitted: DocumentAddExtractionsOutDTO = await client.documents.add_document_extractions(
# document_id,
# AddExtractionsInDTO(extraction_types=[UploadExtractionTypeEnum.INVOICE_LINE_ITEMS]),
# )Each new type is billed like an upload and must allow the stored document's page count. A type already present is rejected; a purged source must be uploaded again. Adding a type also resets the shared retention date for the whole document to the account's current retention setting.
Search & analyze your documents#
Query everything you've processed with structured filters, semantic search, or both — and compute exact, database-backed aggregations without re-reading a document:
from gemina import GeminaClient, RetrievalQueryInDTO
from gemina.generated.models.retrieval_filters_dto import RetrievalFiltersDTO
async def search():
async with GeminaClient("YOUR_API_KEY") as client:
page = await client.retrieval.retrieval_query(RetrievalQueryInDTO(
mode="hybrid", # structured | semantic | hybrid
text="cleaning services",
filters=RetrievalFiltersDTO(total_amount_min=100),
top_k=5,
))
for item in page.items:
print(item.vendor_name, item.total_amount, item.currency,
item.issue_date, item.document_id)
from gemina import GeminaClient, RetrievalAggregateInDTO
from gemina.generated.models.aggregate_metric_dto import AggregateMetricDTO
async def totals_by_vendor():
async with GeminaClient("YOUR_API_KEY") as client:
report = await client.retrieval.retrieval_aggregate(RetrievalAggregateInDTO(
metrics=[
AggregateMetricDTO(op="sum", field="total_amount"),
AggregateMetricDTO(op="count"),
],
group_by=["vendor_name"],
))
for row in report.rows:
print(row.group, row.values["sum_total_amount"].actual_instance,
row.values["count"].actual_instance)Chat with your documents#
Ask in natural language and get grounded answers with citations. Follow-up questions keep their conversation context. A conversation's live context expires after roughly 24h of inactivity — reset and resend to continue in a fresh one. The transcript itself is not lost; it stays readable in chat history below:
from gemina import GeminaClient, ChatQueryInDTO
async def ask():
async with GeminaClient("YOUR_API_KEY") as client:
reply = await client.chat.chat_query(ChatQueryInDTO(
message="What is the total amount of my invoices from last month?",
))
print(reply.answer)
print("confident:", reply.confident)
print("citations:", reply.citations)
async def conversation():
async with GeminaClient("YOUR_API_KEY") as client:
chat = client.conversation()
await chat.send("How much did we spend on cleaning in 2020?")
follow = await chat.send("And which vendor was most expensive?") # remembers 2020 / cleaning
print(follow.answer, "· session:", chat.session_id)
await chat.delete() # end it server-side (or chat.reset() to just forget it locally)Chat history#
Past conversations are kept as sessions you can list, reread, and purge. Each one carries an auto-generated title, its turn count, the end-user it was scoped to, and the date your retention window will delete it:
async def history():
async with GeminaClient("YOUR_API_KEY") as client:
listing = await client.chat.list_chat_sessions(limit=20)
for session in listing.sessions:
print(session.title, "·", session.turn_count, "turns")
transcript = await client.chat.get_chat_session(listing.sessions[0].id)
for msg in transcript.messages:
print(f"[{msg.role}] {msg.content}")
await client.chat.purge_chat_session(listing.sessions[0].id)Purging permanently deletes the transcript and the server-side copy of its content — it cannot be undone. Purged sessions vanish from the list; ask for purged records to see their content-free stubs (title cleared, purge timestamp and reason set; timestamps, turn count, and end-user id survive). Transcripts also age out automatically under your account's data-retention setting.
Browser-safe session tokens#
Exchange your API key server-side for a short-lived, scoped token so a browser can search and chat without ever seeing the key. Tokens are read-and-chat scoped: they can query, aggregate, chat, read history, and end a conversation — but they can never purge one, which takes an API key or a console sign-in:
from gemina import GeminaClient, SessionTokenInDTO
async def mint_token():
async with GeminaClient("YOUR_API_KEY") as client: # server-side only
token = await client.sessions.mint_retrieval_token(SessionTokenInDTO(
end_user_id="customer-42", # omit for a whole-account session
ttl_seconds=600, # clamped server-side to [300, 900]
))
return token.token # ship this to the frontendHuman verification#
Put a person in front of an extraction before it reaches your workflow. Mint a token scoped to that one extraction, render the drop-in reviewer (see Embed in your app), and read the result back afterwards. The corrections arrive as verifiedValues — the same shape as values, so switching your pipeline to human-verified data is a one-name change — alongside verifiedDiff, the list of what changed:
from gemina import GeminaClient, SessionTokenInDTO
from gemina.generated.models.extraction_validation_in_dto import (
ExtractionValidationInDTO,
)
async def mint_verification_token(extraction_id):
"""Server-side. Authorize the end-user against this id FIRST — Gemina
enforces the claim, you decide who gets it."""
async with GeminaClient("YOUR_API_KEY") as client:
token = await client.sessions.mint_retrieval_token(SessionTokenInDTO(
extraction_ids=[extraction_id], # pins the token — up to 10
ttl_seconds=900,
))
return token.token # ship this to the browser
async def read_back(extraction_id):
"""The source of truth. The widget's browser callback is best-effort."""
async with GeminaClient("YOUR_API_KEY") as client:
view = await client.documents.get_document_extraction(extraction_id)
if not view.meta.validated:
return view.values # nobody has reviewed it yet
for change in view.verified_diff: # corrected | added | removed
print(change.status, change.field, change.original, "->", change.verified)
return view.verified_values # same shape as values, corrected
async def submit_without_the_widget(extraction_id, corrected_values):
"""Only if you build your own review UI. One-shot: a second call is a 409."""
async with GeminaClient("YOUR_API_KEY") as client:
return await client.documents.validate_document_extraction(
extraction_id,
ExtractionValidationInDTO(data=corrected_values),
)Verification is one-shot per extraction; a second submission is rejected. Reading the extraction back is the source of truth — the widget's browser callback is best-effort, so if the network drops the response the verification is still recorded but the callback never fires.
Error handling#
Typed errors separate a terminal processing failure from a still-processing timeout you can resume:
from gemina import GeminaError, GeminaProcessingError, GeminaTimeoutError
async def robust():
async with GeminaClient("YOUR_API_KEY") as client:
try:
result = await client.process_document(
"invoice.pdf",
[UploadExtractionTypeEnum.INVOICE_HEADERS],
timeout_seconds=120,
)
except GeminaProcessingError as exc:
print("processing failed:", exc.result.errors)
except GeminaTimeoutError as exc:
print("still running, poll later:", exc.correlation_id)
result = await client.documents.\
get_document_processing_result_by_correlation_id(exc.correlation_id)Process a document in one call#
Authenticate with your API key and extract structured data — the SDK submits the document and polls for the result for you, so there's no upload-then-poll loop to write:
using Gemina.Sdk;
using Gemina.Sdk.Model;
using Newtonsoft.Json.Linq;
var client = new GeminaClient("YOUR_API_KEY");
var result = await client.ProcessDocumentAsync(
GeminaDocumentSource.FromFile("invoice.png"),
new List<UploadExtractionTypeEnum> { UploadExtractionTypeEnum.InvoiceHeaders });
var headers = result.Data.Extractions[0];
Console.WriteLine($"Status: {result.Status}");
Console.WriteLine($"Supplier: {(headers.Values["vendorName"] as JObject)?["value"]}");
Console.WriteLine($"Total: {(headers.Values["totalAmount"] as JObject)?["value"]}");
Console.WriteLine($"Date: {(headers.Values["invoiceDate"] as JObject)?["value"]}");Add extraction types to a stored document#
Run more extraction types on a document Gemina already stores — no re-upload. The convenience helper submits the request, polls to a terminal result, and returns the whole document with its existing and new extractions. The generated submit-only API accepts anAddExtractionsInDTO, returns aDocumentAddExtractionsOutDTO, and exposes the poll correlation ID when you want to control polling:
// Reuse a documentId from ProcessDocumentAsync, FileTag, or a document lookup.
var updated = await client.AddExtractionsAndWaitAsync(
documentId,
new List<UploadExtractionTypeEnum>
{
UploadExtractionTypeEnum.InvoiceLineItems
});
Console.WriteLine(updated.Data.Extractions.Count);
// Submit-only generated surface (poll PollCorrelationId yourself):
// DocumentAddExtractionsOutDTO submitted = await client.Documents
// .AddDocumentExtractionsAsync(documentId, new AddExtractionsInDTO(
// extractionTypes: new List<UploadExtractionTypeEnum>
// {
// UploadExtractionTypeEnum.InvoiceLineItems
// }));Each new type is billed like an upload and must allow the stored document's page count. A type already present is rejected; a purged source must be uploaded again. Adding a type also resets the shared retention date for the whole document to the account's current retention setting.
Search & analyze your documents#
Query everything you've processed with structured filters, semantic search, or both — and compute exact, database-backed aggregations without re-reading a document:
using Gemina.Sdk.Model;
var query = await client.Retrieval.RetrievalQueryAsync(new RetrievalQueryInDTO(
text: "cleaning services invoices",
topK: 5));
foreach (var item in query.Items)
{
Console.WriteLine($"{item.VendorName} — {item.TotalAmount} {item.Currency} " +
$"(issued {item.IssueDate:d}, document {item.DocumentId})");
}
var aggregate = await client.Retrieval.RetrievalAggregateAsync(new RetrievalAggregateInDTO(
metrics: new List<AggregateMetricDTO>
{
new AggregateMetricDTO(AggregateMetricDTO.FieldEnum.TotalAmount, AggregateMetricDTO.OpEnum.Sum),
},
groupBy: new List<RetrievalAggregateInDTO.GroupByEnum>
{
RetrievalAggregateInDTO.GroupByEnum.VendorName,
}));
foreach (var row in aggregate.Rows)
{
Console.WriteLine($"{row.Group["vendor_name"]}: {row.Values["sum_total_amount"].ActualInstance}");
}Chat with your documents#
Ask in natural language and get grounded answers with citations. Follow-up questions keep their conversation context. A conversation's live context expires after roughly 24h of inactivity — reset and resend to continue in a fresh one. The transcript itself is not lost; it stays readable in chat history below:
var reply = await client.Chat.ChatQueryAsync(new ChatQueryInDTO(
message: "How much did I spend on cleaning services this year?"));
Console.WriteLine(reply.Answer);
Console.WriteLine($"Confident: {reply.Confident}");
Console.WriteLine($"Citations: {string.Join(", ", reply.Citations ?? new List<string>())}");
var chat = client.Conversation();
await chat.SendAsync("How much did we spend on cleaning in 2020?");
var follow = await chat.SendAsync("And which vendor was most expensive?"); // remembers 2020 / cleaning
Console.WriteLine($"{follow.Answer} · session: {chat.SessionId}");
await chat.DeleteAsync(); // end it server-side (or chat.Reset() to just forget it locally)Chat history#
Past conversations are kept as sessions you can list, reread, and purge. Each one carries an auto-generated title, its turn count, the end-user it was scoped to, and the date your retention window will delete it:
var listing = await client.Chat.ListChatSessionsAsync(limit: 20);
foreach (var session in listing.Sessions)
Console.WriteLine($"{session.Title} · {session.TurnCount} turns");
var transcript = await client.Chat.GetChatSessionAsync(listing.Sessions[0].Id);
foreach (var msg in transcript.Messages)
Console.WriteLine($"[{msg.Role}] {msg.Content}");
await client.Chat.PurgeChatSessionAsync(listing.Sessions[0].Id);Purging permanently deletes the transcript and the server-side copy of its content — it cannot be undone. Purged sessions vanish from the list; ask for purged records to see their content-free stubs (title cleared, purge timestamp and reason set; timestamps, turn count, and end-user id survive). Transcripts also age out automatically under your account's data-retention setting.
Browser-safe session tokens#
Exchange your API key server-side for a short-lived, scoped token so a browser can search and chat without ever seeing the key. Tokens are read-and-chat scoped: they can query, aggregate, chat, read history, and end a conversation — but they can never purge one, which takes an API key or a console sign-in:
var session = await client.Sessions.MintRetrievalTokenAsync(new SessionTokenInDTO(
endUserId: "user-123",
ttlSeconds: 900));
// Send session.Token to your frontend; it expires in session.ExpiresIn seconds.
var sessionClient = GeminaClient.WithSessionToken(session.Token);Human verification#
Put a person in front of an extraction before it reaches your workflow. Mint a token scoped to that one extraction, render the drop-in reviewer (see Embed in your app), and read the result back afterwards. The corrections arrive as verifiedValues — the same shape as values, so switching your pipeline to human-verified data is a one-name change — alongside verifiedDiff, the list of what changed:
// Server-side. Authorize the end-user against this id FIRST — Gemina enforces
// the claim in the signed token, you decide who gets it.
var session = await client.Sessions.MintRetrievalTokenAsync(new SessionTokenInDTO(
extractionIds: new List<Guid> { extractionId }, // pins the token — up to 10
ttlSeconds: 900));
// Ship session.Token to the browser and render <GeminaVerification />.
// The source of truth. The widget's browser callback is best-effort.
var view = await client.Documents.GetDocumentExtractionAsync(extractionId);
if (view.Meta.Validated == true)
{
foreach (var change in view.VerifiedDiff)
{
// Status: corrected | added | removed
Console.WriteLine($"{change.Status} {change.Field}: {change.Original} -> {change.Verified}");
}
UseThis(view.VerifiedValues); // same shape as Values, corrections merged in
}
else
{
UseThis(view.Values); // nobody has reviewed it yet
}
// Only if you build your own review UI. One-shot: a second call is a 409.
var summary = await client.Documents.ValidateDocumentExtractionAsync(
extractionId,
new ExtractionValidationInDTO(data: correctedValues));Verification is one-shot per extraction; a second submission is rejected. Reading the extraction back is the source of truth — the widget's browser callback is best-effort, so if the network drops the response the verification is still recorded but the callback never fires.
Error handling#
Typed errors separate a terminal processing failure from a still-processing timeout you can resume:
using Gemina.Sdk.Client;
try
{
var result = await client.ProcessDocumentAsync(source, extractionTypes);
}
catch (GeminaProcessingException ex)
{
Console.WriteLine($"Processing failed: {ex.Result.Errors?.Count} error(s)");
}
catch (GeminaTimeoutException ex)
{
// Resume polling on your own schedule:
var result = await client.GetProcessingResultAsync(ex.CorrelationId);
}
catch (ApiException ex)
{
Console.WriteLine($"HTTP {ex.ErrorCode}: {ex.Message}");
}Install#
Add the official Gemina SDK to your project:
<dependency>
<groupId>co.gemina</groupId>
<artifactId>gemina-sdk</artifactId>
<version>0.15.0</version>
</dependency>Process a document in one call#
Authenticate with your API key and extract structured data — the SDK submits the document and polls for the result for you, so there's no upload-then-poll loop to write:
import java.io.File;
import java.util.Collections;
import java.util.Map;
import co.gemina.sdk.GeminaClient;
import co.gemina.sdk.GeminaDocumentSource;
import co.gemina.sdk.generated.model.DocumentProcessingResultOutDTO;
import co.gemina.sdk.generated.model.UploadExtractionTypeEnum;
public class Quickstart {
public static void main(String[] args) throws Exception {
GeminaClient client = new GeminaClient(System.getenv("GEMINA_API_KEY"));
DocumentProcessingResultOutDTO result = client.processDocument(
GeminaDocumentSource.fromFile(new File("invoice.pdf")),
Collections.singletonList(UploadExtractionTypeEnum.INVOICE_HEADERS));
System.out.println("status: " + result.getStatus());
// Each value field is a map: {"value": ..., "confidence": ..., "coordinates": ...}
Map<String, Object> values = result.getData().getExtractions().get(0).getValues();
System.out.println("vendor: " + field(values, "vendorName"));
System.out.println("total: " + field(values, "totalAmount"));
System.out.println("date: " + field(values, "invoiceDate"));
}
@SuppressWarnings("unchecked")
static Object field(Map<String, Object> values, String name) {
Map<String, Object> f = (Map<String, Object>) values.get(name);
return f == null ? null : f.get("value");
}
}Add extraction types to a stored document#
Run more extraction types on a document Gemina already stores — no re-upload. The convenience helper submits the request, polls to a terminal result, and returns the whole document with its existing and new extractions. The generated submit-only API accepts anAddExtractionsInDTO, returns aDocumentAddExtractionsOutDTO, and exposes the poll correlation ID when you want to control polling:
import co.gemina.sdk.ProcessDocumentOptions;
import co.gemina.sdk.generated.model.AddExtractionsInDTO;
import co.gemina.sdk.generated.model.DocumentAddExtractionsOutDTO;
// Reuse a documentId from processDocument, FileTag, or a document lookup.
DocumentProcessingResultOutDTO updated = client.addExtractionsAndWait(
documentId,
Collections.singletonList(UploadExtractionTypeEnum.INVOICE_LINE_ITEMS),
ProcessDocumentOptions.defaults());
System.out.println(updated.getData().getExtractions().size());
// Submit-only generated surface (poll getPollCorrelationId() yourself):
// DocumentAddExtractionsOutDTO submitted = client.documents()
// .addDocumentExtractions(documentId, new AddExtractionsInDTO()
// .extractionTypes(Collections.singletonList(
// UploadExtractionTypeEnum.INVOICE_LINE_ITEMS)));Each new type is billed like an upload and must allow the stored document's page count. A type already present is rejected; a purged source must be uploaded again. Adding a type also resets the shared retention date for the whole document to the account's current retention setting.
Search & analyze your documents#
Query everything you've processed with structured filters, semantic search, or both — and compute exact, database-backed aggregations without re-reading a document:
import co.gemina.sdk.generated.model.*;
RetrievalQueryOutDTO hits = client.retrieval().retrievalQuery(
new RetrievalQueryInDTO()
.text("cloud hosting invoices over 500 euro")
.filters(new RetrievalFiltersDTO().currency("EUR"))
.limit(10));
for (QueryResultItemDTO item : hits.getItems()) {
System.out.println(item.getVendorName() + " " + item.getTotalAmount()
+ " " + item.getIssueDate() + " (document " + item.getDocumentId() + ")");
}
RetrievalAggregateOutDTO totals = client.retrieval().retrievalAggregate(
new RetrievalAggregateInDTO()
.metrics(Collections.singletonList(new AggregateMetricDTO()
.op(AggregateMetricDTO.OpEnum.SUM)
.field(AggregateMetricDTO.FieldEnum.TOTAL_AMOUNT)))
.groupBy(Collections.singletonList(RetrievalAggregateInDTO.GroupByEnum.VENDOR_NAME)));
for (AggregateRowDTO row : totals.getRows()) {
System.out.println(row.getGroup() + " -> " + row.getValues());
}Chat with your documents#
Ask in natural language and get grounded answers with citations. Follow-up questions keep their conversation context. A conversation's live context expires after roughly 24h of inactivity — reset and resend to continue in a fresh one. The transcript itself is not lost; it stays readable in chat history below:
ChatQueryOutDTO reply = client.chat().chatQuery(
new ChatQueryInDTO().message("How much did we spend on hosting last quarter?"));
System.out.println(reply.getAnswer());
System.out.println("confident: " + reply.getConfident());
System.out.println("citations: " + reply.getCitations());
GeminaClient.GeminaChatConversation chat = client.conversation();
chat.send("How much did we spend on cleaning in 2020?");
ChatQueryOutDTO follow = chat.send("And which vendor was most expensive?"); // remembers 2020 / cleaning
System.out.println(follow.getAnswer() + " · session: " + chat.getSessionId());
chat.delete(); // end it server-side (or chat.reset() to just forget it locally)Chat history#
Past conversations are kept as sessions you can list, reread, and purge. Each one carries an auto-generated title, its turn count, the end-user it was scoped to, and the date your retention window will delete it:
ChatSessionListOutDTO listing = client.chat().listChatSessions(null, 20, null, null, null);
for (ChatSessionOutDTO session : listing.getSessions()) {
System.out.println(session.getTitle() + " · " + session.getTurnCount() + " turns");
}
UUID sessionId = listing.getSessions().get(0).getId();
ChatTranscriptOutDTO transcript = client.chat().getChatSession(sessionId, null, null, null);
for (ChatMessageOutDTO msg : transcript.getMessages()) {
System.out.println("[" + msg.getRole() + "] " + msg.getContent());
}
client.chat().purgeChatSession(sessionId);Purging permanently deletes the transcript and the server-side copy of its content — it cannot be undone. Purged sessions vanish from the list; ask for purged records to see their content-free stubs (title cleared, purge timestamp and reason set; timestamps, turn count, and end-user id survive). Transcripts also age out automatically under your account's data-retention setting.
Browser-safe session tokens#
Exchange your API key server-side for a short-lived, scoped token so a browser can search and chat without ever seeing the key. Tokens are read-and-chat scoped: they can query, aggregate, chat, read history, and end a conversation — but they can never purge one, which takes an API key or a console sign-in:
SessionTokenOutDTO session = client.sessions().mintRetrievalToken(
new SessionTokenInDTO()
.endUserId("user-123")
.ttlSeconds(900));
// Send session.getToken() to your frontend.
System.out.println(session.getToken() + " expires in " + session.getExpiresIn() + "s");Human verification#
Put a person in front of an extraction before it reaches your workflow. Mint a token scoped to that one extraction, render the drop-in reviewer (see Embed in your app), and read the result back afterwards. The corrections arrive as verifiedValues — the same shape as values, so switching your pipeline to human-verified data is a one-name change — alongside verifiedDiff, the list of what changed:
// Server-side. Authorize the end-user against this id FIRST — Gemina enforces
// the claim in the signed token, you decide who gets it.
SessionTokenOutDTO session = client.sessions().mintRetrievalToken(
new SessionTokenInDTO()
.extractionIds(Arrays.asList(extractionId)) // up to 10
.ttlSeconds(900));
// Ship session.getToken() to the browser and render <GeminaVerification />.
// The source of truth. The widget's browser callback is best-effort.
ExtractionPrimaryViewOutDTO view = client.documents()
.getDocumentExtraction(extractionId);
if (Boolean.TRUE.equals(view.getMeta().getValidated())) {
for (VerifiedDiffEntryOutDTO change : view.getVerifiedDiff()) {
// status: corrected | added | removed
System.out.println(change.getStatus() + " " + change.getField()
+ ": " + change.getOriginal() + " -> " + change.getVerified());
}
useThis(view.getVerifiedValues()); // same shape as values, corrected
} else {
useThis(view.getValues()); // nobody has reviewed it yet
}
// Only if you build your own review UI. One-shot: a second call is a 409.
ExtractionValidationResultOutDTO summary = client.documents()
.validateDocumentExtraction(
extractionId,
new ExtractionValidationInDTO().data(correctedValues));Verification is one-shot per extraction; a second submission is rejected. Reading the extraction back is the source of truth — the widget's browser callback is best-effort, so if the network drops the response the verification is still recorded but the callback never fires.
Error handling#
Typed errors separate a terminal processing failure from a still-processing timeout you can resume:
try {
DocumentProcessingResultOutDTO result = client.processDocument(source, types, options);
} catch (GeminaProcessingException e) {
System.err.println("processing failed: " + e.getResult().getErrors());
} catch (GeminaTimeoutException e) {
UUID correlationId = e.getCorrelationId(); // resume polling with this
DocumentProcessingResultOutDTO last = client.documents()
.getDocumentProcessingResultByCorrelationId(correlationId);
} catch (ApiException e) {
System.err.println("HTTP " + e.getCode() + ": " + e.getResponseBody());
}Process a document in one call#
Authenticate with your API key and extract structured data — the SDK submits the document and polls for the result for you, so there's no upload-then-poll loop to write:
<?php
require 'vendor/autoload.php';
use Gemina\Sdk\GeminaClient;
$client = new GeminaClient(getenv('GEMINA_API_KEY'));
$result = $client->processDocument('invoice.png', ['invoice_headers']);
echo 'Status: ', $result->getStatus(), PHP_EOL;
$extraction = $result->getData()->getExtractions()[0];
$values = $extraction->getValues();
// Each field is an object with ->value (plus ->coordinates and ->confidence when available)
echo 'Supplier: ', $values['vendorName']->value ?? 'n/a', PHP_EOL;
echo 'Total: ', $values['totalAmount']->value ?? 'n/a', PHP_EOL;
echo 'Date: ', $values['invoiceDate']->value ?? 'n/a', PHP_EOL;Add extraction types to a stored document#
Run more extraction types on a document Gemina already stores — no re-upload. The convenience helper submits the request, polls to a terminal result, and returns the whole document with its existing and new extractions. The generated submit-only API accepts anAddExtractionsInDTO, returns aDocumentAddExtractionsOutDTO, and exposes the poll correlation ID when you want to control polling:
// Reuse a document ID from processDocument(), FileTag, or a document lookup.
$updated = $client->addExtractionsAndWait(
$documentId,
['invoice_line_items'],
['includeCoordinates' => true],
);
echo count($updated->getData()->getExtractions()), PHP_EOL;
// Submit-only generated surface (poll getPollCorrelationId() yourself):
// $submitted = $client->documents()->addDocumentExtractions(
// $documentId,
// new AddExtractionsInDTO(['extraction_types' => ['invoice_line_items']]),
// ); // DocumentAddExtractionsOutDTOEach new type is billed like an upload and must allow the stored document's page count. A type already present is rejected; a purged source must be uploaded again. Adding a type also resets the shared retention date for the whole document to the account's current retention setting.
Search & analyze your documents#
Query everything you've processed with structured filters, semantic search, or both — and compute exact, database-backed aggregations without re-reading a document:
use Gemina\Sdk\Model\RetrievalQueryInDTO;
$out = $client->retrieval()->retrievalQuery(new RetrievalQueryInDTO([
'text' => 'cloud hosting invoices from June',
'top_k' => 10,
]));
foreach ($out->getItems() as $item) {
printf(
"%s | %s | %s %s\n",
$item->getDocumentId(),
$item->getVendorName(),
$item->getTotalAmount(),
$item->getCurrency(),
);
}
use Gemina\Sdk\Model\AggregateMetricDTO;
use Gemina\Sdk\Model\RetrievalAggregateInDTO;
$agg = $client->retrieval()->retrievalAggregate(new RetrievalAggregateInDTO([
'metrics' => [new AggregateMetricDTO(['op' => 'sum', 'field' => 'total_amount'])],
'group_by' => ['vendor_name'],
]));
foreach ($agg->getRows() as $row) {
print_r($row->getGroup());
print_r($row->getValues());
}Chat with your documents#
Ask in natural language and get grounded answers with citations. Follow-up questions keep their conversation context. A conversation's live context expires after roughly 24h of inactivity — reset and resend to continue in a fresh one. The transcript itself is not lost; it stays readable in chat history below:
use Gemina\Sdk\Model\ChatQueryInDTO;
$chat = $client->chat()->chatQuery(new ChatQueryInDTO([
'message' => 'How much did I spend on hosting in June, and with which vendor?',
]));
echo $chat->getAnswer(), PHP_EOL;
echo 'Confident: ', $chat->getConfident() ? 'yes' : 'no', PHP_EOL;
print_r($chat->getCitations());
$chat = $client->conversation();
$chat->send('How much did I spend on hosting in June, and with which vendor?');
$follow = $chat->send('And which month was cheapest?'); // remembers hosting / June
printf("%s · session: %s\n", $follow->getAnswer(), $chat->getSessionId());
$chat->delete(); // end it server-side (or $chat->reset() to just forget it locally)Chat history#
Past conversations are kept as sessions you can list, reread, and purge. Each one carries an auto-generated title, its turn count, the end-user it was scoped to, and the date your retention window will delete it:
$listing = $client->chat()->listChatSessions(0, 20);
foreach ($listing->getSessions() as $session) {
printf("%s · %d turns\n", $session->getTitle(), $session->getTurnCount());
}
$sessionId = $listing->getSessions()[0]->getId();
$transcript = $client->chat()->getChatSession($sessionId);
foreach ($transcript->getMessages() as $msg) {
printf("[%s] %s\n", $msg->getRole(), $msg->getContent());
}
$client->chat()->purgeChatSession($sessionId);Purging permanently deletes the transcript and the server-side copy of its content — it cannot be undone. Purged sessions vanish from the list; ask for purged records to see their content-free stubs (title cleared, purge timestamp and reason set; timestamps, turn count, and end-user id survive). Transcripts also age out automatically under your account's data-retention setting.
Browser-safe session tokens#
Exchange your API key server-side for a short-lived, scoped token so a browser can search and chat without ever seeing the key. Tokens are read-and-chat scoped: they can query, aggregate, chat, read history, and end a conversation — but they can never purge one, which takes an API key or a console sign-in:
use Gemina\Sdk\Model\SessionTokenInDTO;
$token = $client->sessions()->mintRetrievalToken(new SessionTokenInDTO([
'end_user_id' => 'user-42',
'ttl_seconds' => 900,
]));
echo $token->getToken(); // pass to the frontendHuman verification#
Put a person in front of an extraction before it reaches your workflow. Mint a token scoped to that one extraction, render the drop-in reviewer (see Embed in your app), and read the result back afterwards. The corrections arrive as verifiedValues — the same shape as values, so switching your pipeline to human-verified data is a one-name change — alongside verifiedDiff, the list of what changed:
use Gemina\Sdk\Model\SessionTokenInDTO;
use Gemina\Sdk\Model\ExtractionValidationInDTO;
// Server-side. Authorize the end-user against this id FIRST — Gemina enforces
// the claim in the signed token, you decide who gets it.
$token = $client->sessions()->mintRetrievalToken(new SessionTokenInDTO([
'extraction_ids' => [$extractionId], // pins the token — up to 10
'ttl_seconds' => 900,
]));
// Ship $token->getToken() to the browser and render <GeminaVerification />.
// The source of truth. The widget's browser callback is best-effort.
$view = $client->documents()->getDocumentExtraction($extractionId);
if ($view->getMeta()->getValidated()) {
foreach ($view->getVerifiedDiff() as $change) {
// status: corrected | added | removed
printf("%s %s: %s -> %s\n", $change->getStatus(), $change->getField(),
$change->getOriginal(), $change->getVerified());
}
useThis($view->getVerifiedValues()); // same shape as values, corrected
} else {
useThis($view->getValues()); // nobody has reviewed it yet
}
// Only if you build your own review UI. One-shot: a second call is a 409.
$summary = $client->documents()->validateDocumentExtraction($extractionId,
new ExtractionValidationInDTO(['data' => $correctedValues]));Verification is one-shot per extraction; a second submission is rejected. Reading the extraction back is the source of truth — the widget's browser callback is best-effort, so if the network drops the response the verification is still recorded but the callback never fires.
Error handling#
Typed errors separate a terminal processing failure from a still-processing timeout you can resume:
use Gemina\Sdk\ApiException;
use Gemina\Sdk\GeminaProcessingException;
use Gemina\Sdk\GeminaTimeoutException;
try {
$result = $client->processDocument('invoice.png', ['invoice_headers']);
} catch (GeminaProcessingException $e) {
// Terminal "failed" — the full result is attached
print_r($e->getResult()->getErrors());
} catch (GeminaTimeoutException $e) {
echo 'Still processing: ', $e->getCorrelationId(), PHP_EOL;
} catch (ApiException $e) {
// Transport/HTTP errors from the generated client pass through unwrapped
echo $e->getCode(), ': ', $e->getResponseBody(), PHP_EOL;
}Raw HTTP API#
No SDK, or on an unsupported language? Every endpoint is a plain REST call. Search, aggregate, chat & chat history over your processed documents is documented right here in curl; for the upload-and-extract quick start in your language, open the picker below. Response shapes are in Response Fields; what a failure looks like is in Errors.
With Document Intelligence (opt-in per account), every successful extraction is indexed into a searchable layer — query your whole collection with exact filters, natural language, or both, and compute exact totals without re-reading a single document. Every SDK wraps this — see the “Search & analyze”, “Chat”, “Chat history”, and “Session tokens” tabs above; the requests below are the underlying REST API.
Search your documents#
One endpoint, three modes: structured (exact filters), semantic (meaning), and hybrid (both, fused — the best default for free text):
# Hybrid search: keywords + meaning (best default for free text)
curl -X POST https://api.gemina.co/api/v1/retrieval/query \
-H "X-API-Key: your-api-key" \
-H "Content-Type: application/json" \
-d '{
"mode": "hybrid",
"text": "the credit note about the server outage",
"topK": 10,
"filters": { "issueDateFrom": "2026-01-01" }
}'Exact aggregations#
Sums, averages and counts are computed in the database — never estimated by an AI model. Amounts in different currencies are never mixed into one total:
# Exact totals per vendor, computed in the database (never estimated by AI)
curl -X POST https://api.gemina.co/api/v1/retrieval/aggregate \
-H "X-API-Key: your-api-key" \
-H "Content-Type: application/json" \
-d '{
"metrics": [{ "op": "sum", "field": "total_amount" }, { "op": "count" }],
"groupBy": ["vendor_name", "currency"],
"filters": { "issueDateFrom": "2026-01-01", "issueDateTo": "2026-03-31" }
}'Chat with your documents#
Ask in natural language; Gemina routes the question to the right engine and answers grounded in your documents, with citations:
# Grounded natural-language Q&A over your document collection
curl -X POST https://api.gemina.co/api/v1/chat/query \
-H "X-API-Key: your-api-key" \
-H "Content-Type: application/json" \
-d '{ "message": "How much did we spend at Acme Catering this quarter?" }'{
"answer": "You spent a total of 18,450 ILS at Acme Catering this quarter, across 12 invoices.",
"citations": ["5f2b7c1e-2b6b-4f0e-9a1c-4d3f2e1a0b9c"],
"intent": "aggregation",
"confident": true
}Chat history#
Past conversations are kept as sessions you can list, reread, and purge. Each session carries a short auto-generated title, its turnCount, its endUserId (null = a tenant-wide chat over all documents), and purgeAt — when your data-retention window will delete it (null = kept always):
# List past sessions (newest activity first by default)
curl https://api.gemina.co/api/v1/chat/sessions?limit=20 \
-H "X-API-Key: your-api-key"
# -> { "count": 3, "sessions": [ { "id": "c1a4e9d2-...", "title": "Cleaning spend 2020",
# "turnCount": 4, "lastActivityAt": "...", "purgeAt": "...", "purgedAt": null, ... } ] }
# Read one session's transcript (paged with skip/limit)
curl https://api.gemina.co/api/v1/chat/sessions/c1a4e9d2-6b3f-4a71-9e02-... \
-H "X-API-Key: your-api-key"
# -> { "session": {...}, "count": 8, "messages": [ { "role": "user", "content": "...",
# "turnIndex": 0, ... }, { "role": "assistant", "content": "...", "confident": true,
# "citations": [...], ... } ] }
# Permanently purge a session — transcript and server-side content, 204 on success
curl -X DELETE https://api.gemina.co/api/v1/chat/sessions/c1a4e9d2-6b3f-4a71-9e02-.../purge \
-H "X-API-Key: your-api-key"Purge is not “New chat”. DELETE /chat/sessions/{sessionId} only ends the live context — the transcript stays in history; DELETE /chat/sessions/{sessionId}/purge permanently deletes the transcript and the server-side copy of its content, and cannot be undone. Purged sessions vanish from the list; pass with_purged=true to see their content-free stubs (title cleared, purgedAt/purgeReason set). Session tokens can list and read history within their pinned scope, but can never purge — that takes an API key, or a console sign-in on the /purge/user variant.
For end-user-facing apps, exchange your API key server-side for a short-lived, scoped session token (POST /api/v1/sessions/token) — the browser can then query and chat safely without ever seeing your API key. The same capabilities are available to AI agents as MCP tools (query_documents, aggregate_documents, index_document).
Errors#
An error uses the same envelope as a success: status is failed, data is null, and the machine-readable detail is one entry in errors. Below is a real response — an unrecognized API key:
{
"status": "failed",
"meta": { "externalId": null, "correlationId": null, "userId": null },
"data": null,
"errors": [
{
"error_code": "ACCESS_DENIED_ERROR",
"description": "Access Denied: API Key not found"
}
],
"createdAt": null,
"createdAtTimestamp": null,
"servedAt": "2026-08-11T17:01:07.138498",
"servedAtTimestamp": 1786467667.138507
}Branch on errors[0].error_code, not on description: the code is stable, the description is written for humans and changes freely. The code keeps its snake_case key even though the envelope around it is camelCase. On the error path meta is null, or carries only the identifiers your request supplied — the richer success meta is not available.
401 UNAUTHORIZED_ERROR— No credential was sent, or a session token is invalid or expired. Mint a new token.403 ACCESS_DENIED_ERROR— The API key is unknown, revoked or expired, or the account is inactive.403 DOCUMENT_INTELLIGENCE_NOT_IN_PLAN— Search, aggregation and chat are not enabled on this plan.404 CHAT_SESSION_NOT_FOUND— The session is unknown, expired after 24h idle, or belongs to someone else. Retry without asessionIdto start a new conversation.413 REQUEST_ENTITY_TOO_LARGE_ERROR— The file is over 10 MB.415 UNSUPPORTED_MEDIA_TYPE_ERROR— The format is not one we accept.422 UNPROCESSABLE_ERROR— The request is malformed: an incompatible option combination,custom_templatewithout atemplate_id, a file under 1 KB, a semantic query with notext, or a question chat could not process.422 DOCUMENT_MAX_PAGES_EXCEEDED_ERROR— The PDF is longer than the page limit for the extraction types you asked for.429— You hit a rate limit or ran out of credit, anderror_codesays which. Back off on theRetry-Afterresponse header when one is sent. Burst limits (RETRIEVAL_RATE_LIMIT_EXCEEDED,FILETAG_RATE_LIMIT_EXCEEDED) clear in a second; quota and credit exhaustion (QUOTA_EXCEEDED,SPEND_LIMIT_EXCEEDED,CREDIT_EXHAUSTED,INSUFFICIENT_CREDITS,CHAT_QUOTA_EXCEEDED,FILETAG_QUOTA_EXHAUSTED) will not clear by retrying.502 BAD_GATEWAY_ERROR— The chat backend was unreachable. Retry shortly.
Authentication fails in two different ways. Sending no credential is a 401; sending an API key we don't recognize is a 403, not a 401. Only the session-token path answers 401 for a bad credential — so a client that refreshes on 401 alone will loop forever against a revoked API key.
Size & page limits#
Documents are 1 KB to 10 MB. The page ceiling is per extraction type — ask for several types in one request and the most permissive ceiling applies:
custom_template— 30 pagesocr— 15 pagesinvoice_headers,invoice_line_items— 10 pages
Format, size and page checks all run before processing starts, so a rejected upload costs no credits. Single images aren't paged, so they're measured instead: every upload is normalized to 1240 px wide and each 2420 px of height counts as one page equivalent. A long stitched screenshot can exceed the ceiling that way — unlike a PDF it's caught during processing, so it comes back as a failed extraction rather than a rejected upload.
Quick Start#
Install the required package and set up your environment:
pip install requestsimport os
BASE_URL = os.getenv("GEMINA_BASE_URL", "https://api.gemina.co")
API_KEY = os.getenv("GEMINA_API_KEY", "")
HEADERS = {"X-API-Key": API_KEY}Authentication#
All API requests require authentication via the X-API-Key header:
import requests
headers = {"X-API-Key": "your-api-key"}
response = requests.get(
"https://api.gemina.co/api/v1/documents/",
headers=headers
)Upload Document#
Upload a document for extraction using multipart form data:
import requests
url = "https://api.gemina.co/api/v1/documents/uploads"
headers = {"X-API-Key": "your-api-key"}
form_data = [
("extraction_types", "invoice_headers"),
("extraction_types", "invoice_line_items"),
("external_id", "inv-2025-0001"),
("model_type", "invictus"),
]
files = {
"file": ("invoice.pdf", open("./invoice.pdf", "rb"), "application/pdf")
}
response = requests.post(url, headers=headers, data=form_data, files=files)
result = response.json()
print(result)Response Format#
Successful extractions return structured JSON with field values and confidence scores:
{
"status": "success",
"meta": {
"documentId": "9860df92-64fe-4b53-9663-5c11b38a3051",
"externalId": "inv-2026-0001",
"filename": "invoice.pdf"
},
"data": {
"extractions": [
{
"meta": {"extractionType": "invoice_headers"},
"status": "success",
"values": {
"vendorName": {"value": "Acme Beverages Ltd.", "confidence": "high"},
"invoiceNumber": {"value": "IL-2026-04812", "confidence": "high"},
"invoiceDate": {"value": "2026-05-12", "confidence": "high"},
"currency": {"value": "ILS", "confidence": "high"},
"grossSubtotalAmount": {"value": 7663.63, "confidence": "high"},
"discountAmount": {"value": 229.91, "confidence": "high"},
"discountPercentage": {"value": 3.0, "confidence": "high"},
"roundingAmount": {"value": -0.18, "confidence": "high"},
"subtotalAmount": {"value": 7433.90, "confidence": "high"},
"taxes": [
{"type": "vat", "name": "VAT 18%", "rate": 18.0, "amount": 1338.10, "confidence": "high"}
],
"totalAmount": {"value": 8772.00, "confidence": "high"},
"expenseType": {"value": "cost_of_goods_sold", "confidence": "high"}
},
"errors": []
},
{
"meta": {"extractionType": "invoice_line_items"},
"status": "success",
"values": {
"line_items": [
{
"lineNumber": 1,
"description": "Premium 6-pack 330ml beer cans",
"itemCode": "BV-330-6",
"quantity": 12.0,
"listPrice": 65.00,
"unitPrice": 58.50,
"discountAmount": 6.50,
"discountPercentage": 10.0,
"taxRate": 18.0,
"packagingAmount": 0.30,
"depositAmount": 1.20,
"unitsPerPackage": 6,
"packageQuantity": 2.0,
"lineTotal": 703.50
},
{
"lineNumber": 2,
"description": "Olive oil 1L",
"itemCode": "OO-1L",
"quantity": 0.5,
"listPrice": null,
"unitPrice": 45.00,
"discountAmount": null,
"discountPercentage": null,
"taxRate": 18.0,
"packagingAmount": null,
"depositAmount": null,
"unitsPerPackage": null,
"packageQuantity": null,
"lineTotal": 22.50
}
],
"total_lines": 2
},
"errors": []
}
]
},
"errors": []
}Quick Start#
Install the required packages:
npm install axios form-dataimport axios from "axios";
const BASE_URL = process.env.GEMINA_BASE_URL || "https://api.gemina.co";
const API_KEY = process.env.GEMINA_API_KEY || "";
const client = axios.create({
baseURL: BASE_URL,
headers: { "X-API-Key": API_KEY },
timeout: 90_000,
});Authentication#
All API requests require authentication via the X-API-Key header:
import axios from "axios";
const client = axios.create({
baseURL: "https://api.gemina.co",
headers: { "X-API-Key": "your-api-key" },
});
const response = await client.get("/api/v1/documents/");Upload Document#
Upload a document for extraction using form data:
import fs from "fs";
import FormData from "form-data";
import axios from "axios";
const form = new FormData();
form.append("extraction_types", "invoice_headers");
form.append("extraction_types", "invoice_line_items");
form.append("external_id", "inv-2025-0001");
form.append("model_type", "invictus");
form.append("file", fs.createReadStream("./invoice.pdf"));
const response = await axios.post(
"https://api.gemina.co/api/v1/documents/uploads",
form,
{
headers: {
"X-API-Key": "your-api-key",
...form.getHeaders(),
},
}
);
console.log(response.data);Response Format#
Successful extractions return structured JSON with field values and confidence scores:
{
"status": "success",
"meta": {
"documentId": "9860df92-64fe-4b53-9663-5c11b38a3051",
"externalId": "inv-2026-0001",
"filename": "invoice.pdf"
},
"data": {
"extractions": [
{
"meta": {"extractionType": "invoice_headers"},
"status": "success",
"values": {
"vendorName": {"value": "Acme Beverages Ltd.", "confidence": "high"},
"invoiceNumber": {"value": "IL-2026-04812", "confidence": "high"},
"invoiceDate": {"value": "2026-05-12", "confidence": "high"},
"currency": {"value": "ILS", "confidence": "high"},
"grossSubtotalAmount": {"value": 7663.63, "confidence": "high"},
"discountAmount": {"value": 229.91, "confidence": "high"},
"discountPercentage": {"value": 3.0, "confidence": "high"},
"roundingAmount": {"value": -0.18, "confidence": "high"},
"subtotalAmount": {"value": 7433.90, "confidence": "high"},
"taxes": [
{"type": "vat", "name": "VAT 18%", "rate": 18.0, "amount": 1338.10, "confidence": "high"}
],
"totalAmount": {"value": 8772.00, "confidence": "high"},
"expenseType": {"value": "cost_of_goods_sold", "confidence": "high"}
},
"errors": []
},
{
"meta": {"extractionType": "invoice_line_items"},
"status": "success",
"values": {
"line_items": [
{
"lineNumber": 1,
"description": "Premium 6-pack 330ml beer cans",
"itemCode": "BV-330-6",
"quantity": 12.0,
"listPrice": 65.00,
"unitPrice": 58.50,
"discountAmount": 6.50,
"discountPercentage": 10.0,
"taxRate": 18.0,
"packagingAmount": 0.30,
"depositAmount": 1.20,
"unitsPerPackage": 6,
"packageQuantity": 2.0,
"lineTotal": 703.50
},
{
"lineNumber": 2,
"description": "Olive oil 1L",
"itemCode": "OO-1L",
"quantity": 0.5,
"listPrice": null,
"unitPrice": 45.00,
"discountAmount": null,
"discountPercentage": null,
"taxRate": 18.0,
"packagingAmount": null,
"depositAmount": null,
"unitsPerPackage": null,
"packageQuantity": null,
"lineTotal": 22.50
}
],
"total_lines": 2
},
"errors": []
}
]
},
"errors": []
}Quick Start#
Add OkHttp to your Maven or Gradle project:
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>4.12.0</version>
</dependency>String BASE_URL = System.getenv("GEMINA_BASE_URL") != null
? System.getenv("GEMINA_BASE_URL") : "https://api.gemina.co";
String API_KEY = System.getenv("GEMINA_API_KEY");
OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(90, TimeUnit.SECONDS)
.readTimeout(90, TimeUnit.SECONDS)
.build();Authentication#
All API requests require authentication via the X-API-Key header:
Request request = new Request.Builder()
.url("https://api.gemina.co/api/v1/documents/")
.header("X-API-Key", "your-api-key")
.get()
.build();
Response response = client.newCall(request).execute();Upload Document#
Upload a document for extraction using multipart form data:
File invoiceFile = new File("invoice.pdf");
RequestBody requestBody = new MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("extraction_types", "invoice_headers")
.addFormDataPart("extraction_types", "invoice_line_items")
.addFormDataPart("external_id", "inv-2025-0001")
.addFormDataPart("model_type", "invictus")
.addFormDataPart("file", invoiceFile.getName(),
RequestBody.create(invoiceFile, MediaType.parse("application/pdf")))
.build();
Request request = new Request.Builder()
.url("https://api.gemina.co/api/v1/documents/uploads")
.header("X-API-Key", "your-api-key")
.post(requestBody)
.build();
Response response = client.newCall(request).execute();
System.out.println(response.body().string());Response Format#
Successful extractions return structured JSON with field values and confidence scores:
{
"status": "success",
"meta": {
"documentId": "9860df92-64fe-4b53-9663-5c11b38a3051",
"externalId": "inv-2026-0001",
"filename": "invoice.pdf"
},
"data": {
"extractions": [
{
"meta": {"extractionType": "invoice_headers"},
"status": "success",
"values": {
"vendorName": {"value": "Acme Beverages Ltd.", "confidence": "high"},
"invoiceNumber": {"value": "IL-2026-04812", "confidence": "high"},
"invoiceDate": {"value": "2026-05-12", "confidence": "high"},
"currency": {"value": "ILS", "confidence": "high"},
"grossSubtotalAmount": {"value": 7663.63, "confidence": "high"},
"discountAmount": {"value": 229.91, "confidence": "high"},
"discountPercentage": {"value": 3.0, "confidence": "high"},
"roundingAmount": {"value": -0.18, "confidence": "high"},
"subtotalAmount": {"value": 7433.90, "confidence": "high"},
"taxes": [
{"type": "vat", "name": "VAT 18%", "rate": 18.0, "amount": 1338.10, "confidence": "high"}
],
"totalAmount": {"value": 8772.00, "confidence": "high"},
"expenseType": {"value": "cost_of_goods_sold", "confidence": "high"}
},
"errors": []
},
{
"meta": {"extractionType": "invoice_line_items"},
"status": "success",
"values": {
"line_items": [
{
"lineNumber": 1,
"description": "Premium 6-pack 330ml beer cans",
"itemCode": "BV-330-6",
"quantity": 12.0,
"listPrice": 65.00,
"unitPrice": 58.50,
"discountAmount": 6.50,
"discountPercentage": 10.0,
"taxRate": 18.0,
"packagingAmount": 0.30,
"depositAmount": 1.20,
"unitsPerPackage": 6,
"packageQuantity": 2.0,
"lineTotal": 703.50
},
{
"lineNumber": 2,
"description": "Olive oil 1L",
"itemCode": "OO-1L",
"quantity": 0.5,
"listPrice": null,
"unitPrice": 45.00,
"discountAmount": null,
"discountPercentage": null,
"taxRate": 18.0,
"packagingAmount": null,
"depositAmount": null,
"unitsPerPackage": null,
"packageQuantity": null,
"lineTotal": 22.50
}
],
"total_lines": 2
},
"errors": []
}
]
},
"errors": []
}Quick Start#
Create an HttpClient instance for API requests:
using System.Net.Http;
var baseUrl = Environment.GetEnvironmentVariable("GEMINA_BASE_URL")
?? "https://api.gemina.co";
var apiKey = Environment.GetEnvironmentVariable("GEMINA_API_KEY") ?? "";
var client = new HttpClient
{
BaseAddress = new Uri(baseUrl),
Timeout = TimeSpan.FromSeconds(90)
};
client.DefaultRequestHeaders.Add("X-API-Key", apiKey);Authentication#
All API requests require authentication via the X-API-Key header:
using var client = new HttpClient();
client.DefaultRequestHeaders.Add("X-API-Key", "your-api-key");
var response = await client.GetAsync(
"https://api.gemina.co/api/v1/documents/"
);Upload Document#
Upload a document for extraction using multipart form data:
using var form = new MultipartFormDataContent();
form.Add(new StringContent("invoice_headers"), "extraction_types");
form.Add(new StringContent("invoice_line_items"), "extraction_types");
form.Add(new StringContent("inv-2025-0001"), "external_id");
form.Add(new StringContent("invictus"), "model_type");
var fileBytes = await File.ReadAllBytesAsync("invoice.pdf");
var fileContent = new ByteArrayContent(fileBytes);
fileContent.Headers.ContentType =
new MediaTypeHeaderValue("application/pdf");
form.Add(fileContent, "file", "invoice.pdf");
var response = await client.PostAsync(
"https://api.gemina.co/api/v1/documents/uploads",
form
);
var result = await response.Content.ReadAsStringAsync();
Console.WriteLine(result);Response Format#
Successful extractions return structured JSON with field values and confidence scores:
{
"status": "success",
"meta": {
"documentId": "9860df92-64fe-4b53-9663-5c11b38a3051",
"externalId": "inv-2026-0001",
"filename": "invoice.pdf"
},
"data": {
"extractions": [
{
"meta": {"extractionType": "invoice_headers"},
"status": "success",
"values": {
"vendorName": {"value": "Acme Beverages Ltd.", "confidence": "high"},
"invoiceNumber": {"value": "IL-2026-04812", "confidence": "high"},
"invoiceDate": {"value": "2026-05-12", "confidence": "high"},
"currency": {"value": "ILS", "confidence": "high"},
"grossSubtotalAmount": {"value": 7663.63, "confidence": "high"},
"discountAmount": {"value": 229.91, "confidence": "high"},
"discountPercentage": {"value": 3.0, "confidence": "high"},
"roundingAmount": {"value": -0.18, "confidence": "high"},
"subtotalAmount": {"value": 7433.90, "confidence": "high"},
"taxes": [
{"type": "vat", "name": "VAT 18%", "rate": 18.0, "amount": 1338.10, "confidence": "high"}
],
"totalAmount": {"value": 8772.00, "confidence": "high"},
"expenseType": {"value": "cost_of_goods_sold", "confidence": "high"}
},
"errors": []
},
{
"meta": {"extractionType": "invoice_line_items"},
"status": "success",
"values": {
"line_items": [
{
"lineNumber": 1,
"description": "Premium 6-pack 330ml beer cans",
"itemCode": "BV-330-6",
"quantity": 12.0,
"listPrice": 65.00,
"unitPrice": 58.50,
"discountAmount": 6.50,
"discountPercentage": 10.0,
"taxRate": 18.0,
"packagingAmount": 0.30,
"depositAmount": 1.20,
"unitsPerPackage": 6,
"packageQuantity": 2.0,
"lineTotal": 703.50
},
{
"lineNumber": 2,
"description": "Olive oil 1L",
"itemCode": "OO-1L",
"quantity": 0.5,
"listPrice": null,
"unitPrice": 45.00,
"discountAmount": null,
"discountPercentage": null,
"taxRate": 18.0,
"packagingAmount": null,
"depositAmount": null,
"unitsPerPackage": null,
"packageQuantity": null,
"lineTotal": 22.50
}
],
"total_lines": 2
},
"errors": []
}
]
},
"errors": []
}Quick Start#
Configure your API credentials:
<?php
define('GEMINA_BASE_URL', getenv('GEMINA_BASE_URL') ?: 'https://api.gemina.co');
define('GEMINA_API_KEY', getenv('GEMINA_API_KEY') ?: '');
if (empty(GEMINA_API_KEY)) {
throw new Exception('Set GEMINA_API_KEY environment variable');
}Authentication#
All API requests require authentication via the X-API-Key header:
<?php
$ch = curl_init('https://api.gemina.co/api/v1/documents/');
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'X-API-Key: your-api-key',
],
]);
$response = curl_exec($ch);
curl_close($ch);Upload Document#
Upload a document for extraction using cURL:
<?php
$url = 'https://api.gemina.co/api/v1/documents/uploads';
$cfile = new CURLFile('./invoice.pdf', 'application/pdf', 'invoice.pdf');
$postData = [
'extraction_types[0]' => 'invoice_headers',
'extraction_types[1]' => 'invoice_line_items',
'external_id' => 'inv-2025-0001',
'model_type' => 'invictus',
'file' => $cfile,
];
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $postData,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'X-API-Key: your-api-key',
],
]);
$response = curl_exec($ch);
curl_close($ch);
echo $response;Response Format#
Successful extractions return structured JSON with field values and confidence scores:
{
"status": "success",
"meta": {
"documentId": "9860df92-64fe-4b53-9663-5c11b38a3051",
"externalId": "inv-2026-0001",
"filename": "invoice.pdf"
},
"data": {
"extractions": [
{
"meta": {"extractionType": "invoice_headers"},
"status": "success",
"values": {
"vendorName": {"value": "Acme Beverages Ltd.", "confidence": "high"},
"invoiceNumber": {"value": "IL-2026-04812", "confidence": "high"},
"invoiceDate": {"value": "2026-05-12", "confidence": "high"},
"currency": {"value": "ILS", "confidence": "high"},
"grossSubtotalAmount": {"value": 7663.63, "confidence": "high"},
"discountAmount": {"value": 229.91, "confidence": "high"},
"discountPercentage": {"value": 3.0, "confidence": "high"},
"roundingAmount": {"value": -0.18, "confidence": "high"},
"subtotalAmount": {"value": 7433.90, "confidence": "high"},
"taxes": [
{"type": "vat", "name": "VAT 18%", "rate": 18.0, "amount": 1338.10, "confidence": "high"}
],
"totalAmount": {"value": 8772.00, "confidence": "high"},
"expenseType": {"value": "cost_of_goods_sold", "confidence": "high"}
},
"errors": []
},
{
"meta": {"extractionType": "invoice_line_items"},
"status": "success",
"values": {
"line_items": [
{
"lineNumber": 1,
"description": "Premium 6-pack 330ml beer cans",
"itemCode": "BV-330-6",
"quantity": 12.0,
"listPrice": 65.00,
"unitPrice": 58.50,
"discountAmount": 6.50,
"discountPercentage": 10.0,
"taxRate": 18.0,
"packagingAmount": 0.30,
"depositAmount": 1.20,
"unitsPerPackage": 6,
"packageQuantity": 2.0,
"lineTotal": 703.50
},
{
"lineNumber": 2,
"description": "Olive oil 1L",
"itemCode": "OO-1L",
"quantity": 0.5,
"listPrice": null,
"unitPrice": 45.00,
"discountAmount": null,
"discountPercentage": null,
"taxRate": 18.0,
"packagingAmount": null,
"depositAmount": null,
"unitsPerPackage": null,
"packageQuantity": null,
"lineTotal": 22.50
}
],
"total_lines": 2
},
"errors": []
}
]
},
"errors": []
}Embed in your app#
Ship two experiences without building either. @gemina/elements gives you a drop-in React chat component (<GeminaChat>) and a human review-and-correct step (<GeminaVerification>), both behind a security-hardened token manager — citations, confidence handling, and RTL support included, without ever exposing your API key to the browser.
Install#
npm i @gemina/elements @gemina/sdk reactMint session tokens (Next.js App Router)#
The API key stays on your server. This route mints a short-lived, scoped token for the browser — drop it in as-is:
// Next.js (App Router) — app/api/gemina-session/route.ts
import { NextResponse } from "next/server";
import { GeminaClient } from "@gemina/sdk";
const gemina = new GeminaClient(process.env.GEMINA_API_KEY!);
export async function POST(request: Request) {
const user = await requireYourAppAuth(request); // your session check
const minted = await gemina.sessions.mintRetrievalToken({
sessionTokenInDTO: { endUserId: user.id, ttlSeconds: 900 },
});
return NextResponse.json({ token: minted.token, expiresIn: minted.expiresIn });
}Render the chat#
Point the token manager at that route and render the component — conversation memory and the session token are managed for you. Give it your own voice with title (header text — your brand or assistant persona; tint the bar with --gemina-chat-header-bg) and intro (what the assistant can see, shown in the empty conversation until the first message):
import { GeminaTokenManager } from "@gemina/elements/token-manager";
const tokenManager = new GeminaTokenManager({
// Points at YOUR backend — see the mint endpoint below.
fetchToken: async () => {
const res = await fetch("/api/gemina-session", { method: "POST" });
if (!res.ok) throw new Error("Failed to mint Gemina session token");
return res.json(); // { token, expiresIn }
},
// Optional: seconds before expiry to refresh (default 60).
refreshSkewSeconds: 60,
});
import { GeminaChat } from "@gemina/elements";
<GeminaChat
tokenManager={tokenManager}
// Header text — your brand or assistant persona. When set, the header
// is always visible; tint the bar with --gemina-chat-header-bg.
title="Acme Invoices"
// Centered in the empty conversation until the first message. Newlines
// split it into separately spaced paragraphs.
intro="Answers come from your indexed document data."
onCitationClick={(documentId) => openDocumentViewer(documentId)}
/>;Mint an extraction-scoped token#
Verification gets its own mint route. The token is pinned to a single extraction, so a curious end-user with developer tools can't read anything else in your account. Gemina enforces the pin; your endpoint decides who is allowed to ask for it — the extraction id arrives from the browser, so authorize it against your own user before minting:
// Next.js (App Router) — app/api/gemina-verify-session/route.ts
import { NextResponse } from "next/server";
import { GeminaClient } from "@gemina/sdk";
const gemina = new GeminaClient(process.env.GEMINA_API_KEY!);
export async function POST(request: Request) {
const user = await requireYourAppAuth(request); // your session check
const { extractionId } = await request.json();
// YOU decide who may see this extraction. Gemina enforces the claim in the
// signed token; it cannot know whether this user is entitled to that id.
if (!(await userMayVerify(user, extractionId))) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}
const minted = await gemina.sessions.mintRetrievalToken({
sessionTokenInDTO: {
extractionIds: [extractionId], // pins the token — up to 10
ttlSeconds: 900,
},
});
return NextResponse.json({ token: minted.token, expiresIn: minted.expiresIn });
}Render the reviewer#
The document sits next to every extracted field as an editable input. The reviewer corrects what's wrong and submits once. Run the extraction with evaluation enabled and each field also carries a confidence score, with a switch that hides everything already scored high — on a 169-row invoice that is 169 rows down to 7:
import { useMemo } from "react";
import { GeminaVerification } from "@gemina/elements/verification";
import { GeminaTokenManager } from "@gemina/elements/token-manager";
function VerifyStep({ extractionId }: { extractionId: string }) {
// Stable per extraction — never construct the manager inline in JSX.
const tokenManager = useMemo(
() =>
new GeminaTokenManager({
fetchToken: async () => {
const res = await fetch("/api/gemina-verify-session", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ extractionId }),
});
if (!res.ok) throw new Error("Failed to mint Gemina session token");
return res.json(); // { token, expiresIn }
},
}),
[extractionId]
);
return (
<GeminaVerification
extractionId={extractionId}
tokenManager={tokenManager}
// Fires the moment the reviewer submits — good for closing the step.
// Best-effort: if the network drops the response the verification is
// still recorded, so read the extraction back for anything that matters.
onComplete={({ correctedValues, summary }) => {
closeReviewStep();
}}
onError={(reason) => reportToYourMonitoring(reason)}
/>
);
}Then read the extraction back for the corrections — verifiedValues and verifiedDiff, shown per language under Human verification in the SDK guides. Submission is final: an extraction can be verified once.
Response Fields#
Raw HTTP responses use the field names below exactly, including snake_case names such as line_items and confidence_reasons. SDKs may expose language-specific property names. Array<T> means an array of T; Map<string, T> means an object with dynamic string keys. JSON value means a string, number, boolean, object, array, or null.
Read data.extractions[] and select each payload by meta.extractionType. values contains the extracted result; verifiedValues contains the reviewer-finalized result with the same shape. Both can be null. A document can contain multiple extraction results with different statuses.
Field<T> is an object with value (T or null), coordinates (Coordinates or null), and confidence (high, medium, low, or null). A field marked Field<T> | null can also have a null envelope. Enum options apply to value for wrapped fields, or to each element for arrays. Unconstrained strings and numbers have no fixed option list.
Missing scalar values are generally null; collections can be empty. Do not treat null as zero or assume every property is populated. Coordinates are available only when requested and found. Creation timestamps can be null. Confidence levels are categorical, not percentages.
The document extraction types documented below are accepted by document uploads. FileTag uses its own endpoint and response reference: https://www.gemina.co/docs/filetag. The metadata options also include filetag because it is a backend extraction type.
Response envelope#
| Field | JSON type | Description and allowed values |
|---|---|---|
servedAt | string (date-time) | Time this response was served (UTC). |
servedAtTimestamp | number | Time this response was served as Unix seconds. |
createdAt | string (date-time) | null | Creation time, when available. |
createdAtTimestamp | number | null | Creation time as Unix seconds, when available. |
status | string | Processing outcome. See the status meanings in Interpretation and calculations. |
meta | Document metadata | Identifiers and processing metadata for this response. |
data | Document data | null | Response payload; null when no payload is available. |
errors | Array<Map<string, JSON value>> | Error objects supplied by the API. Properties vary by error; may include error_code, description, detail, and status_code. |
invoice_headers fields#
| Field | JSON type | Description and allowed values |
|---|---|---|
vendorName | Field<string> | null | Supplier name |
vendorTaxId | Field<string> | null | Tax/VAT ID if present |
vendorAddress | Field<string> | null | Supplier address |
vendorEmail | Field<string> | null | Supplier email address |
vendorPhone | Field<string> | null | Supplier phone number |
buyerName | Field<string> | null | Buyer name |
buyerTaxId | Field<string> | null | Buyer Tax/VAT ID if present |
buyerAddress | Field<string> | null | Buyer address |
buyerEmail | Field<string> | null | Buyer email address |
buyerPhone | Field<string> | null | Buyer phone number |
invoiceNumber | Field<string> | null | Invoice identifier as printed; kept as a string to preserve leading zeros. |
invoiceDate | Field<string> | null | ISO date if possible |
dueDate | Field<string> | null | ISO date if present |
purchaseOrder | Field<string> | null | Purchase-order reference printed on the invoice. |
currency | Field<string> | null | ISO 4217 code |
grossSubtotalAmount | Field<number> | null | Sum of line items BEFORE any header-level discount or rounding |
discountAmount | Field<number> | null | Header-level discount in document currency, sign preserved as printed |
discountPercentage | Field<number> | null | Header-level discount percentage |
roundingAmount | Field<number> | null | Rounding adjustment, signed as printed |
subtotalAmount | Field<number> | null | Tax base — value the VAT/tax percentage is calculated against (after header-level discount + rounding, before tax) |
taxes | Array<Tax entry> | List of all taxes applied to the invoice |
totalAmount | Field<number> | null | Total amount including taxes and VAT |
paymentMethod | Field<string> | null | Payment method identified on the document. |
documentType | Field<string> | null | Document classification. Allowed values depend on the extraction type. |
expenseType | Field<string> | null | Document-level expense category, classified from the vendor, title and line descriptions. Categories follow the expense lines shared by IRS Schedule C, HMRC SA103F, QuickBooks and Xero. Coordinates are always null (inferred, not printed). |
complianceCode | Field<string> | null | Country-specific compliance or regulatory reference number required by local law, such as ATCUD (Portugal), IRN (India), CFDI UUID (Mexico), or allocation number (Israel). |
overallConfidence | string | null | Overall confidence in the extraction |
| Value | Meaning | Examples |
|---|---|---|
advertising_and_marketing | Advertising, promotion and marketing spend. | Google Ads or Meta Ads invoice, agency retainer, trade-show sponsorship, printed flyers, website campaign. |
bank_and_payment_fees | Charges for holding or moving money. | Monthly bank account fee, Stripe or PayPal processing fees, loan interest statement, foreign-exchange commission. |
cost_of_goods_sold | Goods, merchandise and raw materials bought for resale or production, including food, drink and serving disposables delivered to a restaurant, cafe, bar, caterer or shop. | Wholesaler delivery note to a grocery, drinks supplier invoice to a bar, packaging and cups for a cafe, raw materials for a workshop. |
professional_services | Fees for expert or contracted work. | Lawyer, accountant, auditor, consultant, freelance designer, subcontractor, construction contractor. |
insurance | Any insurance premium. | Liability, property, vehicle or health insurance invoice, policy renewal. |
meals_and_entertainment | A meal or hospitality service the buyer consumed, never a supplier delivery of food stock. | Restaurant bill, catered team event, client dinner, hotel bar receipt. |
office_supplies | Stationery, printing, consumables and small office items. | Paper and toner, pens, print-shop order, small desk accessories. |
software_and_subscriptions | Software, cloud services, licences, dues and memberships. | Microsoft 365, AWS, Slack, a professional association fee, a trade journal subscription. |
rent_and_lease | Premises rent and equipment or operating leases. | Office or warehouse rent, property management fee, leased copier, co-working membership. |
repairs_and_maintenance | Repair, servicing, cleaning and janitorial work or supplies. | Plumber, HVAC service, machine servicing, cleaning company, cleaning and hygiene chemicals. |
shipping_and_delivery | Moving or storing goods and documents. | Courier, freight forwarder, postage, pallet delivery, storage fee. |
taxes_and_licenses | Government fees and charges. | Business licence, permit, municipal business rates, customs duties, regulatory filing fee. |
travel | Costs of a trip. | Airfare, hotel, train ticket, taxi or ride-hailing, airport parking, car rental on a trip. |
vehicle_and_fuel | Running the buyer's own vehicles. | Fuel receipt, tyre replacement, vehicle service, toll charges, vehicle lease running costs. |
utilities | Electricity, water, gas and heating. | Electricity bill, water bill, gas bill, district heating. |
telecommunications | Phone, mobile, internet and fax services. | Mobile plan, office broadband, VoIP service, fax line. |
equipment_and_fixed_assets | Assets bought outright rather than leased or repaired. | Laptops, machinery, furniture, tools, a handset bought from a telecom vendor. |
staffing_and_payroll | Staffing, recruitment and payroll services and employee benefits. | Temp agency invoice, recruitment fee, payroll provider, benefits provider. |
other | No category above fits. | A donation receipt, a one-off purchase with no clear category. |
invoice_line_items fields#
| Field | JSON type | Description and allowed values |
|---|---|---|
line_items | Array<Invoice line item> | List of all line items extracted from the invoice |
total_lines | integer | null | Total count of line items |
overall_confidence | string | null | Overall confidence in the line items extraction |
confidence_reasons | Array<string> | Reasons for the assigned overall confidence level |
ocr fields#
| Field | JSON type | Description and allowed values |
|---|---|---|
text | string | Extracted document text. |
coordinates | Map<string, Array<Coordinates>> | null | Document location, when requested and available. OCR maps text keys to arrays of coordinate objects. |
custom_template fields#
| Field | JSON type | Description and allowed values |
|---|---|---|
data | Map<string, JSON value> | Dynamic fields and tables defined by your template. Leaves use { value, coordinates }; keys, value types, and allowed values come from the template. See meta.validationFeedback for editable field and column types. |
Field envelope#
| Field | JSON type | Description and allowed values |
|---|---|---|
value | T | null | The extracted value of type T (the type named inside Field<T>), or null when not found. |
coordinates | Coordinates | null | Document location, when requested and available. OCR maps text keys to arrays of coordinate objects. |
confidence | string | null | Categorical confidence, not a numeric probability. Null when unavailable. |
Document metadata#
| Field | JSON type | Description and allowed values |
|---|---|---|
externalId | string | null | Your document identifier supplied at upload. |
documentId | string (UUID) | null | Gemina identifier for the stored document. |
userId | string (UUID) | null | Identifier of the account that owns the document. |
endUserId | string | null | Optional end-user identifier supplied by your integration. |
apiKeyId | string (UUID) | null | Identifier of the API key associated with this document. |
filename | string | null | Uploaded document filename. |
fileSize | integer | null | Uploaded file size in bytes. |
contentType | string | null | File MIME type, for example application/pdf. |
documentFileType | string | null | Document file type. |
numberOfPages | integer | null | Number of pages in the document. |
normalizedWidth | integer | null | Normalized document image width in pixels. |
normalizedHeight | integer | null | Normalized document image height in pixels. |
normalizedMegapixels | number | null | Normalized document image size in megapixels. |
storageLocation | string | null | Region where the document is stored. |
imageUrl | string (URL) | null | URL of the rendered document image, when available. |
thumbnailUrl | string (URL) | null | URL of the document thumbnail, when available. |
originalDocumentUrl | string (URL) | null | URL of the original uploaded file, when available. |
correlationId | string (UUID) | null | Identifier linking the upload request to its processing results. |
next | string (URL) | null | URL to poll for results; null when no polling link is provided. |
Document data#
| Field | JSON type | Description and allowed values |
|---|---|---|
extractions | Array<Extraction result> | null | One result per extraction performed on the document. |
Tax entry#
| Field | JSON type | Description and allowed values |
|---|---|---|
type | string | Tax type classification |
name | string | null | Display name as shown on invoice (e.g., 'State Tax', 'TVA', 'MwSt') |
rate | number | null | Tax rate as percentage (e.g., 17 for 17%) |
amount | number | null | Tax amount in document currency |
base | number | null | Taxable base printed for this rate's summary row, when the document prints per-rate bases (multi-rate invoices) |
coordinates | Coordinates | null | Coordinates of the tax amount on the document |
confidence | string | null | Confidence level for this tax entry |
Invoice line item#
| Field | JSON type | Description and allowed values |
|---|---|---|
lineNumber | integer | null | Line item number or position |
description | string | null | Product or service description |
itemCode | string | null | Internal SKU, product code, or item number assigned by the seller |
barcode | string | null | EAN, UPC, or other standardized barcode number (typically 8-14 digits) |
quantity | number | null | Quantity ordered |
unitOfMeasure | string | null | Unit the quantity is counted in, normalized to a closed roster (UNIT, BOX, CARTON, PACK, PAIR, DOZEN, MG, G, KG, TON, LB, OZ, ML, CL, L, GAL, MM, CM, M, M2, M3, HOUR, DAY, MONTH). UNIT for plain item counts; null when the document does not indicate a unit and the quantity is not a plain count. |
unitSize | number | null | Measurable content of ONE unit when printed on the document (e.g. 0.5 for a '12 × 0.5L' line). Always paired with unitSizeUom. Extracted only — never derived. Informational: does NOT contribute to lineTotal math. |
unitSizeUom | string | null | Unit of unitSize, normalized to a closed roster of MEASURE units only (MG, G, KG, TON, LB, OZ, ML, CL, L, GAL, MM, CM, M, M2, M3) — a subset of the unitOfMeasure roster: counts (UNIT, BOX, CARTON, ...) and time units are never a content size. Always paired with unitSize. |
listPrice | number | null | Gross/catalog unit price BEFORE discount (e.g., LIST PRICE, MSRP, מחיר מחירון). Documentation-only — does NOT contribute to line_total math. Populated only when the invoice prints a dedicated column for it; null otherwise. |
unitPrice | number | null | NET price per unit (after discount). Used by line_total math. |
discountAmount | number | null | Discount applied to line |
discountPercentage | number | null | Discount percentage |
taxAmount | number | null | Tax amount for this line |
taxRate | number | null | Tax rate percentage |
packagingAmount | number | null | Additive packaging charge (e.g., סה"כ ערך אריזה). Contributes to line_total like tax_amount does. |
depositAmount | number | null | Additive deposit charge (e.g., סה"כ ערך פיקדון, container/bottle deposit). Contributes to line_total like tax_amount does. |
unitsPerPackage | integer | null | Structural pack size — the whole-number count of singles that fit in one package/case (e.g., 24 cans per case, 6 bottles per pack). Never a volume or weight. Informational only — does NOT contribute to line_total. Null when the invoice has no dedicated pack-size column. |
packageQuantity | number | null | Per-line order quantity expressed in package units (e.g., number of cartons ordered). May be fractional (e.g., 0.2 of a carton). Informational only — does NOT contribute to line_total. Relationship when both are present: quantity ≈ package_quantity × units_per_package. |
lineTotal | number | null | Total amount for this line |
confidence | string | null | Categorical confidence, not a numeric probability. Null when unavailable. |
confidence_reasons | Array<string> | Machine-readable reasons for the assigned confidence; an empty array means no reasons supplied. |
Coordinates#
| Field | JSON type | Description and allowed values |
|---|---|---|
pixels | Array<[integer, integer]> | Exactly four [x, y] integer pairs on the normalized document image. |
relative | Array<[number, number]> | Exactly four [x, y] number pairs relative to image width and height. |
Extraction result#
| Field | JSON type | Description and allowed values |
|---|---|---|
status | string | Processing outcome. See the status meanings in Interpretation and calculations. |
meta | Extraction metadata | Identifiers and processing metadata for this response. |
values | Map<string, JSON value> | null | Extracted payload. Use the section matching meta.extractionType; null while unavailable, failed, or purged. |
verifiedValues | Map<string, JSON value> | null | Reviewer-finalized payload, with the same shape as values. Null before verification or after purge. |
verifiedDiff | Array<Verified difference> | null | Reviewer changes. Null means not verified; an empty array means verified with no changes. |
errors | Array<Map<string, JSON value>> | Error objects supplied by the API. Properties vary by error; may include error_code, description, detail, and status_code. |
Extraction metadata#
| Field | JSON type | Description and allowed values |
|---|---|---|
extractionId | string (UUID) | null | Identifier of this extraction. |
modelType | string | Model used for this extraction. Metadata can include legacy model names. |
thinking | boolean | Whether thinking was enabled for this extraction. |
correction | boolean | Whether correction was enabled for this extraction. |
evaluation | boolean | Whether evaluation was enabled for this extraction. |
includeCoordinates | boolean | Whether document coordinates were requested. |
processorClass | string | Processor identifier reported by the backend. |
extractionType | string | Documented extraction types; selects the schema of values and verifiedValues. |
latencySeconds | number | null | Extraction processing duration in seconds. |
numberOfFields | integer | null | Number of fields reported for the extraction. |
purgeAt | string (date-time) | null | Scheduled purge time, or null when not scheduled. |
purgedAt | string (date-time) | null | Actual purge time, or null before purge. |
purgeReason | string | null | Reason for purging, or null when not purged. |
validationFeedback | Validation feedback | null | Schema for submitting reviewer edits, when available. |
validated | boolean | Whether a reviewer validation has been stored. |
Verified difference#
| Field | JSON type | Description and allowed values |
|---|---|---|
field | string | Label identifying the field changed by the reviewer. |
status | string | Whether the reviewer corrected, added, or removed this field. |
pointer | string | null | JSON pointer locating the value. For a verified difference, null when the field was removed. |
original | string | integer | number | boolean | Array<JSON value> | Map<string, JSON value> | null | Original extracted value, preserving its JSON type. |
verified | string | integer | number | boolean | Array<JSON value> | Map<string, JSON value> | null | Reviewer-finalized value, preserving its JSON type. |
Validation feedback#
| Field | JSON type | Description and allowed values |
|---|---|---|
validationSchema | Array<string> | Opaque validation keys in label:…|ptr:/… format; submit the keys unchanged. |
validationFields | Array<Validation field> | Types and allowed values for editable fields not already described by a table column. |
rowMutableTables | Array<Editable table> | Tables whose rows reviewers may add or remove; columns describe each cell type. |
Validation field#
| Field | JSON type | Description and allowed values |
|---|---|---|
key | string | Opaque validation key; matches an entry in validationSchema. |
label | string | Label identifying the editable field. |
type | string | Value type accepted for this editable field or column. |
enum | Array<string> | null | Allowed string values, when a closed set is supplied; null means no enum constraint. |
format | string | null | Optional format hint (for example date). Null means no format hint. |
description | string | null | Description. |
Editable table#
| Field | JSON type | Description and allowed values |
|---|---|---|
pointer | string | JSON pointer locating the editable table within the extraction payload. |
keyTemplate | string | Template for validation keys in this table. |
columns | Array<Editable column> | Column definitions shared by every row in this editable table. |
Editable column#
| Field | JSON type | Description and allowed values |
|---|---|---|
name | string | Name. |
type | string | Value type accepted for this editable field or column. |
enum | Array<string> | null | Allowed string values, when a closed set is supplied; null means no enum constraint. |
format | string | null | Optional format hint (for example date). Null means no format hint. |
description | string | null | Description. |
Interpretation and calculations#
Invoice header reconciliation: subtotalAmount + sum(taxes[].amount) ≈ totalAmount. subtotalAmount is the tax base after header discounts and rounding. Header discountAmount and roundingAmount retain the printed sign. A discountPercentage of 3 means 3%.
Invoice line math: lineTotal ≈ quantity × unitPrice + taxAmount + packagingAmount + depositAmount (when present). unitPrice is already net of line discounts: do not subtract discountAmount or discountPercentage again. listPrice is the printed gross/catalog price and does not enter this calculation.
Pack and content sizes are informational: quantity ≈ packageQuantity × unitsPerPackage when both are present. packageQuantity can be fractional; unitsPerPackage is an integer count. unitSize and unitSizeUom describe the printed measurable content of one unit, are never derived, and are both null if either is missing. Unsupported measurement units normalize to null.
Invoice header currency is an ISO 4217 string, not a closed backend enum. invoiceDate and dueDate are strings using ISO dates when possible.
Statuses: pending means queued; in_process means processing; success means completed; partial means only part of the work succeeded; failed means an error occurred; empty means no result. Check the status of each extraction as well as the response envelope.
API Reference#
Official SDKs
@gemina/sdk- TypeScript / Node.js (npm)gemina- Python (PyPI)Gemina.Sdk- C# (NuGet)co.gemina:gemina-sdk- Java (Maven)gemina/sdk- PHP (Packagist)@gemina/elements- React chat UI (npm)
Extraction Types
invoice_headers- Invoice header fields (field list →)invoice_line_items- Line item details (field list →)ocr- Full text extraction
Model Types
velox- Fast processingpraetorian- Balanced accuracyinvictus- Highest accuracy
Endpoints
POST /api/v1/documents/uploadsPOST /api/v1/documents/uploads/webGET /api/v1/documents/{id}GET /api/v1/documents/results/{id}GET /api/v1/documents/extractions/{id}PUT /api/v1/documents/extractions/{id}/feedback
Response Statuses
success- Extraction completedpending- Job queuedin_process- Processingfailed- Error occurredpartial- Part of the work succeededempty- No result
Document Intelligence
POST /api/v1/retrieval/queryPOST /api/v1/retrieval/aggregatePOST /api/v1/chat/queryDELETE /api/v1/chat/sessions/{id}GET /api/v1/chat/sessionsGET /api/v1/chat/sessions/{id}DELETE /api/v1/chat/sessions/{id}/purgePOST /api/v1/sessions/token
Errors
- Branch on
errors[0].error_code 401- No credential / bad session token403- Bad or revoked API key422- Malformed request, or over the page limit429- Back off onRetry-After- Error reference
MCP server
https://api.gemina.co/api/v1/mcp/- OAuth 2.1 sign-in or API key
- MCP Server Docs
FileTag API
- Document tagging via REST + MCP
- Free tier: 1,500 tags/month
- FileTag API Docs
Ready to Get Started?
Sign up for a free trial and start extracting data from your documents today.