PackCheck API

REST API behind the PackCheck iOS app — packing lists, templates, and the photo scan. Designed for reuse by any client (iOS today, web later). Version: v1.

Basics

Base URLhttps://packcheck-black.vercel.app/api/v1
FormatJSON requests and responses (Content-Type: application/json)
AuthNo accounts. Every request sends X-Device-Id — a client-generated UUID (8–128 chars), created once on first launch and persisted on the device. All data is scoped to that ID.
ErrorsJSON { "error": "message" } with an appropriate status: 400 bad input · 401 missing device ID · 404 not found · 503 dependency not configured

Types

Item     = { "name": string, "packed": boolean, "verifiedBy": "scan" | "manual" | null,
             "category": string | null }          // e.g. Documents, Tech, Clothing, Toiletries, Other
Template = { "id": uuid, "name": string,
             "items": string[],                    // v1-compatible names
             "itemsDetailed": [{ "name": string, "category": string | null }] }
Trip     = { "id": uuid, "name": string, "tripType": "business" | "leisure",
             "startDate": "YYYY-MM-DD" | null, "endDate": "YYYY-MM-DD" | null,
             "origin": string | null, "destination": string | null,
             "templateName": string | null, "items": Item[],
             "reminderOffsetMinutes": number,      // 5-2880, default 180 (3 hours)
             "createdAt": timestamp, "updatedAt": timestamp }

Endpoints

GET/health no auth

Liveness probe. db reflects database reachability.

{ "ok": true, "db": true }

GET/templates

The two built-in templates plus this device's saved templates.

{
  "builtIn": [
    { "name": "Business", "items": ["Laptop", "Laptop charger", "Dress shoes", "Dress shirts", "Toiletry kit"] },
    { "name": "Leisure",  "items": ["Swimsuit", "Sunscreen", "Sunglasses", "Sandals", "Phone charger", "Toiletry kit"] }
  ],
  "mine": [ { "id": "…", "name": "Boise QBR", "items": ["Laptop", "Dress shoes"] } ]
}

POST/templates

Save (or overwrite) a template by name — this is "Save as template". Upserts on (device, name). Accepts items as plain names or as {name, category} objects (also accepted under itemsDetailed). Returns 201 with the template.

{ "name": "Boise QBR",
  "items": [ { "name": "Laptop", "category": "Tech" },
             { "name": "Dress shoes", "category": "Clothing" } ] }   // 1-100 items

DELETE/templates?id=<uuid>

Delete one of this device's templates. Built-ins cannot be deleted.

GET/trips

All trips for this device, soonest start date first.

POST/trips

Create a trip. Items are seeded from templateName — the device's own template with that name wins, then a built-in; if omitted or unknown, the built-in matching tripType is used (default Business). Returns 201 with the full Trip.

{ "name": "Q3 Sales Kickoff",          // required, 1-120 chars — any name the user wants
  "tripType": "business",              // optional: "business" (default) | "leisure"
  "startDate": "2026-08-10",           // optional
  "endDate":   "2026-08-13",           // optional
  "origin": "SFO",                     // optional, ≤80 chars
  "destination": "Boise, ID",          // optional, ≤80 chars
  "reminderOffsetMinutes": 180,        // optional, 5-2880 (default 180)
  "templateName": "Business" }         // optional

GET/trips/:id · PATCH/trips/:id · DELETE/trips/:id

PATCH accepts any subset of: name, tripType, startDate, endDate, origin, destination, reminderOffsetMinutes, templateName, items (the full replacement array — used for check-offs, adds, swipe-deletes, and list swaps). Unknown fields are ignored.

PATCH { "items": [ { "name": "Laptop", "packed": true, "verifiedBy": "manual" }, … ] }

POST/scan

The one AI call in PackCheck. Send 1–2 photos of the open bag plus the item names; the vision model (Claude Haiku) decides which items are visible. Conservative by design: unsure → missing. Stateless — works without a database; if tripId is provided, found items are also persisted onto the trip as packed + verifiedBy: "scan".

// Request — photos are base64 (no data: prefix). Downscale to ≤1280px JPEG (~200-400 KB).
{ "items": ["Laptop", "Laptop charger", "Dress shoes"],
  "photos": [ { "mediaType": "image/jpeg", "dataBase64": "…" } ],   // 1-2 photos
  "tripId": "…" }                                                    // optional

// Response — every listed item appears in exactly one array, names verbatim.
{ "found": ["Laptop"], "missing": ["Laptop charger", "Dress shoes"] }

Limits: 1–100 items, ≤ ~2.8 MB base64 per photo, ~4 MB total request. Typical latency 2–6 s.

Example: full client flow

# every call:  -H "X-Device-Id: $DEVICE"  (a UUID you generate once)
BASE=https://packcheck-black.vercel.app/api/v1

# 1. templates for the picker
curl -s -H "X-Device-Id: $DEVICE" $BASE/templates

# 2. create the trip (items seeded from Business template)
curl -s -X POST -H "X-Device-Id: $DEVICE" -H "Content-Type: application/json" \
  -d '{"name":"Q3 Sales Kickoff","startDate":"2026-08-10","endDate":"2026-08-13"}' \
  $BASE/trips

# 3. scan the bag
curl -s -X POST -H "X-Device-Id: $DEVICE" -H "Content-Type: application/json" \
  -d "{\"tripId\":\"$TRIP\",\"items\":[\"Laptop\",\"Dress shoes\"],\"photos\":[{\"mediaType\":\"image/jpeg\",\"dataBase64\":\"$B64\"}]}" \
  $BASE/scan

# 4. save the edited list for next time
curl -s -X POST -H "X-Device-Id: $DEVICE" -H "Content-Type: application/json" \
  -d '{"name":"Boise QBR","items":["Laptop","Dress shoes"]}' $BASE/templates

Notes

PackCheck · home · privacy · support