Lewati ke konten utama

API Ingestion dan dokumen

Halaman ini adalah kontrak integrasi frontend untuk penerimaan dokumen dan status ingestion. Semua rute adalah endpoint HTTP yang dipasang di root. Gunakan origin yang sama atau base URL yang dikonfigurasi, misalnya https://api.example.gov/documents; rute Docusaurus untuk halaman ini adalah /docs/api/ingestion-documents.

OpenAPI tersedia sebagai GET /openapi.json di setiap environment. Swagger UI tersedia pada GET /docs di luar production. Redoc tidak diaktifkan.

Rute secara ringkas​

MetodeRuteBodyMengembalikanGunakan untuk
POST/documents/txtJSONTxtIngestResponseIngestion teks inline berukuran kecil tanpa job
POST/documentsmultipart/form-dataUploadJobResponseFile lokal yang dipilih pengguna
POST/ocrmultipart/form-dataOcrResponseEkstraksi khusus OCR dari satu file, tanpa indexing
POST/ingestion/batchJSONUploadJobResponsePrefix object storage atau object key eksplisit
POST/ingestion/urlJSONUploadJobResponseURL dokumen yang ada di allow-list
GET/ingestion/jobs/{job_id}tidak adaUploadJobResponsePolling job upload/batch/URL asinkron
GET/documentsquery opsionalUploadDocumentResponse[]Membuat list, filter, dashboard, dan antrean review
GET/documents/{document_id}tidak adaUploadDocumentResponseMemeriksa satu record dokumen yang di-upload
DELETE/documents/{document_id}tidak ada204 No ContentMenghapus secara permanen dokumen terindeks, chunk-nya, dan objek mentahnya jika ada

Penghapusan tersedia untuk dokumen terindeks melalui DELETE /documents/{document_id}. Tidak ada endpoint pembatalan atau ingestion WebSocket. Kemajuan upload setelah request diterima dipantau dengan polling GET /ingestion/jobs/{job_id}.

Autentikasi dan batas frontend​

Setiap rute di halaman ini memerlukan header API key yang dikonfigurasi. Nama header default adalah X-API-Key.

X-API-Key: <server-injected-api-key>

Key yang tidak ada atau salah mengembalikan:

{ "detail": "Invalid or missing API key" }

Kode browser tidak boleh menyematkan shared API key. Kode browser juga tidak boleh membiarkan pengguna memalsukan header identitas tepercaya seperti X-User-Subject, X-User-Role, X-User-Unit, atau X-User-Level. Tempatkan API ini di balik salah satu pola berikut:

  • backend-for-frontend same-origin yang menyuntikkan API key di sisi server;
  • API gateway yang menyuntikkan shared key setelah mengautentikasi pengguna;
  • layanan upload/admin di sisi server.

Tidak ada kontrak CORS tingkat aplikasi. Jangan mengandalkan panggilan browser lintas origin kecuali gateway Anda menangani CORS dan injeksi kredensial.

Tipe dan batas bersama​

Enum tipe sumber​

Penerimaan upload, batch, dan URL menerima enum tipe sumber berikut:

type DocumentSourceType = "JDIH" | "OpenData" | "PPID" | "SatuData" | "upload";

Jika tidak disertakan, source_type default ke "upload".

source_version bersifat opsional untuk penerimaan upload, batch, dan URL. Jika tidak disertakan pada ketiganya, default service adalah "m0-upload". Ingestion teks inline berbeda: POST /documents/txt memiliki source_version dalam JSON dengan default "m0".

Ekstensi dan tipe MIME yang didukung​

Semua path yang berhubungan dengan file menggunakan ekstensi nama file untuk memilih format dokumen. Upload multipart juga memvalidasi tipe MIME yang dikirimkan.

Format dalam responsEkstensiTipe MIME multipart yang diterima
txt.txttext/plain, application/octet-stream
md.mdtext/markdown, text/plain, application/octet-stream
html.htmltext/html, application/xhtml+xml
csv.csvtext/csv, application/csv, application/vnd.ms-excel, application/octet-stream
docx.docxapplication/vnd.openxmlformats-officedocument.wordprocessingml.document
xlsx.xlsxapplication/vnd.openxmlformats-officedocument.spreadsheetml.sheet
pptx.pptxapplication/vnd.openxmlformats-officedocument.presentationml.presentation
pdf.pdfapplication/pdf
image.png, .jpg, .jpeg, .tif, .tiff, .webp, .bmpimage/png, image/jpeg, image/tiff, image/webp, image/bmp

Untuk job batch dan URL, content_type awal dalam respons dokumen adalah application/octet-stream; key object atau ekstensi URL tetap menentukan format.

PDF born-digital yang berisi tabel diparse melalui rute struktur tabel selektif agar content tabel dipertahankan, bukan diratakan menjadi rangkaian sel yang sulit dibaca (#203). Ini memengaruhi teks yang diekstrak, di-chunk, dan diindeks; hal ini tidak mengubah kontrak request atau respons di halaman ini.

Batas default​

BatasDefaultBerlaku untuk
Per file yang di-upload10 MiBPOST /documents; batas stream fetch URL menggunakan pengaturan byte per file yang sama
Total body batch multipart25 MiBPOST /documents
File per upload multipart20POST /documents
URL per request20Skema request POST /ingestion/url
Entri arsip10.000Dokumen mirip arsip sebelum parse/OCR
Byte arsip tanpa kompresi100 MiBDokumen mirip arsip sebelum parse/OCR
Rasio kompresi arsip100.0Dokumen mirip arsip sebelum parse/OCR
Redirect URL3Fetch worker untuk ingestion URL
Timeout koneksi URL10 detikFetch worker untuk ingestion URL
Timeout pembacaan URL30 detikFetch worker untuk ingestion URL

POST /ingestion/batch tidak menerapkan batas jumlah file multipart atau byte body karena endpoint ini mengirim referensi objek, bukan byte dokumen.

Model status job dan dokumen​

Respons job upload​

POST /documents, POST /ingestion/batch, POST /ingestion/url, dan GET /ingestion/jobs/{job_id} mengembalikan bentuk berikut:

{
"job_id": "job-1b2c3d4e...",
"status": "queued",
"documents": [
{
"id": "doc-1b2c3d4e...",
"filename": "qanun-aceh.pdf",
"title": "qanun-aceh",
"content_type": "application/pdf",
"format": "pdf",
"content_sha256": "64-character lowercase sha256 hex",
"object_ref": "s3://bucket/uploads/doc-.../qanun-aceh.pdf",
"status": "queued",
"detail": null,
"stored_document_id": null,
"chunk_count": 0,
"indexed_chunk_count": 0,
"parked_chunk_count": 0
}
],
"created_at": "2026-07-23T10:15:30.123456+00:00",
"updated_at": "2026-07-23T10:15:30.123456+00:00"
}

Field respons dokumen​

FieldTipeMakna
idstringID dokumen untuk pelacakan upload. Gunakan dengan GET /documents/{document_id}.
filenamestringBasename yang telah disanitasi. Pemisah path dan karakter nama file yang tidak aman tidak dipertahankan.
titlestringStem nama file, atau nama file jika stem tidak tersedia.
content_typestringTipe MIME multipart, atau application/octet-stream untuk record penerimaan batch/URL.
formatstringSalah satu dari txt, md, html, csv, docx, xlsx, pptx, pdf, image.
content_sha256stringSHA-256 yang digunakan untuk idempotensi worker. Untuk upload langsung, nilai ini adalah hash byte file; untuk record antrean batch dan URL, nilai ini melakukan hash seed key object atau URL sampai worker membaca byte.
object_refstringReferensi objek tersimpan, key object, atau URL sumber bergantung pada rute penerimaan. Perlakukan sebagai metadata tampilan/debug, bukan URL download.
statusUploadStatusStatus siklus hidup dokumen saat ini. Lihat status di bawah.
detailstring | nullAlasan kegagalan atau percobaan ulang yang dapat dibaca manusia jika tersedia. Selalu tampilkan untuk state gagal/review.
stored_document_idstring (UUID) | nullUUID dokumen searchable/reviewable yang dipersistenkan setelah ingestion worker membuatnya. Null sebelum persistensi atau pada kegagalan yang dilewati saat enqueue.
chunk_countnumberJumlah chunk yang dilaporkan worker untuk dokumen ini, termasuk chunk yang diparkir oleh confidence gate OCR. 0 valid, khususnya untuk queued, kegagalan, dan beberapa kasus needs_review.
indexed_chunk_countnumberChunk yang memiliki embedding dan dapat menjawab query. Nilai ini, bukan chunk_count atau status, yang menunjukkan apakah dokumen menjadi retrievable.
parked_chunk_countnumberChunk yang diparkir confidence gate OCR (ADR-0005) untuk review manusia. Chunk dipersistenkan tanpa embedding dan tidak pernah menjawab query.

Status upload​

type UploadStatus =
| "queued"
| "processing"
| "retrying"
| "done"
| "needs_review"
| "skipped"
| "failed"
| "permanently_failed";
StatusTerminal?Penanganan UI
queuedTidakDiterima dan menunggu worker. Lanjutkan polling.
processingTidakWorker sudah dimulai. Lanjutkan polling.
retryingTidakError sementara; worker akan mencoba lagi. Tampilkan detail jika ada dan lanjutkan polling.
doneYaPemrosesan selesai. stored_document_id mengidentifikasi dokumen tersimpan dan chunk_count melaporkan chunk.
needs_reviewYaReview manusia diperlukan karena gate OCR/sinyal rendah memarkir setidaknya satu chunk. Parking dapat parsial: baca indexed_chunk_count untuk melihat apakah sisa dokumen masih retrievable. Arahkan ke review; jangan menunggu done selamanya. Tampilkan dokumen anak dengan status ini.
skippedYaTerminal dan wajar, tidak ada embedding ulang. Ada dua makna, keduanya skipped: duplikat byte-identik yang dideduplikasi (lihat Deduplicasi content), atau sumber Open Data / Satu Data terstruktur yang diambil oleh tool saat query, bukan di-embed. Tampilkan detail (misalnya already done); perlakukan sebagai berhasil tanpa indexing baru.
failedYaGagal atau dilewati. Tampilkan detail. Skip ekstensi yang tidak didukung pada batch dilaporkan dengan cara ini.
permanently_failedYaKegagalan terminal worker setelah penanganan retry/dead-letter. Tampilkan detail dan izinkan remediasi operator/pengguna.

Deduplicasi content​

Ingestion menggunakan deduplikasi checksum content (#129, dengan backstop unique-index PostgreSQL otoritatif untuk writer konkurensi, #222). Ketika dokumen yang content_sha256 dan source_version-nya sudah ada dikirim ulang, worker tidak melakukan embedding ulang; dokumen anak mencapai status terminal skipped dengan detail seperti already done, dan dokumen tersimpan asli dibiarkan tanpa perubahan. Dengan demikian, upload ulang yang byte-identik aman dan idempoten, bukan menghasilkan entri corpus duplikat. Deduplicasi didasarkan pada byte content yang tepat plus source_version: byte yang sama yang diingest dengan source_version berbeda diperlakukan sebagai dokumen berbeda.

Status job agregat​

Status job dihitung dari status dokumen anak:

  1. Jika semua anak done, job menjadi done.
  2. Jika tidak, status pertama yang cocok dalam precedence berikut menang: permanently_failed, failed, needs_review, processing, retrying.
  3. Jika tidak ada status tersebut, job tetap queued.

Karena status agregat sengaja konservatif, selalu periksa dan render documents[]. Job dapat berisi campuran dokumen berhasil, gagal, dilewati, perlu review, dan masih aktif. status agregat hanya ringkasan: dapat melaporkan failed, needs_review, atau permanently_failed sementara sibling dengan precedence lebih rendah masih processing atau retrying. Logika penyelesaian frontend harus menunggu status setiap dokumen anak terminal sebelum keluar dari loop polling, lalu bercabang dari dokumen anak:

  • arahkan anak needs_review ke review manusia;
  • tampilkan anak failed dan permanently_failed beserta teks detail-nya;
  • tampilkan anak yang berhasil beserta stored_document_id dan indexed_chunk_count.

POST /documents/txt​

Ingestion teks inline sinkron. Rute ini tidak membuat job upload dan tidak mengembalikan ID dokumen untuk pelacakan upload.

JSON request​

{
"title": "Qanun Aceh example",
"text": "Full text to index...",
"source_version": "m0"
}
FieldWajibTipeValidasi/default
titleYastringPanjang minimum 1
textYastringPanjang minimum 1; harus berisi teks yang dapat diindeks
source_versionTidakstringPanjang minimum 1; default "m0"

Response 200​

{
"document_id": "00000000-0000-0000-0000-000000000123",
"chunk_count": 4,
"status": "indexed"
}

status adalah status dokumen tersimpan untuk ingestion inline, bukan UploadStatus. Pada ingestion teks yang berhasil secara normal, nilainya indexed. Kosakata status dokumen tersimpan adalah pending, processing, indexed, needs_review, failed, archived: dokumen yang semua chunk-nya ditahan oleh gate OCR/low-signal menyimpan needs_review (ingest selesai yang menunggu review manusia), bukan processing selamanya. Nilai yang sama memfilter GET /corpus/documents?status=.

Error​

StatusBentukKapan
400{ "detail": "..." }Teks dokumen kosong atau tidak dapat diindeks
401{ "detail": "Invalid or missing API key" }API key tidak ada atau salah
422Array validasi FastAPIField tidak ada, string kosong, tipe JSON salah
502{ "detail": "..." }Kegagalan klien model/embedding

curl​

curl -sS -X POST "$API_BASE/documents/txt" \
-H "X-API-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"title": "Qanun Aceh example",
"text": "Full text to index...",
"source_version": "m0"
}'

POST /documents​

Upload multipart untuk satu atau beberapa file lokal. API menyimpan byte, membuat satu job, mengantrikan setiap file, dan segera mengembalikan ID yang dapat dipolling.

Field form​

FieldWajibTipeValidasi/default
filesYafield file berulang1-20 file; ekstensi yang didukung dan tipe MIME multipart; setiap file ≤ 10 MiB; batch gabungan ≤ 25 MiB
source_typeTidakDocumentSourceTypeDefault "upload"
source_versionTidakstringPanjang minimum 1 jika ada; default service "m0-upload"

Gunakan nama field yang sama, files, untuk setiap bagian file yang di-upload.

Response 200​

Mengembalikan UploadJobResponse. Status dokumen anak awal biasanya queued.

Error​

StatusBentukKapan
400{ "detail": "..." }Terlalu banyak file, batch terlalu besar, file terlalu besar, nama file tidak aman/tidak ada, ekstensi tidak didukung, tipe MIME tidak didukung, pelanggaran batas arsip
401{ "detail": "Invalid or missing API key" }API key tidak ada atau salah
422Array validasi FastAPIField files tidak ada, body multipart tidak valid, source_type tidak valid, source_version kosong
503{ "detail": "Object storage is unavailable; the upload was not stored." }Kegagalan object storage sebelum enqueue selesai

curl​

curl -sS -X POST "$API_BASE/documents" \
-H "X-API-Key: $API_KEY" \
-F "source_type=upload" \
-F "source_version=pilot-2026-07" \
-F "files=@./qanun-aceh.pdf;type=application/pdf" \
-F "files=@./lampiran.csv;type=text/csv"

POST /ocr​

Ekstraksi khusus OCR dari satu PDF atau image yang di-upload. Rute ini menjalankan parse yang sama lalu fallback OCR yang digunakan ingestion, kemudian mengembalikan teks yang diekstrak dan confidence OCR, tetapi berhenti sebelum embedding atau indexing, sehingga tidak ada yang ditambahkan ke corpus. Gunakan untuk mengevaluasi kualitas OCR pada dokumen secara mandiri, terpisah dari kualitas retrieval dan jawaban, atau untuk melihat pratinjau hasil OCR sebelum memutuskan melakukan ingestion.

Jika text layer born-digital yang dapat digunakan dipertahankan, ocr_performed bernilai false dan field confidence bernilai null. Adanya text layer tetap dapat memicu OCR; lihat aturan routing PDF dan kompromi untuk dokumen dengan sedikit teks. Dokumen yang diproses dengan OCR membawa skor confidence.

Request​

multipart/form-data dengan satu bagian file bernama file. Autentikasi dan batas ukuran sama seperti POST /documents.

FieldWajibTipeValidasi
fileYafileSatu file. Content type yang diterima adalah application/pdf, image/png, image/jpeg, image/webp. Setiap file ≤ 10 MiB (batas byte per file yang sama seperti POST /documents).

Response 200​

{
"format": "image",
"text": "PEMERINTAH ACEH\nKEPUTUSAN ...",
"ocr_performed": true,
"model": "PaddleOCR-VL-0.9B",
"mean_confidence": 0.94,
"min_confidence": 0.71,
"confidence_scores_comparable_with_prod": true,
"confidence_threshold": 0.6,
"review_required": false,
"blocks": [
{
"text": "PEMERINTAH ACEH",
"confidence": 0.98,
"block_type": "title",
"page_number": 1
},
{
"text": "KEPUTUSAN ...",
"confidence": 0.71,
"block_type": "paragraph",
"page_number": 1
}
]
}
FieldTipeMakna
formatstringFormat dokumen yang terdeteksi, misalnya pdf atau image.
textstringTeks lengkap yang diekstrak dari dokumen.
ocr_performedbooleantrue ketika OCR dijalankan; false ketika text layer born-digital digunakan.
modelstring | nullIdentifier model OCR ketika OCR dijalankan; null ketika ocr_performed bernilai false.
mean_confidencenumber | nullConfidence OCR rata-rata per block; null ketika OCR tidak dijalankan.
min_confidencenumber | nullConfidence OCR terendah per block; null ketika OCR tidak dijalankan.
confidence_scores_comparable_with_prodboolean | nullApakah skor ini dapat dibandingkan dengan versi model OCR production (perubahan versi model OCR membuat skor lama dan baru tidak dapat dibandingkan); null ketika OCR tidak dijalankan.
confidence_thresholdnumberAmbang confidence gate (ADR-0005). Skor di bawahnya menandai ekstraksi untuk review manusia.
review_requiredbooleantrue ketika ekstraksi berada di bawah confidence gate dan harus diarahkan ke review manusia sebelum dipercaya.
blocksarray blockEkstraksi per block. Setiap block memiliki text, confidence (number, atau null untuk block born-digital yang diparse tanpa OCR), block_type (misalnya title, paragraph, table), dan page_number (integer atau null).

Rute ini hanya mengekstrak dan memberi skor; rute ini tidak pernah menulis ke corpus. Untuk mengingesti dokumen, gunakan POST /documents, POST /ingestion/batch, atau POST /ingestion/url.

Error​

StatusBentukKapan
400{ "detail": "..." }Tipe file tidak didukung (gunakan PNG, JPEG, WEBP, atau PDF), atau tidak ada teks yang dapat diekstrak dari dokumen
401{ "detail": "Invalid or missing API key" }API key tidak ada atau salah
413{ "detail": "..." }File melebihi batas ukuran per file
502{ "detail": "..." }Backend OCR tidak tersedia. Pesan hanya menyebut role yang gagal (OCR) dan tidak pernah menyebut host model internal, sehingga pemanggil tidak dapat menginventarisasi endpoint inferensi privat; diagnosis lengkap masuk ke log server.

curl​

curl -sS -X POST "$API_BASE/ocr" \
-H "X-API-Key: $API_KEY" \
-F "file=@./scan.pdf;type=application/pdf"

POST /ingestion/batch​

Penerimaan batch mereferensikan objek yang sudah ada dalam object storage yang dikonfigurasi. Request mengirim salah satu prefix untuk dipindai atau manifest key eksplisit. Byte dokumen tidak dikirim melalui request HTTP.

JSON request: pemindaian prefix​

{
"prefix": "corpus/jdih/2026/",
"source_type": "JDIH",
"source_version": "jdih-2026-07"
}

JSON request: key eksplisit​

{
"keys": [
"corpus/jdih/2026/qanun-1.pdf",
"corpus/jdih/2026/qanun-2.docx"
],
"source_type": "JDIH",
"source_version": "jdih-2026-07"
}
FieldWajibTipeValidasi/default
prefixWajib jika keys tidak adastring | nullPanjang minimum 1 saat validasi JSON; prefix yang hanya whitespace ditolak oleh validasi service; harus cocok dengan setidaknya satu objek
keysWajib jika prefix tidak adastring[] | nullTidak boleh kosong; key duplikat dideduplikasi sesuai urutan kemunculan pertama
source_typeTidakDocumentSourceTypeDefault "upload"
source_versionTidakstring | nullPanjang minimum 1 jika ada; default service "m0-upload"

Validasi prefix-vs-keys​

Sediakan tepat satu dari prefix atau keys:

  • keduanya tidak ada: error validasi;
  • keduanya ada: error validasi;
  • keys: []: error validasi;
  • prefix: "": error validasi;
  • prefix: " ": 400 dengan Prefix must not be empty;
  • prefix tanpa objek yang cocok: 400 dengan No objects found under prefix: ....

Ekstensi objek yang tidak didukung tidak menolak seluruh batch. Ekstensi tersebut disertakan di documents[] sebagai record failed dengan detail seperti skipped: [filename] has an unsupported file extension, sedangkan objek yang didukung diantrikan.

Response 200​

Mengembalikan UploadJobResponse. Record anak batch menggunakan:

  • object_ref: key object;
  • content_type: application/octet-stream;
  • content_sha256: seed SHA-256 yang diturunkan dari key object sampai worker membaca byte.

Error​

StatusBentukKapan
400{ "detail": "..." }Prefix whitespace, tidak ada prefix yang cocok, kegagalan validasi batch tingkat service
401{ "detail": "Invalid or missing API key" }API key tidak ada atau salah
422Array validasi FastAPIBentuk JSON tidak valid, prefix dan keys keduanya tidak ada, keys kosong, source_type tidak valid, source_version kosong, field tambahan
503{ "detail": "Object storage is unavailable; the batch was not enqueued." }Dependency object storage list/read tidak tersedia saat enqueue

curl​

curl -sS -X POST "$API_BASE/ingestion/batch" \
-H "X-API-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"keys": ["corpus/jdih/qanun-1.pdf", "corpus/jdih/qanun-2.docx"],
"source_type": "JDIH",
"source_version": "jdih-2026-07"
}'

POST /ingestion/url​

Penerimaan URL mengantrikan satu atau beberapa URL dokumen yang ada di allow-list. API memvalidasi daftar URL dan membuat job; worker mengambil byte nanti. Tidak ada pengambilan jaringan selama request HTTP.

Feature flag dan allow-list​

Ingestion URL dinonaktifkan secara default. Setelah request diautentikasi dan valid menurut skema, endpoint yang dinonaktifkan mengembalikan 403 sebelum validasi URL atau enqueue di service:

{
"detail": "URL ingestion is disabled. Enable ACEH_RAG_URL_INGESTION__ENABLED and configure an allow-list to use this endpoint."
}

Ketika diaktifkan, host URL harus cocok dengan allow-list yang dikonfigurasi. Pencocokan tidak membedakan huruf besar-kecil. Entri biasa cocok secara tepat, misalnya jdih.acehprov.go.id. Entri yang diawali titik juga cocok dengan bare domain dan subdomain, misalnya .go.id cocok dengan go.id dan jdih.acehprov.go.id.

Hanya skema http dan https yang diterima. Basename path URL harus memiliki ekstensi dokumen yang didukung. Query string diabaikan ketika menentukan nama file.

Pada waktu fetch oleh worker, setiap hop redirect divalidasi ulang terhadap skema, allow-list, backstop IP yang di-resolve, jumlah redirect, dan ukuran stream. Kegagalan saat fetch memengaruhi status dokumen anak tersebut; kegagalan tidak mengubah respons HTTP yang sudah dibuat.

JSON request​

{
"urls": [
"https://jdih.acehprov.go.id/dokumen/qanun-aceh-1-2026.pdf"
],
"source_type": "JDIH",
"source_version": "jdih-2026-07"
}
FieldWajibTipeValidasi/default
urlsYastring[]1-20 item berdasarkan skema request; item kosong dipangkas/dibuang oleh validasi service; duplikat dideduplikasi sesuai urutan kemunculan pertama
source_typeTidakDocumentSourceTypeDefault "upload"
source_versionTidakstring | nullPanjang minimum 1 jika ada; default service "m0-upload"

Request bersifat all-or-nothing pada waktu enqueue: satu URL yang tidak valid menolak seluruh request dan tidak ada job yang dibuat.

Response 200​

Mengembalikan UploadJobResponse. Record anak URL menggunakan:

  • object_ref: URL yang dikirim;
  • content_type: application/octet-stream;
  • format: diturunkan dari ekstensi path URL;
  • content_sha256: seed SHA-256 yang diturunkan dari URL sampai worker mengambil byte.

Error​

StatusBentukKapan
400{ "detail": "..." }Kegagalan validasi URL kosong/tidak kosong, skema tidak didukung, host tidak ada/tidak valid, host di luar allow-list, tidak ada ekstensi yang dikenali, ekstensi tidak didukung, jumlah URL melebihi batas service setelah deduplikasi
401{ "detail": "Invalid or missing API key" }API key tidak ada atau salah
403{ "detail": "URL ingestion is disabled..." }Request terautentikasi dan valid skema ketika feature flag nonaktif
422Array validasi FastAPIurls tidak ada, lebih dari 20 item URL dikirim, source_type tidak valid, source_version kosong, field tambahan

Kegagalan redirect, DNS, internal-IP/metadata-IP, batas ukuran, timeout, atau transport saat fetch muncul kemudian pada dokumen anak yang terdampak sebagai failed atau permanently_failed dengan detail terisi.

curl​

curl -sS -X POST "$API_BASE/ingestion/url" \
-H "X-API-Key: $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"urls": ["https://jdih.acehprov.go.id/dokumen/qanun-aceh-1-2026.pdf"],
"source_type": "JDIH",
"source_version": "jdih-2026-07"
}'

GET /ingestion/jobs/{job_id}​

Melakukan polling terhadap satu job ingestion upload, batch, atau URL.

Parameter path​

ParameterTipeMakna
job_idstringID job yang dikembalikan oleh POST /documents, POST /ingestion/batch, atau POST /ingestion/url

Response 200​

Mengembalikan UploadJobResponse saat ini. Server memperbarui state dokumen dari percobaan worker sebelum merespons.

Error​

StatusBentukKapan
401{ "detail": "Invalid or missing API key" }API key tidak ada atau salah
404{ "detail": "Ingestion job not found" }ID job tidak dikenal

Penanganan terminal saat polling​

Gunakan status anak untuk UI dan kontrol loop polling. status job agregat hanya ringkasan; needs_review adalah terminal untuk satu dokumen.

const TERMINAL_UPLOAD_STATUSES = new Set([
"done",
"needs_review",
"skipped",
"failed",
"permanently_failed",
]);

function isTerminalDocument(doc: UploadDocumentResponse): boolean {
return TERMINAL_UPLOAD_STATUSES.has(doc.status);
}

function isTerminalJob(job: UploadJobResponse): boolean {
return job.documents.every(isTerminalDocument);
}

function childFailures(job: UploadJobResponse): UploadDocumentResponse[] {
return job.documents.filter((doc) =>
doc.status === "failed" || doc.status === "permanently_failed"
);
}

function reviewDocuments(job: UploadJobResponse): UploadDocumentResponse[] {
return job.documents.filter((doc) => doc.status === "needs_review");
}

Jika polling timeout di UI Anda, pertahankan respons terakhir agar tetap terlihat. Jangan mengubah timeout polling lokal menjadi kegagalan server.

GET /documents​

Mencantumkan record dokumen untuk pelacakan upload yang diketahui oleh service upload/job.

Parameter query​

ParameterTipeWajibMakna
statusUploadStatusTidakJika ada, hanya mengembalikan dokumen dengan status tersebut. Nilai tidak valid mengembalikan 422.

Contoh:

GET /documents
GET /documents?status=needs_review
GET /documents?status=failed

Response 200​

[
{
"id": "doc-1b2c3d4e...",
"filename": "scan.pdf",
"title": "scan",
"content_type": "application/pdf",
"format": "pdf",
"content_sha256": "64-character lowercase sha256 hex",
"object_ref": "s3://bucket/uploads/doc-.../scan.pdf",
"status": "needs_review",
"detail": null,
"stored_document_id": "00000000-0000-0000-0000-000000000123",
"chunk_count": 4,
"indexed_chunk_count": 3,
"parked_chunk_count": 1
}
]

Gunakan rute ini untuk dashboard upload, daftar kegagalan, dan antrean review. Untuk mengarahkan ke review manusia, filter status=needs_review dan gunakan stored_document_id jika ada.

Error​

StatusBentukKapan
401{ "detail": "Invalid or missing API key" }API key tidak ada atau salah
422Array validasi FastAPIEnum query status tidak valid

GET /documents/{document_id}​

Mengambil satu record dokumen untuk pelacakan upload berdasarkan field id dari UploadDocumentResponse.

Parameter path​

ParameterTipeMakna
document_idstringID dokumen untuk pelacakan upload, biasanya berbentuk doc-...

Response 200​

Mengembalikan satu objek UploadDocumentResponse.

Error​

StatusBentukKapan
401{ "detail": "Invalid or missing API key" }API key tidak ada atau salah
404{ "detail": "Document not found" }ID dokumen tidak dikenal

DELETE /documents/{document_id}​

Menghapus permanen dokumen terindeks dan tidak dapat dibatalkan. Rute ini menggunakan UUID dokumen searchable yang tersimpan (documents.id), bukan id untuk pelacakan upload yang dikembalikan POST /documents (biasanya doc-...).

Autentikasi dan otorisasi​

Rute memerlukan header API key yang dikonfigurasi (default-nya X-API-Key). Pada deployment dengan require_auth=true, rute juga memerlukan principal terverifikasi dengan role admin; principal terverifikasi operator dan user menerima 403. Principal terautentikasi yang tidak ada atau tidak valid menerima 401. Dengan require_auth=false (default pada codebase ini), role tidak ditegakkan dan path principal development/default digunakan.

Perilaku penghapusan​

Service menghapus setiap chunk yang terkait dengan dokumen dan baris documents dalam satu transaksi database, sehingga dokumen dihapus dari retrieval semantik dan leksikal. Setelah transaksi tersebut commit, service menghapus objek mentah yang di-upload jika tercatat. Objek mentah yang tidak ada diperlakukan sebagai no-op; dokumen tanpa objek mentah tetap selesai setelah baris indeksnya dihapus. Penghapusan diaudit dengan actor, ID dokumen, dan hasil, tanpa content dokumen atau kredensial.

Untuk unggahan milik workspace, lihat penghapusan workspace.

500 dengan pesan raw-object-cleanup di bawah memastikan index sudah dihapus: retry mengembalikan 404; eskalasikan objek mentah yang yatim kepada operator. 500 generik berarti hasilnya belum pasti. Rekonsiliasi melalui GET /corpus/documents, lalu coba ulang hanya jika dokumen masih tercantum. Kegagalan yang dipastikan terjadi sebelum commit database membiarkan dokumen tetap terindeks, sehingga aman dicoba ulang.

Response 204​

Pada keberhasilan penuh, termasuk penghapusan objek mentah yang berhasil atau tidak diperlukan, respons memiliki status 204 dan tanpa body. Mengulangi penghapusan setelah dokumen hilang mengembalikan 404.

Error​

StatusBentukKapan
401{ "detail": "Invalid or missing API key" }API key tidak ada atau salah
401{ "detail": "Authenticated principal required" }Principal terautentikasi tidak ada ketika require_auth=true
401{ "detail": "Invalid authenticated principal" }Identitas terautentikasi tidak valid ketika require_auth=true, atau subject yang ada bukan UUID
403{ "detail": "admin role required" }Principal terverifikasi bukan admin ketika require_auth=true
404{ "detail": "Document not found" }ID dokumen tidak dikenal, tidak valid, atau sudah dihapus; ID tidak valid sengaja dibuat tidak dapat dibedakan dari ID tidak dikenal
500{ "detail": "Document removed from the index but its raw object could not be deleted" }Pembersihan database berhasil, tetapi penghapusan objek mentah gagal; indeks bersih, objek mentah menjadi yatim, dan kegagalan diaudit

Upload dan polling dari TypeScript​

Jalankan ini dari BFF sisi server tepercaya atau layanan yang berdekatan dengan gateway. Jangan mengekspos API_KEY ke JavaScript browser.

type DocumentSourceType = "JDIH" | "OpenData" | "PPID" | "SatuData" | "upload";
type UploadStatus =
| "queued"
| "processing"
| "retrying"
| "done"
| "needs_review"
| "skipped"
| "failed"
| "permanently_failed";

type UploadDocumentResponse = {
id: string;
filename: string;
title: string;
content_type: string;
format: "txt" | "md" | "html" | "csv" | "docx" | "xlsx" | "pptx" | "pdf" | "image" | string;
content_sha256: string;
object_ref: string;
status: UploadStatus;
detail: string | null;
stored_document_id: string | null;
chunk_count: number;
indexed_chunk_count: number;
parked_chunk_count: number;
};

type UploadJobResponse = {
job_id: string;
status: UploadStatus;
documents: UploadDocumentResponse[];
created_at: string;
updated_at: string;
};

const TERMINAL = new Set<UploadStatus>([
"done",
"needs_review",
"skipped",
"failed",
"permanently_failed",
]);

async function apiFetch(path: string, init: RequestInit = {}) {
const response = await fetch(`${process.env.API_BASE}${path}`, {
...init,
headers: {
"X-API-Key": process.env.API_KEY!,
...init.headers,
},
});

if (!response.ok) {
const body = await response.json().catch(() => undefined);
throw new Error(
`${response.status} ${response.statusText}: ${JSON.stringify(body)}`
);
}

return response;
}

export async function uploadDocuments(files: File[]): Promise<UploadJobResponse> {
const form = new FormData();
form.set("source_type", "upload" satisfies DocumentSourceType);
form.set("source_version", "frontend-2026-07");
for (const file of files) form.append("files", file, file.name);

const response = await apiFetch("/documents", {
method: "POST",
body: form,
});
return (await response.json()) as UploadJobResponse;
}

export async function createBatchIngestion(input:
| { prefix: string; keys?: never; source_type?: DocumentSourceType; source_version?: string }
| { keys: string[]; prefix?: never; source_type?: DocumentSourceType; source_version?: string }
): Promise<UploadJobResponse> {
const response = await apiFetch("/ingestion/batch", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
...input,
source_type: input.source_type ?? "upload",
}),
});
return (await response.json()) as UploadJobResponse;
}

export async function createUrlIngestion(input: {
urls: string[];
source_type?: DocumentSourceType;
source_version?: string;
}): Promise<UploadJobResponse> {
const response = await apiFetch("/ingestion/url", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
...input,
source_type: input.source_type ?? "upload",
}),
});
return (await response.json()) as UploadJobResponse;
}

export async function pollIngestionJob(
jobId: string,
options: { intervalMs?: number; timeoutMs?: number } = {}
): Promise<UploadJobResponse> {
const intervalMs = options.intervalMs ?? 1_000;
const timeoutMs = options.timeoutMs ?? 120_000;
const deadline = Date.now() + timeoutMs;
let last: UploadJobResponse | undefined;

while (Date.now() < deadline) {
const response = await apiFetch(`/ingestion/jobs/${encodeURIComponent(jobId)}`);
const job = (await response.json()) as UploadJobResponse;
last = job;

// Always surface child-level outcomes; aggregate status is summary only and can
// be terminal while lower-precedence siblings are still active.
const failed = job.documents.filter(
(doc) => doc.status === "failed" || doc.status === "permanently_failed"
);
const needsReview = job.documents.filter((doc) => doc.status === "needs_review");
if (failed.length > 0) console.warn("Ingestion child failures", failed);
if (needsReview.length > 0) console.info("Documents need review", needsReview);

if (job.documents.every((doc) => TERMINAL.has(doc.status))) return job;
await new Promise((resolve) => setTimeout(resolve, intervalMs));
}

throw new Error(`Timed out waiting for ingestion job ${jobId}; last=${JSON.stringify(last)}`);
}

Validasi dan perilaku error​

Bentuk error​

Sebagian besar error non-validasi menggunakan:

{ "detail": "human-readable message" }

Error validasi request FastAPI menggunakan array dalam detail:

{
"detail": [
{
"loc": ["body", "urls"],
"msg": "Field required",
"type": "missing"
}
]
}

Bangun validasi UI dari tabel request di atas, tetapi tetap render nilai detail dari server karena batas dan allow-list dapat dikonfigurasi per deployment.

Checklist validasi sisi klien​

  • Wajibkan setidaknya satu file untuk upload multipart.
  • Periksa ekstensi file dan tipe MIME sebelum upload.
  • Terapkan default yang terlihat: 20 file, 10 MiB per file, batch multipart total 25 MiB.
  • Untuk penerimaan batch, wajibkan tepat satu dari prefix atau keys; jangan kirim keduanya.
  • Untuk penerimaan URL, wajibkan 1-20 string URL, http atau https, ekstensi yang didukung di path, dan daftar domain yang disetujui operator jika UI Anda dapat menampilkannya.
  • Perlakukan needs_review sebagai state terminal berhasil-dengan-tindakan, bukan kegagalan dan bukan masih diproses.
  • Tampilkan setiap dokumen anak dengan status failed, permanently_failed, atau needs_review meskipun status job agregat sudah terminal.

Halaman API terkait​

  • /docs/api/overview untuk konvensi base URL, OpenAPI, dan peta rute.
  • /docs/api/errors untuk penanganan error HTTP bersama.