The developer API is included with Household and Pro. It is for developers and technically confident users who want their own application or script to search inventory, calculate valuations, read supported attachments and make approved changes. If you only want to ask questions in everyday words, connect ChatGPT instead.
You can try everything in the web app first: open How to use API, run a search or valuation report, inspect the request and response and copy a working example.
- Quick start
- Request format
- Sites
- Areas
- Categories
- Things
- Valuations
- Attachments
- Create, edit, move
- Limits and errors
- MCP
API quick start and access tokens
Base URL and authentication
Use HTTPS at https://app.thingzsafe.com. Send a developer token in the request header:
Authorization: Bearer YOUR_TOKEN
Content-Type: application/json
Use a developer token created in the web app, not an Android/device sign-in token or an AI assistant OAuth token. Each token is tied to one household and its verified owner. Active Household or Pro cloud access is required.
Create a token
- Sign in to the web app as the household owner.
- Open How to use API.
- Enter a connection name and an expiry of 1–90 days. The default is 30 days.
- Select the required permissions and choose Create token.
- Copy the token while it is shown. It cannot be displayed again later. Store it in your application's secret configuration.
The initial selections are inventory reading and valuation reports. Inventory changes and file reading are separate optional selections. A household can have up to 20 active, unexpired developer tokens. Use Revoke to stop an unused token; creating a replacement does not automatically revoke the previous one.
| Scope code | Developer API | ChatGPT |
|---|---|---|
inventory.read |
Inventory and location/category lookup | Same |
reports.read |
Valuation reports | Same |
inventory.write |
Supported create/edit/move batches | Same |
files.read |
Selected attachment listing/reading; also requires inventory.read |
Same |
feedback.write |
Not issued by the developer-token screen in this release | Support report drafts/submission; also requires inventory.read |
Developer write routes require inventory.write; include inventory.read as well if your application looks up records before proposing changes. AI assistant write tools require both.
Try a read in PowerShell
The following prompts for the token locally without displaying it. It uses the existing Sites in your household and changes no inventory.
$token = [System.Net.NetworkCredential]::new('', (Read-Host 'Developer token' -AsSecureString)).Password
$headers = @{ Authorization = "Bearer $token" }
$body = @{ kind = 'Site'; offset = 0; limit = 100 } | ConvertTo-Json
Invoke-RestMethod -Method Post `
-Uri 'https://app.thingzsafe.com/v1/developer/query' `
-Headers $headers -ContentType 'application/json' -Body $body
Try a read with curl
Put your token in a local secret/environment variable named THINGZSAFE_TOKEN. Save the JSON below as sites.json:
{"kind":"Site","offset":0,"limit":100}
curl 'https://app.thingzsafe.com/v1/developer/query' \
-H "Authorization: Bearer $THINGZSAFE_TOKEN" \
-H 'Content-Type: application/json' \
--data-binary @sites.json
On Windows PowerShell, use the PowerShell example above, or invoke curl.exe with Windows shell syntax. Never publish a real token in an example, screenshot or support conversation.
Shared request format, IDs and pagination
Endpoint map
| Method | Path | Purpose |
|---|---|---|
| POST | /v1/developer/query |
Sites, Areas, Categories, Things and valuation queries |
| POST | /v1/developer/attachments/list |
List attachments for selected Thing IDs |
| POST | /v1/developer/attachments/read |
Read one selected attachment or PDF page batch |
| POST | /v1/developer/batches/preview |
Preview create/edit/move changes |
| POST | /v1/developer/batches/execute |
Execute exactly one approved preview |
| GET | /v1/developer/batches/{batchId} |
Recover a preview or execution result using the same credential |
These are the developer-facing routes documented here. The web app's own authenticated management routes are not interchangeable with developer-token routes. In particular, a developer token cannot create or enlarge its own permissions.
Query fields
{
"text": null,
"siteId": null,
"areaId": null,
"categoryId": null,
"kind": null,
"relationship": null,
"parentThingId": null,
"includeDescendants": false,
"missingValueOnly": false,
"offset": 0,
"limit": 100,
"report": false
}
| Field | Meaning |
|---|---|
text |
Optional case-insensitive substring search; maximum 160 characters |
siteId, areaId, categoryId |
Optional IDs from previous lookups; omit or use null for no filter |
kind |
Site, Area or Category for lookup; omitted/null means Things |
relationship |
Any, Thing, Part or Content; omitted/null includes all relationships |
parentThingId |
Select direct children of that Thing, or descendants when requested |
includeDescendants |
With a parent ID, include nested descendants; the parent itself is excluded |
missingValueOnly |
Only records with neither an entered value nor an available estimate |
offset |
Zero-based start, 0–5,000 |
limit |
Page size, 1–200; default 100 |
report |
True for valuation totals; requires reports.read |
Do not combine a metadata kind lookup with hierarchy fields, or use a metadata kind with report:true. Use IDs, not location names, in the ID fields. Ambiguous names must be resolved before choosing an ID.
IDs may be compact 32-character GUID strings or standard hyphenated GUID strings. Keep returned IDs unchanged. All IDs in the examples below are fictional placeholders: look up and substitute your own.
| Example | Fictional ID |
|---|---|
| Site | 11111111-1111-4111-8111-111111111111 |
| Area | 22222222-2222-4222-8222-222222222222 |
| Category | 33333333-3333-4333-8333-333333333333 |
| Thing | 44444444-4444-4444-8444-444444444444 |
| Blob | 66666666-6666-4666-8666-666666666666 |
Follow every page
Read totalCount, returnedCount, hasMore and nextOffset. While hasMore is true, repeat the same filters using the returned next offset. Do not report a page's item count as the total count.
Results are live rather than a frozen export. If inventory changes during paging, re-run the query for a consistent fresh assessment.
Sites
Permission: inventory.read
Use the shared query endpoint to discover Site IDs. A Site is a top-level location, such as House or Barn.
{"kind":"Site","text":"House","offset":0,"limit":100}
Read each returned item's id, kind, name and revision. Other generic fields may be present and need not be used for a Site lookup. Follow pagination if there are more results.
Sites are read through this interface. To create, rename or delete a Site, ask your connected AI assistant (it shows a preview for your approval) or use the web or Android app. To retrieve Things under a Site, omit kind and filter by the chosen siteId.
Areas
Permission: inventory.read
An Area belongs to a Site, such as Kitchen in House. Look it up within its Site to avoid mixing identically named Areas from different Sites.
{
"kind":"Area",
"siteId":"11111111-1111-4111-8111-111111111111",
"text":"Kitchen",
"offset":0,
"limit":100
}
Read the Area's returned id and siteId. To search Things in the Area, omit kind and supply its areaId; you may also supply its Site ID. If both filters are supplied, both must match. A mismatched pair returns no matching inventory rather than silently selecting another Area.
Areas are read through this interface. To create, rename or delete an Area, ask your connected AI assistant (it shows a preview for your approval) or use the web or Android app. An Area that still holds things cannot be deleted. In the web tester, changing Site refreshes the available Areas; an invalid old Area selection is cleared.
Categories
Permission: inventory.read
Categories classify Things independently of location. Find an existing Category before using its ID as a filter or assigning it in a supported create/edit batch.
{"kind":"Category","text":"Cameras","offset":0,"limit":100}
The lookup returns IDs for Categories already in the household. Resolve an ambiguous name rather than guessing. To search Things in that Category, omit kind and use categoryId.
This is a lookup interface. Category creation, schema/custom-field editing and deletion are not exposed. A Category lookup is not a complete export of all category settings or custom-field definitions.
Things, Parts and Contents
Permission: inventory.read
Search top-level Things
{
"text":"camera",
"categoryId":"33333333-3333-4333-8333-333333333333",
"relationship":"Thing",
"offset":0,
"limit":100
}
The text search checks name, description, model, serial number and barcode using a case-insensitive substring match. It is not a semantic search and does not search attachment contents or notes. Combine filters only when every filter should apply.
Direct Contents of a Thing
{
"parentThingId":"44444444-4444-4444-8444-444444444444",
"relationship":"Content",
"includeDescendants":false,
"offset":0,
"limit":100
}
For direct Parts, change relationship to Part. For all direct children, omit the relationship filter. Use includeDescendants:true to include nested descendants. A relationship filter still applies to each returned descendant. The parent itself is excluded.
relationship:"Thing" means top-level records only and excludes both Parts and Contents. Any, omitted or null includes all relationships. The model uses the singular Content in the request even though the user interface says Contents.
Important returned fields
| Field | Meaning |
|---|---|
id, revision |
Stable record ID and current revision |
name, kind |
Display name and record kind |
relationship |
Thing, Part or Content |
siteId, areaId, parentThingId |
Location/ownership references |
siteName, areaName, parentThingName |
Available display names |
categoryId |
Resolved Category ID, where available |
quantity |
Quantity represented by that record |
currentValue |
Entered value each, possibly null |
effectiveValue |
Entered value, otherwise the available purchase-price estimate |
estimated, valueSource |
Whether the effective value is estimated; source is entered, estimate or missing |
currency |
Currency used for that record |
status |
Raw stored status representation; no status-filter parameter is exposed |
createdAt, purchaseDate |
Available recorded dates; neither is an installation/fitting date |
This response is a defined projection, not the complete underlying Thing JSON. Notes, full custom fields, loan/rental/contact records and structured event histories are not returned by this query. Use the attachment endpoints to inspect supported attached content separately.
Valuations and missing values
Permission for reports: reports.read
Send the same relevant Thing filters with report:true. Omit kind.
{
"areaId":"22222222-2222-4222-8222-222222222222",
"relationship":"Thing",
"report":true
}
A valuation report calculates across all matches on the server; you do not need to page through item lists to obtain its totals. A report's items is null, returnedCount is zero and hasMore is false, while totalCount still gives the matching-record count.
Illustrative response excerpt—not a full response:
{
"totalCount":12,
"returnedCount":0,
"hasMore":false,
"nextOffset":null,
"summary":{
"totalsByCurrency":{"GBP":1250.50},
"missingValuations":2,
"estimatedValuations":4,
"enteredValuations":6
},
"items":null
}
Each effective value is multiplied by quantity. Currency totals remain separate. The counts describe records, not a sum of quantities.
An entered current value, including zero, takes precedence. Otherwise ThingzSafe may calculate an estimate from purchase price, purchase date, warranty/category rules and the household's new-for-old preference. Where the existing inventory rules permit it, undated Contents can have a purchase-price estimate; not every undated top-level Thing or Part has one. An estimate is not a fresh AI market valuation. This route does not call the Pro AI valuation service.
To list records with neither an entered nor an estimated value:
{"missingValueOnly":true,"offset":0,"limit":20}
By default, reports include all statuses and Parts/Contents. Specify relationship:"Thing" for top-level-only totals. The API does not apply all the web inventory valuation report's optional risk, exclusion or ownership filters, and it does not convert currencies.
Photos and documents
Permissions: inventory.read + files.read
List selected attachments
POST /v1/developer/attachments/list:
{
"itemIds":["44444444-4444-4444-8444-444444444444"],
"offset":0,
"limit":50
}
Select 1–20 Thing IDs per request. The result is wrapped in isError, code, structuredContent and content. In structuredContent, inspect totalCount, returnedCount, hasMore, nextOffset and items.
Each file entry identifies its owning itemId, blobId, fileName, source, available mimeType and byte length, plus available. A file reference can exist even if its blob is unavailable. The same blob referenced repeatedly on one Thing is listed once; the same blob associated with two Things may appear for both.
Include the IDs of Parts/Contents separately when their attachments matter. A parent Thing alone does not implicitly include its children's files.
Read an attachment
POST /v1/developer/attachments/read:
{
"itemId":"44444444-4444-4444-8444-444444444444",
"blobId":"66666666-6666-4666-8666-666666666666",
"page":1
}
Both IDs must come from the selected Thing and its file listing. The service checks the live relationship and household. It does not accept arbitrary storage URLs.
Illustrative response shape, with image data omitted:
{
"isError":false,
"code":null,
"structuredContent":{
"itemId":"44444444444444448444444444444444",
"blobId":"66666666666646668666666666666666",
"fileName":"Example receipt.pdf",
"mimeType":"application/pdf",
"pageCount":3,
"firstPage":1,
"returnedPages":2,
"hasMore":true,
"nextPage":3
},
"content":[
{"type":"text","text":"Attachment metadata is supplied here."},
{"type":"image","mimeType":"image/jpeg","data":"BASE64_JPEG_PAGE_1"},
{"type":"image","mimeType":"image/jpeg","data":"BASE64_JPEG_PAGE_2"}
]
}
For this example, call again with page:3. PDF page numbering is one-based. Images and text documents use page 1 only.
Decode image blocks' base64 data as the stated MIME type. Text blocks contain extracted text. The returned images are prepared views, not an original-file download API. PDFs are rendered, so scanned documents can be inspected too. No public/SAS download URL is returned, and no AI analysis or receipt classification occurs on this API route.
Supported formats and omissions are listed in the ChatGPT files guide. In particular, DOCX is main-document text only, and XLSX/RTF are unsupported. Treat unavailable, unreadable, unsupported or partial reads as unexamined. Do not count them as negative evidence.
Create, edit and move Things
Use a two-stage flow: preview the exact proposal, show it to the user for approval, then execute the saved preview. Previewing does not change or lock the inventory while the user decides.
Each proposal needs a fresh requestId of 1–80 characters; a generated UUID is convenient. Use the same request ID and identical payload only for retries of that proposal. A developer batch is bound to the credential that created it. Use the same token for preview, execution and recovery.
Create top-level Things
POST /v1/developer/batches/preview:
{
"requestId":"example-create-001",
"action":"create",
"siteId":"11111111-1111-4111-8111-111111111111",
"areaId":"22222222-2222-4222-8222-222222222222",
"categoryId":"33333333-3333-4333-8333-333333333333",
"items":[{"name":"API test — delete later","quantity":1,"notes":"Created for API testing"}]
}
Use a real destination Site. The Area, if supplied, must belong to it. Omit Area to put the Thing directly in the Site. If Category is omitted, an existing Other category is used; if it is unavailable, choose an existing category explicitly. Creation is for top-level Things, not new Parts/Contents under a parent.
Edit existing Things
{
"requestId":"example-edit-001",
"action":"edit",
"itemIds":["44444444-4444-4444-8444-444444444444"],
"values":{"notes":"Checked through the API","needsReview":false}
}
The supplied values apply to every selected record. Omitted fields are preserved. An optional top-level categoryId can reassign the Category. Use a move action for location changes.
Supported values/create-item fields:
| Field group | Supported keys |
|---|---|
| Text | name, notes, make, model, barcode, position, size |
| Numbers | quantity, purchasePrice, currentValue |
| Dates | purchaseDate, warrantyEndDate, currentValueDate |
| Other | currentValueCurrency (three uppercase letters), needsReview (boolean) |
Name must be non-empty; ordinary text is limited to 200 characters and notes to 4,000. Quantity must be 0–1,000,000, with whole quantities for top-level Things and Parts; existing Contents may permit fractions. Money values must be non-negative and no greater than 1,000,000,000,000. Use ISO date strings such as 2026-10-25. Optional nullable fields can be cleared with null; quantity and currency cannot be null, and name cannot be empty/null.
This is not a general record-patching interface: do not send status, photo IDs, parent IDs, loan/disposal information or arbitrary custom fields. Existing unrelated data is preserved on supported edits.
Move Things and their owned children
{
"requestId":"example-move-001",
"action":"move",
"itemIds":["44444444-4444-4444-8444-444444444444"],
"siteId":"11111111-1111-4111-8111-111111111111",
"areaId":"22222222-2222-4222-8222-222222222222"
}
A selected Thing moves with its owned Parts/Contents, including nested children. Inspect the full expanded preview: the 100-record batch limit includes affected descendants. Selecting both parent and child does not move the child twice. Moving a selected child makes it a top-level Thing at the destination; moving a parent retains the descendants' ownership links beneath it. This operation does not move stock into another Thing or split a quantity.
Review the response
A preview includes batchId, previewHash, status, expiresAt, action, affectedCount and changes. Each change includes an ID, expected revision, before and after details. A create has no before record.
Show all affected records and before/after details to the user. Resolve display names from the location/category lookups where needed. A preview expires after 15 minutes. Relevant changes to records or a moved ownership tree make execution fail; prepare a new preview and obtain approval again.
Execute the approved preview
POST /v1/developer/batches/execute:
{
"batchId":"BATCH_ID_FROM_PREVIEW",
"previewHash":"EXACT_HASH_FROM_PREVIEW",
"confirm":true
}
The two placeholder strings must be replaced with the server's actual values. confirm:true is the calling application's assertion that approval was obtained; it is not independent proof of a human click. Your application must implement the review/approval step.
Execution is all-or-nothing. A successful result has status Executed, the action, affected count and record IDs/revisions. Changes join normal cloud sync and have integration Events. Devices will see them when they next sync.
Recover after a timeout
GET /v1/developer/batches/{batchId} with the same developer credential. If already Executed, use the saved result. Otherwise, retry the exact batch/hash while valid. Do not generate another create request merely because a response was lost. Re-executing a completed batch returns its original result. Old completed records may be cleaned up after 90 days; do not use them as indefinite job storage.
Limits, errors and unsupported operations
Current bounds
| Area | Limit |
|---|---|
| Shared household read allowance | 30 developer/MCP queries/reads per minute; retry handling can differ by route |
| Inventory evaluation | 5,000 source records and 8 MiB encrypted data |
| Inventory result page | 200 records maximum |
| Query request body | 4,096 bytes |
| Developer write request body | 65,536 bytes |
| Active inventory previews | 10 per household; 15-minute expiry |
| Inventory batch | Up to 100 affected Things including moved descendants; saved proposal up to 512 KiB |
| Committed write allowance | 1,000 Things and 10 MiB compact audit payload per 24-hour usage window, shared across credentials |
| Attachment request body / MCP request body | 16,384 bytes |
| Attachment selection/page | 20 selected Thing IDs; up to 50 file references per page |
| Attachment metadata processing | 10,000 relevant records / 16 MiB encrypted data; 5,000 selected file references |
| Source attachment | 20 MiB per read |
| Prepared image/PDF page | Maximum 1,800-pixel edge; up to 3 MiB encoded JPEG per image |
| Image source | Up to 40 million pixels |
| Up to 1,000 pages; at most two pages returned per read | |
| TXT/CSV/HTML | Up to 200,000 source bytes |
| DOCX extraction | Up to 100,000 text characters; no layout/embedded-image inspection |
| File processing concurrency | One download/render operation at a time per API process; retry busy responses |
| ChatGPT feedback | Ten committed reports per rolling 24 hours, plus shared request allowance; 15-minute draft expiry |
The shared minute allowance is used by inventory queries, attachment calls, new inventory previews and feedback preview/submit calls. Inventory execution and saved-batch recovery have their own checks; successful retries do not consume another committed-write allowance. Other HTTP rate limits also apply. Limits are not an entitlement to unlimited CPU, attachment analysis or ChatGPT usage. Treat explicit processing-limit errors as incomplete work rather than inventing totals.
Read failures correctly
Inventory query/write routes generally return a JSON code on application errors. Attachment routes use an isError/code wrapper as well as HTTP status. Inspect both the HTTP status and response body. MCP tools can return HTTP 200 with result.isError:true; inspect the tool result rather than assuming transport success means the operation succeeded.
| Status or code | Meaning / next step |
|---|---|
400 / InvalidQuery, validation codes |
Correct field names, types, IDs or bounds |
401 / InvalidCredential |
Missing, expired, wrong-type or revoked credential; check/reconnect without exposing it |
403 / ScopeRequired, WriteAccessRequired, AttachmentAccessRequired |
Required permission or active owner/household access is missing |
404 / BatchNotFound, ThingNotFound, AttachmentNotLinkedToThing, AttachmentUnavailable |
Check ownership/IDs and whether the resource still exists; do not guess another blob |
409 / PreviewStale, OwnershipTreeChanged |
Read current state, prepare a new preview and obtain approval again |
409 / PreviewHashMismatch, RequestIdReused |
Use the saved hash/exact original retry payload, or a fresh request ID for a genuinely new proposal |
410 / PreviewExpired, FeedbackPreviewExpired |
Unused preview expired; review a new draft before approval |
413 / BatchTooLarge, request-size failure |
Reduce the request or affected set |
422 / InventoryProcessingLimit, IntegrationInventoryLimit, attachment processing errors |
Work could not be completed within bounds; do not present a partial result as complete |
429 / HouseholdReadLimit, WriteAllowance, FeedbackDailyLimit |
Pause and retry within the relevant allowance; observe Retry-After where supplied |
503 / HouseholdBusy, CommitBusy, FeedbackBusyRetrySamePreview |
Short-lived contention: retry the same operation, not a new create proposal |
AttachmentRendererBusy |
Retry the same attachment/page later; for MCP, this is a tool-level error |
AttachmentContentInvalid, AttachmentUnreadable, AttachmentFormatUnsupported |
The attachment was not successfully inspected; report it as unexamined |
Error names are route-specific; an error body need not contain a human-readable message. Log the code and request context, but keep tokens and attachment payloads out of application logs.
Not exposed in this release
Site/Area/Category creation or deletion; Thing deletion; creating new Parts/Contents under a parent; attachment uploads or original-file downloads; contact queries; structured loan/rental/reservation queries or actions; fitted/installation history; Pro AI recognition/valuation actions; arbitrary status/custom-field changes; currency conversion; public access to the support log. Use the ordinary app/web features where available.
MCP tools and advanced connection details
Most developers should use the developer-token routes above. MCP is the separate interface used by the ChatGPT, Claude and Google Gemini connections; it uses OAuth rather than a developer token. All three use the same server address. ChatGPT uses client ID thingzsafe-chatgpt and Claude uses thingzsafe-claude. Gemini connects through the published ThingzSafe app, so it needs no client details.
| MCP tool | Purpose |
|---|---|
get_connection_profile |
Linked household and granted scopes |
list_inventory_locations |
Site/Area/Category lookup |
search_inventory |
Thing/Part/Content search |
inventory_valuation |
Valuation summary |
list_inventory_attachments |
Selected file metadata |
read_inventory_attachment |
Images, PDF pages or supported text |
preview_inventory_changes |
Saved create/edit/move proposal |
execute_inventory_changes |
Commit the approved inventory proposal |
get_inventory_change_batch |
Recover proposal/execution result |
preview_feedback_report |
Exact bug/change report draft; no submission |
submit_feedback_report |
Submit the approved support report |
MCP endpoint: https://app.thingzsafe.com/mcp. It supports stateless Streamable HTTP POST with negotiated protocol versions 2025-03-26 and 2025-06-18. Tool discovery is public; private tool calls require authorisation.
Discovery endpoints are /.well-known/oauth-protected-resource, /.well-known/oauth-protected-resource/mcp and /.well-known/oauth-authorization-server. The static public client ID is thingzsafe-chatgpt; token endpoint authentication is none, with mandatory PKCE S256. The default configured callback is https://chatgpt.com/connector_platform_oauth_redirect. This is not a promise that arbitrary third-party OAuth client IDs or redirect URLs can register themselves.
Read the tool schemas rather than copying developer query bodies unchanged: for example, inventory_valuation selects report behavior through the tool name and does not accept a report field. list_inventory_locations uses kind; search_inventory selects Things without a kind argument. Follow isError, scopes and continuation fields.
Support reports map Bug to a failed supported operation and Change to a feature request. Both report tools require inventory.read plus feedback.write. Show the complete comment, destination and account/household association before submission. Submit the exact previewId/previewToken with confirm:true. The sealed token is connection-bound and expires in 15 minutes if unused. There is no equivalent developer-token feedback endpoint in this release.
Never publish a real token in an example, screenshot or support conversation. If a token is exposed, revoke it in How to use API.