@gemina/sdk
v0.2.1
Published
Official TypeScript/JavaScript SDK for the Gemina API — invoice OCR and document intelligence.
Maintainers
Readme
Gemina TypeScript SDK
The official TypeScript/JavaScript client for the Gemina API — invoice OCR and document intelligence. Upload invoices and financial documents, get typed structured data back, then search, aggregate, and chat over everything you've processed. Works in Node.js (18+) and the browser, with zero runtime dependencies (native fetch).
Install
npm i @gemina/sdkRequires Node.js >= 18 (or any modern browser). Ships ESM and CommonJS builds with full type declarations.
Authenticate
Get an API key from the Gemina Console. The client sends it as the X-API-Key header on every request:
import { GeminaClient } from "@gemina/sdk";
const client = new GeminaClient(process.env.GEMINA_API_KEY!);Never ship the API key in browser or mobile code. For browser embedding, mint short-lived session tokens server-side (POST /v1/sessions/token) and use GeminaClient.withSessionToken(...) — see Session tokens below and the Document Intelligence guide in the docs.
Quickstart — process an invoice in one call
processDocument submits the document through the async endpoint, polls with exponential backoff until processing completes, and returns the final typed result — one call, no manual polling:
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);Processing a document by URL works the same way — pass { url } instead of a file:
const result = await client.processDocument(
{ url: "https://example.com/invoice.pdf" },
["invoice_headers"],
);What you get back
processDocument resolves to a DocumentProcessingResultOutDTO:
status—"success" | "partial" | "empty" | "failed".partialandemptystill resolve (they carry usable data/meta);failedthrows aGeminaProcessingErrorinstead.data.extractions— one entry per requested extraction type. Each entry hasmeta.extractionType, its ownstatus, andvalues— the extracted payload. Forinvoice_headers,valuesfields (e.g.vendorName,invoiceNumber,invoiceDate,totalAmount,currency) are each{ value, coordinates, confidence }ornullwhen not found.meta.documentId— the stored document's ID (use it withclient.documents).meta.correlationId— the async processing correlation ID.
Extraction types (ExtractionTypeModel):
| Type | What it extracts |
|------|------------------|
| ocr | Raw OCR text |
| invoice_headers | Invoice header fields (vendor, buyer, dates, amounts, taxes) |
| invoice_line_items | Invoice line items |
| document_details_hebrew | Hebrew document details |
| document_line_items_hebrew | Hebrew document line items |
| custom_template | Fields defined by your custom template (pass templateId) |
| filetag | File classification metadata (FileTag) |
Search & aggregate your documents
Everything you process is indexed for retrieval. Use the retrieval group to query with natural language plus structured filters — results carry documentId / documentExtractionId citations back to the underlying documents:
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})`);Aggregate across your documents with metrics and group-by:
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);
}Check how many of your documents are indexed with client.retrieval.retrievalStatus() (returns { indexedDocuments }).
Advanced filters & match highlights. Beyond the promoted filters, filter on any structured field a document has with structuredFilters (op is one of eq / neq / gt / lt / contains / exists, max 8), and read back the line-item snippet that made a document match via matchedChunks:
const { items } = await client.retrieval.retrievalQuery({
retrievalQueryInDTO: {
mode: "hybrid",
text: "27-inch monitors",
structuredFilters: [{ path: "position", op: "contains", value: "engineer" }],
},
});
for (const item of items ?? []) {
for (const chunk of item.matchedChunks ?? []) {
console.log(item.documentId, "matched on:", chunk.text);
}
}Discover which fields you can filter on with client.retrieval.retrievalFields() — it returns the structured field names per document type (names only, never values), so you can build a field picker from real data:
const { fields } = await client.retrieval.retrievalFields();
// -> [{ documentType: "invoice", field: "vendor_name", count: 42 }, ...]Chat with your documents
Ask free-form questions over everything you've processed. Answers come back with a confidence signal and citations to the source documents:
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);Chat requires a plan with Document Intelligence enabled — see pricing. Without it the API responds 402/403.
Multi-turn conversations (memory). For a back-and-forth where follow-ups keep context, use a conversation — it threads the server-issued sessionId for you:
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)A conversation expires after 24h of inactivity; the next send then throws the API's 404 CHAT_SESSION_NOT_FOUND — call chat.reset() and resend to continue in a fresh one. One-shot client.chat.chatQuery({ chatQueryInDTO: { message, sessionId } }) is still available if you'd rather hold the id yourself; every response returns a sessionId.
Session tokens (browser embedding)
To embed search or chat in your own frontend, mint a short-lived session token server-side and hand that to the browser — never the API key:
// 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" },
});For a drop-in chat UI, see @gemina/elements on npm.
Going deeper
Full API surface. The convenience layer sits on top of a complete generated client. Every API group is available on the client as a lazy accessor: documents, retrieval, chat, templates, files, fileTag, sessions, subscriptions, billing. For example, list your stored documents:
const view = await client.documents.findDocuments({ limit: 10 });
for (const doc of view.data?.documents ?? []) {
console.log(doc.meta.documentId, doc.meta.filename, doc.meta.createdAt);
}Polling knobs. processDocument accepts timeoutSeconds (default 300), initialIntervalSeconds (default 2, grows ×1.5 per attempt), and maxIntervalSeconds (default 15). Transient poll errors (connection blips, 5xx) are retried automatically on the same schedule — up to 3 in a row — since the document is already submitted. On timeout it throws GeminaTimeoutError carrying the correlationId, so you can resume polling yourself:
import { GeminaTimeoutError } from "@gemina/sdk";
try {
await client.processDocument(file, ["invoice_headers"], { timeoutSeconds: 60 });
} catch (err) {
if (err instanceof GeminaTimeoutError) {
// Still processing — poll later with the correlation ID:
const result = await client.documents.getDocumentProcessingResultByCorrelationId({
correlationId: err.correlationId,
});
}
}Error handling. A terminal failed status throws GeminaProcessingError — its result.errors lists the failure details. Transport/HTTP errors from the generated client (ResponseError, FetchError) pass through unwrapped:
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;
}
}Custom base URL. Point the client at a staging or self-hosted deployment via the second constructor argument:
const staging = new GeminaClient(apiKey, "https://api.staging.gemina.co");Requirements & support
- Node.js >= 18, or any browser with
fetch(the SDK has zero runtime dependencies). - Docs: https://console.gemina.co/docs
- Issues: https://github.com/tommyil/gemina-sdk/issues
- Email: [email protected]
