Media uploads & attachments.
Stop hosting your own media pipeline. Upload an image or a video to a community once; images get an EXIF-stripped rendition ladder, videos get a streaming-ready MP4 and poster frame — all stored privately. Attach by id to posts, comments, and DMs — reads hand back short-lived presigned URLs.
1
Upload an image
A direct multipart upload. PNG / JPEG / WEBP, up to 10 MB — the content type is verified against the actual bytes, not the declared header. Processing is synchronous: by the time the call returns status: "ready", every rendition is stored. EXIF (including GPS) is stripped and the orientation baked into the pixels.
curl -X POST -H "Authorization: Bearer pcft_live_..." \
-F "owner_actor_id=<actor>" \
-F "file=@photo.jpg" \
"https://social.productcraft.co/v1/communities/<c>/assets"
# → { "id": "<assetId>", "status": "ready" }2
Read renditions
Fetch an asset to get its renditions — original, medium (≤1280px), and thumb (≤320px). Each url is a presigned GET valid for 15 minutes, freshly minted on every read; the bucket itself is private, so there are no durable public URLs to leak. Re-fetch to refresh.
curl -H "Authorization: Bearer pcft_live_..." \
"https://social.productcraft.co/v1/communities/<c>/assets/<assetId>"
# → { id, status, content_type, width, height,
# renditions: [ { name: "medium", url: "https://…?X-Amz-Signature=…", width, height, … } ] }3
Attach to posts, comments, DMs
Pass ordered assets on create — up to 20, each an asset you own in this community that has finished processing. Reads echo the attachment refs back (id + alt text); resolve each to renditions with the fetch above. Attachments are additive: the legacy url / attributes fields still work.
curl -X POST -H "Authorization: Bearer pcft_live_..." \
-H "Content-Type: application/json" \
-d '{
"actor_id": "<actor>",
"body": "Beach day",
"assets": [
{ "asset_id": "<assetId>", "alt_text": "A golden retriever on a beach" }
]
}' \
"https://social.productcraft.co/v1/communities/<c>/posts"The same assets array works on POST .../posts/:id/comments and POST .../conversations/:id/messages. An asset that isn't ready, isn't yours, or belongs to another community is rejected at create time.
4
Upload a video
Videos are too large to proxy, so they upload straight to the bucket in three steps: declare, PUT, complete. Declare the container (video/mp4, video/quicktime, or video/webm) and the exact byte size — up to 512 MB. The response carries a presigned PUT URL valid for 15 minutes; send the bytes with exactly the returned headers (they are bound into the signature).
# 1. Declare — returns the asset in "uploading" + a presigned PUT
curl -X POST -H "Authorization: Bearer pcft_live_..." \
-H "Content-Type: application/json" \
-d '{ "owner_actor_id": "<actor>", "kind": "video",
"content_type": "video/mp4", "byte_size": 10485760 }' \
"https://social.productcraft.co/v1/communities/<c>/assets"
# → { id, status: "uploading", upload: { url, method: "PUT", headers, expires_at } }
# 2. PUT the bytes to upload.url, sending upload.headers verbatim
# (curl derives Content-Length from the file)
curl -X PUT -H "Content-Type: video/mp4" \
--data-binary @clip.mp4 "<upload.url>"
# 3. Complete — verifies the stored object and enqueues the transcode
curl -X POST -H "Authorization: Bearer pcft_live_..." \
"https://social.productcraft.co/v1/communities/<c>/assets/<assetId>/complete"
# → { id, status: "processing" }Transcoding runs out-of-band: up to 3 minutes of H.264/HEVC or VP8/VP9 video becomes one progressive, streaming-ready MP4 (downscaled to 1080p if larger, never upscaled) plus a poster JPEG. Poll the asset or subscribe to asset.ready / asset.failed — when ready, renditions carries original, video, and poster, and the row carries duration_s / width / height. Only ready assets attach — images and videos alike.
Failures are explicit state, never limbo: a size mismatch or a missing object leaves the asset uploading and retryable (ASSET_UPLOAD_INCOMPLETE); oversize or non-video bytes, a disallowed codec, or an over-length clip flip it to failed with a stable failure_reason (ASSET_TOO_LARGE, ASSET_INVALID_CONTAINER, ASSET_DURATION_EXCEEDED). Uploads never finalized are reaped after 24 hours. Each community can have 10 videos uploading or processing at once; the create call returns 429 TOO_MANY_ACTIVE_UPLOADS beyond that.
5
List & delete
List an actor's uploads (the “your media” surface, and part of the data-subject export walk). Deleting an asset removes every stored rendition object, not just the row; erasing the owning actor cascades and wipes their objects too.
# List an actor's assets (cursor-paginated newest-first)
curl -H "Authorization: Bearer pcft_live_..." \
"https://social.productcraft.co/v1/communities/<c>/actors/<actor>/assets"
# Delete an asset (removes the R2 objects + the row)
curl -X DELETE -H "Authorization: Bearer pcft_live_..." \
"https://social.productcraft.co/v1/communities/<c>/assets/<assetId>" # 204Subscribe to asset.ready / asset.failed on the webhooks surface to track processing out-of-band.