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

# Upload Recording

> Upload an audio recording file for a specific appointment.

## Overview

Uploads an audio recording file for a specific appointment. The file is stored securely and the appointment is moved into the `transcribing` state so that transcription begins automatically.

Only one recording may exist per appointment. If a recording already exists, the upload is rejected. You must be the appointment's owning doctor to upload a recording.

<Warning>
  Only one recording can be uploaded per appointment. If a recording already exists, the upload is rejected with `Recording already exists`.
</Warning>

## Authentication

<ParamField header="Authorization" type="string" required>
  Bearer JWT token. The calling user is derived from the token; there is no `uid` parameter.

  ```
  Authorization: Bearer <JWT>
  ```
</ParamField>

## Path Parameters

<ParamField path="appointmentId" type="string" required>
  The appointment ObjectId to associate the recording with. The caller must be this appointment's owning doctor.

  ```
  665f8a1b2c3d4e5f6789012f3
  ```
</ParamField>

## Body Parameters

The request body must be `multipart/form-data`.

<ParamField body="audio" type="file" required>
  The audio recording file to upload. Maximum size 500 MB.
</ParamField>

<ParamField body="source" type="string" default="live">
  Origin of the recording. Accepted values: `live` (default), `upload` (a pre-recorded audio file), and `online_meeting`. The source determines which plan feature is required; if your plan does not include the relevant capability the request is rejected with a `403 FEATURE_NOT_AVAILABLE` response.
</ParamField>

## Response

<ResponseField name="success" type="boolean" required>
  Indicates whether the recording was uploaded successfully.
</ResponseField>

<ResponseField name="data" type="object">
  The created recording record.

  <Expandable title="Recording Object">
    <ResponseField name="_id" type="string">
      Unique recording identifier.
    </ResponseField>

    <ResponseField name="appointment_id" type="string">
      The associated appointment ObjectId.
    </ResponseField>

    <ResponseField name="recording_url" type="string">
      URL of the stored recording file.
    </ResponseField>

    <ResponseField name="recording_key" type="string">
      Storage key of the recording file.
    </ResponseField>

    <ResponseField name="append_recordings" type="array">
      Additional recording segments attached to this recording. Empty on a fresh upload.
    </ResponseField>

    <ResponseField name="file_size_bytes" type="number">
      Size of the uploaded file in bytes. May be `null`.
    </ResponseField>

    <ResponseField name="createdAt" type="string">
      Creation timestamp (ISO 8601).
    </ResponseField>

    <ResponseField name="updatedAt" type="string">
      Last-update timestamp (ISO 8601).
    </ResponseField>
  </Expandable>
</ResponseField>

<Info>
  Additional fields (for example `duration_seconds`, `merged_recording`, `replaced_recording`, `__v`) may appear on the recording object. Treat any field not documented here as opaque.
</Info>

## Example Request

<CodeGroup>
  ```bash cURL theme={null}
  curl -X POST \
    'https://app.medisync.me/api/recordings/add/665f8a1b2c3d4e5f6789012f3' \
    -H 'Authorization: Bearer <JWT>' \
    -F 'audio=@recording.wav'
  ```

  ```javascript JavaScript theme={null}
  const formData = new FormData();
  formData.append('audio', audioFile);

  const response = await fetch(
    'https://app.medisync.me/api/recordings/add/665f8a1b2c3d4e5f6789012f3',
    {
      method: 'POST',
      headers: {
        'Authorization': 'Bearer <JWT>'
      },
      body: formData
    }
  );

  const result = await response.json();
  ```

  ```python Python theme={null}
  import requests

  files = {'audio': open('recording.wav', 'rb')}
  headers = {'Authorization': 'Bearer <JWT>'}

  response = requests.post(
      'https://app.medisync.me/api/recordings/add/665f8a1b2c3d4e5f6789012f3',
      headers=headers,
      files=files
  )

  result = response.json()
  ```
</CodeGroup>

## Example Response

```json Success (200) theme={null}
{
  "success": true,
  "data": {
    "_id": "665f8a1b2c3d4e5f6789012f4",
    "appointment_id": "665f8a1b2c3d4e5f6789012f3",
    "recording_url": "https://storage.medisync.me/recordings/665f8a1b2c3d4e5f6789012f3_recording.wav",
    "recording_key": "665f8a1b2c3d4e5f6789012f3_recording.wav",
    "append_recordings": [],
    "file_size_bytes": 4831200,
    "createdAt": "2026-06-30T10:30:00.000Z",
    "updatedAt": "2026-06-30T10:30:00.000Z"
  }
}
```

## Error Responses

```json 401 Unauthorized theme={null}
{
  "success": false,
  "message": "Unauthorized"
}
```

```json 403 Feature Not Available theme={null}
{
  "success": false,
  "code": "FEATURE_NOT_AVAILABLE",
  "feature": "transcription.audio_upload",
  "message": "This feature is not included in your current plan."
}
```

```json 403 Forbidden (not the owning doctor) theme={null}
{
  "success": false,
  "error": "Forbidden"
}
```

```json 400 No File Uploaded theme={null}
{
  "success": false,
  "error": "No file uploaded"
}
```

```json 400 Recording Already Exists theme={null}
{
  "success": false,
  "error": "Recording already exists"
}
```

<Note>
  The `Unauthorized` response uses the `message` field, whereas handler-level errors use the `error` field. A session ended on another device returns `401 { "success": false, "message": "Session ended on this device because you signed in elsewhere.", "code": "SESSION_REVOKED" }`.
</Note>

## Behavior Notes

* **One recording per appointment**: A second upload for the same appointment is rejected with `Recording already exists`.
* **Ownership**: Only the appointment's owning doctor can upload a recording; otherwise the request returns `403 Forbidden`.
* **Automatic transcription**: On success the appointment status is set to `transcribing` and transcription begins automatically. When transcription completes the status advances to `processing`.
* **File size limit**: Uploads larger than 500 MB are rejected.
