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
| Metode | Rute | Body | Mengembalikan | Gunakan untuk |
|---|---|---|---|---|
POST | /documents/txt | JSON | TxtIngestResponse | Ingestion teks inline berukuran kecil tanpa job |
POST | /documents | multipart/form-data | UploadJobResponse | File lokal yang dipilih pengguna |
POST | /ocr | multipart/form-data | OcrResponse | Ekstraksi khusus OCR dari satu file, tanpa indexing |
POST | /ingestion/batch | JSON | UploadJobResponse | Prefix object storage atau object key eksplisit |
POST | /ingestion/url | JSON | UploadJobResponse | URL dokumen yang ada di allow-list |
GET | /ingestion/jobs/{job_id} | tidak ada | UploadJobResponse | Polling job upload/batch/URL asinkron |
GET | /documents | query opsional | UploadDocumentResponse[] | Membuat list, filter, dashboard, dan antrean review |
GET | /documents/{document_id} | tidak ada | UploadDocumentResponse | Memeriksa satu record dokumen yang di-upload |
DELETE | /documents/{document_id} | tidak ada | 204 No Content | Menghapus 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 respons | Ekstensi | Tipe MIME multipart yang diterima |
|---|---|---|
txt | .txt | text/plain, application/octet-stream |
md | .md | text/markdown, text/plain, application/octet-stream |
html | .html | text/html, application/xhtml+xml |
csv | .csv | text/csv, application/csv, application/vnd.ms-excel, application/octet-stream |
docx | .docx | application/vnd.openxmlformats-officedocument.wordprocessingml.document |
xlsx | .xlsx | application/vnd.openxmlformats-officedocument.spreadsheetml.sheet |
pptx | .pptx | application/vnd.openxmlformats-officedocument.presentationml.presentation |
pdf | .pdf | application/pdf |
image | .png, .jpg, .jpeg, .tif, .tiff, .webp, .bmp | image/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
| Batas | Default | Berlaku untuk |
|---|---|---|
| Per file yang di-upload | 10 MiB | POST /documents; batas stream fetch URL menggunakan pengaturan byte per file yang sama |
| Total body batch multipart | 25 MiB | POST /documents |
| File per upload multipart | 20 | POST /documents |
| URL per request | 20 | Skema request POST /ingestion/url |
| Entri arsip | 10.000 | Dokumen mirip arsip sebelum parse/OCR |
| Byte arsip tanpa kompresi | 100 MiB | Dokumen mirip arsip sebelum parse/OCR |
| Rasio kompresi arsip | 100.0 | Dokumen mirip arsip sebelum parse/OCR |
| Redirect URL | 3 | Fetch worker untuk ingestion URL |
| Timeout koneksi URL | 10 detik | Fetch worker untuk ingestion URL |
| Timeout pembacaan URL | 30 detik | Fetch 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
| Field | Tipe | Makna |
|---|---|---|
id | string | ID dokumen untuk pelacakan upload. Gunakan dengan GET /documents/{document_id}. |
filename | string | Basename yang telah disanitasi. Pemisah path dan karakter nama file yang tidak aman tidak dipertahankan. |
title | string | Stem nama file, atau nama file jika stem tidak tersedia. |
content_type | string | Tipe MIME multipart, atau application/octet-stream untuk record penerimaan batch/URL. |
format | string | Salah satu dari txt, md, html, csv, docx, xlsx, pptx, pdf, image. |
content_sha256 | string | SHA-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_ref | string | Referensi objek tersimpan, key object, atau URL sumber bergantung pada rute penerimaan. Perlakukan sebagai metadata tampilan/debug, bukan URL download. |
status | UploadStatus | Status siklus hidup dokumen saat ini. Lihat status di bawah. |
detail | string | null | Alasan kegagalan atau percobaan ulang yang dapat dibaca manusia jika tersedia. Selalu tampilkan untuk state gagal/review. |
stored_document_id | string (UUID) | null | UUID dokumen searchable/reviewable yang dipersistenkan setelah ingestion worker membuatnya. Null sebelum persistensi atau pada kegagalan yang dilewati saat enqueue. |
chunk_count | number | Jumlah 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_count | number | Chunk yang memiliki embedding dan dapat menjawab query. Nilai ini, bukan chunk_count atau status, yang menunjukkan apakah dokumen menjadi retrievable. |
parked_chunk_count | number | Chunk 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";
| Status | Terminal? | Penanganan UI |
|---|---|---|
queued | Tidak | Diterima dan menunggu worker. Lanjutkan polling. |
processing | Tidak | Worker sudah dimulai. Lanjutkan polling. |
retrying | Tidak | Error sementara; worker akan mencoba lagi. Tampilkan detail jika ada dan lanjutkan polling. |
done | Ya | Pemrosesan selesai. stored_document_id mengidentifikasi dokumen tersimpan dan chunk_count melaporkan chunk. |
needs_review | Ya | Review 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. |
skipped | Ya | Terminal 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. |
failed | Ya | Gagal atau dilewati. Tampilkan detail. Skip ekstensi yang tidak didukung pada batch dilaporkan dengan cara ini. |
permanently_failed | Ya | Kegagalan 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:
- Jika semua anak
done, job menjadidone. - Jika tidak, status pertama yang cocok dalam precedence berikut menang:
permanently_failed,failed,needs_review,processing,retrying. - 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_reviewke review manusia; - tampilkan anak
faileddanpermanently_failedbeserta teksdetail-nya; - tampilkan anak yang berhasil beserta
stored_document_iddanindexed_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"
}
| Field | Wajib | Tipe | Validasi/default |
|---|---|---|---|
title | Ya | string | Panjang minimum 1 |
text | Ya | string | Panjang minimum 1; harus berisi teks yang dapat diindeks |
source_version | Tidak | string | Panjang 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
| Status | Bentuk | Kapan |
|---|---|---|
400 | { "detail": "..." } | Teks dokumen kosong atau tidak dapat diindeks |
401 | { "detail": "Invalid or missing API key" } | API key tidak ada atau salah |
422 | Array validasi FastAPI | Field 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
| Field | Wajib | Tipe | Validasi/default |
|---|---|---|---|
files | Ya | field file berulang | 1-20 file; ekstensi yang didukung dan tipe MIME multipart; setiap file ≤ 10 MiB; batch gabungan ≤ 25 MiB |
source_type | Tidak | DocumentSourceType | Default "upload" |
source_version | Tidak | string | Panjang 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
| Status | Bentuk | Kapan |
|---|---|---|
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 |
422 | Array validasi FastAPI | Field 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.
| Field | Wajib | Tipe | Validasi |
|---|---|---|---|
file | Ya | file | Satu 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
}
]
}
| Field | Tipe | Makna |
|---|---|---|
format | string | Format dokumen yang terdeteksi, misalnya pdf atau image. |
text | string | Teks lengkap yang diekstrak dari dokumen. |
ocr_performed | boolean | true ketika OCR dijalankan; false ketika text layer born-digital digunakan. |
model | string | null | Identifier model OCR ketika OCR dijalankan; null ketika ocr_performed bernilai false. |
mean_confidence | number | null | Confidence OCR rata-rata per block; null ketika OCR tidak dijalankan. |
min_confidence | number | null | Confidence OCR terendah per block; null ketika OCR tidak dijalankan. |
confidence_scores_comparable_with_prod | boolean | null | Apakah 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_threshold | number | Ambang confidence gate (ADR-0005). Skor di bawahnya menandai ekstraksi untuk review manusia. |
review_required | boolean | true ketika ekstraksi berada di bawah confidence gate dan harus diarahkan ke review manusia sebelum dipercaya. |
blocks | array block | Ekstraksi 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
| Status | Bentuk | Kapan |
|---|---|---|
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"
}
| Field | Wajib | Tipe | Validasi/default |
|---|---|---|---|
prefix | Wajib jika keys tidak ada | string | null | Panjang minimum 1 saat validasi JSON; prefix yang hanya whitespace ditolak oleh validasi service; harus cocok dengan setidaknya satu objek |
keys | Wajib jika prefix tidak ada | string[] | null | Tidak boleh kosong; key duplikat dideduplikasi sesuai urutan kemunculan pertama |
source_type | Tidak | DocumentSourceType | Default "upload" |
source_version | Tidak | string | null | Panjang 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: " ":400denganPrefix must not be empty;- prefix tanpa objek yang cocok:
400denganNo 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
| Status | Bentuk | Kapan |
|---|---|---|
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 |
422 | Array validasi FastAPI | Bentuk 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"
}
| Field | Wajib | Tipe | Validasi/default |
|---|---|---|---|
urls | Ya | string[] | 1-20 item berdasarkan skema request; item kosong dipangkas/dibuang oleh validasi service; duplikat dideduplikasi sesuai urutan kemunculan pertama |
source_type | Tidak | DocumentSourceType | Default "upload" |
source_version | Tidak | string | null | Panjang 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
| Status | Bentuk | Kapan |
|---|---|---|
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 |
422 | Array validasi FastAPI | urls 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
| Parameter | Tipe | Makna |
|---|---|---|
job_id | string | ID 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
| Status | Bentuk | Kapan |
|---|---|---|
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
| Parameter | Tipe | Wajib | Makna |
|---|---|---|---|
status | UploadStatus | Tidak | Jika 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
| Status | Bentuk | Kapan |
|---|---|---|
401 | { "detail": "Invalid or missing API key" } | API key tidak ada atau salah |
422 | Array validasi FastAPI | Enum query status tidak valid |
GET /documents/{document_id}
Mengambil satu record dokumen untuk pelacakan upload berdasarkan field id dari UploadDocumentResponse.
Parameter path
| Parameter | Tipe | Makna |
|---|---|---|
document_id | string | ID dokumen untuk pelacakan upload, biasanya berbentuk doc-... |
Response 200
Mengembalikan satu objek UploadDocumentResponse.
Error
| Status | Bentuk | Kapan |
|---|---|---|
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
| Status | Bentuk | Kapan |
|---|---|---|
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
prefixataukeys; jangan kirim keduanya. - Untuk penerimaan URL, wajibkan 1-20 string URL,
httpatauhttps, ekstensi yang didukung di path, dan daftar domain yang disetujui operator jika UI Anda dapat menampilkannya. - Perlakukan
needs_reviewsebagai state terminal berhasil-dengan-tindakan, bukan kegagalan dan bukan masih diproses. - Tampilkan setiap dokumen anak dengan status
failed,permanently_failed, atauneeds_reviewmeskipun status job agregat sudah terminal.
Halaman API terkait
/docs/api/overviewuntuk konvensi base URL, OpenAPI, dan peta rute./docs/api/errorsuntuk penanganan error HTTP bersama.