---
title: "Api Portfolio Post Import Fetch"
method: POST
path: "/api/portfolio/post-import-fetch"
tags: ["Portfolio"]
---

# Api Portfolio Post Import Fetch

`POST /api/portfolio/post-import-fetch`

## Request

- Base URL: `https://www.oxfordledge.com` (the document declares no server; this is the origin it was published from)
- URL: `https://www.oxfordledge.com/api/portfolio/post-import-fetch`
- Auth: none declared

Phase 1: validate + estimate credit cost for a post-import AI fetch.

Accepts a freshly-imported ticker list + action type ('news' or
'research') and returns the gate decision: how many credits the
Phase 2 fetch will cost (low + high bounds), or a 402/422 with
branch-specific context if the request can't proceed.

Plus-tier gated (`require_min_tier("plus")`). `plus_trial` users
are accepted by the tier check but rejected at the AI-action gate
per OWNER Q3 ratification (research-deck + news AI summaries are
upgrade-only; non-AI actions remain trial-accessible — none
implemented in Phase 1).

Request body:
    {action: "news"|"research", tickers: [str, ...], idempotency_key: str}

Response: `PostImportFetchEstimateResponse`. Always 200 on the OK
branch; 402 on tier/balance/cap rejection; 422 on overflow; 400 on
malformed input.

Does NOT call the AI provider, NOT decrement credits, NOT stream.
Phase 2 wires those in.

## Response `200`

Successful Response

- PostImportFetchEstimateResponse: `POST /api/portfolio/post-import-fetch` — M1-1 Phase 1 quota gate. Plan: docs/board/audit/2026-05-28_CHAOS_m1_post_import_quota_gate_scoping.md OWNER ratifications baked in: see services/post_import_fetch.py module docstring. Phase 1 returns ONLY the estimate + the gate decision — no AI call, no credit decrement, no SSE stream (that's Phase 2). The FE renders a confirm dialog from this response and POSTs the same payload to the Phase 2 commit route once shipped. Required fields (typed-client wire contract — FE deref's without null-check): - estimated_credits_low: charge-after-success lower bound. - estimated_credits_high: low + 50% retry headroom (Q5). - tickers_validated: server-side cleaned/deduped/capped list. - idempotency_key: echoed back so Phase 2 can collapse retries. Optional fields cover the rejection branches — only ONE of `trial_locked` / `insufficient` / `monthly_cap_exceeded` / `too_many_tickers` is set per call. The FE branches on whichever is truthy and renders the matching upsell / error copy.
  - `estimated_credits_low` integer, required: Lower-bound credit charge (1 credit per validated ticker on success). FE renders this as 'will cost N credits'.
  - `estimated_credits_high` integer, required: Upper-bound credit charge — low + 50% retry headroom (per OWNER Q5 ratification 2026-05-28). FE renders this as 'up to N if retries needed'.
  - `tickers_validated` string[], required: Server-validated, deduped, and capped ticker list. May be empty on rejection branches.
  - `idempotency_key` string, required: Echo of the client-supplied UUID, threaded through to Phase 2 for collapse-on-retry.
  - `trial_locked` boolean, nullable: True on the plus_trial + AI-action branch (402). Per OWNER Q3 ratification 2026-05-28: trials may not consume AI features.
  - `insufficient` boolean, nullable: True when credit_balance < estimated_credits_low (402). Inline upsell branch.
  - `monthly_cap_exceeded` boolean, nullable: True when MTD post-import spend + low > 80% of TIER_BUNDLED_CREDITS (402). Per OWNER Q8 ratification 2026-05-28.
  - `too_many_tickers` boolean, nullable: True when deduped ticker count > POST_IMPORT_MAX_TICKERS_PER_CALL (422). Per OWNER Q1 ratification 2026-05-28: global 25-ticker cap.
  - `balance` integer, nullable: Current credit balance — surfaced on the insufficient + OK branches.
  - `needed` integer, nullable: Credits needed to clear the lower bound — surfaced on the insufficient branch.
  - `monthly_cap` integer, nullable: The 80% hard cap on MTD post-import AI spend — surfaced on the monthly_cap_exceeded branch.
  - `monthly_spend_to_date` integer, nullable: MTD post-import-AI credit spend — surfaced on the monthly_cap_exceeded branch.
  - `max_tickers_per_call` integer, nullable: The per-call ticker cap — surfaced on the too_many_tickers branch (gives FE the exact value for the error copy).
  - `submitted_count` integer, nullable: Number of unique tickers the client submitted — surfaced on the too_many_tickers branch.
  - `upgrade_url` string, nullable: Plan upsell URL — surfaced on the trial_locked / insufficient / monthly_cap_exceeded branches.
  - `message` string, nullable: Human-readable copy for the active branch. Placeholder pending VOICE+DESIGN co-pass before user-visible launch.
  - `decision` string, nullable: Machine-readable decision code ('ok', 'trial_locked', 'insufficient_credits', 'monthly_cap_exceeded', 'too_many_tickers', 'bad_request'). Mirrors HTTP status.
  - `action` string, nullable: Echoed action type ('news' or 'research') on the OK branch.
  - `model` string, nullable: AI model identifier the Phase 2 fetch will use — surfaced on the OK branch so the FE can show a 'powered by' chip if desired.
  - `max_tokens` integer, nullable: Per-ticker output-token cap the Phase 2 fetch will apply — surfaced on the OK branch.

---

[API](https://skmtc.dev/oxfordledge/apis/oxford-ledge-api.md) · [All operations](https://skmtc.dev/oxfordledge/apis/oxford-ledge-api/llms.txt) · [OpenAPI document](https://skmtc.dev/oxfordledge/apis/oxford-ledge-api/revisions/e832a1260f0b?raw)
