Skip to main content
Every Portal.io API request must include an X-MSS-SIGNATURE header containing a Base64-encoded HMAC-SHA256 signature. The signature is computed from a canonical message you build from properties of the request itself, then signed using your Secret Key. This page explains exactly how to construct the canonical message and compute the signature.

Canonical message format

The canonical message is a single string formed by concatenating these components with no separator: For GET requests:
[HTTP method][base URL][timestamp][user API key]
For POST, PUT, and other non-GET requests:
[HTTP method][base URL][content type][timestamp][user API key]
The components map to your request as follows:
  • HTTP method — uppercase, e.g. GET, POST, PUT
  • Base URL — the scheme, host, and path only. Do not include query parameters. For example, use https://api.portal.io/public/proposals even if the actual request URL has ?PageNumber=1&PageSize=10 appended.
  • Content type — the exact value of the Content-Type header. Include this segment only for non-GET requests. Most Portal.io POST endpoints use application/x-www-form-urlencoded. The AI Builder endpoints (generate-outline and build-proposal) use application/json. Always check the endpoint’s documentation for the correct value — the content type in your signing string must match the Content-Type header exactly or the request will fail with 401.
  • Timestamp — the exact value you send in X-MSS-CUSTOM-DATE
  • User API key — the exact value you send in X-MSS-API-USERKEY

Rules

  • Use the base URL without query parameters. This is by design — query parameters are sent in the request as normal, but the server intentionally excludes them from signature verification. Only the scheme, host, and path are signed.
  • For GET requests, omit the content-type segment completely. Do not include an empty string in its place.
  • For non-GET requests, include the exact Content-Type value from the request header. The value in the signing string and the value in the header must match exactly — including case and any suffixes (e.g. application/x-www-form-urlencoded, not Application/X-WWW-Form-Urlencoded).
  • The request body is not part of the canonical message. Only the content type is included, not the body itself.
  • The timestamp must exactly match the value in X-MSS-CUSTOM-DATE, character for character.
  • The user API key must exactly match the value in X-MSS-API-USERKEY, character for character.
  • For the initial credential exchange, the user API key is an empty string in both the header and the canonical message.
Do NOT Base64-decode the Secret Key before computing the HMAC. Use it exactly as provided — as raw ASCII bytes. Base64-decoding the key before use is a common mistake that produces an invalid signature.

Examples

GET request (credential exchange)

For the initial credential exchange, where the user API key is empty, the canonical message looks like this:
GEThttps://api.portal.io/authenticate/apikeyexchangeMon, 06 Apr 2026 00:22:19 GMT
Breaking that down:
  • Method: GET
  • Base URL: https://api.portal.io/authenticate/apikeyexchange
  • Content type: (omitted — this is a GET request)
  • Timestamp: Mon, 06 Apr 2026 00:22:19 GMT
  • User API key: (empty string — this is the initial exchange)
Note that the actual HTTP request includes query parameters (?UserName=...&Password=...), but the canonical message uses only the base URL without them.

GET request (with query parameters)

When listing proposals with pagination, the canonical message is:
GEThttps://api.portal.io/public/proposalsMon, 06 Apr 2026 00:22:19 GMTqBOSOYDeZaSzTxqMCL1Kr66JpU2H6wHCLz7xviZUOcA=
The actual request URL includes ?PageNumber=1&PageSize=10, but those query parameters are not in the signed string.

POST request (adding an area to a proposal)

For a POST request, the content type is included between the URL and the timestamp:
POSThttps://api.portal.io/public/proposals/1042/areaapplication/x-www-form-urlencodedMon, 06 Apr 2026 00:22:19 GMTqBOSOYDeZaSzTxqMCL1Kr66JpU2H6wHCLz7xviZUOcA=
Breaking that down:
  • Method: POST
  • Base URL: https://api.portal.io/public/proposals/1042/area
  • Content type: application/x-www-form-urlencoded
  • Timestamp: Mon, 06 Apr 2026 00:22:19 GMT
  • User API key: qBOSOYDeZaSzTxqMCL1Kr66JpU2H6wHCLz7xviZUOcA=
The request body (Name=Living+Room) is sent normally but is not part of the canonical message.

Computing the signature

Once you have the canonical message, compute the HMAC-SHA256 using your Secret Key as raw ASCII bytes, then Base64-encode the raw digest.
import hmac
import hashlib
import base64
from urllib.parse import urlsplit, urlunsplit

def sign_request(method, url, content_type, timestamp, user_key, secret_key):
    # Strip query string — only scheme + host + path are signed
    parts = urlsplit(url)
    base_url = urlunsplit((parts.scheme, parts.netloc, parts.path, '', ''))

    # Build canonical message
    message_parts = [method.upper(), base_url]
    if method.upper() != "GET":
        message_parts.append(content_type)
    message_parts.append(timestamp)
    message_parts.append(user_key)
    canonical = "".join(message_parts)

    # Compute HMAC-SHA256
    # Use secret_key as raw ASCII bytes — do NOT base64-decode it first
    signature = hmac.new(
        secret_key.encode("ascii"),
        canonical.encode("utf-8"),
        hashlib.sha256
    ).digest()

    return base64.b64encode(signature).decode("ascii")

Full request example

Here is how the computed signature fits into a complete request. This example performs the initial credential exchange:
curl -i -X GET \
  "https://sandbox.api.portal.io/authenticate/apikeyexchange?UserName=user%40example.com&Password=MyP%40ss123" \
  -H "Accept: application/json" \
  -H "X-MSS-API-APPID: YOUR_APP_ID" \
  -H "X-MSS-API-USERKEY: " \
  -H "X-MSS-CUSTOM-DATE: Mon, 06 Apr 2026 00:22:19 GMT" \
  -H "X-MSS-SIGNATURE: BASE64_HMAC_SIGNATURE"
Replace BASE64_HMAC_SIGNATURE with the output of your signing function. Replace YOUR_APP_ID with your API Application Key. The X-MSS-API-USERKEY header is intentionally empty for this call. Once you have your User API Key, include it in both X-MSS-API-USERKEY and your canonical message on all subsequent requests.