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

# Migrate Media URLs (Batch)

> Convert up to 1000 legacy Google Cloud Storage media URLs to the new Periskope media URL format in a single request

This endpoint converts up to **1000** legacy `storage.googleapis.com` attachment URLs (or existing Periskope media URLs) to the new Periskope media URL format in a single request.

Each URL succeeds or fails independently — `results[i]` in the response corresponds to `urls[i]` in the request, so one invalid URL does not fail the whole batch.

<Note>
  All media files must belong to the organization of the API key used. URLs from other organizations or non-Periskope buckets fail individually with an `error` in their result entry.
</Note>

### Headers

<ParamField header="Authorization" type="string" required>
  Bearer token with your API key e.g. `Bearer <api_key>`
</ParamField>

### Body

<ParamField body="urls" type="string[]" required>
  The media URLs to migrate. Maximum `1000` URLs per request.
</ParamField>

<ParamField body="access" type="string">
  Access mode for the returned URLs. One of `private` or `public`.

  * `private` — returns tokenized links that expire
  * `public` — returns bare links without a token

  If omitted, follows your organization's [Media Privacy](/features/media-privacy) setting.
</ParamField>

<ParamField body="expires_in_seconds" type="number">
  Validity of the returned links in seconds. Only valid for private links — returns an error if passed for public links. Clamped between `60` (1 minute) and `604800` (7 days). Defaults to your organization's configured link expiry (24 hours unless changed). A single token is minted and shared across all URLs in the batch.
</ParamField>

### Response

<ResponseField name="results" type="object[]">
  One result per input URL, in the same order as the request.

  <Expandable title="properties">
    <ResponseField name="index" type="number">
      Index of the URL in the request `urls` array.
    </ResponseField>

    <ResponseField name="success" type="boolean">
      Whether this URL was migrated successfully.
    </ResponseField>

    <ResponseField name="url" type="string">
      The migrated Periskope media URL. Only present when `success` is `true`.
    </ResponseField>

    <ResponseField name="error" type="string">
      Reason the URL could not be migrated. Only present when `success` is `false`.
    </ResponseField>
  </Expandable>
</ResponseField>

<ResponseField name="migrated_count" type="number">
  Number of URLs migrated successfully.
</ResponseField>

<ResponseField name="failed_count" type="number">
  Number of URLs that failed to migrate.
</ResponseField>

<ResponseField name="access" type="string">
  The access mode applied to the returned URLs — `private` or `public`.
</ResponseField>

<ResponseField name="expires_in_seconds" type="number">
  Validity of the returned links in seconds. Only present for private links.
</ResponseField>

<RequestExample>
  ```bash cURL theme={null}
  curl --request POST \
    --url https://api.periskope.app/v1/media/migrate-url/batch \
    --header 'Authorization: Bearer <api_key>' \
    --header 'Content-Type: application/json' \
    --data '{
      "urls": [
        "https://storage.googleapis.com/periskope-attachments/2997dd64-89bf-48d3-9a22-b314fca017e5/919876543210/msg_ABCDEF123/file.jpg",
        "https://storage.googleapis.com/periskope-attachments/other-org-id/919876543210/msg_XYZ789/file-2.pdf"
      ],
      "access": "private",
      "expires_in_seconds": 86400
    }'
  ```
</RequestExample>

<ResponseExample>
  ```json 200 OK theme={null}
  {
    "results": [
      {
        "index": 0,
        "success": true,
        "url": "https://api.periskope.app/app/media/periskope-attachments/2997dd64-89bf-48d3-9a22-b314fca017e5%2F919876543210%2Fmsg_ABCDEF123%2Ffile.jpg?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
      },
      {
        "index": 1,
        "success": false,
        "error": "URL belongs to a different organization"
      }
    ],
    "migrated_count": 1,
    "failed_count": 1,
    "access": "private",
    "expires_in_seconds": 86400
  }
  ```
</ResponseExample>
