HTTP status codes
Success codes
| Code | Meaning | When you’ll see it |
|---|---|---|
| 200 | OK | GET requests that return data, and most POST updates |
| 201 | Created | POST requests that create a new resource (proposal, area, contact, etc.) |
Client error codes
| Code | Meaning | Common cause |
|---|---|---|
| 400 | Bad Request | Missing or invalid parameters. For example, creating a duplicate area name or exceeding the 3-option limit per area. |
| 401 | Unauthorized | HMAC signature is invalid, missing auth headers, or credentials are wrong. This is the most common error during integration development — see Debugging 401 errors below. |
| 402 | Payment Required | The Portal.io account’s subscription is inactive or expired. Contact your Portal.io representative. |
| 403 | Forbidden | The authenticated user does not have permission for the requested action. Check user role and permissions in Portal.io. |
| 404 | Not Found | The resource ID in the URL does not exist, or belongs to a different account. |
| 409 | Conflict | The resource is in a state that prevents the requested action. Most commonly, you are trying to edit a proposal that has reached a terminal status (Accepted, Completed, Declined). Use a change order instead. |
Server error codes
| Code | Meaning | What to do |
|---|---|---|
| 500 | Internal Server Error | An unexpected error on Portal.io’s side. Retry after a brief delay. If it persists, contact support@portal.io with the request details and timestamp. |
Debugging 401 errors
A401 Unauthorized means the API could not verify your request’s authenticity. The response body for a signature mismatch is:
Checklist
Verify your credentials are correct
Confirm that your
X-MSS-API-APPID matches the Application Key provided by Portal.io, and that your X-MSS-API-USERKEY matches the User Key returned from the credential exchange. Copy-paste errors (trailing spaces, missing characters) are the most frequent cause.Check your timestamp
The
X-MSS-CUSTOM-DATE header must be the current UTC time in RFC 7231 format (e.g. Mon, 06 Apr 2026 00:22:19 GMT). The same exact string must appear in both the header and the HMAC canonical message. If there is any difference — even a single space — the signature will not match.Common mistakes: using local time instead of UTC, or generating the timestamp at one point but computing the signature later with a new timestamp.Verify the canonical message format
The HMAC canonical message must be assembled in the exact order specified in Signing requests. For GET requests the order is: HTTP method, base URL, timestamp, User Key. For POST requests, content type is added between the URL and timestamp. Two common mistakes: including query parameters in the URL (only the base path is signed), and omitting the content type on POST requests. See the signing guide for worked examples of both GET and POST.
Check your Secret Key handling
The Secret Key must be used as-is (ASCII bytes) when creating the HMAC. Do not Base64-decode it first — use the literal string value as the key material.
Inspect the actual request
Use the Postman console or your HTTP client’s debug output to see the exact headers sent. Compare each header value against what your code intended to send.
GET vs. POST: different failure modes
Authentication failures behave differently on GET and POST requests, and understanding the asymmetry will save you significant debugging time. GET requests — the server does not include query parameters in signature verification. This means a query-string encoding bug in your signer will not produce a 401. Auth passes, but you may get unexpected results (wrong page, missing filters) because the actual query params differ from what you intended. If you’re debugging a GET that returns wrong data but authenticates fine, check your query parameter encoding — not your signing code. POST requests — the server strictly validates the content-type segment of the canonical message against theContent-Type header. If there is any mismatch (wrong case, extra suffix, different value), the request fails with 401. When debugging a POST auth failure, check content-type first: is the value in your signing string character-for-character identical to the Content-Type header you’re sending?
Portal.io’s signature verification layer and body parsing layer operate independently. The signature layer is strict about content-type matching (mismatch = 401). But the body parsing layer can be lenient — some endpoints accept multiple body formats or even an empty body, as long as the required identifiers are present in the URL path. This means a request with the wrong body format may authenticate successfully and return a 200/204, even though it doesn’t match the documented contract. Always follow the documented content type and body format for each endpoint to avoid silent drift.
Common integration problems
”I get 401 on my first request after the credential exchange”
The credential exchange (GET /authenticate/apikeyexchange) uses a special auth flow where X-MSS-API-USERKEY is an empty string and excluded from the canonical message. After the exchange, every subsequent request must include the returned User Key both in the X-MSS-API-USERKEY header and in the canonical message. If you forget to switch, the signature will not match.
”I get 401 on POST requests but GET requests work fine”
The canonical message for POST requests includes theContent-Type value between the URL and the timestamp. GET requests do not include it. If your signing function omits content type for all requests, GETs will pass but POSTs will fail. Make sure you include the exact content-type string (typically application/x-www-form-urlencoded for Portal.io endpoints) in the canonical message for non-GET requests. See the POST signing example.
”I get 402 on every request”
A402 means the Portal.io account tied to your credentials does not have an active subscription. This can happen in sandbox if your test account was not provisioned correctly. Contact your Portal.io representative to check the account status.
”I get 409 when updating a proposal”
A409 Conflict means the proposal has reached a terminal status — typically Accepted, Completed, or Declined. The API prevents direct edits at that point. To make changes, create a change order against the proposal instead.
”I get 400 when adding an area”
Area names must be unique within a proposal. If you try to create an area with the same name as an existing one, the API returns400. Similarly, each area supports a maximum of 3 options — adding a fourth returns 400.
”I get 400 on a paginated request”
Pagination validation varies by endpoint. Some endpoints rejectPageNumber=0 with a structured 400 error, others silently treat it as page 1, and one (catalog search) accepts 0 but rejects negative values. To avoid issues across all endpoints, always pass PageNumber=1 or higher. Also note that error response formats differ — some return a structured responseStatus object with errorCode and field-level details, while others return a bare Bad Request string. Your error handling should account for both shapes.
”My tax totals are always zero”
Tax is calculated based on the location assigned to the proposal. Until you assign a location withPOST /public/proposals/{id}/location/{LocationId}, all tax amounts remain zero. See Financial summary for details.
”Webhook events are not arriving”
First, confirm your subscription is active by callingGET /public/webhooks. Then verify that your endpoint URL is publicly reachable and returns a 200 within a reasonable timeout. Portal.io will retry failed deliveries, but if your endpoint consistently fails, the subscription may be deactivated. Check the webhook concepts page for the full event delivery model.
Getting help
If you have worked through the troubleshooting steps above and are still stuck, email support@portal.io with the following details:- The full request (method, URL, headers — redact your Secret Key)
- The response status code and body
- The UTC timestamp of the request
- Your API Application Key (this is safe to share — it identifies your integration but does not grant access on its own)