HomeGuidesAPI Reference
Guides

Submitting Data

The V3 submission endpoint supports deferred file uploads, allowing you to submit form data first and upload files separately using presigned S3 URLs. File fields can be submitted at the root level (for the primary customer record) or within role-entity submissions (for associated entities), or both.

Integration Flow

  1. Submit form data with file names (not file content)
  2. Receive presigned URLs for each file that needs uploading
  3. Upload files to S3 using the presigned URLs
  4. Confirm the submission to verify uploads and resume execution

Step 1: Submit Form Data

Submit your form data including file field values as filenames (strings). The endpoint accepts file fields at the root level, within role-entity arrays, or both simultaneously.

Endpoint: POST /v3/public/actions/{token}

Example: Root-Level Submission with Files

Use this when the form collects files for the primary customer record directly.

{
  "first_name": "John",
  "last_name": "Smith",
  "date_of_birth": "1985-03-15",
  "id_document": "passport_scan.pdf",
  "proof_of_address": "utility_bill.pdf"
}

Example: Role-Entity Submission with Files

Use this when the form collects files for multiple related entities.

Important: Each reference value must be unique across all client records in the workspace, not just within a single submission. If a reference matches an existing client record, the request will be rejected with a 400 error (DUPLICATE_REFERENCE). Use globally unique identifiers (e.g., UUIDs or prefixed IDs) to avoid collisions.

{
  "role-entity": [
    {
      "reference": "company_reference_id_1",
      "customer_type": "company",
      "company_name": "Acme Corporation",
      "registration_number": "12345678",
      "company_document": "registration_certificate.pdf"
    },
    {
      "reference": "owner_reference_id_1",
      "customer_type": "individual",
      "first_name": "John",
      "last_name": "Smith",
      "id_document": "passport_scan.pdf"
    }
  ],
  "role-entity_relations": [
    {
      "from": "company_reference_id_1",
      "to": "root",
      "role": "owner"
    },
    {
      "from": "owner_reference_id_1",
      "to": "company_reference_id_1",
      "role": "ubo",
      "ownership_percentage": 75
    }
  ]
}

Example: Combined Root-Level and Role-Entity Files

You can submit files at both levels in a single request. Root-level files attach to the primary customer record, while role-entity files attach to their respective entities.

{
  "company_document": "main_registration.pdf",
  "role-entity": [
    {
      "reference": "director_ref_1",
      "customer_type": "individual",
      "first_name": "Jane",
      "last_name": "Doe",
      "id_document": "director_passport.pdf"
    }
  ],
  "role-entity_relations": [
    {
      "from": "director_ref_1",
      "to": "root",
      "role": "director"
    }
  ]
}

Response (status: accepted)

When files need uploading, you receive presigned URLs. Root-level files omit the entityReference field, while role-entity files include it to identify which entity the file belongs to.

Root-level files response:

{
  "status": "accepted",
  "fileUploads": [
    {
      "fileName": "passport_scan.pdf",
      "spektrDataField": "id_document",
      "url": "https://s3.eu-north-1.amazonaws.com/bucket-name",
      "fields": {
        "key": "workspace-id/endClient/record-id/passport_scan.pdf",
        "Content-Type": "application/pdf",
        "X-Amz-Algorithm": "AWS4-HMAC-SHA256",
        "X-Amz-Credential": "...",
        "X-Amz-Date": "20260203T120000Z",
        "Policy": "eyJ...",
        "X-Amz-Signature": "..."
      }
    },
    {
      "fileName": "utility_bill.pdf",
      "spektrDataField": "proof_of_address",
      "url": "https://s3.eu-north-1.amazonaws.com/bucket-name",
      "fields": {
        "key": "workspace-id/endClient/record-id/utility_bill.pdf",
        "Content-Type": "application/pdf",
        "X-Amz-Algorithm": "AWS4-HMAC-SHA256",
        "X-Amz-Credential": "...",
        "X-Amz-Date": "20260203T120000Z",
        "Policy": "eyJ...",
        "X-Amz-Signature": "..."
      }
    }
  ]
}

Role-entity files response:

{
  "status": "accepted",
  "fileUploads": [
    {
      "entityReference": "company_reference_id_1",
      "fileName": "registration_certificate.pdf",
      "spektrDataField": "company_document",
      "url": "https://s3.eu-north-1.amazonaws.com/bucket-name",
      "fields": {
        "key": "workspace-id/endClient/entity-id-1/registration_certificate.pdf",
        "Content-Type": "application/pdf",
        "X-Amz-Algorithm": "AWS4-HMAC-SHA256",
        "X-Amz-Credential": "...",
        "X-Amz-Date": "20260203T120000Z",
        "Policy": "eyJ...",
        "X-Amz-Signature": "..."
      }
    },
    {
      "entityReference": "owner_reference_id_1",
      "fileName": "passport_scan.pdf",
      "spektrDataField": "id_document",
      "url": "https://s3.eu-north-1.amazonaws.com/bucket-name",
      "fields": {
        "key": "workspace-id/endClient/entity-id-2/passport_scan.pdf",
        "Content-Type": "application/pdf",
        "X-Amz-Algorithm": "AWS4-HMAC-SHA256",
        "X-Amz-Credential": "...",
        "X-Amz-Date": "20260203T120000Z",
        "Policy": "eyJ...",
        "X-Amz-Signature": "..."
      }
    }
  ]
}

Response (status: completed)

When no files need uploading:

{
  "status": "completed"
}

Step 2: Upload Files to S3

For each file in the fileUploads array, send a multipart/form-data POST request to the presigned URL.

Important: Include all fields as form fields before the file content.

Example: cURL

curl -X POST "https://s3.eu-north-1.amazonaws.com/bucket-name" \
  -F "key=workspace-id/endClient/record-id/passport_scan.pdf" \
  -F "Content-Type=application/pdf" \
  -F "X-Amz-Algorithm=AWS4-HMAC-SHA256" \
  -F "X-Amz-Credential=..." \
  -F "X-Amz-Date=20260203T120000Z" \
  -F "Policy=eyJ..." \
  -F "X-Amz-Signature=..." \
  -F "file=@/path/to/passport_scan.pdf"

Example: JavaScript/TypeScript

interface FileUploadInfo {
  entityReference?: string;
  fileName: string;
  spektrDataField: string;
  url: string;
  fields: Record<string, string>;
}

async function uploadToS3(
  fileUpload: FileUploadInfo,
  fileContent: File | Blob
) {
  const formData = new FormData();

  // Add all presigned fields BEFORE the file
  Object.entries(fileUpload.fields).forEach(([key, value]) => {
    formData.append(key, value);
  });

  // Add the file content last
  formData.append('file', fileContent, fileUpload.fileName);

  const response = await fetch(fileUpload.url, {
    method: 'POST',
    body: formData,
  });

  if (!response.ok) {
    throw new Error(`Upload failed: ${response.status}`);
  }

  return response;
}

Step 3: Confirm Submission

After uploading all files, confirm the submission to verify uploads and resume process execution.

Endpoint: POST /v3/public/actions/{token}/confirm

Confirm also identifies each uploaded file by its bytes and converts it to a canonical format where needed, before execution resumes. See File Normalization for what this changes about the stored object.

Success Response (200)

{
  "status": "completed"
}

Missing Files Response (409 Conflict)

If any files are missing from S3, you'll receive new presigned URLs:

{
  "status": "pending",
  "fileUploads": [
    {
      "entityReference": "owner_reference_id_1",
      "fileName": "passport_scan.pdf",
      "spektrDataField": "id_document",
      "url": "https://s3.eu-north-1.amazonaws.com/bucket-name",
      "fields": { ... }
    }
  ]
}

Upload the missing files and call /confirm again.

Rejected File Response (400 Bad Request)

If a file's bytes are not an accepted document type, or cannot be decoded, the file is rejected. No document is attached to the record and execution does not resume.

{
  "errorCode": "document_type_not_accepted",
  "message": "File 'workspace-id/endClient/record-id/notes.txt' was rejected: the uploaded bytes are text/plain, which is not an accepted document type.",
  "key": "workspace-id/endClient/record-id/notes.txt"
}
errorCodeMeaning
document_type_unrecognizedThe bytes match no known file type
document_type_not_acceptedThe bytes are a known type that is not in the accepted list below
document_conversion_failedThe file is a HEIC or HEIF image, but it could not be decoded

key identifies which upload was rejected. Re-uploading the same bytes and calling /confirm again returns the same error — POST a replacement file to that file's presigned URL first.


File Normalization

Confirm reads each uploaded object back from storage and identifies it by its bytes, not by its filename or by the Content-Type you declared. This is what guarantees that every document attached to a record can be previewed, downloaded and processed by downstream services.

Accepted types

Detected typeStored as
application/pdf.pdf
image/jpeg.jpg
image/png.png
image/heic, image/heif.jpg (converted)
text/csv.csv
application/json.json
application/msword.doc
application/vnd.openxmlformats-officedocument.wordprocessingml.document.docx
application/vnd.ms-excel.xls
application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.xlsx

A plain text upload is detected only when its content is CSV or JSON. Any other text file is rejected.

The stored key can differ from the key you uploaded to

The stored extension follows the detected type, so a file whose extension does not match its bytes is stored under a new key:

You upload toStored keyBytes
.../passport.pdf.../passport.pdfUnchanged — the extension already matches
.../photo.jfif.../photo.jpgUnchanged — a JFIF is a JPEG, so only the key and the content type change
.../photo.heic.../photo.jpgRe-encoded as JPEG. The original is retained at .../original/photo.heic

The /confirm response does not return the new key, so do not treat the fields.key value from Step 1 as a durable reference to the stored document.


Field Reference

FieldDescription
referenceA unique identifier for the entity within role-entity. Must be unique across all client records in the workspace — a duplicate will cause the request to fail with a 400 error (DUPLICATE_REFERENCE).
entityReferenceIdentifies which entity the file belongs to (matches reference in role-entity). Only present for role-entity files; omitted for root-level files.
spektrDataFieldThe field name for the file (e.g., company_document, id_document)
fileNameThe original filename provided in the submission
urlThe S3 presigned URL to POST the file to
fieldsForm fields that must be included in the multipart upload

Error Handling

StatusMeaningAction
200 with status: completedNo files to uploadDone
200 with status: acceptedFiles need uploadingUpload files, then call /confirm
400Validation errorCheck error message, fix data
400 on /confirmA file was rejectedUpload a replacement file to that file's presigned URL, then call /confirm again
401Token expired/invalidRestart the flow
409 on /confirmFiles missing in S3Re-upload missing files using new URLs
500Server errorRetry or contact support

Best Practices

  1. Always check the response status - Handle both completed and accepted cases
  2. Include all fields before file - S3 presigned POST requires form fields before the file content
  3. Implement retry logic - The /confirm endpoint provides new presigned URLs if uploads fail
  4. Check for entityReference - Use it to associate files with the correct entity; its absence indicates a root-level file
  5. Handle timeouts - Presigned URLs expire (typically 1 hour), so upload promptly after submission
  6. Send the real file - /confirm identifies each upload by its bytes, so a mislabelled or corrupt file is rejected there rather than silently accepted
  7. Don't reuse the upload key as a document reference - Conversion can store the file under a different extension

Did this page help you?