Upload a new file
files.upload
POST/api/v1/households/{household_id}/files
- Route
Creates a file from its first copy, sent as the raw request body with its Content-Type and an exact Content-Length. A display copy also needs width and height. Send an Idempotency-Key (a command ID, cmd_…) to make retries safe: the file's ID derives from it. See the files guide.
Parameters#
| Field | Description |
|---|---|
household_idpathstringrequired | The household's ID. |
namequerystringrequired | The file's name, such as IMG_2231.HEIC. |
copyqueryone of: original, displayrequired | Which copy: original (exactly the bytes that were uploaded, of any accepted type) or display (an image that stands for the file on screen). |
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. |
Idempotency-Keyheaderstringoptional | The upload's command ID. Reuse it when retrying. |
Request body#
The body is the raw bytes, sent as */*. The file's bytes, exactly as they are.
Response#
201 The file was stored. Location points at the file's details.
| 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.
Upload a photo
The first copy goes in the request body as raw bytes, with its type and exact length. Here it's the original.
curl -X POST 'https://rigel.house/api/v1/households/hh_01M6V9CS6GGVFCY7NAS8ZTQV95/files?name=pantry-middle-shelf.png©=original' \
-H "Cookie: rigel_session=$RIGEL_SESSION" \
-H 'Content-Type: image/png' \
-H 'Idempotency-Key: cmd_01M6V9CS96SD4HV9RTZ811FTV6' \
--data-binary @photo.jpgimport { readFile } from "node:fs/promises";
const response = await fetch("https://rigel.house/api/v1/households/hh_01M6V9CS6GGVFCY7NAS8ZTQV95/files?name=pantry-middle-shelf.png©=original", {
method: "POST",
headers: {
"Content-Type": "image/png",
"Idempotency-Key": "cmd_01M6V9CS96SD4HV9RTZ811FTV6",
Cookie: `rigel_session=${process.env.RIGEL_SESSION}`,
},
body: await readFile("photo.jpg"),
});
const data = await response.json();POST /api/v1/households/hh_01M6V9CS6GGVFCY7NAS8ZTQV95/files?name=pantry-middle-shelf.png©=original HTTP/1.1
Host: rigel.house
Cookie: rigel_session=<your session>
Content-Type: image/png
Content-Length: 655
Idempotency-Key: cmd_01M6V9CS96SD4HV9RTZ811FTV6
(655 bytes of image/png; The picture's bytes, a PNG.)HTTP/1.1 201 Created
Content-Type: application/json
X-Request-Id: req_01M6V9CS97BZJDWW3BF2BRA0F6
Location: /api/v1/households/hh_01M6V9CS6GGVFCY7NAS8ZTQV95/queries/files.file.get?file_id=fil_01M6V9CS96RRJQEBVXVCKMMR0T
{
"id": "fil_01M6V9CS96RRJQEBVXVCKMMR0T",
"name": "pantry-middle-shelf.png",
"kind": "image",
"original": {
"type": "image/png",
"size": 655
},
"display": null,
"created_at": "2026-11-07T15:17:52.045Z"
}Bytes that aren't the type they claim
The API checks a file's first bytes against its Content-Type.
curl -X POST 'https://rigel.house/api/v1/households/hh_01M6V9CS6GGVFCY7NAS8ZTQV95/files?name=not-a-photo.png©=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?name=not-a-photo.png©=original", {
method: "POST",
headers: {
"Content-Type": "image/png",
Cookie: `rigel_session=${process.env.RIGEL_SESSION}`,
},
body: await readFile("photo.jpg"),
});
const data = await response.json();POST /api/v1/households/hh_01M6V9CS6GGVFCY7NAS8ZTQV95/files?name=not-a-photo.png©=original HTTP/1.1
Host: rigel.house
Cookie: rigel_session=<your session>
Content-Type: image/png
Content-Length: 31
(31 bytes of image/png; Text, not a PNG.)HTTP/1.1 415 Unsupported Media Type
Content-Type: application/json
X-Request-Id: req_01M6V9CS9QXM9J3R8Z6VP67MKH
{
"error": {
"code": "unsupported_file_type",
"message": "The bytes aren't image/png. No changes were applied.",
"details": {},
"recovery": "Send a file of the declared type, or declare its real Content-Type.",
"request_id": "req_01M6V9CS9QXM9J3R8Z6VP67MKH"
}
}Too many uploads
Retry-After says how many seconds to wait.
curl -X POST 'https://rigel.house/api/v1/households/hh_01M6V9CTE3NDKQ0VACXB8Y2AY4/files?name=pantry-middle-shelf.png©=original' \
-H "Cookie: rigel_session=$RIGEL_SESSION" \
-H 'Content-Type: image/png' \
-H 'Idempotency-Key: cmd_01M6V9CTEGTWAR885N2XD4QSHE' \
--data-binary @photo.jpgimport { readFile } from "node:fs/promises";
const response = await fetch("https://rigel.house/api/v1/households/hh_01M6V9CTE3NDKQ0VACXB8Y2AY4/files?name=pantry-middle-shelf.png©=original", {
method: "POST",
headers: {
"Content-Type": "image/png",
"Idempotency-Key": "cmd_01M6V9CTEGTWAR885N2XD4QSHE",
Cookie: `rigel_session=${process.env.RIGEL_SESSION}`,
},
body: await readFile("photo.jpg"),
});
const data = await response.json();POST /api/v1/households/hh_01M6V9CTE3NDKQ0VACXB8Y2AY4/files?name=pantry-middle-shelf.png©=original HTTP/1.1
Host: rigel.house
Cookie: rigel_session=<your session>
Content-Type: image/png
Content-Length: 655
Idempotency-Key: cmd_01M6V9CTEGTWAR885N2XD4QSHE
(655 bytes of image/png; The picture's bytes, a PNG.)HTTP/1.1 429 Too Many Requests
Content-Type: application/json
X-Request-Id: req_01M6V9CTETFQWHE1RNY0TR28Q4
Retry-After: 60
{
"error": {
"code": "rate_limited",
"message": "Too many uploads. No changes were applied.",
"details": {
"retry_after_seconds": 60
},
"recovery": "Wait a minute, then upload again.",
"request_id": "req_01M6V9CTETFQWHE1RNY0TR28Q4"
}
}