Upload CV with Presign URL
Step 1 of 3-step upload process
Generate a secure, temporary URL to upload your CV file directly to cloud storage.
Upload Process
- Generate URL (this endpoint) → Get
uploadIdandpresignedUrl - Upload file → PUT your file directly to S3 using the
presignedUrl - Finalize → POST to
https://api.floreal.ai/v1/public/documents/upload-presigned-finalizewithuploadId
Rate limit
For rate limit, please check the rate limit in the dedicated API documentation section.
Request Body
Rate limit
For rate limit, please check the rate limit in the dedicated API documentation section.
Required Fields
| Field | Type | Required | Description | Example |
|---|---|---|---|---|
| fileName | string | Yes | CV filename (1-255 characters) | "john-doe-cv.pdf" |
| contentType | string | Yes | File MIME type (see below) | "application/pdf" |
| fileSize | integer | Yes | File size in bytes (max 10MB) | 245760 |
Supported Content Types
| Content Type | File Extension | Description |
|---|---|---|
application/pdf | PDF documents (recommended) | |
application/vnd.openxmlformats-officedocument.wordprocessingml.document | .docx | Microsoft Word 2007+ |
application/msword | .doc | Microsoft Word 97-2003 |
text/plain | .txt | Plain text files |
Important: Use the exact MIME type string from the table above.
Response
Success (200 OK)
{
"uploadId": "550e8400-e29b-41d4-a716-446655440000",
"presignedUrl": "https://voiceformdocumentstaging.s3.eu-west-3.amazonaws.com/uploads/550e8400-.../resume.pdf?X-Amz-Algorithm=...",
"expiresAt": "2025-11-05T11:40:00.000Z",
"instructions": {
"step2": "Upload your file to the presignedUrl using PUT request",
"step3": "Call POST /v1/public/documents/upload-presigned-finalize with the uploadId"
}
}Save the uploadId - you'll need it for Step 3.
URL expires in 1 hour - complete upload before expiresAt.
Complete Example
Step 1: Get Presigned URL
curl -X POST https://api.floreal.ai/v1/public/documents/upload-presigned \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"fileName": "john-doe-resume.pdf",
"contentType": "application/pdf",
"fileSize": 245760
}'Response:
{
"uploadId": "550e8400-e29b-41d4-a716-446655440000",
"presignedUrl": "https://voiceformdocumentstaging.s3.eu-west-3.amazonaws.com/uploads/550e8400-.../resume.pdf?X-Amz-Algorithm=...",
"expiresAt": "2025-11-05T11:40:00.000Z"
}Step 2: Upload File Directly to S3
Important: This step does NOT go through your API. You upload directly to Amazon S3.
# Replace the URL below with the actual presignedUrl from Step 1 response
curl -X PUT "https://voiceformdocumentstaging.s3.eu-west-3.amazonaws.com/uploads/550e8400-.../resume.pdf?X-Amz-Algorithm=..." \
-H "Content-Type: application/pdf" \
--data-binary @john-doe-resume.pdfWhat happens:
- ✅ File uploads directly to Amazon S3 (bypasses your API)
- ✅ No authentication needed (presigned URL contains temporary credentials)
- ✅ Faster upload (no proxy through your servers)
- ✅ S3 returns 200 OK with empty body if successful
- ✅ S3 returns 403 Forbidden if URL expired or Content-Type doesn't match
⚠️ Critical:
- Use PUT method, not POST
- Set Content-Type header to match the contentType from Step 1
- Don't modify the presigned URL in any way
Step 3: Finalize Upload and Create Document
After S3 upload succeeds, call your API to finalize:
curl -X POST https://api.floreal.ai/v1/public/documents/upload-presigned-finalize \
-H "X-API-Key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"uploadId": "550e8400-e29b-41d4-a716-446655440000",
"documentName": "John Doe - Software Engineer",
"documentType": "cv",
"documentDate": "11-2025"
}'Response:
{
"documentId": "789e4567-e89b-12d3-a456-426614174000",
"status": "uploading",
"message": "Document is being processed"
}JavaScript Example
// Get file from file input
const fileInput = document.querySelector('input[type="file"]');
const file = fileInput.files[0];
if (!file) {
throw new Error('Please select a file first');
}
// Step 1: Get presigned URL
const step1Response = await fetch(
'https://api.floreal.ai/v1/public/documents/upload-presigned',
{
method: 'POST',
headers: {
'X-API-Key': 'YOUR_API_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({
fileName: file.name,
contentType: file.type,
fileSize: file.size
})
}
);
const { uploadId, presignedUrl } = await step1Response.json();
console.log('✅ Step 1: Got presigned URL', uploadId);
// Step 2: Upload file directly to S3 (NOT to your API!)
const step2Response = await fetch(presignedUrl, {
method: 'PUT',
headers: {
'Content-Type': file.type
},
body: file
});
if (!step2Response.ok) {
throw new Error('S3 upload failed: ' + step2Response.status);
}
console.log('✅ Step 2: File uploaded to S3');
// Optional: Verify upload
const verifyResponse = await fetch(
`https://api.floreal.ai/v1/public/documents/upload-presigned/${uploadId}`,
{ headers: { 'X-API-Key': 'YOUR_API_KEY' } }
);
const verification = await verifyResponse.json();
console.log('✅ Step 2.5: Upload verified', verification.file.sizeFormatted);
// Step 3: Finalize and create document record
const step3Response = await fetch(
'https://api.floreal.ai/v1/public/documents/upload-presigned-finalize',
{
method: 'POST',
headers: {
'X-API-Key': 'YOUR_API_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({
uploadId,
documentName: file.name.replace(/.(pdf|docx|doc|txt)$/i, ''),
documentType: 'cv',
documentDate: '11-2025'
})
}
);
const { documentId, status } = await step3Response.json();
console.log('✅ Step 3: Document created:', documentId);
console.log('Initial status:', status); // "uploading"
// Step 4: Poll for completion
const pollStatus = async () => {
const statusResponse = await fetch(
`https://api.floreal.ai/v1/public/documents/${documentId}`,
{ headers: { 'X-API-Key': 'YOUR_API_KEY' } }
);
const data = await statusResponse.json();
console.log('Current status:', data.status);
if (data.status === 'completed') {
console.log('✅ Processing complete!');
console.log('Candidate:', data.contact);
console.log('Profile:', data.profile);
return data;
}
if (data.status === 'failed') {
console.error('❌ Processing failed:', data.error?.message);
throw new Error('Processing failed: ' + data.error?.message);
}
// Still processing, check again in 5 seconds
await new Promise(resolve => setTimeout(resolve, 5000));
return pollStatus();
};
const finalResult = await pollStatus();
console.log('🎉 Done! Document ID:', finalResult.documentId);Error Responses
| Status | Error | Cause | Solution |
|---|---|---|---|
| 400 | Invalid contentType | Wrong MIME type format | Use exact string from supported types table |
| 400 | File too large | File exceeds 10MB | Compress or split document |
| 400 | Invalid fileName | Empty or too long | Provide 1-255 character filename |
| 401 | Unauthorized | Invalid API key | Check X-API-Key header |
| 500 | Server error | System issue | Retry or contact support |
Common contentType Errors
❌ Wrong:
"pdf"- Missing application/ prefix"PDF"- Uppercase not allowed"application/PDF"- Uppercase not allowed"coucou"- Not a valid MIME type
✅ Correct:
"application/pdf""application/msword""application/vnd.openxmlformats-officedocument.wordprocessingml.document""text/plain"
Important Notes
⚠️ Step 2 is NOT to your API - Upload goes directly to S3
⚠️ Use HTTP PUT for S3 upload (not POST)
⚠️ Content-Type must match - Use same contentType in Step 1 and Step 2
⚠️ URL expires in 1 hour - If expired, generate a new one
⚠️ Case-sensitive - MIME types must be lowercase
⚠️ Don't modify presigned URL - Use it exactly as returned
Validation & Limits
File Requirements
✅ Size: Maximum 10 MB (10,485,760 bytes) ✅ Types: PDF, DOC, DOCX, TXT only ✅ Name: 1-255 characters
URL Expiry
⏰ 1 hour validity - Upload must complete before expiry ⏰ Time zone: UTC (ISO 8601 format)
Rate limit
For rate limit, please check the rate limit in the dedicated API documentation section.
Troubleshooting
"Invalid contentType" Error (Step 1)
Problem: Validation error on contentType field
Solutions:
- Check spelling and case (must be lowercase)
- Use exact MIME type string from supported types table
- Don't use file extension (e.g., "pdf") - use full MIME type
- Copy-paste from examples to avoid typos
Example Fix:
// ❌ Wrong
{ contentType: "pdf" }
{ contentType: "PDF" }
{ contentType: "application/PDF" }
// ✅ Correct
{ contentType: "application/pdf" }S3 Upload Fails with 403 Forbidden (Step 2)
Problem: S3 returns 403 when trying to upload
Causes & Solutions:
-
URL Expired
- Cause: More than 1 hour passed since Step 1
- Solution: Generate a new presigned URL (call Step 1 again)
-
Content-Type Mismatch
- Cause: Content-Type header in Step 2 doesn't match Step 1
- Solution: Ensure both use exact same content type
// Step 1 { contentType: "application/pdf" } // Step 2 - MUST MATCH fetch(presignedUrl, { headers: { 'Content-Type': 'application/pdf' } // Same as Step 1! }) -
Wrong HTTP Method
- Cause: Using POST instead of PUT
- Solution: Use PUT method for S3 upload
// ❌ Wrong fetch(presignedUrl, { method: 'POST' }) // ✅ Correct fetch(presignedUrl, { method: 'PUT' }) -
Modified URL
- Cause: Presigned URL was altered
- Solution: Use URL exactly as returned from Step 1
Step 3 Returns "Upload not found" (404)
Problem: Finalize endpoint can't find your upload
Causes & Solutions:
-
Skipped Step 2
- Cause: Called Step 3 without uploading to S3 first
- Solution: Complete Step 2 (upload to presigned URL)
-
Step 2 Failed Silently
- Cause: S3 upload returned error but wasn't checked
- Solution: Check Step 2 response status
const s3Response = await fetch(presignedUrl, { method: 'PUT', body: file }); if (!s3Response.ok) { throw new Error('S3 upload failed: ' + s3Response.status); } -
Wrong uploadId
- Cause: Using different uploadId than Step 1 returned
- Solution: Use exact uploadId from Step 1 response
How to Verify Step 2 Succeeded
Optional: Use the verify endpoint before Step 3:
curl -X GET https://api.floreal.ai/v1/public/documents/upload-presigned/550e8400-... \
-H "X-API-Key: YOUR_API_KEY"Response if upload succeeded:
{
"uploadId": "550e8400-...",
"exists": true,
"status": "uploaded",
"file": {
"name": "john_doe_resume.pdf",
"size": 245760
}
}Best Practices
Content-Type Detection
function getContentType(filename) {
const ext = filename.split('.').pop().toLowerCase();
const types = {
'pdf': 'application/pdf',
'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'doc': 'application/msword',
'txt': 'text/plain'
};
return types[ext] || null;
}
// Usage
const contentType = getContentType('resume.pdf');
if (!contentType) {
throw new Error('Unsupported file type');
}Validation Before Upload
function validateFile(file) {
const maxSize = 10 * 1024 * 1024; // 10MB
if (file.size > maxSize) {
throw new Error('File exceeds 10MB limit');
}
const allowedTypes = [
'application/pdf',
'application/msword',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'text/plain'
];
if (!allowedTypes.includes(file.type)) {
throw new Error('Unsupported file type: ' + file.type);
}
return true;
}Complete Upload Flow with Error Handling
async function uploadCV(file, metadata) {
try {
// Validate file first
validateFile(file);
// Step 1: Get presigned URL
const { uploadId, presignedUrl, expiresAt } = await fetch(
'https://api.floreal.ai/v1/public/documents/upload-presigned',
{
method: 'POST',
headers: {
'X-API-Key': 'YOUR_API_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({
fileName: file.name,
contentType: file.type,
fileSize: file.size
})
}
).then(r => {
if (!r.ok) throw new Error('Failed to get presigned URL');
return r.json();
});
console.log('✅ Step 1 complete. URL expires at:', expiresAt);
// Step 2: Upload to S3
const s3Response = await fetch(presignedUrl, {
method: 'PUT',
headers: {
'Content-Type': file.type
'x-amz-server-side-encryption': 'aws:kms'
},
body: file
});
if (!s3Response.ok) {
throw new Error(`S3 upload failed: ${s3Response.status} ${s3Response.statusText}`);
}
console.log('✅ Step 2 complete. File uploaded to S3');
// Optional: Verify upload
const verification = await fetch(
`https://api.floreal.ai/v1/public/documents/upload-presigned/${uploadId}`,
{ headers: { 'X-API-Key': 'YOUR_API_KEY' } }
).then(r => r.json());
if (!verification.exists) {
throw new Error('Upload verification failed');
}
console.log('✅ Verification complete. File size:', verification.file.sizeFormatted);
// Step 3: Finalize
const { documentId } = await fetch(
'https://api.floreal.ai/v1/public/documents/upload-presigned-finalize',
{
method: 'POST',
headers: {
'X-API-Key': 'YOUR_API_KEY',
'Content-Type': 'application/json'
},
body: JSON.stringify({
uploadId,
...metadata
})
}
).then(r => {
if (!r.ok) throw new Error('Failed to finalize upload');
return r.json();
});
console.log('✅ Step 3 complete. Document ID:', documentId);
return { documentId, uploadId };
} catch (error) {
console.error('❌ Upload failed:', error.message);
throw error;
}
}
// Usage
uploadCV(file, {
documentName: 'John Doe - Software Engineer',
documentType: 'cv',
documentDate: '11-2025'
});Why 3 Steps?
Direct-to-S3 Upload Benefits:
- ✅ Faster uploads (no proxy through API)
- ✅ Better for large files (up to 10MB without timeout)
- ✅ More reliable (fewer network hops)
- ✅ Scales better (S3 handles the load, not your API)
When to Use This Method:
- Large files (>1MB)
- Client-side uploads from browser
- Mobile app uploads
- Bandwidth optimization needed
- Multiple concurrent uploads
When to Use Direct Upload Instead:
- Server-to-server integration
- Simpler implementation needed
- Files already in memory on your server
- See:
POST /v1/public/documents/upload-direct
Next Steps
- ✅ Generate presigned URL (this endpoint)
- ✅ Upload file to S3 using the presigned URL
- ✅ Finalize upload with POST /v1/public/documents/upload-presigned-finalize
- ✅ Poll status to check processing
- ✅ Retrieve data once status is
completed
Related Endpoints
- Finalize Upload -
POST /v1/public/documents/upload-presigned-finalize- Step 3 of this process - Verify Upload -
GET /v1/public/documents/upload-presigned/:uploadId- Optional verification - Get Document -
GET /v1/public/documents/:documentId- Check processing status - Direct Upload -
POST /v1/public/documents/upload-direct- Simpler 1-step upload - URL Upload -
POST /v1/public/documents/upload-from-url- Upload from URL
API key for public API access. Get yours at https://app.floreal.ai?tab=api
In: header
1 <= length <= 2550 < value <= 10485760Response Body
curl -X POST "https://api.floreal.ai/v1/public/documents/upload-presigned" \
-H "Content-Type: application/json" \
-d '{
"fileName": "string",
"contentType": "string",
"fileSize": 10485760
}'{
"uploadId": "550e8400-e29b-41d4-a716-446655440000",
"presignedUrl": "https://voiceformdocumentstaging.s3.eu-west-3.amazonaws.com/uploads/550e8400-.../resume.pdf?X-Amz-Algorithm=...",
"expiresAt": "2025-11-05T11:40:00.000Z",
"instructions": {
"step2": "Upload your file to the presignedUrl using PUT request",
"step3": "Call POST /v1/public/documents/upload-presigned-finalize with the uploadId"
}
}{
"error": "string",
"message": "string"
}{
"error": "string"
}{
"error": "string",
"message": "string"
}