ImageBundle
Professional photo distribution & IPTC automation platform. Upload once, route intelligently, deliver everywhere.
What is ImageBundle?
ImageBundle is a server-side platform for sports and news photographers to automate photo distribution. You upload images once via FTP, and the system automatically reads the embedded IPTC/XMP metadata, evaluates your routing rules, rewrites metadata where needed, and delivers images to multiple FTP destinations — all without manual intervention.
Typical use cases:
- Route images of Athlete A to their agency FTP, while images of Athlete B go to a different one — simultaneously
- Strip a credit line from descriptions before sending to Shutterstock
- Send all images from an event to 5 general outlets, plus one athlete-specific outlet per image
- Manage multiple independent client workflows (Bundles) from a single server
Core Concepts
How it Works
Every image upload goes through this pipeline:
- Upload: You drop a JPEG into the
bundle_<id>folder via FTP. - Bundle Detection: The FTP handler reads the folder name and matches it to a Bundle in the database.
- Metadata Read: The IPTC engine extracts embedded IPTC-IIM fields and XMP fields (including "Persons Shown" written by Photo Mechanic).
- Rules Evaluated: The rules engine checks metadata against the bundle's routing rules to determine which destinations should receive the image.
- IPTC Rewrite: Per-destination transforms are applied (e.g., strip copyright text from caption for Shutterstock).
- Delivery: The image (with rewritten metadata) is uploaded to each matched destination FTP server via a background Celery worker.
Logging In
All configuration is done via the REST API. The easiest way is through the Swagger UI at /docs.
Navigate to https://imagebundle.nakatori.agency/docs
Expand the endpoint, click "Try it out", enter your username and password, then Execute.
From the response body, copy the value of access_token (the long string starting with eyJ…).
Click the Authorize button at the top of /docs, paste the token in the Value field, and click Authorize. You're in.
Tokens expire after 30 minutes. If requests start returning 401, just repeat the login flow to get a fresh token.
Bundles
A Bundle is the top-level container for a workflow. Create one per client, event type, or distribution channel.
Creating a Bundle
Use POST /api/bundles with:
{
"name": "Orange Pictures — General",
"description": "Main distribution bundle for all events"
}
The response includes the bundle's id. Note it down — you'll need it for the upload folder name.
Upload Folder
After creating a bundle with id 3, create a subfolder named bundle_3 in your FTP home directory. Any JPEG you upload there will be processed by Bundle 3's rules.
The folder must be named exactly bundle_<id> — e.g. bundle_1, bundle_3, bundle_12. The system matches on this pattern.
Bundle Rules
Rules are stored as JSON inside the bundle. Set them via PUT /api/bundles/{id} with a rules field. See the Rules Engine section for the full schema.
Destinations
A Destination is an FTP server that images are delivered to. Each destination belongs to a Bundle.
Creating a Destination
Use POST /api/destinations:
{
"bundle_id": 1,
"name": "Getty Images FTP",
"destination_type": "ftp",
"host": "ftp.gettyimages.com",
"port": 21,
"username": "your-username",
"password": "your-password",
"remote_path": "/uploads"
}
The response includes the destination's id — you'll reference this in your rules.
Testing a Destination
Use POST /api/destinations/{id}/test to verify connectivity before going live. The server will attempt an FTP connection and report success or failure.
Passwords are stored encrypted (AES-256 Fernet). They are never returned in plain text after creation.
FTP Upload
ImageBundle runs its own FTP server. Connect with your FTP credentials and upload images into the correct bundle subfolder.
Connection Details
| Setting | Value |
|---|---|
| Host | imagebundle.nakatori.agency |
| Port | 21 |
| Mode | Passive (PASV) |
| Username | your FTP username (e.g. admin_ftp) |
| Password | your FTP password |
Folder Structure
After connecting, you will land in your home directory. Create subfolders named after your bundle IDs:
# Your FTP home directory
/
├── bundle_1/ # → processed by Bundle 1
├── bundle_2/ # → processed by Bundle 2
└── bundle_6/ # → processed by Bundle 6 (test)
Upload JPEG files into the appropriate folder. Processing starts automatically within a few seconds. Only JPEG files are processed; other formats are ignored.
What Happens Next
After upload, a background job is created. You can monitor it via GET /api/jobs. Each job shows the processing status and a list of distributions (one per matched destination) with their delivery status.
Rules Overview
Rules are stored as a JSON object on the Bundle. They tell ImageBundle what to do with each uploaded image. Rules are set via PUT /api/bundles/{id} with a rules field.
The top-level structure:
{
"version": 1,
"rules": [ /* array of rule objects */ ],
"destination_transforms": { /* optional, per-destination rewrites */ }
}
Rule Types
distribute_all
Send every image to one or more destinations, unconditionally.
{
"type": "distribute_all",
"destination_ids": [1, 2, 3]
}
filtered_distribute
Send images only when metadata matches a condition. Supports multiple filters, each with their own destination list.
{
"type": "filtered_distribute",
"filters": [
{
"field": "persons_shown",
"operator": "contains",
"value": "Femke Kok",
"destination_ids": [4]
}
]
}
Combine both types in the rules array: a distribute_all rule sends to your 5 general destinations, and a filtered_distribute rule adds an athlete-specific destination on top.
Full example: athlete routing
{
"version": 1,
"rules": [
{
"type": "distribute_all",
"destination_ids": [1, 2, 3, 4, 5] // always send to all 5
},
{
"type": "filtered_distribute",
"filters": [
{
"field": "persons_shown",
"operator": "contains",
"value": "Athlete One",
"destination_ids": [6] // also send to Athlete One's agency
},
{
"field": "persons_shown",
"operator": "contains",
"value": "Athlete Two",
"destination_ids": [7] // also send to Athlete Two's agency
}
]
}
]
}
Metadata Fields
These are the field names to use in rule filters. They are read from the JPEG's embedded IPTC-IIM data and XMP block.
| Field Name | Source | Description | Type |
|---|---|---|---|
| persons_shown | XMP (Photo Mechanic) | "Persons Shown" — athlete/person names | list |
| keywords | IPTC tag 25 | Keywords / tags | list |
| caption | IPTC tag 120 | Caption / description | string |
| headline | IPTC tag 105 | Headline | string |
| credit | IPTC tag 110 | Credit line | string |
| byline | IPTC tag 80 | Photographer name | string |
| city | IPTC tag 90 | City | string |
| country | IPTC tag 101 | Country name | string |
| event | XMP (Photo Mechanic) | Event name | string |
| category | IPTC tag 15 | Category code | string |
persons_shown is written by Photo Mechanic into the XMP block of the JPEG (not the IPTC-IIM block). ImageBundle reads both, so this field works seamlessly.
Operators
| Operator | Works on | Behaviour |
|---|---|---|
| contains | string, list | Value is a substring of the field, or a substring of any list item |
| not_contains | string, list | Inverse of contains |
| equals | string | Exact match (case-sensitive) |
| not_equals | string | Field does not exactly equal the value |
| starts_with | string | Field starts with the value |
| ends_with | string | Field ends with the value |
| regex | string | Field matches the regular expression |
| exists | any | Field is present and non-empty |
| not_exists | any | Field is absent or empty |
IPTC Transforms
Transforms let you rewrite metadata before an image is delivered to a specific destination. They are defined in the destination_transforms key at the top level of the bundle's rules JSON.
{
"version": 1,
"rules": [ /* ... */ ],
"destination_transforms": {
"8": [ // destination id 8 (e.g. Shutterstock)
{
"field": "caption",
"operation": "regex_replace",
"pattern": "\\s*\\(Photo by [^)]+\\)",
"replacement": ""
}
]
}
}
Available transform operations:
| Operation | Parameters | Description |
|---|---|---|
| set | value | Replace the entire field with a fixed value |
| prefix | value | Prepend text to the field |
| suffix | value | Append text to the field |
| replace | find, replacement | Find and replace a literal string |
| regex_replace | pattern, replacement | Find and replace using a regular expression |
| remove | value | Remove an exact value from a list field |
| remove_pattern | pattern | Remove any list items matching a regex pattern |
| add | value | Add a value to a list field (e.g. add a keyword) |
Transforms apply only to the copy sent to that destination. The original file on disk is never modified, and other destinations receive the unmodified metadata.
Multi-destination Routing
ImageBundle supports fully flexible multi-destination routing. A single image can be sent to any combination of destinations simultaneously, with different metadata rewrites per destination.
Scenarios
| Scenario | How to configure |
|---|---|
| All images → 5 general FTPs | distribute_all with 5 destination_ids |
| Athlete A → 5 general + FTP 6 | distribute_all + filtered_distribute on persons_shown |
| Athlete A → FTP Y only, Athlete B → FTP Z only | Two filtered_distribute rules, no distribute_all |
| Remove credit line for Shutterstock | destination_transforms with regex_replace on caption |
| Different caption per agency | Multiple entries in destination_transforms |
Split routing example
Send Athlete A only to FTP Y, and Athlete B only to FTP Z (completely separate):
{
"version": 1,
"rules": [
{
"type": "filtered_distribute",
"filters": [
{
"field": "persons_shown",
"operator": "contains",
"value": "Athlete A",
"destination_ids": [10]
},
{
"field": "persons_shown",
"operator": "contains",
"value": "Athlete B",
"destination_ids": [11]
}
]
}
]
}
FTP Credentials
FTP credentials are managed per user account. Each user gets a dedicated FTP username and password, independent of their API login.
To view or update credentials, use the Users API:
GET /api/users— list all usersPUT /api/users/{id}— update a user's FTP password
If you forget your password, an admin can reset it server-side using scripts/reset_password.py <username> <new_password>.
API Reference
Full interactive documentation is at /docs. Quick reference:
Authentication
| Method | Path | Description |
|---|---|---|
| POST | /api/auth/login | Get access token |
| GET | /api/auth/me | Current user info |
Bundles
| Method | Path | Description |
|---|---|---|
| GET | /api/bundles | List all bundles |
| POST | /api/bundles | Create a bundle |
| GET | /api/bundles/{id} | Get bundle details + rules |
| PUT | /api/bundles/{id} | Update bundle (including rules) |
| DELETE | /api/bundles/{id} | Delete a bundle |
Destinations
| Method | Path | Description |
|---|---|---|
| GET | /api/destinations | List destinations (filter by ?bundle_id=) |
| POST | /api/destinations | Create a destination |
| GET | /api/destinations/{id} | Get destination details |
| PUT | /api/destinations/{id} | Update destination |
| POST | /api/destinations/{id}/test | Test FTP connectivity |
| DELETE | /api/destinations/{id} | Delete destination |
Jobs
| Method | Path | Description |
|---|---|---|
| GET | /api/jobs | List jobs (filter by ?status=) |
| GET | /api/jobs/{id} | Get job details + distributions |
Users
| Method | Path | Description |
|---|---|---|
| GET | /api/users | List users |
| POST | /api/users | Create user |
| PUT | /api/users/{id} | Update user / reset FTP password |
| DELETE | /api/users/{id} | Delete user |