Uploading a cover image¶
Every content item — article, video, event, series, track, guide, course — can carry a cover image. PlusPlus shows it on cards and detail pages, and falls back to a generated or default cover when none is set. The upload mirrors guides and transcripts: request a URL, PUT the image, poll until ready. If the image is already online, import it from its URL in one call instead.
Flow at a glance¶
sequenceDiagram
participant Your app
participant PlusPlus API
participant Storage
Your app->>PlusPlus API: POST /content-items/{public_id}/cover/uploads/ { content_type }
PlusPlus API-->>Your app: 201 { upload_id, upload_url, expires_at }
Your app->>Storage: PUT (image/png, image/jpeg, image/gif or image/webp)
loop until ready or errored
Your app->>PlusPlus API: GET /content-items/{public_id}/cover/uploads/{upload_id}/status/
PlusPlus API-->>Your app: 200 { status, cover_url }
end
1. Request an upload URL¶
Declare the image's MIME type. The presigned URL is signed for that type, so the PUT must carry it as its Content-Type header. Accepted types are image/png, image/jpeg, image/gif and image/webp; files up to 10 MiB are accepted.
curl -X POST https://acme.plusplus.app/api/v2/content-items/01HXYDBKW5V2H6F3M8R9N0Q2YP/cover/uploads/ \
-H "Authorization: Bearer your_token_here" \
-H "Content-Type: application/json" \
-d '{ "content_type": "image/png" }'
{
"upload_id": "3f2a9c1e5b7d4e8f9a0b1c2d3e4f5a6b",
"upload_url": "https://plusplus-storage.s3.amazonaws.com/…",
"upload_type": "s3_presigned_put",
"content_item_id": "01HXYDBKW5V2H6F3M8R9N0Q2YP",
"expires_at": "2026-04-29T13:00:00Z",
"status": "awaiting_upload"
}
You need management access to the item (author, facilitator, channel owner, organizer or admin); the request returns 403 otherwise.
2. Upload the image¶
The URL expires one hour after it was issued. Past that, request a new one.
3. Poll until ready¶
curl https://acme.plusplus.app/api/v2/content-items/01HXYDBKW5V2H6F3M8R9N0Q2YP/cover/uploads/3f2a9c1e5b7d4e8f9a0b1c2d3e4f5a6b/status/ \
-H "Authorization: Bearer your_token_here"
Polling is what attaches the image: the first poll after your PUT completes reads the file, checks that it is a readable image of an accepted format, and makes it the item's cover. Only one poll at a time does this, so polling from several places is safe. Poll every 2–3 seconds after the upload finishes.
status |
Meaning |
|---|---|
awaiting_upload |
The file has not landed in storage yet. |
processing |
Another poll is attaching the file right now. Poll again in a moment. |
ready |
The image is now the item's cover. cover_url points at it, and GET /content-items/{public_id}/ reports the same cover_url. |
errored |
The file was refused: not a readable image, not an accepted format, or larger than 10 MiB. error_message says which. Request a new upload URL to try again. |
{
"upload_id": "3f2a9c1e5b7d4e8f9a0b1c2d3e4f5a6b",
"content_item_id": "01HXYDBKW5V2H6F3M8R9N0Q2YP",
"status": "ready",
"message": "Cover image is ready.",
"cover_url": "https://plusplus-storage.s3.amazonaws.com/…/covers/…_cover.png",
"error_message": null
}
Alternative: import from a URL¶
When the image is already hosted somewhere public, skip the upload and let PlusPlus fetch it:
curl -X PUT https://acme.plusplus.app/api/v2/content-items/01HXYDBKW5V2H6F3M8R9N0Q2YP/cover/ \
-H "Authorization: Bearer your_token_here" \
-H "Content-Type: application/json" \
-d '{ "source_url": "https://images.example.com/onboarding-cover.png" }'
The response is the updated content item, with cover_url pointing at the new cover. The URL must be http(s), point at a public host (private, local and internal addresses are refused, as are redirects to them; at most 3 redirects are followed) and answer with a PNG, JPEG, GIF or WebP image of at most 10 MiB. Anything else — an unreachable host, a non-200 answer, an HTML page, an oversized file — is a 400 with code invalid_cover_source and a message saying which.
What replacing a cover does¶
- The new image replaces the item's current cover, whether it was uploaded by hand or generated by PlusPlus.
- An item with an uploaded cover is never overwritten by automatic cover generation, and
POST /content-items/{public_id}/regenerate-cover/returns422for it. To go back to a generated cover, remove the uploaded one in the PlusPlus dashboard. - An event with its own cover no longer inherits the cover of its series.
Session lifetime¶
The upload session — the upload_id and its status — lives for one hour from the request. After that, the status endpoint returns 404, whether or not the file was attached; the item itself keeps its cover and stays reachable through GET /content-items/{public_id}/.