Documentation

Send frames to a camera

One endpoint, one device key. Post an image whenever you like — every 10 seconds is typical — and the public live view picks it up on its next refresh.

Endpoint

POST https://your-site.lovable.app/api/public/ingest

Authentication — any of these

  • x-device-key: KEY header
  • Authorization: Bearer KEY header
  • Authorization: Basic (key as the password)
  • ?key=KEY in the URL — for devices that only let you set a URL

Body — any of these

  • Raw binary image (JPEG, PNG, WebP)
  • multipart/form-data with an image / file field
  • JSON { "image": "<base64 or data URL>" }
  • Optional captured_at (ISO 8601) to backfill a timestamp

Max 20MB per frame. Responses are JSON; CORS is open so browser and kiosk uploads work.

Recipes

Replace $AUVI_KEY with the device key for your camera.

1. Test the key first

A GET returns the camera the key belongs to and how many frames it already has. Run this before wiring a schedule.

curl "https://your-site.lovable.app/api/public/ingest" -H "x-device-key: $AUVI_KEY"

# {"ok":true,"camera":{"slug":"riverside-north","name":"…","intervalSeconds":10},"frames":0}

2. curl — raw image body

The simplest possible upload. Works from any shell, cron job, or CI runner.

curl -X POST "https://your-site.lovable.app/api/public/ingest" \
  -H "x-device-key: $AUVI_KEY" \
  -H "content-type: image/jpeg" \
  --data-binary @frame.jpg

3. curl — multipart form upload

Use this when your tool speaks form uploads rather than raw bodies.

curl -X POST "https://your-site.lovable.app/api/public/ingest" \
  -H "x-device-key: $AUVI_KEY" \
  -F "image=@frame.jpg" \
  -F "captured_at=$(date -u +%Y-%m-%dT%H:%M:%SZ)"

4. Every 10 seconds from a Linux box

A shell loop is more reliable than cron for sub-minute cadence. Run it under systemd or supervisor so it restarts on reboot.

#!/usr/bin/env bash
export AUVI_KEY="paste-your-device-key"

while true; do
  # grab a still from a USB camera (fswebcam) or an RTSP stream (ffmpeg)
  fswebcam -r 1920x1080 --no-banner /tmp/frame.jpg

  curl -sf -X POST "https://your-site.lovable.app/api/public/ingest" \
    -H "x-device-key: $AUVI_KEY" \
    -H "content-type: image/jpeg" \
    --data-binary @/tmp/frame.jpg > /dev/null || echo "upload failed"

  sleep 10
done

5. RTSP / IP camera via ffmpeg — one frame

Pull a still from any RTSP or HTTP camera and pipe it straight up — no local storage needed.

ffmpeg -rtsp_transport tcp -i "rtsp://user:pass@10.0.0.42:554/stream1" \
  -frames:v 1 -q:v 3 -f image2 - 2>/dev/null |
curl -X POST "https://your-site.lovable.app/api/public/ingest" \
  -H "x-device-key: $AUVI_KEY" \
  -H "content-type: image/jpeg" \
  --data-binary @-

6. ffmpeg RTSP — a frame every 10 seconds

A supervised loop: pull one still per cycle, post it, and sleep only for the time the capture didn't use, so cadence stays on 10s even when the camera is slow to answer. Download auvi-rtsp-ingest.sh below for the hardened version with key checks, 429 backoff, and every auth method.

#!/usr/bin/env bash
set -uo pipefail
export AUVI_KEY="paste-your-device-key"
RTSP="rtsp://user:pass@10.0.0.42:554/stream1"
FRAME=/tmp/auvi-frame.jpg

while true; do
  started=$(date +%s)
  captured_at=$(date -u +%Y-%m-%dT%H:%M:%SZ)

  ffmpeg -hide_banner -loglevel error -y \
    -rtsp_transport tcp -i "$RTSP" \
    -frames:v 1 -q:v 3 -f image2 "$FRAME" || { echo "capture failed"; sleep 5; continue; }

  curl -sf -X POST "https://your-site.lovable.app/api/public/ingest" \
    -H "x-device-key: $AUVI_KEY" \
    -H "content-type: image/jpeg" \
    -H "x-captured-at: $captured_at" \
    --max-time 30 \
    --data-binary @"$FRAME" > /dev/null || echo "upload failed"

  sleep $(( 10 - ($(date +%s) - started) > 0 ? 10 - ($(date +%s) - started) : 1 ))
done

7. Snapshot URL → Auvi, in every auth method

Most IP cameras expose a still at an HTTP snapshot path. Pipe it straight into Auvi — same command, four ways to authenticate. Pick whichever your network or device allows.

SNAP="http://user:pass@10.0.0.42/cgi-bin/snapshot.cgi"   # Axis: /axis-cgi/jpg/image.cgi
AUVI_KEY="paste-your-device-key"

# a) Header — the default, works everywhere
curl -sf "$SNAP" | curl -X POST "https://your-site.lovable.app/api/public/ingest" \
  -H "x-device-key: $AUVI_KEY" \
  -H "content-type: image/jpeg" --data-binary @-

# b) Bearer token
curl -sf "$SNAP" | curl -X POST "https://your-site.lovable.app/api/public/ingest" \
  -H "authorization: Bearer $AUVI_KEY" \
  -H "content-type: image/jpeg" --data-binary @-

# c) Basic auth — key as the password, any username
curl -sf "$SNAP" | curl -X POST "https://your-site.lovable.app/api/public/ingest" \
  --user "auvi:$AUVI_KEY" \
  -H "content-type: image/jpeg" --data-binary @-

# d) Query string — for devices that only let you paste a URL
curl -sf "$SNAP" | curl -X POST "https://your-site.lovable.app/api/public/ingest?key=$AUVI_KEY" \
  -H "content-type: image/jpeg" --data-binary @-

# Add "?captured_at=2026-08-29T10:00:00Z" or an "x-captured-at" header
# to backfill the capture time on a raw upload.

8. Cameras that only accept a URL (Axis, Hikvision, Reolink, trail cams)

Many devices have an HTTP/FTP-to-HTTP push setting where you can only paste a destination URL. Put the key in the query string and point the device's periodic upload at it.

https://your-site.lovable.app/api/public/ingest?key=YOUR_DEVICE_KEY

# Device settings:
#   Method:        POST
#   Content type:  image/jpeg  (or multipart form with field name "image")
#   Interval:      10 seconds

9. Python

Good for Raspberry Pi / Jetson deployments using picamera2 or OpenCV.

import os, time, requests

ENDPOINT = "https://your-site.lovable.app/api/public/ingest"
KEY = os.environ["AUVI_KEY"]

while True:
    with open("/tmp/frame.jpg", "rb") as f:
        r = requests.post(
            ENDPOINT,
            headers={"x-device-key": KEY, "content-type": "image/jpeg"},
            data=f.read(),
            timeout=20,
        )
    print(r.status_code, r.text)
    time.sleep(10)

10. Node.js

Uses fetch, so no dependencies on Node 18+.

import { readFile } from "node:fs/promises";

const ENDPOINT = "https://your-site.lovable.app/api/public/ingest";
const KEY = process.env.AUVI_KEY;

setInterval(async () => {
  const body = await readFile("./frame.jpg");
  const res = await fetch(ENDPOINT, {
    method: "POST",
    headers: { "x-device-key": KEY, "content-type": "image/jpeg" },
    body,
  });
  console.log(await res.json());
}, 10_000);

11. JSON / base64 (Zapier, Make, ESP32-CAM, webhooks)

For platforms that can only send JSON. A data URL works too.

curl -X POST "https://your-site.lovable.app/api/public/ingest" \
  -H "authorization: Bearer $AUVI_KEY" \
  -H "content-type: application/json" \
  -d "{\"image\":\"$(base64 -w0 frame.jpg)\",\"captured_at\":\"2026-08-29T10:00:00Z\"}"

Reference clients

Complete, dependency-free clients that cover every supported auth method (header, bearer, basic, query) and upload format (raw, multipart, json), with a startup key check, capture hook, timestamping, and retry with backoff.

auvi-ingest.mjs

Node 18+ · no dependencies

Download
AUVI_ENDPOINT="https://your-site.lovable.app/api/public/ingest" \
AUVI_KEY="$AUVI_KEY" \
AUVI_AUTH=header AUVI_FORMAT=raw AUVI_INTERVAL=10 \
AUVI_CAPTURE="ffmpeg -y -rtsp_transport tcp -i rtsp://user:pass@10.0.0.42:554/stream1 -frames:v 1 -q:v 3 frame.jpg" \
node auvi-ingest.mjs ./frame.jpg

auvi_ingest.py

Python 3.8+ · standard library only

Download
AUVI_ENDPOINT="https://your-site.lovable.app/api/public/ingest" \
AUVI_KEY="$AUVI_KEY" \
AUVI_AUTH=bearer AUVI_FORMAT=multipart AUVI_INTERVAL=10 \
AUVI_CAPTURE="fswebcam -r 1920x1080 --no-banner frame.jpg" \
python3 auvi_ingest.py ./frame.jpg

auvi-rtsp-ingest.sh

bash · ffmpeg + curl only

Download
chmod +x auvi-rtsp-ingest.sh

AUVI_ENDPOINT="https://your-site.lovable.app/api/public/ingest" \
AUVI_KEY="$AUVI_KEY" \
RTSP_URL="rtsp://user:pass@10.0.0.42:554/stream1" \
AUVI_INTERVAL=10 AUVI_AUTH=header \
./auvi-rtsp-ingest.sh

auvi_onvif_ingest.py

Python 3.8+ · standard library only · ONVIF Profile S cameras

Download
# discover profiles and snapshot URLs first
ONVIF_HOST=10.0.0.42 ONVIF_USER=admin ONVIF_PASS=secret \
python3 auvi_onvif_ingest.py --list

AUVI_ENDPOINT="https://your-site.lovable.app/api/public/ingest" \
AUVI_KEY="$AUVI_KEY" \
AUVI_AUTH=header AUVI_FORMAT=raw AUVI_INTERVAL=10 \
ONVIF_HOST=10.0.0.42 ONVIF_USER=admin ONVIF_PASS=secret \
python3 auvi_onvif_ingest.py

Swap AUVI_AUTH and AUVI_FORMAT to match what your device or network allows — the API accepts any combination. Leave AUVI_INTERVAL at 0 to post a single frame and exit.

Responses

200Frame stored. Response includes capturedAt, bytes, and the storage path.
401Missing or invalid device key.
413Image larger than 20MB.
415Body wasn't a JPEG, PNG, or WebP.
400Empty body, or no image field found in the form/JSON.
429Posting faster than this camera's allowance. Honour the Retry-After header.

Rate limits

Limits are derived from each camera's own capture interval, not a fixed global number — a one-second camera and an hourly camera each get their own allowance with room for retries and catch-up bursts.

Per cameraFour times its configured cadence, with a burst of at least 20 frames so a device that buffered while offline can catch up in one go.
Per network address5 requests/second sustained, burst of 120 — covers a whole site posting from one connection.
Failed keysTen bad-key attempts, then one every 10 seconds, so key guessing stalls.
429 responsesInclude a Retry-After header and a scope field. Wait that many seconds and retry — the reference clients already back off.

Need a faster or slower cadence? Change the camera's interval and the limit follows it. Rate-limited attempts appear on the ingest health page; they don't fire failure webhooks.

Good to know

  • Device keys are stored only as a SHA-256 hash — keep your copy safe, and ask us to rotate it if a device is lost or replaced.
  • A camera goes online the moment its first frame lands. Until then the public view shows a "waiting for first frame" standby screen — it never shows sample imagery in place of your site.
  • Frames are stored privately; public views render short-lived signed URLs, watermarked and download-free.
  • Posting faster than the camera's cadence is fine — the live view always shows the newest frame and the rest go into the archive.