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

# Upload File

> Upload a file and deliver it to a target system.

The file travels through a file transfer domain mapping that has Twine as its source, so one must be configured. If your organization has exactly one, it is used automatically; otherwise name it with `domain_mapping_id` — list them with `GET /v1/org/domain-mappings` and pick the one whose `from_domain` is `file_transfer`. If none is configured at all, the upload is still recorded but nothing is sent and `status` is `undeliverable`.

Delivery is asynchronous. This call returns as soon as the file is accepted, with `status` `pending` and `remote_id` null. Poll `GET /v1/org/file-transfers/{id}` until `status` becomes `delivered`, then read `remote_id` (or `remote_path`) for the identifier the target assigned — that is the value to supply when referencing the document elsewhere, for example as a competence assignment's document.

Twine does not retain the uploaded file. The record of the transfer, including the target's identifier, is kept indefinitely; the file itself is removed shortly after delivery.

Supply `idempotency_key` to make retries safe. Repeating a request with a key already used always returns 200 and the original transfer's id, never a duplicate record. Whether it also starts a new delivery depends on how the previous attempt ended: `delivered`, `pending` and `unknown` are returned unchanged, while `failed`, `rejected` and `undeliverable` start a fresh attempt on the same record and go back to `pending`. So a failed document can be retried under the key it already has — a retry re-sends the file posted with it, since Twine does not keep the original bytes.




## OpenAPI

````yaml https://api.twine.se/spec/openapi.json post /v1/org/file-transfers
openapi: 3.0.3
info:
  contact:
    email: help@twine.se
    name: Twine Support
  description: >+
    # Introduction

    Welcome to the Twine Public API reference. This API allows you to read data
    from Twine's system, and to write data back for supported domains. See the
    Pushing data section below for what can be written.


    # Versioning

    The API is versioned. The current version is v1.1, served under the `/v1`
    path. As the API is subject to change, new versions may be released. Always
    refer to the latest documentation to ensure you're using the correct
    version.


    ## Dated model attributes

    In v1.1, every entity field is tracked by change date. This allows you to
    fetch data as it existed at a specific point in time, or retrieve all
    changes from a specific point in time up to the present. You can also supply
    a shape to the API to get data in a format that suits your needs.


    ### Dated values


    In the v1.1 model, all fields are represented by an array of values with an
    optional valid_from timestamp. A null value for valid_from indicates that
    the field value is valid from the beginning of time, meaning it has no
    recorded history in the system. When multiple values exist in the array,
    they are sorted in descending order with the most recent value first. The
    last value may or may not have a null timestamp. An array can never contain
    more than one value with a null timestamp.


    # Authentication

    The Twine public API uses Bearer token authentication. The token should be
    sent in the `Authorization` header with the value `Bearer <token>`.


    ## Provisioning a token

    To provision a token, you must first generate a long-lived refresh token
    using Twine backoffice. This token can then be used to generate short-lived
    access tokens. The refresh token should be kept secret and never shared.

    Follow these steps to obtain an access token:


    1. Invoke `/provisioning/refresh-token/begin` using the JTI you received
    when provisioning the refresh token.

    2. The response will contain a `salt` value

    3. Concatenate the salt with the refresh token, separated by a colon (`:`)

    4. Hash the concatenated string using SHA256, e.g. `sha256
    "<salt>:<refresh_token>"`

    5. The hash should be in lower case

    6. Invoke `/provisioning/refresh-token/refresh` with the JTI (as `jti`) and
    the hash (as `signature`) in the body

    7. The response will contain an access token


    ## Legacy tokens

    Legacy tokens are still supported. These tokens are long-lived and can be
    used to authenticate against the API. However, we recommend using the new
    token provisioning system.


    ## Token expiration

    Provisioned tokens are short-lived. Decode them to find the expiry time.
    Legacy tokens have longer expiry times.


    # Filtering

    Some endpoints allow filtering on certain fields. Not all fields are
    filterable, and filter operations may vary between fields. Refer to specific
    endpoints to discover what is possible. All filters follow the same
    structure:


    ```bash

    GET
    /v1/org/employees?filters[updated_at][value]=2022-01-01T00:00:00Z&filters[updated_at][op]=gt

    ```


    # Ordering

    Some endpoints allow ordering on certain fields. Not all fields are
    orderable. Refer to specific endpoints to discover what is possible. All
    ordering follows the same structure:


    ```bash

    GET /v1/org/employees?order[field]=updated_at&order[direction]=desc

    ```


    # Data reliability

    A given piece of data is a snapshot of the latest data fetched from the
    Vendor. In some cases the data returned by Twine will be out of sync with
    the Vendor.

    This is due to the nature of the integration and the data provided by the
    Vendor. Twine will always strive to provide the most accurate data possible.

    Depending on the Customer, the data may be updated in near real-time, or
    with a longer delay.


    # Permissions

    All API requests are subject to a permissions check. Permissions are
    configured by Twine staff or an approved external admin.


    # Rate limiting

    The API is not currently rate limited. However, you should expect this to
    change in the near future. At that time you will start receiving 429 Too
    Many Requests responses. More information will be provided here when rate
    limiting is implemented.


    ## Fetching single resources

    When requesting single resources with inadequate permissions, you will
    receive a 403 Forbidden in response.


    ## Fetching lists of resources

    When requesting a list of resources, the API will filter out resources you
    do not have access to. This means that the

    response may contain fewer resources than requested. The API will always
    return a 200 OK response, even if the list is empty.


    # Pushing data

    NOTE: The API currently only supports the `employee` domain for writing
    data.


    There are multiple considerations to take into account when writing data to
    the API.


    * In order to write data, the role being used must have the `create` and/or
    `update` permissions for the `employee` domain.

    * As per the RESTful API design specs, the `POST` method is used to create
    new resources, while the `PUT` method is used to update existing resources,
    and the `PATCH` method is used to partially update existing resources.

    * Even though `PATCH` will not touch unspecified dated properties, it will
    still replace all entries of targeted dated properties with the new values
    provided in the request body. This means that if you want to keep existing
    historical values for a given dated property, you must include them in the
    request body. This behaviour can be changed in the future given enough
    demand.

    * If there are incoming domain mappings for the `employee` domain, you
    should avoid updating any properties that are mapped in those integrations.
    Otherwise there will be uncertainty about which integration is responsible
    for the data.

    * If there is a configured `twine` integration with outgoing domain mappings
    for the `employee` domain, a replication job will be created to push the
    changes to the configured integration. If you want to avoid this, create a
    condition in the target integration.


    # Nomenclature


    Throughout the API, we use the following terms:


    | Term        | Description |

    |-------------|-------------|

    | Customer    | The entity on whose behalf Twine processes personnel data |

    | Vendor      | Third party supplier of data with which Twine integrates on
    behalf of Customer |


    # FAQ


    ### Do you have a sandbox environment?

    Yes. Please contact Twine support to get access.


    ### Are there webhooks?

    Not yet, but it is on the roadmap.


    ### How can I keep the response body small?

    Use the `select` parameter to only fetch the fields you need. This will
    reduce the response size and speed up the request. Combine this

    with filtering by `updated_at` to only fetch the data that has changed since
    your last request.

  title: |
    Twine Public API
  version: |
    v1.2
  x-logo:
    altText: Twine Logo
    backgroundColor: '#00000000'
    url: >-
      https://images.squarespace-cdn.com/content/v1/63cec8699b6d396be083ed8b/a2746b2d-2582-49f0-93bb-99a8765c5450/LogoTwineHorizontal.png?format=12w
servers:
  - description: Twine Public Production API
    url: https://api.twine.se
    variables: {}
  - description: Twine Public Staging API
    url: https://api-stage.twine.se
    variables: {}
security:
  - BearerAuth: []
tags:
  - description: >+
      # Fetching

      Employees can be retrieved in paginated lists, or one by one. The employee
      object remains the same in both cases.


      # Data consistency

      Twine allows Customers to map their data to their own wishes. In some
      cases, fields might not contain what you expect.

      Fields like `first_name` and `private_id_number` are more or less
      guaranteed to contain what you expect. However, fields like `phone1`,
      `phone2`

      might mean different things depending on the Customer. Some Customers will
      want to store an employee's private phone number in `phone1`,

      while others will store the work phone number instead. Always refer to the
      Customer's data mapping (upcoming API endpoint) to understand what the
      fields contain.

    name: Employees
  - description: >
      # Fetching

      Organizational units can be retrieved in paginated lists, or one by one.
      The organizational unit object remains the same in both cases.


      # Data consistency

      Twine will attempt to normalize the source system's organizational unit
      tier. If successful, the `tier` field will contain the normalized tier and
      be of type [OrganizationalUnitTier](#model/organizationalunittier)
    name: Organizational Units
  - description: >+
      # Fetching

      Time reports can be retrieved in paginated lists.


      # Time types

      Twine allows Customers to map their time types to their own wishes. In
      some cases, time types might not contain what you expect.

    name: Time Reports
paths:
  /v1/org/file-transfers:
    post:
      tags:
        - File Transfers
      summary: Upload File
      description: >
        Upload a file and deliver it to a target system.


        The file travels through a file transfer domain mapping that has Twine
        as its source, so one must be configured. If your organization has
        exactly one, it is used automatically; otherwise name it with
        `domain_mapping_id` — list them with `GET /v1/org/domain-mappings` and
        pick the one whose `from_domain` is `file_transfer`. If none is
        configured at all, the upload is still recorded but nothing is sent and
        `status` is `undeliverable`.


        Delivery is asynchronous. This call returns as soon as the file is
        accepted, with `status` `pending` and `remote_id` null. Poll `GET
        /v1/org/file-transfers/{id}` until `status` becomes `delivered`, then
        read `remote_id` (or `remote_path`) for the identifier the target
        assigned — that is the value to supply when referencing the document
        elsewhere, for example as a competence assignment's document.


        Twine does not retain the uploaded file. The record of the transfer,
        including the target's identifier, is kept indefinitely; the file itself
        is removed shortly after delivery.


        Supply `idempotency_key` to make retries safe. Repeating a request with
        a key already used always returns 200 and the original transfer's id,
        never a duplicate record. Whether it also starts a new delivery depends
        on how the previous attempt ended: `delivered`, `pending` and `unknown`
        are returned unchanged, while `failed`, `rejected` and `undeliverable`
        start a fresh attempt on the same record and go back to `pending`. So a
        failed document can be retried under the key it already has — a retry
        re-sends the file posted with it, since Twine does not keep the original
        bytes.
      operationId: createFileTransfer
      parameters: []
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/FileTransferCreateRequest'
        description: File Transfer Create Request
        required: false
      responses:
        '200':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FileTransferResponse'
          description: >-
            Response (an earlier request with this idempotency key already
            exists)
        '201':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FileTransferResponse'
          description: Response
        '400':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/BadRequestError'
          description: Bad Request Response
        '403':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ForbiddenError'
          description: Forbidden Response
        '422':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UnprocessableEntityError'
          description: Unprocessable Entity Response
        '500':
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ForbiddenError'
          description: Internal Error Response
      callbacks: {}
components:
  schemas:
    FileTransferCreateRequest:
      description: Represents a request to upload and deliver a file
      properties:
        data:
          $ref: '#/components/schemas/FileTransferCreate'
        job_options:
          $ref: '#/components/schemas/FileTransferJobOptions'
      required:
        - data
      title: FileTransferCreateRequest
      type: object
    FileTransferResponse:
      description: A file transfer response
      properties:
        data:
          $ref: '#/components/schemas/FileTransfer'
      required:
        - data
      title: FileTransferResponse
      type: object
    BadRequestError:
      description: Bad request error
      properties:
        code:
          example: 400
          type: integer
        errors:
          default: []
          items:
            description: Singular error
            properties:
              domain:
                type: string
              message:
                type: string
              reason:
                type: string
            required:
              - domain
              - reason
              - message
            title: InnerError
            type: object
          type: array
        message:
          example: Bad Request
          type: string
      required:
        - code
        - message
        - errors
      title: BadRequestError
      type: object
    ForbiddenError:
      description: Forbidden error
      properties:
        code:
          example: 403
          type: integer
        errors:
          default: []
          items:
            description: Singular error
            properties:
              domain:
                type: string
              message:
                type: string
              reason:
                type: string
            required:
              - domain
              - reason
              - message
            title: InnerError
            type: object
          type: array
        message:
          example: Forbidden
          type: string
      required:
        - code
        - message
        - errors
      title: ForbiddenError
      type: object
    UnprocessableEntityError:
      description: Unprocessable entity error
      properties:
        code:
          example: 422
          type: integer
        errors:
          default: []
          items:
            description: Singular error
            properties:
              domain:
                type: string
              message:
                type: string
              reason:
                type: string
            required:
              - domain
              - reason
              - message
            title: InnerError
            type: object
          type: array
        message:
          example: Unprocessable Entity
          type: string
      required:
        - code
        - message
        - errors
      title: UnprocessableEntityError
      type: object
    FileTransferCreate:
      description: A file to deliver to a target system.
      properties:
        content_base64:
          description: >-
            The file itself, base64 encoded. The decoded file may be at most
            5242880 bytes.
          example: JVBERi0xLjcKJcfsj6IKNSAwIG9iago8PC9MZW5ndGggNiAwIFI+PgpzdHJlYW0K
          format: byte
          type: string
        content_type:
          description: MIME type. Inferred from `file_name` when omitted.
          example: application/pdf
          nullable: true
          type: string
        domain_mapping_id:
          description: >-
            The domain mapping to deliver through. Optional when the
            organization has exactly one file transfer domain mapping from
            Twine; required when it has several. List them with `GET
            /v1/org/domain-mappings` and pick the one whose `from_domain` is
            `file_transfer`.
          example: 018eae56-8f9d-7ca0-bea3-17f3db6cf75b
          format: uuid
          nullable: true
          type: string
        employee_id:
          description: >-
            The Twine employee this document belongs to. Supply the Twine UUID —
            Twine resolves it to the employee's id in the target system. Omit
            for a document that belongs to no one in particular; note that
            target systems which file documents under an employee will reject
            one that has none.
          example: 018eae56-8f9d-7ca0-bea3-17f3db6cf75b
          format: uuid
          nullable: true
          type: string
        file_name:
          description: >-
            The file's name, used as the document name in the target system
            unless the domain mapping is configured to rename it.
          example: first-aid-2026.pdf
          type: string
        idempotency_key:
          description: >-
            A key of your choosing, unique within your organization. Repeating a
            request with a key that has been used before returns the original
            file transfer (200) instead of uploading again (201), which makes
            retries after a timeout safe.
          example: lms-certificate-99213
          nullable: true
          type: string
        metadata:
          additionalProperties: true
          description: >-
            Your own reference data, returned unchanged on every read of this
            file transfer. Flat object, string keys, string/number/boolean
            values, at most 2048 bytes encoded. Also readable from property
            mappings on the file transfer domain, so a value sent here can drive
            the delivery itself — choosing the destination folder per upload,
            for example. `source` and `content_sha256` are reserved and always
            describe the transfer itself.
          example:
            course_id: FA-101
          nullable: true
          type: object
      required:
        - file_name
        - content_base64
      title: FileTransferCreate
      type: object
    FileTransferJobOptions:
      description: Options for the background job that delivers the file.
      properties:
        queue_job:
          default: true
          description: >-
            Whether to deliver the file. Set to false to record the upload
            without sending it anywhere; the file transfer stays pending and no
            job is created. Defaults to true.
          example: true
          type: boolean
        schedule_in:
          default: 0
          description: >-
            Delay delivery by this many seconds. Defaults to 0 — unlike the
            other write endpoints, file transfers are delivered immediately,
            since callers are normally waiting on the resulting document
            identifier.
          example: 0
          nullable: true
          type: integer
      title: FileTransferJobOptions
      type: object
    FileTransfer:
      description: >-
        A record of one file Twine delivered to a target system. Twine does not
        keep uploaded files — they are removed shortly after delivery — but this
        record is kept, including the identifier the target system assigned to
        the document.
      properties:
        bytes_retain_until:
          description: >-
            When the uploaded file itself is removed. This record and the
            identifiers on it are kept indefinitely; the file is not.
          format: date-time
          nullable: true
          type: string
        content_sha256:
          description: >-
            SHA-256 of the uploaded file, hex encoded. Useful for verifying what
            was sent.
          nullable: true
          type: string
        content_type:
          example: application/pdf
          nullable: true
          type: string
        delivered_at:
          description: When the target accepted the file.
          format: date-time
          nullable: true
          type: string
        delivery_job_id:
          description: The job that performed the upload to the target, once one has run.
          format: uuid
          nullable: true
          type: string
        domain_mapping_id:
          description: The domain mapping used to deliver the file.
          format: uuid
          nullable: true
          type: string
        employee_id:
          description: >-
            The Twine employee the document belongs to, if any. Twine resolves
            this to the employee's id in the target system when delivering.
          format: uuid
          nullable: true
          type: string
        file_name:
          description: The uploaded file's name
          example: first-aid-2026.pdf
          type: string
        id:
          description: File transfer UUID
          format: uuid
          type: string
        idempotency_key:
          nullable: true
          type: string
        inserted_at:
          format: date-time
          type: string
        job_id:
          description: >-
            The job that carried out the transfer. Look it up under
            `/v1/org/jobs` for detail.
          format: uuid
          nullable: true
          type: string
        metadata:
          additionalProperties: true
          description: The metadata supplied on upload, returned unchanged.
          type: object
        org_id:
          description: Organization UUID
          format: uuid
          type: string
        remote_employee_id:
          description: >-
            The employee's id in the target system, when the document hangs off
            an employee there. Some targets only address a document as the pair
            of this and `remote_id`.
          example: E-10422
          nullable: true
          type: string
        remote_id:
          description: >-
            The identifier the target system assigned to the uploaded document.
            Null until the transfer is delivered, and also null afterwards for
            targets that do not return one (file area targets identify a
            document by path, and some HR systems return nothing at all on
            upload) — or when a document of that name was already present and
            the domain mapping does not overwrite, in which case nothing was
            uploaded and `remote_metadata.note` says so.
          example: 3fa85f64-5717-4562-b3fc-2c963f66afa6
          nullable: true
          type: string
        remote_metadata:
          additionalProperties: true
          description: >-
            Any further identifying detail the target returned. May also carry a
            `note` explaining an outcome the other fields cannot express —
            notably a transfer that is `delivered` with no `remote_id` because a
            document of that name was already present and the domain mapping
            does not overwrite, so nothing was uploaded and the target was never
            asked for an identifier.
          type: object
        remote_path:
          description: >-
            Where the file landed in the target, for targets addressed by path
            rather than by id (SFTP, S3).
          example: /certificates/2026/first-aid-2026.pdf
          nullable: true
          type: string
        size:
          description: Size in bytes of the uploaded file
          nullable: true
          type: integer
        status:
          description: >
            Where the transfer has got to.


            - `pending` — accepted and on its way to the target. Poll until this
            changes.

            - `delivered` — the target accepted the file. `remote_id` and
            `remote_path` hold whatever identifier it returned.

            - `rejected` — the file reached the target's domain mapping but was
            deliberately not sent: a condition on the system integration or
            domain mapping excluded it, or that mapping is disabled. Nothing
            malfunctioned, so there is no error to look up.

            - `failed` — delivery was attempted and did not succeed. Look up
            `job_id` under `/v1/org/jobs` for the reason.

            - `undeliverable` — no file transfer domain mapping from Twine
            matched, so nothing was sent. The file was recorded but never
            delivered.

            - `unknown` — the underlying job record is no longer available. This
            record outlives the job, so a very old transfer can end up here.


            Derived from the delivery job rather than stored, so it cannot be
            filtered on.
          enum:
            - pending
            - delivered
            - rejected
            - failed
            - undeliverable
            - unknown
          example: delivered
          type: string
        system_integration_id:
          description: The target system integration the file was delivered to.
          format: uuid
          nullable: true
          type: string
        updated_at:
          format: date-time
          type: string
      required:
        - id
        - org_id
        - file_name
        - status
      title: FileTransfer
      type: object
  securitySchemes:
    BearerAuth:
      bearerFormat: JWT
      description: Bearer token for authentication
      scheme: bearer
      type: http

````