Errors, limits and security
One error shape, a small number of status codes, and rate limits on the routes a stranger can reach. This page also covers the rules applied to webhook destinations, which are stricter than most people expect and occasionally reject a URL that looks fine.
The error shape
Every failure from the API returns the same JSON object with a single error field, whatever the status code. There is no nested detail object and no field-level breakdown.
{
"error": "That URL is too long (limit 600 characters)."
}The error string is written for a person to read and is not stable enough to branch on. Use the HTTP status code for logic. Show the message to a human, log it, and do not pattern match it.
Status codes
- 400 Bad Request
- The request was understood and rejected: a missing field, a value out of range, an unusable URL. Retrying it unchanged will fail the same way.
- 401 Unauthorized
- No valid signed-in session. Since access is by browser session rather than by key, this normally means the session expired and the person needs to sign in again.
- 403 Forbidden
- Signed in, but not permitted. Reaching for another company’s data, or an admin route without an admin account.
- 404 Not Found
- No such record, or none that you are allowed to see. The two are answered identically on purpose, so a missing record cannot be told apart from someone else’s.
- 409 Conflict
- The request collides with something that already exists. Only account setup answers this way, when an account that already belongs to a company tries to create another one. Limits elsewhere, such as the cap on webhook endpoints, are answered with 400.
- 429 Too Many Requests
- A rate limit was hit. Wait and retry, guided by the headers below.
- 500 Internal Server Error
- Something failed on our side. The response carries a generic message; the detail is in our logs, not in the body.
Rate limits
Limits are per IP address, on a rolling one-minute window. They cover the public assessment routes that anyone can reach, and one authenticated route, /storage/uploads/request-url, where the limiter runs ahead of the sign-in check, so attempts count whether or not the caller turns out to be signed in. Nothing else is limited: elsewhere the signed-in session is the limit.
- Public assessment routes
- 120 requests
- Public writes, on top of the above
- 30 requests
- Upload URL requests
- 20 requests
A rejected request returns 429 with standard RateLimit headers describing the window, the allowance and the time until it resets. A normal homeowner completing an assessment, photographs included, is nowhere near these numbers.
Browsers and CORS
The API answers cross-origin requests from any origin with credentials allowed, which is what lets the widget run inside your page. That is not an invitation to call dashboard routes from your own front end: they need a session cookie that only exists in a browser already signed in to Inspecta, and building on that would break the moment someone signs out.
Where a webhook may point
A webhook destination is a URL we connect to on your instruction, so it is validated twice: once when you save it, and again against the addresses it resolves to at the moment of sending. A hostname that is public when saved and private when used is refused at connect time.
https://only. Plain HTTP is refused.- At most 600 characters.
- No credentials in the URL. A
user:password@prefix is rejected outright. - Fragments are dropped, since they never reach a server anyway. Query strings are kept and sent exactly as given.
- Hostnames ending in
.local,.localhost,.internalor.home.arpaare refused. - Loopback, link-local, private and other non-public ranges are refused, in both IPv4 and IPv6, whether written directly or reached through DNS.
A destination that fails one of these is refused with a message explaining which rule it broke. If the failure only appears in your delivery history rather than when saving, DNS resolved somewhere private at send time.
Security notes
Secrets
A webhook signing secret is shown once, when the endpoint is created and when it is rotated. It is never returned again by any route. Keep it in your own secret storage, never in client-side code or a repository, and rotate it if it is ever printed into a log or a support message.
Links that are their own permission
A report URL and an uploaded photograph URL both contain a long random token, and holding the link is what grants access. That is deliberate, so a homeowner can read their assessment without an account, but it means those links should be treated as confidential. Anyone you forward one to can read it.
What is in a payload
Webhook bodies carry a homeowner's name, email address, phone number and their description of the problem, along with the assessment written from it. That is personal data belonging to your customer. Receive it over HTTPS, store it where the rest of your customer data lives, and give it the same retention rules.
Widget IDs are public
A widget ID appears in the page source of every site the widget is on. It identifies which assessment to show and authorises nothing else. There is no need to protect it, and no benefit in trying.
Reporting a problem
If you believe you have found a security issue, report it from your dashboard rather than in public, and include enough detail to reproduce it. Do not include a signing secret, a session cookie or a live report link in the report itself. For anything else, the delivery history is usually the fastest way to see what actually happened.