The document module stores files/documents attached to product entities (charitable cause, commodity financing, investment, transaction) via the polymorphic product_type + product_id columns. The /v1/documents endpoint handles direct upload (multipart) or registers files already uploaded via the file-manager presigned flow. The /v1/official-documents endpoint stores official identities (passport, KTP, SIM, NPWP) separately because the lifecycle and fields differ.
Property Value Base URL {HOST}/v1Auth Bearer JWT (header Authorization) or cookie access_token Content-Type application/json (or multipart/form-data for direct upload)Error envelope { "message": string | string[], "statusCode": number, "error": string }Validation Global ValidationPipe · whitelist: true, forbidNonWhitelisted: true · unknown field → 400 Related modules file-manager, charitable-cause, commodity-financing, investments, transactions Document version v1 · 2026-05-20 Audience Internal FE devs (mobile + web)
Primary upload path: direct upload (POST /v1/documents/upload, multipart) — the server performs the S3 upload and stores metadata in one transaction. POST /v1/documents still exists for registering an object that was uploaded through another channel (side-channel); most FE callers don’t need it. Once registered, a presigned (expiring) view URL can be retrieved via /v1/documents/:id/view-url. Official identities (KTP, passport, SIM, NPWP) use the separate /v1/official-documents endpoint.
Method Path Auth Summary POST /v1/documents/uploadbearer upload-document Direct upload (multipart) — server-side to S3 POST /v1/documentsbearer create-document Register a document for an existing S3 object (side-channel) GET /v1/documentsbearer read-document List all documents GET /v1/documents/type/:typebearer read-document List documents by type GET /v1/documents/:idbearer read-document Detail of a single document GET /v1/documents/:id/view-urlbearer read-document Presigned URL to view document contents PATCH /v1/documents/:idbearer update-document Update document metadata DELETE /v1/documents/:idbearer delete-document Delete a document record POST /v1/official-documentsbearer create-official-document Create an official identity GET /v1/official-documentsbearer read-official-document List official identities GET /v1/official-documents/:idbearer read-official-document Detail of a single official identity PATCH /v1/official-documents/:idbearer update-official-document Update an official identity DELETE /v1/official-documents/:idbearer delete-official-document Delete an official identity
Notes
The user profile picture is not part of this module — it is stored directly as a string in users.profile_image. Only artifacts attached to a product live in the documents table.
The url field stores the S3 file-key (encrypted in DB). public_url in the response is the resolved URL for direct consumption.
The DTOs for create-official_document and update-official_document are currently empty in the repo — the service layer determines the actual fields. The endpoints are still documented because the routes are active.
Documents attach to a parent via two columns on the documents row: product_type (which kind of parent — charitable_cause, commodity_financing, investment, transaction) and product_id (the parent’s UUID). There is no photo_document_id on the parent — the relationship is reversed. The FE flow is therefore create parent first, then upload docs referencing the parent ID .
Key takeaways
Create the parent first (cause, financing, investment, transaction). The response contains data.id — that’s product_id for step 2.
Upload each document with that ID via POST /v1/documents/upload. Repeat per attachment (image, video, regulations PDF, signature, …). One multipart call per file.
On subsequent GETs of the parent , the server runs DocumentService.findByProductAndType(product_type, product_id) and embeds the matching docs in the response — FE does not need to call /documents separately.
To display a private doc later outside the parent context, call GET /v1/documents/:id/view-url for a presigned URL (TTL-limited). For public docs use public_url from the original response directly.
Profile picture is the exception — it’s a plain URL string on users.profile_image, not a row in documents. Don’t use this flow for profile photos.
Official identity docs (KTP, passport, SIM, NPWP) live in /v1/official-documents, a separate flow (not polymorphic — keyed by user).
Upload a file directly via multipart. The server performs the S3 upload and stores the document metadata in one transaction (atomic — if S3 fails, no DB row is created).
Field Type Required Notes filefile yes Binary file. Limit 50 MB. document_typeenum DocumentType yes legal_agreement, signature, cause_report, donation_certificate, receipt, image, videoproduct_typeenum ProductType yes charitable_cause, commodity_financing, investment, transactionproduct_idUUID yes ID of the parent row the document is attached to visibilityenum DocumentVisibility optional public / private (default private)
POST /v1/documents/upload
Content-Type: multipart/form-data; boundary=...
Content-Disposition: form-data; name="file"; filename="receipt.pdf"
Content-Type: application/pdf
Content-Disposition: form-data; name="document_type"
Content-Disposition: form-data; name="product_type"
Content-Disposition: form-data; name="product_id"
550e8400-e29b-41d4-a716-446655440000
"message" : " Document uploaded successfully " ,
"id" : " 550e8400-e29b-41d4-a716-446655440000 " ,
"url" : " documents/2026/receipt.pdf " ,
"document_type" : " receipt " ,
"category_id" : " 550e8400-e29b-41d4-a716-446655440000 " ,
"public_url" : " https://cdn.example.com/documents/receipt.pdf " ,
"created_by" : " 550e8400-e29b-41d4-a716-446655440000 " ,
"updated_by" : " 550e8400-e29b-41d4-a716-446655440000 " ,
"created_at" : " 2026-05-20T08:30:00.000Z " ,
"updated_at" : " 2026-05-20T08:30:00.000Z "
Status When 400 Bad RequestFile not provided, file > 50 MB, or DTO validation failed 401 UnauthorizedBearer/cookie invalid
Register a Documents row for an object that already exists in S3 (e.g. uploaded via a side-channel). Most FE callers should use POST /v1/documents/upload instead — this endpoint is only for special cases.
Field Type Required Notes urlstring yes S3 file key of an object that already exists in the bucket document_typeenum DocumentType yes See enum on the reference slide product_typeenum ProductType optional Parent kind product_idUUID optional UUID of the parent row
"url" : " legal-agreements/agreement-123.pdf " ,
"document_type" : " legal_agreement " ,
"product_type" : " charitable_cause " ,
"product_id" : " 550e8400-e29b-41d4-a716-446655440000 "
"message" : " document registered successfully " ,
"document" : { "..." : " same shape as the upload response " }
Status When 400 Bad Requesturl empty, unknown enum, invalid UUID401 UnauthorizedBearer/cookie invalid
List all documents visible to the user. Ownership filtering happens at the service layer.
"message" : " all documents fetched successfully " ,
"id" : " 550e8400-e29b-41d4-a716-446655440000 " ,
"url" : " documents/2026/receipt.pdf " ,
"document_type" : " receipt " ,
"category_id" : " 550e8400-e29b-41d4-a716-446655440000 " ,
"public_url" : " https://cdn.example.com/documents/receipt.pdf " ,
"created_by" : " 550e8400-e29b-41d4-a716-446655440000 " ,
"updated_by" : " 550e8400-e29b-41d4-a716-446655440000 " ,
"created_at" : " 2026-05-20T08:30:00.000Z " ,
"updated_at" : " 2026-05-20T08:30:00.000Z "
Status When 401 UnauthorizedBearer/cookie invalid
List documents filtered by document_type.
Param Type Notes typeenum DocumentType One of legal_agreement, signature, cause_report, donation_certificate, receipt, image, video
"message" : " documents fetched successfully " ,
Status When 401 UnauthorizedBearer/cookie invalid
Detail of a single document by ID.
Param Type Notes idUUID Document ID
"message" : " document fetched successfully " ,
"document" : { "..." : " DocumentDto shape " }
Status When 401 UnauthorizedBearer/cookie invalid 404 Not FoundDocument not found
Retrieve a presigned (expiring) S3 URL to display the document contents. Use response.data.view_url as the href for an image/iframe — the link is time-limited.
Param Type Notes idUUID Document ID
"message" : " presigned url to view document fetched successfully " ,
"document_id" : " 550e8400-e29b-41d4-a716-446655440000 " ,
"file_key" : " documents/2026/passport.pdf " ,
"view_url" : " https://s3.example.com/documents/passport.pdf?X-Amz-Signature=... "
Status When 401 UnauthorizedBearer/cookie invalid 404 Not FoundDocument not found
Update document metadata (e.g. move to another product, change document_type). All fields are optional — send only what changed.
Param Type Notes idUUID Document ID
Field Type Notes urlstring New file key document_typeenum DocumentType Change type product_typeenum ProductType Change parent kind product_idUUID Move to a different parent row
"document_type" : " donation_certificate "
"message" : " document updated successfully " ,
"document" : { "..." : " DocumentDto shape " }
Status When 401 UnauthorizedBearer/cookie invalid 404 Not FoundDocument not found
Delete a document record (soft delete — deleted_at + deleted_by are set).
Param Type Notes idUUID Document ID
"message" : " document removed successfully " ,
Status When 401 UnauthorizedBearer/cookie invalid 404 Not FoundDocument not found
Create an official identity record (passport, KTP, SIM, NPWP). The CreateOfficialDocumentDto is currently empty in the repo — the actual fields are determined by the service layer (likely accepts number, type, issue/expiry date, file_key). Confirm with backend before FE implementation.
The DTO is currently empty (no fields). Check the service layer for the actual field conventions.
"message" : " official document created " ,
"data" : { "..." : " shape determined by service " }
Status When 400 Bad RequestValidation failed 401 UnauthorizedBearer/cookie invalid
List all accessible official identities.
"message" : " official documents fetched " ,
"data" : { "official_documents" : [ " ... " ] }
Status When 401 UnauthorizedBearer/cookie invalid
Detail of a single official identity.
Param Type Notes idstring (numeric) Official identity ID. Service casts to +id (numeric).
"message" : " official document fetched " ,
"data" : { "official_document" : { "..." } }
Status When 401 UnauthorizedBearer/cookie invalid 404 Not FoundNot found
Update an official identity. UpdateOfficialDocumentDto = PartialType of the create DTO (currently empty).
Param Type Notes idstring (numeric) Official identity ID
"message" : " official document updated " ,
"data" : { "official_document" : { "..." } }
Delete an official identity.
Param Type Notes idstring (numeric) Official identity ID
"message" : " official document removed " ,
Status When 401 UnauthorizedBearer/cookie invalid 404 Not FoundNot found
legal_agreement
signature
cause_report
donation_certificate
receipt
image
video
charitable_cause
commodity_financing
investment
transaction
passport
national_id
driving_license
taxpayer_id
"message" : " Document not found " ,
message can be an array of strings for multi-field validation errors.
400 body/param validation
401 invalid Bearer
403 permission mismatch
404 document not found
413 file > 50 MB (direct upload)
500 internal — show a generic toast
Direct upload limit: 50 MB per file.
The S3 file key in the url column is encrypted at rest. The public_url field in the response is already resolved.