Upload a file's missing copy
files.add_copy
PUT/api/v1/households/{household_id}/files/{file_id}/{copy}
- Route
Adds the original or the display copy a file doesn't have yet, sent as the raw request body. A display copy also needs width and height. A file that already has that copy answers 409 file_copy_exists, which a client retrying a dropped connection treats as success. See the files guide.
Parameters#
| Field | Description |
|---|---|
household_idpathstringrequired | The household's ID. |
file_idpathstringrequired | The file's ID, such as fil_01JC… |
copypathone of: original, displayrequired | Which copy: original or display. |
widthqueryintegeroptional | The display copy's width in pixels, 1–4096. Required for a display copy. |
heightqueryintegeroptional | The display copy's height in pixels, 1–4096. Required for a display copy. |
Content-Typeheaderstringrequired | The media type of the bytes, such as image/jpeg. Parameters are ignored. |
Content-Lengthheaderstringrequired | The exact size of the body in bytes. |
Request body#
The body is the raw bytes, sent as */*. The file's bytes, exactly as they are.
Response#
200 The copy was stored.
| Field | Description |
|---|---|
idstringrequired | A file ID, such as fil_01JC0000000000000000000000. |
namestringrequired | The file's name as uploaded, used for downloads. |
kindone of: image, pdf, video, audio, otherrequired | What the file is: image, pdf, video, audio, or other (anything else, which is only ever offered as a download). |
originalobject or nullrequired | The bytes as uploaded, or null until the original is stored. |
original.typestringrequired | The original's media type, such as image/heic. |
original.sizeintegerrequired | The original's size in bytes. |
displayobject or nullrequired | The image that stands for the file on screen, or null when it has none. |
display.typestringrequired | The display copy's media type: image/jpeg, image/webp or image/png. |
display.sizeintegerrequired | The display copy's size in bytes. |
display.widthintegerrequired | The display copy's width in pixels. |
display.heightintegerrequired | The display copy's height in pixels. |
created_atstring (date-time)required | When the file was first stored. |
Errors#
Every error has the same envelope: error.code, error.message, error.details, error.recovery and error.request_id.
| Status | Code | When |
|---|---|---|
400 | validation_failed | A field is missing, misspelled or out of range. details.fields lists each problem. |
401 | unauthenticated | The request has no valid session. |
404 | not_found | The household doesn't exist, or you aren't a member of it. |
409 | Read error.code | Described above. |
411 | Read error.code | Described above. |
413 | Read error.code | Described above. |
415 | Read error.code | Described above. |
429 | Read error.code | Described above. |
Examples#
These are real. Each was recorded from a sample household, the Okafors, running through the whole API when this site was built.
Add the display copy
A phone makes a smaller copy for screens and sends it second. A display copy needs its width and height.
curl -X PUT 'https://rigel.house/api/v1/households/hh_01M6V9CS6GGVFCY7NAS8ZTQV95/files/fil_01M6V9CS96RRJQEBVXVCKMMR0T/display?width=96&height=64' \
-H "Cookie: rigel_session=$RIGEL_SESSION" \
-H 'Content-Type: image/png' \
--data-binary @photo.jpgimport { readFile } from "node:fs/promises";
const response = await fetch("https://rigel.house/api/v1/households/hh_01M6V9CS6GGVFCY7NAS8ZTQV95/files/fil_01M6V9CS96RRJQEBVXVCKMMR0T/display?width=96&height=64", {
method: "PUT",
headers: {
"Content-Type": "image/png",
Cookie: `rigel_session=${process.env.RIGEL_SESSION}`,
},
body: await readFile("photo.jpg"),
});
const data = await response.json();PUT /api/v1/households/hh_01M6V9CS6GGVFCY7NAS8ZTQV95/files/fil_01M6V9CS96RRJQEBVXVCKMMR0T/display?width=96&height=64 HTTP/1.1
Host: rigel.house
Cookie: rigel_session=<your session>
Content-Type: image/png
Content-Length: 342
(342 bytes of image/png; The picture's bytes, a PNG.)HTTP/1.1 200 OK
Content-Type: application/json
X-Request-Id: req_01M6V9CS9JGB776GF77JRAC0GC
{
"id": "fil_01M6V9CS96RRJQEBVXVCKMMR0T",
"name": "pantry-middle-shelf.png",
"kind": "image",
"original": {
"type": "image/png",
"size": 655
},
"display": {
"type": "image/png",
"size": 342,
"width": 96,
"height": 64
},
"created_at": "2026-11-07T15:17:52.045Z"
}Add a copy the file already has
A client that retries after a dropped connection can treat this as success.
curl -X PUT 'https://rigel.house/api/v1/households/hh_01M6V9CS6GGVFCY7NAS8ZTQV95/files/fil_01M6V9CS96RRJQEBVXVCKMMR0T/original' \
-H "Cookie: rigel_session=$RIGEL_SESSION" \
-H 'Content-Type: image/png' \
--data-binary @photo.jpgimport { readFile } from "node:fs/promises";
const response = await fetch("https://rigel.house/api/v1/households/hh_01M6V9CS6GGVFCY7NAS8ZTQV95/files/fil_01M6V9CS96RRJQEBVXVCKMMR0T/original", {
method: "PUT",
headers: {
"Content-Type": "image/png",
Cookie: `rigel_session=${process.env.RIGEL_SESSION}`,
},
body: await readFile("photo.jpg"),
});
const data = await response.json();PUT /api/v1/households/hh_01M6V9CS6GGVFCY7NAS8ZTQV95/files/fil_01M6V9CS96RRJQEBVXVCKMMR0T/original HTTP/1.1
Host: rigel.house
Cookie: rigel_session=<your session>
Content-Type: image/png
Content-Length: 655
(655 bytes of image/png; The picture's bytes, a PNG.)HTTP/1.1 409 Conflict
Content-Type: application/json
X-Request-Id: req_01M6V9CS9PMP8ZEGZM9401XTCQ
{
"error": {
"code": "file_copy_exists",
"message": "That file already has its original copy. No changes were applied.",
"details": {
"file_id": "fil_01M6V9CS96RRJQEBVXVCKMMR0T",
"copy": "original"
},
"recovery": "Read the file's current state, then try again.",
"request_id": "req_01M6V9CS9PMP8ZEGZM9401XTCQ"
}
}