Skip to main content
A file arrives in two steps, because those are two different requests: the file itself is bytes, and what it belongs to is a JSON:API document. So you send the file first, get a handle back, and name that handle in the thing the file belongs to.
Until it is named, the file belongs to nobody.

Two ways in

multipart/form-data, with a single file part. What every HTTP client on every platform can send without help, and what an app with a photograph from a camera has.
Both answer the same thing:
string
The storage service’s own path for the file, and the only name either of us has for it. This is what you send back.
string
image or video, worked out from the file.
There is no self link and no Location: a pending upload has no address to read it back at. It is written once, named once, and then it is an attachment.

Naming it

On a listing

imageHandles, up to 8, in the order the gallery should be in.

On a message

attachmentHandles, up to 4.
Nothing after the upload knows which road the file came in by, so a chat attachment can come from a URL as easily as from a camera: fetch it into a handle, then name the handle on the message.

What we take

Images and video up to 10 MB: jpg, jpeg, png, gif, webp, heic, heif, mp4, mov, m4v. Anything else is 422. Scope uploads:write.
A whole gallery is the one case not to use this for. Eight photographs is eight calls, each waiting on a fetch. Give the listing imageSources instead — the URLs, all at once — and we fetch the set in the background and skip it entirely when it has not changed since the last sync. One call, and the second run costs nothing.