Skip to content
Rigel docs
Open Rigel

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#

Upload a new file: parameters
FieldDescription
household_idpathstringrequired

The household's ID.

namequerystringrequired

The file's name, such as IMG_2231.HEIC.

1 to 255 characters
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.

1 to 4,096
heightqueryintegeroptional

The display copy's height in pixels, 1–4096. Required for a display copy.

1 to 4,096
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.

Upload a new file: response fields
FieldDescription
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.

Upload a new file: errors
StatusCodeWhen
400validation_failedA field is missing, misspelled or out of range. details.fields lists each problem.
401unauthenticatedThe request has no valid session.
404not_foundThe household doesn't exist, or you aren't a member of it.
409Read error.codeDescribed above.
411Read error.codeDescribed above.
413Read error.codeDescribed above.
415Read error.codeDescribed above.
429Read error.codeDescribed 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.

Request, curl
curl -X POST 'https://rigel.house/api/v1/households/hh_01M6V9CS6GGVFCY7NAS8ZTQV95/files?name=pantry-middle-shelf.png&copy=original' \
  -H "Cookie: rigel_session=$RIGEL_SESSION" \
  -H 'Content-Type: image/png' \
  -H 'Idempotency-Key: cmd_01M6V9CS96SD4HV9RTZ811FTV6' \
  --data-binary @photo.jpg
Request, TypeScript
import { readFile } from "node:fs/promises";

const response = await fetch("https://rigel.house/api/v1/households/hh_01M6V9CS6GGVFCY7NAS8ZTQV95/files?name=pantry-middle-shelf.png&copy=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();
Request, HTTP
POST /api/v1/households/hh_01M6V9CS6GGVFCY7NAS8ZTQV95/files?name=pantry-middle-shelf.png&copy=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.)
Response, 201
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.

Request, curl
curl -X POST 'https://rigel.house/api/v1/households/hh_01M6V9CS6GGVFCY7NAS8ZTQV95/files?name=not-a-photo.png&copy=original' \
  -H "Cookie: rigel_session=$RIGEL_SESSION" \
  -H 'Content-Type: image/png' \
  --data-binary @photo.jpg
Request, TypeScript
import { readFile } from "node:fs/promises";

const response = await fetch("https://rigel.house/api/v1/households/hh_01M6V9CS6GGVFCY7NAS8ZTQV95/files?name=not-a-photo.png&copy=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();
Request, HTTP
POST /api/v1/households/hh_01M6V9CS6GGVFCY7NAS8ZTQV95/files?name=not-a-photo.png&copy=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.)
Response, 415
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.

Request, curl
curl -X POST 'https://rigel.house/api/v1/households/hh_01M6V9CTE3NDKQ0VACXB8Y2AY4/files?name=pantry-middle-shelf.png&copy=original' \
  -H "Cookie: rigel_session=$RIGEL_SESSION" \
  -H 'Content-Type: image/png' \
  -H 'Idempotency-Key: cmd_01M6V9CTEGTWAR885N2XD4QSHE' \
  --data-binary @photo.jpg
Request, TypeScript
import { readFile } from "node:fs/promises";

const response = await fetch("https://rigel.house/api/v1/households/hh_01M6V9CTE3NDKQ0VACXB8Y2AY4/files?name=pantry-middle-shelf.png&copy=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();
Request, HTTP
POST /api/v1/households/hh_01M6V9CTE3NDKQ0VACXB8Y2AY4/files?name=pantry-middle-shelf.png&copy=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.)
Response, 429
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"
  }
}

Rigel is in early access, and sign-up is by invitation. Open Rigel