---
title: Hub Media API
slug: editorial/hub-media-api
createdAt: 2026-04-16T12:03:42.966Z
---

## Overview

The Media API provides endpoints for uploading, updating, deleting, and managing WordPress media attachments programmatically. It supports both direct file uploads and URL-based imports, with advanced features like hash-based deduplication and multilingual support. Media items can also be deleted, on their own or together with a post, see Deleting Media.

## Endpoint

```javascript
POST /wp-json/purple/v3/import/media
```

**Authentication Required:** Yes (Bearer token). Please take a look into the [Hub Import API](https://docs.purplepublish.com/editorial/hub-import-api#authentication) for more information
**Required Capability:** `edit_posts`

***

## Table of Contents

1. [Quick Start](./#quick-start)
2. [Request Parameters](./#request-parameters)
3. [Response Format](./#response-format)
4. [Use Cases & Examples](./#use-cases--examples)
5. [Deduplication Modes](./#deduplication-modes)
6. [Error Handling](./#error-handling)
7. [Best Practices](docId:)

***

## Quick Start

### Upload a New Image

```bash
curl -X POST 'https://your-site.com/wp-json/purple/v3/import/media' \
  -H 'Authorization: Bearer YOUR_TOKEN' \
  -F 'file=@/path/to/image.jpg' \
  -F 'metadata[title]=My Image' \
  -F 'metadata[alt]=Image description'
```

### Import from URL

```bash
curl -X POST 'https://your-site.com/wp-json/purple/v3/import/media' \
  -H 'Authorization: Bearer YOUR_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
    "fileUrl": "https://example.com/image.jpg",
    "metadata": {
      "title": "Remote Image",
      "alt": "Description"
    }
  }'
```

***

## Request Parameters

### File Source (Choose One)

| Parameter | Type   | Description                              |
| --------- | ------ | ---------------------------------------- |
| `file`    | File   | Direct file upload (multipart/form-data) |
| `fileUrl` | String | URL to download the file from            |

**Note:** Provide either `file` OR `fileUrl`, not both. When an existing attachmentId or mediaHash (see below) is provided, it is not necessary to provide one of these.

### Update Existing Attachment (Optional)

| Parameter      | Type    | Description                         |
| -------------- | ------- | ----------------------------------- |
| `attachmentId` | Integer | WordPress attachment ID to update   |
| `mediaHash`    | String  | SHA-256 hash to identify attachment |

**Note:** If provided without a new file, only metadata is updated.

### Metadata (Optional)

| Field                     | Type    | Description                 |
| ------------------------- | ------- | --------------------------- |
| `metadata[title]`         | String  | Attachment title            |
| `metadata[alt]`           | String  | Alt text for images         |
| `metadata[caption]`       | String  | Caption text                |
| `metadata[description]`   | String  | Full description            |
| `metadata[focalPoint][x]` | Integer | Focal point X               |
| `metadata[focalPoint][y]` | Integer | Focal point Y               |
| `metadata[acf]`           | Object  | Advanced Custom Fields data |

### Multilingual Support (Polylang)

| Parameter                             | Type    | Description                            |
| ------------------------------------- | ------- | -------------------------------------- |
| `metadata[languageSlug]`              | String  | Language code (e.g., 'en', 'de', 'fr') |
| `metadata[duplicateAssetsByLanguage]` | Boolean | Enable language-based deduplication    |

### Post Association (Optional)

| Parameter     | Type  | Description                           |
| ------------- | ----- | ------------------------------------- |
| `targetPosts` | Array | Post IDs to associate with this media |

***

## Response Format

### Success Response (200 OK)

```json
{
  "attachmentId": 123,
  "mediaHash": "a7f3c9d2e8b1f4a6c5d8e9f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1",
  "url": "https://your-site.com/wp-content/uploads/2024/01/image.jpg"
}
```

### Error Response

```json
{
  "code": "invalid_file_type",
  "message": "Invalid file type. File type not allowed by WordPress.",
  "data": {
    "status": 400
  }
}
```

***

## Use Cases & Examples

### 1. Upload New Image with Metadata

```bash
curl -X POST 'https://your-site.com/wp-json/purple/v3/import/media' \
  -H 'Authorization: Bearer YOUR_TOKEN' \
  -F 'file=@product.jpg' \
  -F 'metadata[title]=Product Photo' \
  -F 'metadata[alt]=Red bicycle in store' \
  -F 'metadata[caption]=Our new 2024 model' \
  -F 'metadata[description]=High-quality mountain bike' \
  -F 'metadata[focalPoint][x]=60' \
  -F 'metadata[focalPoint][y]=40'
```

### 2. Import from External URL

```bash
curl -X POST 'https://your-site.com/wp-json/purple/v3/import/media' \
  -H 'Authorization: Bearer YOUR_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
    "fileUrl": "https://cdn.example.com/images/product-123.jpg",
    "metadata": {
      "title": "Product 123",
      "alt": "Product photo",
      "caption": "Imported from CDN"
    }
  }'
```

### 3. Update Existing Attachment

**Update metadata only:**

```bash
curl -X POST 'https://your-site.com/wp-json/purple/v3/import/media' \
  -H 'Authorization: Bearer YOUR_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
    "attachmentId": 123,
    "metadata": {
      "title": "Updated Title",
      "alt": "Updated alt text"
    }
  }'
```

**Replace file and update metadata:**

```bash
curl -X POST 'https://your-site.com/wp-json/purple/v3/import/media' \
  -H 'Authorization: Bearer YOUR_TOKEN' \
  -F 'attachmentId=123' \
  -F 'file=@new-image.jpg' \
  -F 'metadata[title]=Replaced Image'
```

### 4. Update by Media Hash

```bash
curl -X POST 'https://your-site.com/wp-json/purple/v3/import/media' \
  -H 'Authorization: Bearer YOUR_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
    "mediaHash": "a7f3c9d2e8b1f4a6c5d8e9f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1",
    "metadata": {
      "caption": "Updated via hash"
    }
  }'
```

### 5. Associate with Multiple Posts

```bash
curl -X POST 'https://your-site.com/wp-json/purple/v3/import/media' \
  -H 'Authorization: Bearer YOUR_TOKEN' \
  -F 'file=@shared-image.jpg' \
  -F 'targetPosts[]=101' \
  -F 'targetPosts[]=202' \
  -F 'targetPosts[]=303' \
  -F 'metadata[title]=Shared Image'
```

### 6. Multilingual Upload (Polylang)

**Upload English version:**

```bash
curl -X POST 'https://your-site.com/wp-json/purple/v3/import/media' \
  -H 'Authorization: Bearer YOUR_TOKEN' \
  -F 'file=@product.jpg' \
  -F 'metadata[languageSlug]=en' \
  -F 'metadata[duplicateAssetsByLanguage]=true' \
  -F 'metadata[title]=English Product' \
  -F 'metadata[alt]=English description'
```

**Upload German version (reuses file!):**

```bash
curl -X POST 'https://your-site.com/wp-json/purple/v3/import/media' \
  -H 'Authorization: Bearer YOUR_TOKEN' \
  -F 'file=@product.jpg' \
  -F 'metadata[languageSlug]=de' \
  -F 'metadata[duplicateAssetsByLanguage]=true' \
  -F 'metadata[title]=Deutsches Produkt' \
  -F 'metadata[alt]=Deutsche Beschreibung'
```

**Result:** Two attachment posts, one physical file.

### 7. Advanced Custom Fields (ACF)

```bash
curl -X POST 'https://your-site.com/wp-json/purple/v3/import/media' \
  -H 'Authorization: Bearer YOUR_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{
    "fileUrl": "https://example.com/image.jpg",
    "metadata": {
      "title": "Product Image",
      "acf": {
        "photographer": "John Doe",
        "license": "CC-BY-SA",
        "shooting_date": "2024-01-15"
      }
    }
  }'
```

***

## Deduplication Modes

The API supports two deduplication strategies:

### 1. Hash-Based Deduplication (Default)

**How it works:**

- Calculates SHA-256 hash of file content
- If hash exists → Returns existing attachment
- Prevents duplicate files globally

**Best for:**

- Single-language sites
- Preventing duplicate uploads
- Saving disk space

**Example:**

```bash
# First upload
curl -F 'file=@image.jpg' ...
# Response: attachmentId: 123

# Second upload (same file)
curl -F 'file=@image.jpg' ...
# Response: attachmentId: 123 (same!)
```

### 2. Language-Based Deduplication

**How it works:**

- Checks by filename + language
- If filename exists in same language → Returns existing
- If filename exists in different language → Creates new attachment, **reuses file**
- Creates separate attachment posts per language
- All language variants share the same physical file

**Best for:**

- Multilingual sites (Polylang)
- Language-specific metadata (alt text, captions)
- Disk space efficiency

**Example:**

```bash
# Upload English version
curl -F 'metadata[languageSlug]=en' -F 'metadata[duplicateAssetsByLanguage]=true' -F 'file=@image.jpg' ...
# Response: attachmentId: 123, file: /uploads/image.jpg

# Upload German version (same file content)
curl -F 'metadata[languageSlug]=de' -F 'metadata[duplicateAssetsByLanguage]=true' -F 'file=@image.jpg' ...
# Response: attachmentId: 124, file: /uploads/image.jpg (SAME file!)
```

**Result:**

- ✅ Two attachment posts (#123 EN, #124 DE)
- ✅ Language-specific metadata
- ✅ One physical file on disk
- ✅ No disk space waste

**When to use:**

- Content with language-specific alt text/captions
- PDFs with embedded text in different languages
- Images with localized graphics/text

***

## Error Handling

### Common Error Codes

| Code                     | HTTP Status | Description                                           |
| ------------------------ | ----------- | ----------------------------------------------------- |
| `invalid_file_upload`    | 400         | Missing or invalid file                               |
| `invalid_file_type`      | 400         | File type not allowed by WordPress                    |
| `file_too_large`         | 400         | File exceeds 50MB limit                               |
| `invalid_url`            | 400         | Invalid or malformed URL                              |
| `ssrf_protection`        | 400         | URL points to private/reserved IP                     |
| `attachment_not_found`   | 404         | Attachment ID doesn't exist                           |
| `media_hash_not_found`   | 404         | No attachment with that hash                          |
| `polylang_required`      | 400         | duplicateAssetsByLanguage set but Polylang not active |
| `upload_directory_error` | 500         | WordPress upload directory issue                      |

## Deleting Media

Media can be deleted via the API in two ways: a single media item on its own, or all media of a post together with the post.

### Delete a single media item

```javascript
POST /wp-json/purple/v3/import/media/delete
```

**Authentication Required:** Yes (Bearer token)
**Required Capability:** `delete_posts`

The media item is deleted permanently: the media library entry, the file in all image sizes, and, within a few minutes, the copies cached by the CDN. There is no trash for media, so a deleted media item can't be restored.

Identify the media item by one of these parameters (JSON body):

| Parameter    | Type    | Description                                            |
| ------------ | ------- | ------------------------------------------------------ |
| attachmentId | Integer | WordPress attachment ID                                |
| mediaHash    | String  | SHA-256 hash returned when the media item was imported |

If both are given, they must belong to the same media item.

:::BlockQuote
**Warning:** The media item is deleted even if posts still use it; these posts then show a broken image. To delete only media that no other post uses, delete it together with the post (see below).
:::

```bash
curl -X POST 'https://your-site.com/wp-json/purple/v3/import/media/delete' \
  -H 'Authorization: Bearer YOUR_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{ "attachmentId": 123 }'
```

Success response (200 OK):

```json
{
  "attachmentId": 123,
  "message": "Attachment deleted permanently"
}
```

Error responses contain the error message as plain text:

| HTTP Status | Description                                                                                                    |
| ----------- | -------------------------------------------------------------------------------------------------------------- |
| 400         | Neither attachmentId nor mediaHash is given, or attachmentId and mediaHash don't belong to the same media item |
| 403         | Not allowed to delete this media item                                                                          |
| 404         | No media item with this attachmentId or mediaHash                                                              |
| 500         | The media item could not be deleted                                                                            |

### Delete media together with a post

When deleting a post via the Hub Import API (`POST /wp-json/purple/v3/import/delete`), set `deleteAssets` to `true` to delete the post's media in the same call. This requires `deletePermanently: true`, because deleted media can't be restored together with a trashed post.

```json
{
  "externalId": "3c6fcf10-0f6c-11ed-a8b4-07f9801ba5be",
  "deletePermanently": true,
  "deleteAssets": true
}
```

These media items of the post are deleted:

- the featured image
- images in the content whose block contains the attachment ID (the `id` attribute or a `wp-image-<id>` class)
- media in ACF image, file and gallery fields
- media that was uploaded to the post

A media item is kept if another post uses it, whatever that post's status (including drafts and posts in the trash). Language copies of a media item that share the same file are kept or deleted together.

:::BlockQuote
**Warning:** Images that another post only embeds by URL, without the attachment ID in the image block, are not recognized as used and are deleted. When importing posts, send image blocks with an attachment ID; any number works, the importer replaces it with the ID of the imported image:
:::

```html
<!-- wp:image {"id":1,"sizeSlug":"large"} -->
<figure class="wp-block-image size-large"><img src="https://example.com/image.jpg" alt="" class="wp-image-1"/></figure>
<!-- /wp:image -->
```

Success response (200 OK):

```json
{
  "postId": 77289,
  "message": "Post deleted permanently",
  "deletedAssetIds": [101, 102],
  "failedAssetIds": [],
  "keptAssetIds": [103]
}
```

| Field           | Description                                                       |
| --------------- | ----------------------------------------------------------------- |
| deletedAssetIds | Media items deleted together with the post                        |
| failedAssetIds  | Media items that could not be deleted; the post is deleted anyway |
| keptAssetIds    | Media items kept because another post uses them                   |

| HTTP Status | Code                                          | Description                                                             |
| ----------- | --------------------------------------------- | ----------------------------------------------------------------------- |
| 400         | DELETE\_ASSETS\_REQUIRES\_PERMANENT\_DELETION | deleteAssets is set without deletePermanently; the post stays untouched |

Without `deleteAssets`, deleting a post keeps its media as before.
