Guide to Integrating S3/MinIO Storage API
Documentation for S3/MinIO storage API integration following S3-Compatible standards: endpoint configuration, AWS Signature V4 authentication, presigned URLs, AWS CLI, MinIO Client, Node.js/PHP SDKs, CORS, security policies, and handover checklist.
1. Overview
MinIO is an object storage system compatible with Amazon S3. Client applications can upload,
download, list, delete files, and generate temporary download links via S3 API.
Important note:requests called directly to the API must be signed using AWS Signature Version 4.
Do not use standard Basic Auth or Bearer Tokens for S3 APIs.
Key concepts
- Bucket: file container, similar to a root folder.
- Object: file stored in the bucket.
- Object key: the file path, for example invoices/2026/INV001.pdf.
- Presigned URL: A time-limited link for uploading or downloading without exposing secret keys.
Common usage flow
- Backend system receives files from users.
- The backend uploads the file to MinIO/S3 using the access key and secret key.
- The backend records the bucket, object key, mime type, and file size in the database.
- When downloading a file is needed, the backend generates a presigned URL and returns it to the client.
2. Connection information
Replace the sample values below with real information before handing over to the client.
| Information | Sample value | Note |
|---|---|---|
| API Endpoint | https://s3.example.com | Endpoint used for S3 API. HTTPS should be enabled. |
| Console Endpoint | https://console-s3.example.com | MinIO admin panel, granted only to authorized operators. |
| Region | us-east-1 | MinIO usually uses this value if no specific region is configured. |
| Access Key | Provided privately via secure channels | Do not send together in public documents. |
| Secret Key | Provided privately via secure channels | Do not commit to source code, do not send via public chat. |
| Default bucket | customer-files | Buckets can be separated by environment: dev, staging, production. |
| Path style | true | Should enable forcePathStyle when using MinIO with AWS SDK. |
Recommended object key structure
{module}/{yyyy}/{mm}/{uuid}-{safe-file-name}Example:
orders/2026/06/8f3c9d7a-invoice-001.pdf
avatars/2026/06/user-1024.png
backups/2026/06/database-2026-06-20.sql.gz
Handover specifications to finalize before integration
| Group | Value to provide | Actual entry example |
|---|---|---|
| Production environment | Endpoint, bucket, region, file size limit | https://s3.company.vn, crm-prod, maximum 50MB/file |
| Staging environment | Endpoint, test bucket, dedicated access key | crm-staging, do not share the production key |
| Allowed file types | MIME type list | image/jpeg, image/png, application/pdf |
| Link expiration | Presigned URL lifespan | Upload: 10 minutes, Download: 15-60 minutes |
| Retention/backup | File retention period and deletion policy | Keep contract files for at least 5 years; temporary files are deleted after 7 days |
Recommended permission matrix
| Account/key | Permissions | Used for | Should not grant |
|---|---|---|---|
| app-uploader | s3:PutObject, s3:GetObject, s3:ListBucket | File upload and download link generator app | Do not grant user admin permissions; do not grant access to all other buckets |
| app-readonly | s3:GetObject, s3:ListBucket | Read-only file service, data synchronization | Do not grant s3:DeleteObject |
| backup-service | s3:PutObject, s3:GetObject, s3:DeleteObject | Periodic backups, scheduled cleanup of old files | Do not share with web applications |
Example S3 REST request
The example below illustrates the path and header. The Authorization value must be signed by an SDK or library
request generated, do not copy this raw sample to run in production.
PUT /customer-files/orders/2026/06/invoice.pdf HTTP/1.1
Host: s3.example.com
Content-Type: application/pdf
x-amz-date: 20260620T030000Z
x-amz-content-sha256: <sha256-payload>
Authorization: AWS4-HMAC-SHA256 Credential=<access-key>/20260620/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=<signature>
Body:
Nội dung file PDF dạng binary được gửi ở đâyThe Body section in the example represents real file data. When using the SDK or fetch to upload a file, this section corresponds to the file/blob variable sent to MinIO.
Sample response upon successful upload
HTTP/1.1 200 OK
ETag: "9b2cf535f27731c974343645a3985328"
x-amz-request-id: 183A6F4A9D1C4B2E
Date: Sat, 20 Jun 2026 03:00:00 GMT3. Recommended backend API
This is an API layer that should be placed within the client's backend system. The backend is responsible for authenticating users,
verify business permissions, write to database, and generate presigned URLs. Frontend does not call MinIO using secret keys.
1. Create Upload Link
| Information | Details |
|---|---|
| Endpoint | POST /api/files/presign-upload |
| Purpose | Create a temporary URL for the frontend to upload files directly to MinIO. |
| Suggested duration | 5-10 minutes. |
Request
{
"module": "orders",
"fileName": "invoice-001.pdf",
"contentType": "application/pdf",
"size": 245760,
"ownerType": "order",
"ownerId": "ORD-1001"
}Response
{
"success": true,
"data": {
"bucket": "customer-files",
"objectKey": "orders/2026/06/8f3c9d7a-invoice-001.pdf",
"uploadUrl": "https://s3.example.com/customer-files/orders/2026/06/8f3c9d7a-invoice-001.pdf?X-Amz-Algorithm=AWS4-HMAC-SHA256...",
"method": "PUT",
"expiresIn": 600,
"requiredHeaders": {
"Content-Type": "application/pdf"
}
}
}Frontend uploads file using signed URL
await fetch(uploadUrl, {
method: "PUT",
headers: {
"Content-Type": "application/pdf"
},
body: file
});2. Confirm upload and save metadata
| Information | Details |
|---|---|
| Endpoint | POST /api/files/complete |
| Purpose | The backend checks that the object exists on MinIO, then saves the file record to the database. |
Request
{
"bucket": "customer-files",
"objectKey": "orders/2026/06/8f3c9d7a-invoice-001.pdf",
"originalName": "invoice-001.pdf",
"contentType": "application/pdf",
"ownerType": "order",
"ownerId": "ORD-1001"
}Response
{
"success": true,
"data": {
"id": "file_01J0YQACV8K7Z1P6D4X9",
"bucket": "customer-files",
"objectKey": "orders/2026/06/8f3c9d7a-invoice-001.pdf",
"size": 245760,
"etag": "9b2cf535f27731c974343645a3985328",
"createdAt": "2026-06-20T10:00:00+07:00"
}
}3. Create download link
| Information | Details |
|---|---|
| Endpoint | GET /api/files/{id}/download |
| Purpose | Return a temporary file download link after verifying user permissions. |
Response
{
"success": true,
"data": {
"downloadUrl": "https://s3.example.com/customer-files/orders/2026/06/8f3c9d7a-invoice-001.pdf?X-Amz-Algorithm=AWS4-HMAC-SHA256...",
"expiresIn": 900,
"fileName": "invoice-001.pdf",
"contentType": "application/pdf"
}
}4. Delete file
| Information | Details |
|---|---|
| Endpoint | DELETE /api/files/{id} |
| Purpose | Delete objects on MinIO or flag as deleted in the database depending on data policy. |
Operational recommendations:if files relate to invoices, contracts, or legal data, use
soft delete in database first. Only permanently delete the actual object after the mandatory retention period expires.
Metadata table recommended to be stored in database
files
- id UUID/string primary key
- bucket varchar(100)
- object_key varchar(1024)
- original_name varchar(255)
- content_type varchar(120)
- size bigint
- etag varchar(100)
- owner_type varchar(50)
- owner_id varchar(100)
- uploaded_by user id
- status uploaded | deleted | failed
- created_at datetime
- deleted_at datetime nullable4. Authentication
The system uses an accessKeyId and secretAccessKey pair. Each S3 request must be signed with
AWS Signature V4. In practice, AWS SDK or MinIO SDK should be used so that the SDK automatically signs requests.
Do not call the S3 API directly from the browser using secret keys.
If the frontend needs to upload/download, the backend should generate a short-lived presigned URL and return it to the frontend.
Recommended environment variables
S3_ENDPOINT=https://s3.example.com
S3_REGION=us-east-1
S3_BUCKET=customer-files
S3_ACCESS_KEY=your-access-key
S3_SECRET_KEY=your-secret-key
S3_FORCE_PATH_STYLE=trueAuthentication headers when calling S3 API directly
If the client calls HTTP directly without an SDK, the request must include headers conforming to the AWS Signature V4 standard.
This method is more error-prone than the SDK and should only be used if the customer's system already has a stable request-signing library.
| Header/Parameter | Example | Meaning |
|---|---|---|
| Host | s3.example.com | Domain endpoint S3/MinIO. |
| x-amz-date | 20260620T030000Z | UTC time used to sign the request. |
| x-amz-content-sha256 | UNSIGNED-PAYLOAD or SHA256 payload | Hash of request payload. Some SDKs use UNSIGNED-PAYLOAD for presigned URLs. |
| Authorization | AWS4-HMAC-SHA256 Credential=... | The signature is calculated from the method, path, query, header, and secret key. |
How Presigned URLs work
- Frontend requests backend to generate an upload/download link for a specific file.
- The backend verifies user permissions within the customer's system.
- The backend uses the secret key to generate a short-lived presigned URL.
- The frontend uses that URL to upload/download directly with MinIO.
- When the URL expires, the client must request a new URL and not reuse the old one.
Recommendation:secret keys remain strictly on the backend. Frontend/mobile apps must never store secret keys,
even when the application is packaged or obfuscated.
5. Commonly used S3 APIs
The endpoints below follow S3 standards. When using the SDK, simply call the corresponding command; the SDK will automatically generate
method, path, headers, and valid signature.
| Business Operations | HTTP | Path | Note |
|---|---|---|---|
| Upload file | PUT | /{bucket}/{objectKey} | Send file content in the body. Content-Type should be set. |
| Download file | GET | /{bucket}/{objectKey} | A private bucket requires a signed request or presigned URL. |
| Check metadata | HEAD | /{bucket}/{objectKey} | Used to check file existence, size, and mime type. |
| Delete file | DELETE | /{bucket}/{objectKey} | Permissions should be checked before deleting. |
| List files | GET | /{bucket}?list-type=2&prefix={prefix} | Use prefix to filter by logical folder. |
Common headers used during upload
| Header | Example | Purpose |
|---|---|---|
| Content-Type | application/pdf | Helps browsers display or download files in the correct format. |
| Content-Disposition | attachment; filename="invoice.pdf" | Suggested filename upon download. |
| x-amz-meta-* | x-amz-meta-owner-id: 1024 | Store custom metadata if needed. |
6. AWS CLI Example
AWS CLI can be used to quickly verify connections. All commands require adding --endpoint-url when working
with MinIO.
Profile configuration
aws configure --profile minio
# Nhập:
# AWS Access Key ID: your-access-key
# AWS Secret Access Key: your-secret-key
# Default region name: us-east-1
# Default output format: jsonList buckets
aws --profile minio \
--endpoint-url https://s3.example.com \
s3 lsUpload file
aws --profile minio \
--endpoint-url https://s3.example.com \
s3 cp ./invoice.pdf s3://customer-files/orders/2026/06/invoice.pdf \
--content-type application/pdfDownload file
aws --profile minio \
--endpoint-url https://s3.example.com \
s3 cp s3://customer-files/orders/2026/06/invoice.pdf ./invoice.pdfCreate presigned URL to download files
aws --profile minio \
--endpoint-url https://s3.example.com \
s3 presign s3://customer-files/orders/2026/06/invoice.pdf \
--expires-in 36007. MinIO Client
mc is the official MinIO command-line tool, suitable for administrative operations and bucket inspection,
user permissions and storage view.
Connect to MinIO using alias
mc alias set company-s3 https://s3.example.com your-access-key your-secret-key
# Kiểm tra kết nối
mc admin info company-s3
mc ls company-s3Create bucket and check files
mc mb company-s3/customer-files
mc ls company-s3/customer-files
mc cp ./invoice.pdf company-s3/customer-files/orders/2026/06/invoice.pdf
mc stat company-s3/customer-files/orders/2026/06/invoice.pdfCreate user for application
mc admin user add company-s3 app-uploader APP_UPLOADER_SECRET_CHANGE_MESample policy for upload/read application
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:ListBucket"
],
"Resource": [
"arn:aws:s3:::customer-files"
]
},
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject",
"s3:DeleteObject"
],
"Resource": [
"arn:aws:s3:::customer-files/*"
]
}
]
}Attach policy to user
# Lưu policy vào file app-uploader-policy.json
mc admin policy create company-s3 app-uploader-policy ./app-uploader-policy.json
mc admin policy attach company-s3 app-uploader-policy --user app-uploaderEnable versioning for critical buckets
mc version enable company-s3/customer-files
mc version info company-s3/customer-filesLifecycle delete temporary files after 7 days
{
"Rules": [
{
"ID": "delete-temp-files-after-7-days",
"Status": "Enabled",
"Filter": {
"Prefix": "tmp/"
},
"Expiration": {
"Days": 7
}
}
]
}# Lưu nội dung lifecycle vào lifecycle.json
mc ilm import company-s3/customer-files < lifecycle.json
mc ilm ls company-s3/customer-filesCheck bucket storage capacity
mc du company-s3/customer-files
mc find company-s3/customer-files --name "*.pdf" --older-than 30d8. SDK examples
Node.js with AWS SDK v3
import { S3Client, PutObjectCommand, GetObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
import fs from "node:fs";
const s3 = new S3Client({
endpoint: process.env.S3_ENDPOINT,
region: process.env.S3_REGION || "us-east-1",
forcePathStyle: true,
credentials: {
accessKeyId: process.env.S3_ACCESS_KEY,
secretAccessKey: process.env.S3_SECRET_KEY,
},
});
const bucket = process.env.S3_BUCKET;
const key = "orders/2026/06/invoice.pdf";
await s3.send(new PutObjectCommand({
Bucket: bucket,
Key: key,
Body: fs.createReadStream("./invoice.pdf"),
ContentType: "application/pdf",
}));
const downloadUrl = await getSignedUrl(
s3,
new GetObjectCommand({ Bucket: bucket, Key: key }),
{ expiresIn: 3600 }
);
console.log(downloadUrl);Node.js generates presigned URL for frontend upload
import { S3Client, PutObjectCommand, GetObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
const s3 = new S3Client({
endpoint: process.env.S3_ENDPOINT,
region: process.env.S3_REGION || "us-east-1",
forcePathStyle: true,
credentials: {
accessKeyId: process.env.S3_ACCESS_KEY,
secretAccessKey: process.env.S3_SECRET_KEY,
},
});
export async function createUploadUrl({ bucket, key, contentType }) {
const command = new PutObjectCommand({
Bucket: bucket,
Key: key,
ContentType: contentType,
});
return getSignedUrl(s3, command, { expiresIn: 600 });
}
export async function createDownloadUrl({ bucket, key, fileName }) {
const command = new GetObjectCommand({
Bucket: bucket,
Key: key,
ResponseContentDisposition: `attachment; filename="${fileName}"`,
});
return getSignedUrl(s3, command, { expiresIn: 900 });
}PHP with aws/aws-sdk-php
<?php
require __DIR__ . "/vendor/autoload.php";
use Aws\S3\S3Client;
use Aws\S3\Exception\S3Exception;
$s3 = new S3Client([
"version" => "latest",
"region" => getenv("S3_REGION") ?: "us-east-1",
"endpoint" => getenv("S3_ENDPOINT"),
"use_path_style_endpoint" => true,
"credentials" => [
"key" => getenv("S3_ACCESS_KEY"),
"secret" => getenv("S3_SECRET_KEY"),
],
]);
$bucket = getenv("S3_BUCKET");
$key = "orders/2026/06/invoice.pdf";
try {
$s3->putObject([
"Bucket" => $bucket,
"Key" => $key,
"SourceFile" => __DIR__ . "/invoice.pdf",
"ContentType" => "application/pdf",
]);
$cmd = $s3->getCommand("GetObject", [
"Bucket" => $bucket,
"Key" => $key,
]);
$request = $s3->createPresignedRequest($cmd, "+60 minutes");
echo (string) $request->getUri();
} catch (S3Exception $e) {
error_log($e->getAwsErrorCode() . ": " . $e->getMessage());
}9. CORS & proxy
If the frontend uploads directly using a presigned URL, the bucket must have CORS configured for the application domain.
If CORS is not configured, the browser will block requests even if the URL is properly signed.
Sample CORS for private bucket
{
"CORSRules": [
{
"AllowedOrigins": [
"https://app.example.com",
"https://admin.example.com"
],
"AllowedMethods": [
"GET",
"PUT",
"POST",
"HEAD"
],
"AllowedHeaders": [
"Authorization",
"Content-Type",
"x-amz-date",
"x-amz-content-sha256",
"x-amz-security-token",
"x-amz-meta-*"
],
"ExposeHeaders": [
"ETag",
"Content-Length",
"Content-Type"
],
"MaxAgeSeconds": 3600
}
]
}Apply CORS using AWS CLI
aws --profile minio \
--endpoint-url https://s3.example.com \
s3api put-bucket-cors \
--bucket customer-files \
--cors-configuration file://cors.jsonApply CORS using MinIO Client
mc cors set company-s3/customer-files cors.json
mc cors info company-s3/customer-filesNote:do not set AllowedOrigins to * in production systems
contains private data. Please declare the customer's frontend domain correctly.
Sample Nginx reverse proxy for MinIO API
server {
listen 443 ssl http2;
server_name s3.example.com;
client_max_body_size 200M;
location / {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_connect_timeout 300;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_pass http://127.0.0.1:9000;
}
}Sample Nginx reverse proxy for MinIO Console
server {
listen 443 ssl http2;
server_name console-s3.example.com;
location / {
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_pass http://127.0.0.1:9001;
}
}10. Conventions&security
Bucket conventions
- Do not set production buckets to public if they contain customer data, contracts, invoices, or personal information.
- Separate buckets by environment or by data group if clear permission separation is required.
- Do not use the original filename as the sole object key as duplicates may occur. Add a UUID or identifier instead.
Access permission conventions
- The access key used for an application should only have permissions on the required bucket.
- Upload accounts do not necessarily need delete permissions if the workflow does not require deletion.
- The secret key must be stored in environment variables or a secret manager, not hardcoded into source code.
- Presigned URLs should have short lifespans, such as 5 to 60 minutes depending on business logic.
Security notes:if the secret key is leaked, immediately disable or rotate it, then check access logs
to detect abnormal uploads/downloads.
11. Common Errors
| Error code | Common causes | Resolution |
|---|---|---|
| AccessDenied | Access key lacks permissions for the bucket or object. | Check policy, bucket name, object key, and read/write/delete permissions. |
| NoSuchBucket | The bucket does not exist or the bucket name is incorrect. | Create a bucket or update the S3_BUCKET variable. |
| NoSuchKey | Object key does not exist. | Recheck object path; verify case sensitivity. |
| SignatureDoesNotMatch | Wrong secret key, wrong endpoint, wrong region, or request altered after signing. | Check credentials, region, server time synchronization, and enable path-style for MinIO. |
| RequestTimeTooSkewed | Client/server time difference is too large. | Sync NTP on application servers. |
| EntityTooLarge | File exceeds the upload limit set by proxy, application, or infrastructure configuration. | Check Nginx/Apache, backend upload limits, and consider multipart upload. |
| CORS error | Browser blocked because the bucket has not allowed the frontend domain. | Check AllowedOrigins, AllowedMethods, AllowedHeaders in CORS. |
| 403 when using presigned URL | Expired URL, mismatched method, or upload headers differing from signing time. | If signed with Content-Type: application/pdf, the frontend must send that exact header upon upload. |
| InvalidAccessKeyId | The access key is incorrect, locked, or does not exist on MinIO. | Check the user using mc admin user info and create a new key if needed. |
| Connection refused | MinIO service is not running or the proxy points to the wrong port. | Check API port 9000, console 9001, firewall, and Nginx upstream. |
Checklist debug nhanh
- Check if endpoint connects via HTTPS: curl -I https://s3.example.com.
- Verify credentials using aws s3 ls --endpoint-url ... or mc ls.
- Verify bucket name and object key for case sensitivity.
- Check if the application server clock drifts from UTC time.
- Check that presigned URL is still valid and frontend uses the correct PUT/GET method.
- Check CORS if errors only occur in browsers but succeed via CLI.
12. Handover Checklist
| Category | Status | Note |
|---|---|---|
| HTTPS API endpoint active | ☐ | Verify SSL certificate is still valid. |
| Production bucket created | ☐ | Clearly state the handed-over bucket name. |
| Issued access key/secret key | ☐ | Send secret via a separate secure channel. |
| Restricted permission policy | ☐ | Grant only necessary permissions to the application. |
| Tested upload/download/delete | ☐ | Save logs or screenshots of test results if needed. |
| Backup/replication mechanism | ☐ | Specify the backup schedule and retention period. |
| Support person in charge | ☐ | Name, email, phone number, or ticket channel. |
Customer handover information template
API Endpoint: https://s3.example.com
Console: https://console-s3.example.com
Region: us-east-1
Bucket: customer-files
Access Key: gửi riêng
Secret Key: gửi riêng
Path Style: true
Presigned URL: khuyến nghị dùng cho frontend, thời hạn 5-60 phútIntegration acceptance test case
| STT | Scenario | How to test | Result achieved |
|---|---|---|---|
| 1 | Backend generates presigned upload URL | Call POST /api/files/presign-upload with a valid PDF file. | Receive uploadUrl, objectKey, expiresIn. |
| 2 | Frontend upload file | Send a PUT file request to uploadUrl. | MinIO returns HTTP 200 or 204, with an ETag. |
| 3 | Backend validates file | Call POST /api/files/complete. | Database has file records, correct size, and content type. |
| 4 | Download file private | Call GET /api/files/{id}/download then open downloadUrl. | The downloaded file has the correct content, and the URL expires after the configured period. |
| 5 | Block invalid file formats | Uploaded .exe file or disallowed MIME type. | The backend rejects the request before generating a presigned URL. |
| 6 | Block oversized files | File upload exceeds the agreed limit. | The backend returns a clear error without creating an object on MinIO. |
| 7 | Check user permissions | User A attempts to download a file belonging to User B. | The backend returns 403 Forbidden without returning a presigned URL. |
| 8 | Hard delete or soft delete | Call DELETE /api/files/{id}. | File status adhering to policy: delete object or mark as deleted. |
Internal API error response template
{
"success": false,
"error": {
"code": "FILE_TYPE_NOT_ALLOWED",
"message": "Định dạng file không được hỗ trợ.",
"details": {
"allowedTypes": ["image/jpeg", "image/png", "application/pdf"]
}
}
}VPTS – Comprehensive Digital Infrastructure Solutions Provider
VPTS is a digital infrastructure solution provider in Vietnam, serving deployment, operation, and data storage needs for individuals, developers, and enterprises. The service ecosystem includesVietnam VPS, International VPS, VPS Anti DDoS, Hosting cPanel, dedicated server, Rack rental, Proxy IPv4/IPv6, S3 MinIOandCloud Storage.
Besides providing infrastructure resources, VPTS also focuses on assisting customers with deployment, configuration, and practical troubleshooting. Content on the VPTS Blog is drawn from common scenarios in operating VPS, servers, and online systems, helping readers understand root causes and apply clear solutions.
VPTS – Stable infrastructure, flexible solutions, and continuous partnership throughout operations.
VPTS Team Contact Information
Website: https://vpts.shop
Facebook: https://www.facebook.com/trang.vpsttt/
Zalo OA: https://zalo.me/vpstttgroup
Hotline: 0328 812 674