> ## Documentation Index
> Fetch the complete documentation index at: https://code.dcycle.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Upload Workforce Trainings CSV

> Request a presigned S3 URL to upload a trainings CSV for asynchronous ingestion

[← Own Workforce API](/api-reference/own-workforce/overview)

Mint a presigned S3 URL and upload a trainings CSV to it. The call ingests nothing by itself: it returns a URL, and the file is parsed asynchronously once it lands.

<Note>
  **Prefer the [Imports API](/api-reference/imports/overview) for new integrations** — template `own_workforce_trainings`, documented in [Own Workforce Import Templates](/api-reference/imports/own-workforce-templates). It validates each row before submission and reports per-row errors; this path has no validation step, so a malformed file fails after the upload rather than at request time.
</Note>

Trainings attach to employees by `external_employee_id`, so the employees must already exist before you upload their trainings.

## How it works

<Steps>
  <Step title="Ask for a URL">
    `POST` here with the file name. You get back `upload_url`, a `file_id` and the destination key.
  </Step>

  <Step title="PUT the file">
    Upload the CSV bytes to `upload_url`. That request goes to S3 and carries no Dcycle headers.
  </Step>

  <Step title="Wait for ingestion">
    Poll [List Workforce Trainings Paginated](/api-reference/own-workforce/list-trainings-paginated) filtering on the `file_id` you were given, and read each row's `status`.
  </Step>
</Steps>

## Request

### Headers

<ParamField header="x-api-key" type="string" required>
  Your API key for authentication

  **Example:** `sk_live_1234567890abcdef`
</ParamField>

<ParamField header="x-organization-id" type="string" required>
  Your organization UUID

  **Example:** `a8315ef3-dd50-43f8-b7ce-d839e68d51fa`
</ParamField>

### Body

<ParamField body="file_name" type="string" required>
  Name of the file you are about to upload

  **Example:** `trainings_2026.csv`
</ParamField>

## Response

`201 Created`.

<ResponseField name="upload_url" type="string">
  Presigned S3 URL. `PUT` the file bytes here. Short-lived — request it immediately before uploading.
</ResponseField>

<ResponseField name="file_id" type="string">
  Id of the file record, to follow ingestion and later to delete everything from this upload
</ResponseField>

<ResponseField name="destination_file_key" type="string">
  Object key the file will be stored under
</ResponseField>

<ResponseField name="file_name" type="string">
  Echo of the name you sent
</ResponseField>

<ResponseField name="organization_id" type="string">
  Organization the upload is attributed to
</ResponseField>

<ResponseField name="message" type="string">
  Human-readable confirmation
</ResponseField>

## Example

<CodeGroup>
  ```bash cURL theme={"theme":{"light":"github-light","dark":"github-dark"}}
  curl -X POST "https://api.dcycle.io/v1/own_workforce_trainings/bulk/csv" \
    -H "x-api-key: ${DCYCLE_API_KEY}" \
    -H "x-organization-id: ${DCYCLE_ORG_ID}" \
    -H "Content-Type: application/json" \
    -d '{"file_name": "trainings_2026.csv"}'

  curl -X PUT "${UPLOAD_URL}" --upload-file trainings_2026.csv
  ```

  ```python Python theme={"theme":{"light":"github-light","dark":"github-dark"}}
  import os

  import requests

  headers = {
      "x-api-key": os.getenv("DCYCLE_API_KEY"),
      "x-organization-id": os.getenv("DCYCLE_ORG_ID"),
  }

  presigned = requests.post(
      "https://api.dcycle.io/v1/own_workforce_trainings/bulk/csv",
      headers=headers,
      json={"file_name": "trainings_2026.csv"},
  ).json()

  with open("trainings_2026.csv", "rb") as handle:
      requests.put(presigned["upload_url"], data=handle).raise_for_status()

  print("ingesting under file_id", presigned["file_id"])
  ```

  ```javascript JavaScript theme={"theme":{"light":"github-light","dark":"github-dark"}}
  const axios = require('axios');
  const fs = require('fs');

  const headers = {
    'x-api-key': process.env.DCYCLE_API_KEY,
    'x-organization-id': process.env.DCYCLE_ORG_ID
  };

  axios.post('https://api.dcycle.io/v1/own_workforce_trainings/bulk/csv',
    { file_name: 'trainings_2026.csv' }, { headers })
    .then(({ data }) =>
      axios.put(data.upload_url, fs.createReadStream('trainings_2026.csv'))
        .then(() => console.log('ingesting under file_id', data.file_id))
    )
    .catch(error => console.error(error));
  ```
</CodeGroup>

### Successful Response

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "upload_url": "https://dcycle-uploads.s3.eu-west-1.amazonaws.com/...",
  "file_id": "aa11bb22-cc33-4d44-8e55-ff6677889900",
  "destination_file_key": "dcycle/a8315ef3-dd50-43f8-b7ce-d839e68d51fa/own_workforce_trainings/trainings_2026.csv",
  "file_name": "trainings_2026.csv",
  "organization_id": "a8315ef3-dd50-43f8-b7ce-d839e68d51fa",
  "message": "Presigned URL created successfully"
}
```

## Common Errors

### 401 Unauthorized

**Cause:** the key is invalid, or it does not belong to the organization in `x-organization-id` — the two are looked up as a pair. A request carrying no credentials at all answers `AUTH_REQUIRED` instead.

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "detail": "Invalid API key for organization",
  "code": "INVALID_API_KEY"
}
```

### 403 Forbidden

**Cause:** the key's owner is not an enabled member of the organization, or their role cannot write.

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "detail": "Logged User is not Member of Organization",
  "code": "LOGGED_USER_NOT_MEMBER"
}
```

### 422 Unprocessable Entity

**Cause:** `file_name` missing from the body.

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "detail": [
    {
      "type": "missing",
      "loc": ["body", "file_name"],
      "msg": "Field required"
    }
  ]
}
```

## Related Endpoints

<CardGroup cols={2}>
  <Card title="Own workforce import templates" icon="table-columns" href="/api-reference/imports/own-workforce-templates">
    The seven columns this CSV must carry
  </Card>

  <Card title="Create an import session" icon="file-import" href="/api-reference/imports/create-session">
    The recommended write path
  </Card>
</CardGroup>
