# Gemina API Documentation

Integrate Gemina's production harness for AI document data extraction. Official SDKs for TypeScript, Python, C#, Java, and PHP process a document in one call, then search and chat over your results. A REST API and MCP server are available too.

- HTML version: https://www.gemina.co/docs
- FileTag docs (HTML): https://www.gemina.co/docs/filetag
- FileTag docs (markdown): https://www.gemina.co/docs/filetag.md
- MCP manifest: https://www.gemina.co/.well-known/mcp.json

## Getting Started

Extract structured data from documents programmatically. The fastest path is an official SDK — install it and process a document in a single call (submit and poll are handled for you), then search and chat over your results. Prefer raw HTTP? Every endpoint is a plain REST call (see “API (raw HTTP)” below). Responses are structured JSON with extracted fields, coordinates, and confidence scores.

- **Base URL:** `https://api.gemina.co`
- **Authentication:** `X-API-Key: your-api-key` header
- **Get an API key:** https://console.gemina.co/registration/create-account?planId=trial

## SDKs (recommended)

Official client libraries. Install from your package manager, then process a document in one call — the SDK submits and polls for you. Each library also covers search, chat, and browser-safe session tokens.

| Language | Package | Registry |
|---|---|---|
| TypeScript / Node.js | `@gemina/sdk` | npm |
| Python | `gemina` | PyPI |
| C# | `Gemina.Sdk` | NuGet |
| Java | `co.gemina:gemina-sdk` | Maven Central |
| PHP | `gemina/sdk` | Packagist |
| React chat UI | `@gemina/elements` | npm |

### TypeScript / Node.js SDK

**Install**

```bash
npm i @gemina/sdk
```

**Process a document in one call**

Authenticate and extract structured data (submit + poll handled for you):

```typescript
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);
```

**Search & analyze your documents**

Structured, semantic, or hybrid search, plus exact database-backed aggregations:

```typescript
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**

Grounded, cited answers; follow-ups keep their conversation context:

```typescript
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)
```

**Browser-safe session tokens**

Exchange your API key server-side for a short-lived, scoped token:

```typescript
// 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" },
});
```

**Error handling**

Typed errors separate a terminal failure from a resumable timeout:

```typescript
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;
  }
}
```

### Python SDK

**Install**

```bash
pip install gemina
```

**Process a document in one call**

Authenticate and extract structured data (submit + poll handled for you):

```python
import asyncio
from gemina import GeminaClient, ExtractionTypeModel

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
            [ExtractionTypeModel.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())
```

**Search & analyze your documents**

Structured, semantic, or hybrid search, plus exact database-backed aggregations:

```python
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**

Grounded, cited answers; follow-ups keep their conversation context:

```python
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)
```

**Browser-safe session tokens**

Exchange your API key server-side for a short-lived, scoped token:

```python
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 frontend
```

**Error handling**

Typed errors separate a terminal failure from a resumable timeout:

```python
import asyncio
from gemina import GeminaClient, GeminaProcessingError, GeminaTimeoutError
from gemina import ExtractionTypeModel

async def main():
    async with GeminaClient("YOUR_API_KEY") as client:
        try:
            result = await client.process_document(
                "invoice.png", [ExtractionTypeModel.INVOICE_HEADERS],
            )
        except GeminaProcessingError as err:      # terminal "failed" status
            print("processing failed:", err.result.errors)
        except GeminaTimeoutError as err:          # still processing at deadline
            print("timed out; resume from:", err.last_result)

asyncio.run(main())
```

### C# SDK

**Install**

```bash
dotnet add package Gemina.Sdk
```

**Process a document in one call**

Authenticate and extract structured data (submit + poll handled for you):

```csharp
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<ExtractionTypeModel> { ExtractionTypeModel.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"]}");
```

**Search & analyze your documents**

Structured, semantic, or hybrid search, plus exact database-backed aggregations:

```csharp
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**

Grounded, cited answers; follow-ups keep their conversation context:

```csharp
var chat = await client.Chat.ChatQueryAsync(new ChatQueryInDTO(
    message: "How much did I spend on cleaning services this year?"));

Console.WriteLine(chat.Answer);
Console.WriteLine($"Confident: {chat.Confident}");
Console.WriteLine($"Citations: {string.Join(", ", chat.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)
```

**Browser-safe session tokens**

Exchange your API key server-side for a short-lived, scoped token:

```csharp
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);
```

**Error handling**

Typed errors separate a terminal failure from a resumable timeout:

```csharp
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}");
}
```

### Java SDK

**Install**

```xml
<dependency>
    <groupId>co.gemina</groupId>
    <artifactId>gemina-sdk</artifactId>
    <version>0.2.1</version>
</dependency>
```

**Process a document in one call**

Authenticate and extract structured data (submit + poll handled for you):

```java
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.ExtractionTypeModel;

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(ExtractionTypeModel.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");
    }
}
```

**Search & analyze your documents**

Structured, semantic, or hybrid search, plus exact database-backed aggregations:

```java
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**

Grounded, cited answers; follow-ups keep their conversation context:

```java
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)
```

**Browser-safe session tokens**

Exchange your API key server-side for a short-lived, scoped token:

```java
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");
```

**Error handling**

Typed errors separate a terminal failure from a resumable timeout:

```java
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());
}
```

### PHP SDK

**Install**

```bash
composer require gemina/sdk
```

**Process a document in one call**

Authenticate and extract structured data (submit + poll handled for you):

```php
<?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;
```

**Search & analyze your documents**

Structured, semantic, or hybrid search, plus exact database-backed aggregations:

```php
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**

Grounded, cited answers; follow-ups keep their conversation context:

```php
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)
```

**Browser-safe session tokens**

Exchange your API key server-side for a short-lived, scoped token:

```php
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 frontend
```

**Error handling**

Typed errors separate a terminal failure from a resumable timeout:

```php
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;
}
```

## Drop-in Chat UI — @gemina/elements

A headless-styled React chat component (`<GeminaChat>`) plus a security-hardened token manager — citations, low-confidence handling, and RTL support included — without ever exposing your API key to the browser.

**Install**

```bash
npm i @gemina/elements @gemina/sdk react
```

**Mint session tokens (Next.js App Router)**

The API key stays on your server; this route mints a scoped token for the browser:

```typescript
// 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**

```tsx
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}
  onCitationClick={(documentId) => openDocumentViewer(documentId)}
/>;
```

## Document Intelligence — Search, Analytics & Chat

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. Below are the raw HTTP calls; the SDKs above wrap the same endpoints.

### Search Your Documents

One endpoint, three modes: `structured` (exact filters), `semantic` (meaning), and `hybrid` (both, fused — the best default for free text):

```bash
# 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:

```bash
# 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 answers grounded in your documents, with citations:

```bash
# 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?" }'
```

```json
{
  "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
}
```

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`).

## API (raw HTTP)

Prefer to integrate without an SDK? Call the REST API directly with any HTTP client.

### Python

**Quick Start**

Install the required package and set up your environment:

```bash
pip install requests
```

```python
import 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:

```python
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:

```python
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)
```

### Node.js

**Quick Start**

Install the required package and set up your environment:

```bash
npm install axios form-data
```

```typescript
import 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:

```typescript
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 multipart form data:

```typescript
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);
```

### Java

**Quick Start**

Install the required package and set up your environment:

```xml
<dependency>
    <groupId>com.squareup.okhttp3</groupId>
    <artifactId>okhttp</artifactId>
    <version>4.12.0</version>
</dependency>
```

```java
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:

```java
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:

```java
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());
```

### C#

**Quick Start**

```csharp
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:

```csharp
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:

```csharp
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);
```

### PHP

**Quick Start**

```php
<?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
<?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 multipart form data:

```php
<?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:

```json
{
  "status": "success",
  "meta": {
    "documentId": "9860df92-64fe-4b53-9663-5c11b38a3051",
    "externalId": "inv-2026-0001",
    "filename": "invoice.pdf"
  },
  "data": {
    "extractions": [
      {
        "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"}
        }
      },
      {
        "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
        }
      }
    ]
  }
}
```

## Response Fields

The response includes structured extraction values keyed by extraction type. Unpopulated fields are `null`.

### `invoice_headers` fields

Each header field uses an envelope shape: `{ value, coordinates, confidence }`. When the invoice does not print the value, the whole envelope is `null` — a defensive client can safely guard with `if (response.discountAmount) { … }`.

- `grossSubtotalAmount` — Sum of line items before any header-level discount or rounding.
- `discountAmount` — Header-level discount in the document’s currency. Sign is verbatim from the invoice: some templates print positives (`229.91`), others negatives (`-229.91`) or parenthesized values. Clients that subtract on their side must handle both.
- `discountPercentage` — Header-level discount as a percentage (e.g. `3.0` means 3%). Only populated when the invoice prints it.
- `roundingAmount` — Rounding adjustment (e.g. "round off", agorot rounding). Signed as printed; magnitude typically < 1.0 in document currency.
- `subtotalAmount` — The tax base: the value the invoice’s VAT/tax percentage is calculated against, after any header-level discount and rounding, before tax.

The reconciliation identity (modulo printing artifacts):

```text
subtotalAmount + Σ taxes[].amount ≈ totalAmount
```

### `invoice_line_items` fields

Each item in the `line_items` array is a flat object (no envelope). Unpopulated fields are `null`.

- `listPrice` — Gross/catalog unit price before any line-level discount. Populated only when the invoice prints a dedicated "list price" / "catalog price" / "MSRP" column. Documentation-only — do not use it in line-total math.
- `unitPrice` — NET price per unit, after any line-level discount. The `lineTotal` math uses this value, so the per-line `discountAmount` and `discountPercentage` should not be subtracted again. For the gross/catalog price, use `listPrice` when populated.
- `packagingAmount` — Additive packaging charge (crate fee, palletizing fee). Positive. Contributes to `lineTotal`.
- `depositAmount` — Additive deposit/refund charge (bottle deposit, container deposit). Positive. Contributes to `lineTotal`.
- `unitsPerPackage` — Structural pack size: whole-number count of units per package (e.g. `24` cans per case). Informational; never a volume or weight.
- `packageQuantity` — Order quantity in package units; may be fractional (e.g. `2.1` cartons). Informational. Most invoices print only one of `unitsPerPackage` or `packageQuantity` — both can be `null` independently.

Line-total math contract:

```text
lineTotal ≈ quantity × unitPrice
          + taxAmount        (if present)
          + packagingAmount  (if present)
          + depositAmount    (if present)
```

When both pack-size fields are present, `quantity ≈ packageQuantity × unitsPerPackage` — the relationship is approximate, not enforced.

## API Reference

### Official SDKs

- `@gemina/sdk` — TypeScript / Node.js (npm)
- `gemina` — Python (PyPI)
- `Gemina.Sdk` — C# (NuGet)
- `co.gemina:gemina-sdk` — Java (Maven Central)
- `gemina/sdk` — PHP (Packagist)
- `@gemina/elements` — React chat UI (npm)

### Extraction Types

- `invoice_headers` — Invoice header fields (see [field list](#invoice_headers-fields))
- `invoice_line_items` — Line item details (see [field list](#invoice_line_items-fields))
- `ocr` — Full text extraction
- `document_details_hebrew` — Hebrew documents

### Model Types

- `velox` — Fast processing
- `praetorian` — Balanced accuracy
- `invictus` — Highest accuracy

### Endpoints

- `POST /api/v1/documents/uploads`
- `POST /api/v1/documents/uploads/web`
- `GET /api/v1/documents/{id}`
- `GET /api/v1/documents/results/{id}`
- `POST /api/v1/retrieval/query`
- `POST /api/v1/retrieval/aggregate`
- `POST /api/v1/chat/query`
- `POST /api/v1/sessions/token`

### Response Statuses

- `success` — Extraction completed
- `pending` — Job queued
- `in_process` — Processing
- `failed` — Error occurred

### FileTag API

- Document tagging via REST + MCP
- Free tier: 1,500 tags/month
- HTML docs: https://www.gemina.co/docs/filetag
- Markdown docs: https://www.gemina.co/docs/filetag.md
