Floreal Logo
DocumentsUpload CVsPresigned url

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

  1. Generate URL (this endpoint) → Get uploadId and presignedUrl
  2. Upload file → PUT your file directly to S3 using the presignedUrl
  3. Finalize → POST to https://api.floreal.ai/v1/public/documents/upload-presigned-finalize with uploadId

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

FieldTypeRequiredDescriptionExample
fileNamestringYesCV filename (1-255 characters)"john-doe-cv.pdf"
contentTypestringYesFile MIME type (see below)"application/pdf"
fileSizeintegerYesFile size in bytes (max 10MB)245760

Supported Content Types

Content TypeFile ExtensionDescription
application/pdf.pdfPDF documents (recommended)
application/vnd.openxmlformats-officedocument.wordprocessingml.document.docxMicrosoft Word 2007+
application/msword.docMicrosoft Word 97-2003
text/plain.txtPlain 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.pdf

What 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

StatusErrorCauseSolution
400Invalid contentTypeWrong MIME type formatUse exact string from supported types table
400File too largeFile exceeds 10MBCompress or split document
400Invalid fileNameEmpty or too longProvide 1-255 character filename
401UnauthorizedInvalid API keyCheck X-API-Key header
500Server errorSystem issueRetry 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:

  1. Check spelling and case (must be lowercase)
  2. Use exact MIME type string from supported types table
  3. Don't use file extension (e.g., "pdf") - use full MIME type
  4. 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:

  1. URL Expired

    • Cause: More than 1 hour passed since Step 1
    • Solution: Generate a new presigned URL (call Step 1 again)
  2. 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!
    })
  3. 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' })
  4. 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:

  1. Skipped Step 2

    • Cause: Called Step 3 without uploading to S3 first
    • Solution: Complete Step 2 (upload to presigned URL)
  2. 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);
    }
  3. 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

  1. Generate presigned URL (this endpoint)
  2. Upload file to S3 using the presigned URL
  3. Finalize upload with POST /v1/public/documents/upload-presigned-finalize
  4. Poll status to check processing
  5. Retrieve data once status is completed

  • 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
POST
/v1/public/documents/upload-presigned
X-API-Key<token>

API key for public API access. Get yours at https://app.floreal.ai?tab=api

In: header

fileNamestring
Length1 <= length <= 255
contentTypestring
fileSizeinteger
Range0 < value <= 10485760

Response 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"
}