Skip to main content
The Portal.io API uses standard HTTP status codes to indicate whether a request succeeded or failed. Every error response returns an appropriate status code, and most include a JSON body with additional context. This page covers the status codes you will encounter, the most common causes of each, and how to resolve them.

HTTP status codes

Success codes

CodeMeaningWhen you’ll see it
200OKGET requests that return data, and most POST updates
201CreatedPOST requests that create a new resource (proposal, area, contact, etc.)

Client error codes

CodeMeaningCommon cause
400Bad RequestMissing or invalid parameters. For example, creating a duplicate area name or exceeding the 3-option limit per area.
401UnauthorizedHMAC signature is invalid, missing auth headers, or credentials are wrong. This is the most common error during integration development — see Debugging 401 errors below.
402Payment RequiredThe Portal.io account’s subscription is inactive or expired. Contact your Portal.io representative.
403ForbiddenThe authenticated user does not have permission for the requested action. Check user role and permissions in Portal.io.
404Not FoundThe resource ID in the URL does not exist, or belongs to a different account.
409ConflictThe 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

CodeMeaningWhat to do
500Internal Server ErrorAn 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

A 401 Unauthorized means the API could not verify your request’s authenticity. The response body for a signature mismatch is:
"You are not authorized. Your request signature (hash) is invalid."
This specific message indicates the HMAC signature didn’t match what the server expected. Other 401 causes (expired user key, missing app ID) may return different messages. When you see the “signature (hash) is invalid” message, focus your debugging on the canonical message construction below.

Checklist

1

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.
2

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.
3

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.
4

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.
5

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.
The Postman collection computes HMAC signatures automatically. If the same credentials work in Postman but not in your code, the problem is in your signature computation — compare your canonical message string character-by-character against what Postman generates.

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 the Content-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 the Content-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”

A 402 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”

A 409 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 returns 400. 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 reject PageNumber=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 with POST /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 calling GET /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)
This information lets the Portal.io team trace your request in their logs and identify the issue quickly.