Troubleshooting
Common failures and what actually causes them.
Authentication#
401 unauthorized
The token is missing, malformed, revoked, or the user disconnected your app.
Check the Authorization: Bearer ... header actually reached the server — a
missing header and an invalid one look identical from the client side. If a
user disconnected, send them back through
the authorization flow.
403 forbidden
The token is valid but lacks the scope for what you asked. A Connect token needs
models.run for model calls, kv.read/kv.write for the key/value store, and
files.read/files.write for files. Scopes are fixed at grant time — you
cannot widen them without a new authorization.
"It works with my API key but not with a Connect token."
Different subjects. pm_ spends your credits; pmc_ spends the connected
user's and is limited by their scopes. Check
Authentication.
Money#
402 on a model call
The user's balance is too low. Tell them so plainly and link them to a top-up —
don't retry in a loop.
402 storage quota exceeded
The user's storage subscription is full. See
Storage plans.
Credits didn't arrive after a payment. Credits are granted when the payment processor confirms, not on redirect. Wait a moment and check billing history before paying again.
Rate limits and size#
429 too many requests
You crossed 60 requests per minute. Read the retry-after header and back off
rather than retrying immediately. If you are fanning out, add a queue.
413 on a file upload
Direct uploads are capped at 10MB. Use a
presigned upload for anything larger.
400 value exceeds 64KB limit
Key/value entries are small by design. Put large payloads in
file storage and keep a pointer in the key/value
store.
Connect flows#
The consent screen never redirects back.
Your redirect_uri must match the one registered for the app exactly —
scheme, host, port, and path. A trailing slash counts as a difference.
invalid_grant on token exchange.
Use https://app.portmodels.com/connect/authorize for the browser consent page
and https://api.portmodels.com/connect/token for the exchange. Authorization
codes are single-use and expire in 10 minutes. Exchange immediately after the
redirect, keep redirect_uri byte-for-byte identical, and if you use PKCE make
sure the S256 hash of code_verifier matches the code_challenge sent to the
authorize endpoint. Never retry the same code after an exchange attempt.
If a fresh code fails on its first exchange after those checks pass, record the timestamp and response body and contact support. Redact the verifier, client secret, access token, and any other credentials from the report.
PKCE keeps failing.
The challenge is the base64url encoding of the SHA-256 of the verifier, without
padding. Base64url, not base64 — - and _, no + or /.
Device flow stops with slow_down.
You polled faster than the interval you were given. Increase your delay and
continue; the code stays valid until expires_in runs out.
Development#
Presigned URLs return 501.
Your development server is using the in-memory storage backend, which has no
real S3 to sign against. Use the direct upload path locally.
Streaming responses look truncated.
The stream ends with a cost event and then data: [DONE]. Make sure your client
reads to [DONE] rather than stopping at the first event that doesn't parse as
a chat chunk.
Still stuck#
Collect the request id if you have one, the exact error body, the timestamp, and what you expected instead — then see Support.