Errors & limits
The error shape
Every failure is JSON with the same two keys. Validation failures add errors,
keyed by field, in Laravel's standard shape.
{
"message": "The given data was invalid.",
"errors": {
"local_capture_id": ["This field is required."]
}
}
Status codes
| Code | Meaning |
|---|---|
200 |
Success. On an upload, also means the capture was already stored โ check `duplicate`. |
201 |
Created. On an upload, a genuinely new photo. |
401 |
No token, an expired token, or a revoked one. |
403 |
The token is valid but lacks the scope, or the user's role does not permit it. |
404 |
No such resource โ including anything belonging to another team. Trace does not distinguish the two, because doing so confirms the row exists. |
422 |
Validation failed, or a rule refused: a checklist item that requires a photo, for instance. |
429 |
Rate limited. Retry after the seconds in `Retry-After`. |
503 |
The install is in demo mode, which blocks writes. |
404 rather than 403, across tenants
Asking for a project that belongs to another company returns a 404, not a 403. A 403 would confirm the id exists, which is a slow enumeration of somebody else's data. The tenant scope fails closed at the query layer, so this is the natural outcome rather than a special case anyone has to remember to write.
Rate limits
| Bucket | Limit | Applies to |
|---|---|---|
api | 240 / min | Everything on /api/v1 |
uploads | 120 / min | Photo upload only |
login | 20 / min | Sign-in, by IP |
share | 90 / min | Public share and portfolio pages |
Uploads have their own bucket deliberately. A phone that has been offline for two days draining a large queue must not be able to lock the office out of the API it is using at the same time.
Every limit is a config value the buyer owns โ this is their server. See
config/trace.php.
Idempotency
Photo upload is idempotent on local_capture_id, generated on the device at
the moment of capture and sent unchanged on every retry. A repeat returns
200 with duplicate: true and the existing photo. Both
200 and 201 are success; a client that treats only
201 as success will re-upload forever.