Complaints
Let an AI agent report a problem with supporting evidence and check its status and resolution.
AI agents and applications can submit a problem report to PortModels and check its status later. Write the title, explanation, expected behavior, and reproduction steps in English. Supporting log excerpts can retain their original language. Describe what you observed and distinguish it from your suspected cause. Never invent evidence or claim a fix was verified without a supporting check.
Submitting a complaint does not automatically run a fix or guarantee a response time. Complaints are separate from automatic error reports.
Authentication#
Use an API key with submit_complaints and view_own_complaints permissions,
or a Connect token with account.manage consent and the same current account
permissions. No additional complaint scopes are required with account.manage.
Existing narrow complaints.write and complaints.read grants also work.
Read and write access are separate. Connect reports and reads are isolated to
the authenticated user and app. The server derives their identity from the
token; do not submit user_id or app_id.
Apps using narrow grants must obtain consent for the corresponding read or write scope. An existing account session may need to refresh or sign in again to pick up new account permissions.
Submit a complaint#
curl https://api.portmodels.com/complaints \
-H "Authorization: Bearer $PORTMODELS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"client_complaint_id": "chat-failure-feb8e6ae",
"language": "en",
"title": "Chat returned an empty HTTP 400 before streaming",
"explanation": "I sent a chat request. It returned HTTP 400 before any reply started. The response body was empty and no PortModels request ID was returned. I cannot determine which input was rejected.",
"expected_behavior": "The API should return a reply or an explanation of the rejected request.",
"reproduction_steps": ["Send a chat request", "Observe HTTP 400 before the first stream event"],
"occurred_at": "2026-10-08T19:15:24Z",
"reported_severity": "normal",
"context": {
"operation": "chat.completions",
"model": "deepseek-v4.1-flash",
"app_version": "1.2.3",
"environment": "production",
"agent_name": "Example agent",
"agent_run_id": "run-123"
},
"evidence": [{
"type": "request_reference",
"client_diagnostic_id": "feb8e6ae",
"http_status": 400,
"occurred_at": "2026-10-08T19:15:24Z"
}]
}'Use the actual failure time, version, model, and diagnostic identifiers rather
than copying the example values. Include request_id whenever PortModels
returned one. Timestamps must be ISO 8601 with a timezone.
For model API failures, copy the response's error.log_id into an evidence
item's request_id. Keep your app's own diagnostic ID separately in
client_diagnostic_id. This helps correlate the reported failure without
including the original prompt, image, credentials or full request body.
The response is HTTP 201 and includes complaint.id, complaint.status,
and complaint.revision. Save the ID for polling and additional evidence.
The initial status is submitted when evidence is supplied.
Reuse the same client_complaint_id when retrying the same submission. An
identical retry returns HTTP 200, the same complaint ID, and replayed: true.
Reusing the ID for different content returns 409 idempotency_conflict.
Additional evidence belongs on the existing complaint, not in a modified
submission using the same ID.
Required fields and limits#
| Field | Requirement |
|---|---|
client_complaint_id | Required stable identifier; at most 128 bytes |
title | Required English summary; at most 200 bytes |
explanation | Required long English text describing the action, failure, and impact; at most 20,000 bytes |
expected_behavior | Optional English text; at most 4,000 bytes |
reproduction_steps | Optional array of at most 20 English steps, each at most 2,000 bytes |
language | en; defaults to en |
reported_severity | low, normal, high, or critical; defaults to normal |
evidence | At most 20 typed items per request |
evidence_unavailable_reason | Required when evidence is empty; at most 4,000 bytes |
context | Optional app_version, operation, model, environment, agent_name, agent_version, agent_run_id; each at most 256 bytes |
Requests must fit within 256,000 bytes. Severity is the submitter's assessment,
not a promise of priority. Write English prose even when language is omitted;
the field is a declaration, not automatic translation or language detection.
If evidence cannot be collected, explain the reason and submit an empty
evidence array. The complaint starts in needs_evidence.
Evidence formats#
Each item has one of these types:
| Type | Supporting data |
|---|---|
request_reference | At least one request_id or client_diagnostic_id; optionally http_status, occurred_at, and text |
log_excerpt | Required text containing a small, relevant, redacted excerpt |
reproduction | Required text describing a reproduction or verification result |
Text is limited to 8,000 bytes per item. Identifiers are limited to 128 bytes.
Only these fields are accepted: type, text, request_id,
client_diagnostic_id, http_status, and occurred_at.
Remove credentials, cookies, personal data, and unrelated page content before submitting. Do not submit full HTTP captures or authorization headers. Recognizable secrets are additionally redacted before storage, and URLs lose credentials, query strings, and fragments. This does not replace careful redaction by the submitting agent. File attachments are not supported.
Add evidence#
curl https://api.portmodels.com/complaints/COMPLAINT_ID/evidence \
-H "Authorization: Bearer $PORTMODELS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"client_evidence_id": "follow-up-1",
"evidence": [{"type": "log_excerpt", "text": "HTTP 400; no response body; no PortModels request ID."}]
}'Evidence is appended; the original report remains unchanged. Use a stable
client_evidence_id for retry deduplication. Identical retries return 200;
different evidence under the same ID returns 409. Adding evidence to a
needs_evidence report returns it to submitted for review. A complaint can
hold at most 100 evidence items and 200,000 bytes of evidence in total.
Check status#
curl https://api.portmodels.com/complaints/COMPLAINT_ID \
-H "Authorization: Bearer $PORTMODELS_API_KEY"The response contains the original submission, accumulated evidence,
status, revision, a status activity history, and any shared resolution.
It does not expose internal review notes or other accounts' complaints.
Statuses are submitted, needs_evidence, validated, in_progress,
resolved, duplicate, and not_reproducible. A resolved report includes a
resolution explanation and verification evidence. Check
resolution.deployment_status separately: pending, deployed, or
not_required. Resolved does not by itself mean a fix has been deployed.
Use GET /complaints?limit=25&status=submitted to list your own reports. The
response contains complaints and next_cursor; pass a non-null cursor as
cursor on the next request. limit is 1–100. Optional since filters by
server receipt time. Poll conservatively; do not repeatedly submit the same
failure or poll in a tight loop.
Errors and quotas#
Errors have a JSON error object with code and message.
| HTTP status | Meaning |
|---|---|
400 | Invalid fields or evidence; correct the request before retrying |
401 | Missing, expired, revoked, or inactive-account token |
403 | Required account permission or Connect scope is missing |
404 | Complaint absent or inaccessible to this account/app |
409 | An identifier was reused with different content |
413 | Request or accumulated evidence limit exceeded |
429 | Reporting quota reached; honor Retry-After |
Each account can create 20 complaints and append evidence 60 times per hour, across its apps. Identical retries do not consume another slot. Complaints are kept as a review history rather than using the seven-day automatic diagnostic retention period.