Hub Media API
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
POST /wp-json/purple/v3/import/mediaAuthentication Required: Yes (Bearer token). Please take a look into the Hub Import API for more information Required Capability: edit_posts
Table of Contents
- Quick StartQuick Start
- Request ParametersRequest Parameters
- Response FormatResponse Format
- Use Cases & ExamplesUse Cases & Examples
- Deduplication ModesDeduplication Modes
- Error HandlingError Handling
- Best PracticesBest Practices
Quick Start
Upload a New Image
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
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)
{
"attachmentId": 123,
"mediaHash": "a7f3c9d2e8b1f4a6c5d8e9f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1",
"url": "https://your-site.com/wp-content/uploads/2024/01/image.jpg"
}Error Response
{
"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
curl -X POST 'https://your-site.com/wp-json/purple/v3/import/media' \
-H 'Authorization: Bearer YOUR_TOKEN' \
-F '[email protected]' \
-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
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:
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:
curl -X POST 'https://your-site.com/wp-json/purple/v3/import/media' \
-H 'Authorization: Bearer YOUR_TOKEN' \
-F 'attachmentId=123' \
-F '[email protected]' \
-F 'metadata[title]=Replaced Image'4. Update by Media Hash
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
curl -X POST 'https://your-site.com/wp-json/purple/v3/import/media' \
-H 'Authorization: Bearer YOUR_TOKEN' \
-F '[email protected]' \
-F 'targetPosts[]=101' \
-F 'targetPosts[]=202' \
-F 'targetPosts[]=303' \
-F 'metadata[title]=Shared Image'6. Multilingual Upload (Polylang)
Upload English version:
curl -X POST 'https://your-site.com/wp-json/purple/v3/import/media' \
-H 'Authorization: Bearer YOUR_TOKEN' \
-F '[email protected]' \
-F 'metadata[languageSlug]=en' \
-F 'metadata[duplicateAssetsByLanguage]=true' \
-F 'metadata[title]=English Product' \
-F 'metadata[alt]=English description'Upload German version (reuses file!):
curl -X POST 'https://your-site.com/wp-json/purple/v3/import/media' \
-H 'Authorization: Bearer YOUR_TOKEN' \
-F '[email protected]' \
-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)
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:
# First upload
curl -F '[email protected]' ...
# Response: attachmentId: 123
# Second upload (same file)
curl -F '[email protected]' ...
# 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:
# Upload English version
curl -F 'metadata[languageSlug]=en' -F 'metadata[duplicateAssetsByLanguage]=true' -F '[email protected]' ...
# Response: attachmentId: 123, file: /uploads/image.jpg
# Upload German version (same file content)
curl -F 'metadata[languageSlug]=de' -F 'metadata[duplicateAssetsByLanguage]=true' -F '[email protected]' ...
# 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
POST /wp-json/purple/v3/import/media/deleteAuthentication 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.
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):
{
"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.
{
"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.
<!-- 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):
{
"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.