# JamesPeralta.com API

Use Resume Coach, AI coding interviews, and Peralta 75 practice from your own code or AI
agent. It's the same coach, interviewer, and progress as the website: a resume edit you accept
over the API shows up on the website, and a question you solve on the website counts here.

- **Base URL:** `https://jamesperalta.com/api/v1`
- **OpenAPI 3.1 spec:** [`/api/v1/openapi.json`](https://jamesperalta.com/api/v1/openapi.json)
- **This guide as Markdown:** [`/developers/api.md`](https://jamesperalta.com/developers/api.md)

## Authentication

1. Sign in at [jamesperalta.com/developers](https://jamesperalta.com/developers) and create a
   key. It starts with `jpk_` and is shown once.
2. Send it on every request:

```bash
export JP_API_KEY=jpk_...
curl https://jamesperalta.com/api/v1/me -H "Authorization: Bearer $JP_API_KEY"
# {"email":"you@example.com"}
```

A key acts as you: it sees your resume, interviews, and progress, and nothing else. Revoke a
key on the same page. You can have 10 active keys.

## Conventions

- JSON in, JSON out, camelCase fields.
- Every response has an `X-Request-Id` header. Include it when you report a problem.
- Ids you didn't create, or that belong to someone else, return `404`.
- Some calls run a model or your code and take a while: coach and interviewer messages up to a
  few minutes, code submissions up to about a minute and a half. Use a generous client timeout.
- `POST /coding-interviews`, `POST /practice/questions/{id}/interview`, and
  `POST /resume-coach/sessions` accept an `Idempotency-Key` header. Retrying with the same key
  returns the first response instead of starting a second session. That response is a snapshot
  from when the session started; `GET` the resource for its current state.
- Each operation in the [OpenAPI spec](https://jamesperalta.com/api/v1/openapi.json) lists the
  error statuses it can return.

## Errors and limits

Errors have one shape:

```json
{ "error": { "code": "not_found", "message": "Coding interview not found.", "requestId": "req_…" } }
```

| `code`             | HTTP | What to do                                                        |
| ------------------ | ---- | ----------------------------------------------------------------- |
| `invalid_request`  | 400  | Fix the request; `message` says what's wrong                      |
| `unauthorized`     | 401  | Check the key; it may be revoked                                  |
| `payment_required` | 402  | Free interview allowance exhausted; upgrade to Pro at `/pricing`  |
| `not_found`        | 404  | Check the id                                                      |
| `conflict`         | 409  | The resource can't do that now (turn running, interview ended, …) |
| `rate_limited`     | 429  | Wait `Retry-After` seconds                                        |
| `upstream_error`   | 502  | The model or code runner failed. Retry                            |
| `internal_error`   | 500  | Retry; report the `requestId` if it keeps happening               |

Limits: 120 requests per minute per key, and 20 Resume Coach messages per minute (shared with
the website).

## Resume Coach

The coach reads and edits your master resume. It never changes the resume directly: an edit is
staged as a **proposal**, and your resume changes only when you accept it. There is at most one
pending proposal at a time; a newer one replaces the older.

```bash
API=https://jamesperalta.com/api/v1
AUTH="Authorization: Bearer $JP_API_KEY"

# 1. Your current resume ({"resume": null} if you don't have one yet)
curl $API/resume -H "$AUTH"

# 2. Start a conversation
curl -X POST $API/resume-coach/sessions -H "$AUTH"
# {"id":"583c3299-…","messages":[],"pendingProposal":null}

# 3. Send a message. To import a resume, paste its text in `message`, or send extracted text
#    as attachments: [{"name":"resume.pdf","text":"…"}]
curl -X POST $API/resume-coach/sessions/583c3299-…/turns -H "$AUTH" \
  -H "Content-Type: application/json" \
  -d '{"message":"Review my resume and tell me the highest-leverage change."}'
```

A message returns:

```json
{
  "sessionId": "583c3299-…",
  "reply": "I staged a Skills section listing Go and PostgreSQL.",
  "changeSummary": ["created Skills"],
  "pendingProposal": {
    "id": "d785ff32-…",
    "status": "proposed",
    "baseVersion": 1,
    "content": { "basics": {}, "sections": [] }
  },
  "resume": {
    "version": 1,
    "updatedAt": "2026-09-24T04:39:16Z",
    "content": { "basics": {}, "sections": [] }
  }
}
```

`resume` is what's saved; `pendingProposal.content` is the full resume with the edit applied.

If you have no resume yet, importing one works the same way: the coach stages the imported
resume as a proposal with `baseVersion: 0` (together with any edit you asked for), and it is
saved as version 1 when you accept it.

```bash
# 4. Apply (or reject) the staged edit
curl -X POST $API/resume-coach/proposals/d785ff32-…/accept -H "$AUTH"
# {"status":"applied","resume":{"version":2,…}}

# 5. Keep talking in the same session, and read the transcript any time
curl $API/resume-coach/sessions/583c3299-… -H "$AUTH"
```

Accepting returns `409` if your resume changed after the edit was staged, or if the proposal
was rejected or replaced by a newer one; ask the coach again. Repeating the same decision is
harmless: accepting an applied proposal again returns `applied`, and rejecting a rejected one
returns `rejected`. Rejecting an applied proposal is a `409`.

## Coding interviews

An AI interviewer runs a real coding interview: you explain your approach, write code, run the
tests, and state complexity, then get a scorecard.

- **Practice** (`questionId` given): one question, no clock.
- **Standard** (no `questionId`): two questions picked for `level`, with a session clock
  (`endsAt`).

Languages: `python` (default), `java`, `cpp`, `csharp`, `go`, `kotlin`, `swift`, `rust`.

```bash
# 1. Start
curl -X POST $API/coding-interviews -H "$AUTH" -H "Content-Type: application/json" \
  -H "Idempotency-Key: my-run-1" \
  -d '{"questionId":"leetcode-1","language":"python"}'
```

```json
{
  "id": 4,
  "mode": "practice",
  "language": "python",
  "status": "active",
  "startedAt": "2026-09-24T04:52:10Z",
  "endsAt": null,
  "question": {
    "index": 0,
    "total": 1,
    "title": "1. Two Sum",
    "difficulty": "Easy",
    "prompt": "…",
    "starterCode": "def two_sum(nums: list[int], target: int) -> list[int]:\n    pass"
  },
  "code": "def two_sum(…",
  "steps": { "approach": false, "passingRun": false, "complexity": false },
  "remainingStep": null,
  "messages": [{ "role": "assistant", "content": "Hi, welcome! …", "createdAt": "…" }]
}
```

```bash
# 2. Talk to the interviewer: explain your approach before coding
curl -X POST $API/coding-interviews/4/turns -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"message":"I will use a hash map from value to index: one pass, O(n) time and space."}'
# → the interview, plus "reply": ["That's a clear plan: …"]

# 3. Submit code; the server runs the tests
curl -X POST $API/coding-interviews/4/submissions -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"code":"def two_sum(nums, target):\n    seen = {}\n    for i, n in enumerate(nums):\n        if target - n in seen:\n            return [seen[target - n], i]\n        seen[n] = i\n"}'
# {"passed":true,"passedCount":4,"totalCount":4,"results":[{"label":"…","passed":true,"actual":"[0, 1]"}],"stdout":"","stderr":""}

# 4. State complexity (or anything the interviewer asks)
curl -X POST $API/coding-interviews/4/turns -H "$AUTH" -H "Content-Type: application/json" \
  -d '{"message":"All tests pass. Time O(n), space O(n)."}'
# → once every step is done, the interviewer wraps up and "status" becomes "completed"

# 5. Finish (only needed if you want to stop early; harmless if already completed)
curl -X POST $API/coding-interviews/4/finish -H "$AUTH"

# 6. Scorecard (generated on first request)
curl $API/coding-interviews/4/scorecard -H "$AUTH"
# {"recommendation":"Hire","summary":"…","dimensions":[{"name":"Communication","score":4,"label":"Strong",…}],…}
```

How an interview moves:

- `steps` is the current question's checklist: `approach` (you explained your plan),
  `passingRun` (a submission passed every test), and `complexity` (you stated time and space).
  When all three are `true`, the interviewer wraps up the question. It is `null` once the
  interview has ended.
- `remainingStep` (`{ id, label, detail }`) highlights the one step left once you've made
  progress, with `id` `approach`, `passing_run`, or `complexity`. It is `null` before you start
  and after you finish; use `steps` for the full picture.
- Talk to the interviewer as you would in a real interview. If it asks you to say something
  specific (for example "ready for review"), send that as a message.
- Pass `code` with a message to update the editor before the interviewer reads it. Without it,
  the interviewer reads the last code you sent or submitted.
- The interviewer may move to the next question or end the interview on its own. Check
  `question.index` and `status` after each turn. `POST /finish` ends it early; calling it on an
  interview that already ended is harmless.
- A message sent while another is still running, or after the interview ended, returns `409`.
- The scorecard needs a completed interview with at least one submission. Otherwise it's `409`.
- Interviews and practice are tracked separately: passing a question inside an interview does
  not mark it solved in your practice progress.

## Peralta 75 practice

Peralta 75 is a curated track of 75 questions: 70 LeetCode-style questions, available here,
and 5 multi-phase practical scenarios, available on the website only. Question ids look like
`leetcode-1`, and titles include the LeetCode number (`"1. Two Sum"`).

```bash
# Browse the track (filters are optional and ignore case)
curl "$API/practice/questions?category=Arrays/Strings&difficulty=Easy" -H "$AUTH"
# {"questions":[{"id":"leetcode-1","title":"1. Two Sum","difficulty":"Easy","category":"Arrays/Strings","pattern":"…"}],"categories":["Arrays/Strings",…]}

# A question, with starter code for your language
curl "$API/practice/questions/leetcode-1?language=go" -H "$AUTH"
# {…,"prompt":"…","languages":["python","cpp",…],"language":"go","starterCode":"package main\n\nfunc two_sum(…","examples":[{"label":"…","input":"nums=[2,7,11,15], target=9","expectedOutput":"[0,1]"}]}

# Submit a solution: the server runs every test
curl -X POST $API/practice/questions/leetcode-1/submissions -H "$AUTH" \
  -H "Content-Type: application/json" -d '{"language":"go","code":"package main\n…"}'
# {"passed":true,"passedCount":4,"totalCount":4,"results":[…],"stdout":"","stderr":"","progress":"solved"}

# Your progress
curl $API/practice/progress -H "$AUTH"
# {"total":70,"solved":1,"questions":[{"questionId":"leetcode-1","status":"solved",…}]}

# Practice the same question as an AI coding interview
curl -X POST $API/practice/questions/leetcode-1/interview -H "$AUTH" \
  -H "Content-Type: application/json" -d '{"language":"go"}'
# → a CodingInterview; continue with the Coding interviews routes above
```

Write the function the starter code declares, and keep its name and signature. Submit the
whole file, including anything the starter code defines for you, such as a `TreeNode` or
`ListNode` class: the test harness builds those inputs from the `examples` and calls your
function. `actual` and `expectedOutput` are the values as your language prints them (Python
prints `True`, Java prints `true`).

A full pass marks the question solved; a later failing run keeps it solved.

## MCP server

AI agents can use the same products through a remote [MCP](https://modelcontextprotocol.io)
server. It speaks Streamable HTTP and takes the same API key.

- **URL:** `https://jamesperalta.com/api/mcp`
- **Auth header:** `Authorization: Bearer jpk_...`

| Tool                       | What it does                                                        |
| -------------------------- | ------------------------------------------------------------------- |
| `get_resume`               | The saved resume                                                    |
| `coach_resume`             | Talk to the Resume Coach (omit `sessionId` to start a conversation) |
| `decide_resume_proposal`   | Accept or reject the coach's staged edit                            |
| `list_practice_questions`  | The Peralta 75 track, with filters                                  |
| `get_practice_question`    | Prompt, examples, and starter code                                  |
| `submit_practice_solution` | Run a solution against the tests                                    |
| `get_practice_progress`    | Attempted and solved questions                                      |
| `start_coding_interview`   | Start an interview (optionally from a practice question)            |
| `get_coding_interview`     | Status, question, code, checklist, transcript                       |
| `send_interview_message`   | Talk to the interviewer                                             |
| `submit_interview_code`    | Run code against the current question's tests                       |
| `finish_coding_interview`  | End the interview                                                   |
| `get_interview_scorecard`  | The scorecard                                                       |

Each tool returns the same JSON as the matching API route, as `structuredContent` and as text.
A failed call is a tool result with `isError: true` and the error envelope as its text, so the
agent can read the message and recover.

**Claude Code**

```bash
claude mcp add --transport http jamesperalta https://jamesperalta.com/api/mcp \
  --header "Authorization: Bearer $JP_API_KEY"
```

**Clients configured with JSON** (Cursor, VS Code, and others)

```json
{
  "mcpServers": {
    "jamesperalta": {
      "url": "https://jamesperalta.com/api/mcp",
      "headers": { "Authorization": "Bearer jpk_..." }
    }
  }
}
```

**OpenAI Responses API**

```python
from openai import OpenAI

client = OpenAI()
response = client.responses.create(
    model="gpt-5",
    tools=[{
        "type": "mcp",
        "server_label": "jamesperalta",
        "server_url": "https://jamesperalta.com/api/mcp",
        "headers": {"Authorization": "Bearer jpk_..."},
        "require_approval": "never",
    }],
    input="List three Easy Peralta 75 questions in the Arrays/Strings category.",
)
print(response.output_text)
```

## Reference

Every route, field, and error is in the
[OpenAPI spec](https://jamesperalta.com/api/v1/openapi.json).
