The vFound API and webhooks
Connect your lost and found to the systems you already run: property management, service desks, data warehouses or your own apps. Read and add found items, follow claims and lost reports, pull analytics, and get a signed webhook the moment anything happens.
Authentication
The API is included in vFound Pro. An owner or manager creates keys in Settings, API and webhooks, choosing what each key may do. The full key is shown once. Send it as a Bearer token:
curl https://vfound.io/api/v1/items \
-H "Authorization: Bearer vf_live_xxxxxxxxxx_…"
| Scope | Allows | Endpoints |
|---|---|---|
items:read | Read found items and categories | GET /items, /items/{id}, /categories |
items:write | Add and update found items | POST /items, PATCH /items/{id} |
claims:read | Read claims, with the claimant’s contact details | GET /claims, /claims/{id} |
lost_reports:read | Read guests’ lost reports | GET /lost-reports |
analytics:read | Read the analytics summary | GET /analytics |
A key acts as the person who created it and stops working if that person no longer manages the venue. Each key can make 120 requests a minute. Base address: https://vfound.io/api/v1.
Items
| Request | What it does | Scope |
|---|---|---|
| GET/items | Items, newest change first. Filters: status, updated_since (ISO 8601), page, per_page (up to 100). | items:read |
| GET/items/{id} | One item by its id. | items:read |
| GET/categories | The category names an item can use. | items:read |
| POST/items | Add a found item. It is published to your guest page, runs alerts and matching, and appears in your audit trail like one added by hand. | items:write |
| PATCH/items/{id} | Change details or status. | items:write |
Add an item
curl https://vfound.io/api/v1/items -X POST \
-H "Authorization: Bearer $VFOUND_KEY" -H "Content-Type: application/json" \
-d '{"title":"Black leather wallet","category":"Wallets & Purses","found_date":"2026-09-23",
"location":"Front desk","colour":"Black","brand":"Fossil","tags":["leather","cards"],
"photo":"data:image/jpeg;base64,…","photo_public":true}'
Fields: title and category are required. Optional: found_date, found_time (HH:MM), keep_until, location, description, colour, brand, tags, value_band (unknown, low, medium, high), notes (staff only), photo (a base64 data URL up to 8 MB) and photo_public.
The item object
{"id":"9b2f…","title":"Black leather wallet","status":"in_care","category":"Wallets & Purses",
"found_date":"2026-09-23","found_time":"14:30","keep_until":"2026-10-21","location":"Front desk",
"description":null,"colour":"Black","brand":"Fossil","tags":["leather","cards"],"value_band":"low",
"photo_public":true,"photo_url":"https://vfound.io/uploads/…","created_at":"…","updated_at":"…"}
Status is one of in_care, claimed, expired, donated, disposed or trash. A PATCH may set in_care, expired, donated, disposed or trash; returns to a guest happen through claims.
Claims
| Request | What it does | Scope |
|---|---|---|
| GET/claims | Claims, newest change first. Filters: status, updated_since, page, per_page. | claims:read |
| GET/claims/{id} | One claim by its reference. | claims:read |
{"id":"k3f9x2a1b7c4d8e6","status":"ready_for_pickup","item":{"id":"9b2f…","title":"Black leather wallet"},
"claimant":{"first_name":"Ana","last_name":"Ruiz","email":"ana@example.com","phone":null},
"description":"…","place_lost":"Bar","date_lost":"2026-09-22","language":"es","created_at":"…","updated_at":"…"}
Claim status: unread, open, accepted, awaiting_information, ready_for_pickup, ready_for_shipment, shipped, closed or rejected. Claimants’ details are personal data: keep them only as long as you need them.
Lost reports
GET/lost-reports lists what guests reported losing, with status (open or closed), the reporter’s contact details, the language they used and any Find My link they shared. Scope: lost_reports:read.
Analytics
GET/analytics?days=30 returns the same summary as Reports: items logged and returned, return rate, median days to return, where items ended up, categories, hotspots, busiest days, team output, the trend and guest feedback. days is 7, 30, 90 or 365. Scope: analytics:read.
Webhooks
Add an HTTPS address in Settings, API and webhooks and choose events. vFound sends a JSON POST within seconds, retries failures for about three hours, and pauses an address after 15 failed deliveries in a row.
| Event | When | Data |
|---|---|---|
item.created | A found item is added | Item |
item.updated | Its status, title, location, category, keep until date or photo visibility changes | Item |
claim.created | A guest submits a claim | Claim |
claim.status_changed | A claim moves to a new status | Claim |
lost_report.created | A guest reports a loss | Lost report |
{"id":"evt_…","type":"claim.created","created":"2026-09-23T14:30:00+00:00",
"venue":{"code":"harbourhotel","name":"Harbour Hotel"},"data":{ …the claim… }}
Check the signature
Every delivery carries Vfound-Signature: t=<unix time>,v1=<hex>, where the hex is an HMAC SHA256 of t + "." + raw body with your endpoint’s secret. Reject deliveries older than five minutes.
// Node.js
const crypto = require("crypto");
function verified(rawBody, header, secret) {
const { t, v1 } = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const expected = crypto.createHmac("sha256", secret).update(t + "." + rawBody).digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(t)) < 300;
return fresh && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}
// PHP
[$t, $v1] = array_map(fn ($p) => explode('=', $p, 2)[1], explode(',', $header));
$ok = abs(time() - (int) $t) < 300
&& hash_equals(hash_hmac('sha256', $t.'.'.$rawBody, $secret), $v1);
Headers also include Vfound-Event and a unique Vfound-Delivery id you can use to ignore repeats. Webhooks are sent only to public HTTPS addresses on port 443.
Errors
Errors are JSON with a message; validation errors add errors by field. 401 means no valid key, 403 a missing scope, a paused subscription or a plan without the API, 404 not found in your venue, 422 invalid input and 429 too many requests.