extract data from documents javajava document parsing apijava pdf extraction

How to Extract Data from Documents in Java: Dokyumi API Integration Guide

March 16, 2026 · Updated

This guide shows a working Java 11+ integration with Dokyumi’s shared document extraction endpoint. Send one multipart request with a file and optional schema slug; a successful request returns structured JSON synchronously.

The API contract

  • Endpoint: POST https://dokyumi.com/api/v1/extract
  • Authentication: Authorization: Bearer dk_live_...
  • Multipart fields: required file, optional schema slug
  • Successful status: completed or review
  • Self-serve limits: 20MB per file and 50 pages per document

Hard failures return a non-2xx error envelope. A successful extraction includes id, schema, data, confidence, validation, meta, and request_id.

A reusable Java client

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.Duration;
import java.util.UUID;

public final class DokyumiClient {
    private static final URI EXTRACT_URI =
        URI.create("https://dokyumi.com/api/v1/extract");
    private static final long MAX_FILE_BYTES = 20L * 1024 * 1024;

    private final String apiKey;
    private final HttpClient http;

    public DokyumiClient(String apiKey) {
        this.apiKey = apiKey;
        this.http = HttpClient.newBuilder()
            .connectTimeout(Duration.ofSeconds(30))
            .build();
    }

    public String extract(Path file, String schemaSlug)
            throws IOException, InterruptedException {
        if (Files.size(file) > MAX_FILE_BYTES) {
            throw new IllegalArgumentException("Dokyumi files must be 20MB or smaller");
        }

        String boundary = "dokyumi-" + UUID.randomUUID();
        byte[] body = buildMultipartBody(boundary, file, schemaSlug);

        HttpRequest request = HttpRequest.newBuilder(EXTRACT_URI)
            .header("Authorization", "Bearer " + apiKey)
            .header("Content-Type", "multipart/form-data; boundary=" + boundary)
            .timeout(Duration.ofSeconds(120))
            .POST(HttpRequest.BodyPublishers.ofByteArray(body))
            .build();

        HttpResponse<String> response = http.send(
            request,
            HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)
        );

        if (response.statusCode() / 100 != 2) {
            throw new IOException(
                "Dokyumi HTTP " + response.statusCode() + ": " + response.body()
            );
        }

        return response.body();
    }

    private static byte[] buildMultipartBody(
            String boundary, Path file, String schemaSlug) throws IOException {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        String filename = file.getFileName().toString()
            .replace("\"", "")
            .replace("\r", "")
            .replace("\n", "");

        write(out, "--" + boundary + "\r\n");
        write(out, "Content-Disposition: form-data; name=\"file\"; filename=\""
            + filename + "\"\r\n");
        write(out, "Content-Type: application/octet-stream\r\n\r\n");
        Files.copy(file, out);
        write(out, "\r\n");

        if (schemaSlug != null && !schemaSlug.isBlank()) {
            write(out, "--" + boundary + "\r\n");
            write(out, "Content-Disposition: form-data; name=\"schema\"\r\n\r\n");
            write(out, schemaSlug + "\r\n");
        }

        write(out, "--" + boundary + "--\r\n");
        return out.toByteArray();
    }

    private static void write(ByteArrayOutputStream out, String value)
            throws IOException {
        out.write(value.getBytes(StandardCharsets.UTF_8));
    }
}

The request body is valid multipart data, including literal \r\n separators. It buffers at most the supported 20MB file size; it does not pretend a raw file stream is a multipart upload.

Map the response with Jackson

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.List;
import java.util.Map;

public final class ExtractionResult {
    public String id;
    public String status; // "completed" or "review"
    public String schema;
    public Map<String, Object> data;
    public Map<String, Double> confidence;
    public Validation validation;
    public Metadata meta;
    public String request_id;

    public static final class Validation {
        public boolean valid;
        public JsonNode errors;
        public List<String> low_confidence_fields;
    }

    public static final class Metadata {
        public long processing_time_ms;
        public int page_count;
        public int credits_used;
        public boolean ocr_cached;
        public String model;
    }
}
ObjectMapper mapper = new ObjectMapper();
String json = client.extract(Path.of("invoice.pdf"), "invoice-parser");
ExtractionResult result = mapper.readValue(json, ExtractionResult.class);

if ("review".equals(result.status)) {
    System.out.println(
        "Review errors: " + result.validation.errors
            + "; low-confidence fields: " + result.validation.low_confidence_fields
    );
}

System.out.println("Extraction " + result.id + ": " + result.data);

Batch safely

For a batch, use a bounded executor and let each task call the same synchronous method. Keep concurrency below the configured limit on your API key, persist each response before acknowledging local work, and resume from stored extraction IDs. Do not create parallel keys to bypass a rate limit.

Retry a transient 429 rate_limited response after its Retry-After delay and retry selected 5xx failures with bounded backoff. Do not retry quota_exceeded (whether a 402 additional-credit shortage or 429 monthly quota), 413 page_limit_exceeded, authentication errors, or schema errors without changing the request or account state.

Upload-site webhooks are separate

Direct API calls return synchronously and do not trigger webhooks. Webhook delivery applies only to documents submitted through a configured upload site. Set the URL under Sites → Settings and deduplicate deliveries with the top-level extraction_id.

{
  "event": "extraction.completed",
  "extraction_id": "6f8c2d4a-7b31-4e95-9a20-c1d7f6b84210",
  "site_id": "3e5b7c19-8d42-4fa1-a760-c2e9d4b18306",
  "data": { "invoice_number": "INV-2026-0342", "total_amount": 4250.00 },
  "timestamp": "2026-08-23T19:12:00.000Z"
}

When a signing secret has been provisioned, X-Dokyumi-Signature is the raw 64-character lowercase hexadecimal HMAC-SHA256 digest of the exact request body. Reject missing or malformed signatures and await a durable queue write before returning 2xx.

Next steps

Use the API reference for the complete success and error envelopes, try the invoice parser, or map this client into an accounts-payable workflow. The bulk-processing guide covers queue sizing and page-weighted credits.

These articles are selected from the same editorial cluster, not generated from keyword overlap.

Put build and ship api pipelines to work

Use the API and webhook reference

Confirm request fields, response data, validation, confidence, and webhook signing.

Trace an accounts-payable workflow

See how schema, endpoint, confidence review, and ledger delivery fit together.

Build the first endpoint

Create a schema and test the pipeline against a real source document.

Test the extraction on your own documents

25 free credits each month. One credit covers a document up to 5 pages; self-serve documents can be up to 50 pages. No credit card required.