Base URL: https://cyberdeduction.initial-energy.com
A portal key lets your own bot read your trips and file suggestions. A suggestion does not change a trip. You confirm or dismiss it in CyberDeduct (or with your session token). Until you accept, the miles, category, purpose, and signature stay as they were.
The machine-readable description is /api/portal-v1.yaml. Send it to the assistant as the tool schema. Authenticate the bot with the portal key, not your session token.
Keys are managed by the signed-in user. The account must be active.
curl -s -X POST "$BASE/portal/keys" \
-H "Authorization: Bearer $SESSION" \
-H "Content-Type: application/json" \
-d '{"name":"Trip bot","scopes":["read","suggest"]}'
The response includes key once (cdk_...). Store it in the assistant's secret field. Later calls show only the prefix.
curl -s "$BASE/portal/keys" -H "Authorization: Bearer $SESSION"
curl -s -X DELETE "$BASE/portal/keys/$KEY_ID" -H "Authorization: Bearer $SESSION"
A revoked key receives 401.
| Scope | Allows |
|---|---|
read |
GET /portal/v1/trips and GET /portal/v1/review-queue |
suggest |
POST /portal/v1/trips/{id}/suggestions |
Omitting scopes (or sending none that are recognized) creates a read key. Unknown scope names are dropped. A read-only key that posts a suggestion receives 403. A suggest-only key that lists trips receives 403. Give a bot both.
Each key may make 60 requests per 60 seconds. Reads and suggestion writes share that per-key bucket. The limiter fails open if its storage is briefly unavailable.
429 body:
{ "error": "rate_limited", "message": "Too many requests. Please wait a moment and try again." }
Wait and retry. The query-string email is ignored; the key's owner is the caller.
from and to are YYYY-MM-DD (inclusive) on the trip's local date. limit defaults to 50 and caps at 200. Follow nextCursor until it is null.
curl -s "$BASE/portal/v1/trips?from=2026-01-01&to=2026-01-31" \
-H "Authorization: Bearer $PORTAL_KEY"
curl -s "$BASE/portal/v1/review-queue" \
-H "Authorization: Bearer $PORTAL_KEY"
The review queue is trips whose purpose is still unconfirmed. Each item can include matching appointments and any pending suggestions. Responses are a whitelist: no signatures, coordinates, odometers, or notes.
Trip ids look like {vin}:{start}. URL-encode the id when you put it in a path.
TRIP_ID='5YJ3E1EA7KF000000:2026-04-02T15:00:00.000Z'
ENC=$(python3 -c 'import urllib.parse,sys; print(urllib.parse.quote(sys.argv[1], safe=""))' "$TRIP_ID")
curl -s -X POST "$BASE/portal/v1/trips/$ENC/suggestions" \
-H "Authorization: Bearer $PORTAL_KEY" \
-H "Content-Type: application/json" \
-d '{"category":"business","purpose":"Client meeting","note":"Matches the 2pm site visit","source":"grok"}'
category is business or personal (Business and Personal are accepted).purpose is required for business, optional for personal, at most 200 characters.note is an optional explanation for you (max 500). It is not written onto the trip.source is an optional bot name (max 60).404 means that trip is not yours. The latest pending suggestion for the same trip and key replaces the older one. The trip row is not modified.
The bot cannot accept its own suggestion. In CyberDeduct, review the pending list and accept or reject. The same calls, with your session token:
curl -s "$BASE/portal/suggestions" -H "Authorization: Bearer $SESSION"
curl -s -X POST "$BASE/portal/suggestions/$SUGGESTION_ID/accept" \
-H "Authorization: Bearer $SESSION"
curl -s -X POST "$BASE/portal/suggestions/$SUGGESTION_ID/reject" \
-H "Authorization: Bearer $SESSION"
Accept applies the category and purpose through the same correction used when you edit a trip yourself: the previous values are kept in a dated edit record, and the record's origin is portal suggestion. The trip is then your confirmed edit. Reject only marks the suggestion rejected; the trip stays as it was.
Another user's session cannot accept your suggestion (404).
read and suggest./api/portal-v1.yaml as the action / tool schema. Set the server URL to the base URL above.cdk_...). Do not paste your session token.ChatGPT: Custom GPT → Actions → import the schema → Authentication → Bearer.
Claude: add the schema as a tool (or an MCP/proxy in front of these routes) with the same Bearer key.
Grok: add a custom connector or tool from the schema, Bearer auth, and the same instruction. The model calls GET /portal/v1/review-queue and POST /portal/v1/trips/{id}/suggestions. It does not call the accept route.