Skip to main content

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.

Authentication

  1. Sign in at jamesperalta.com/developers and create a key. It starts with jpk_ and is shown once.
  2. Send it on every request:
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 lists the error statuses it can return.

Errors and limits

Errors have one shape:

{ "error": { "code": "not_found", "message": "Coding interview not found.", "requestId": "req_…" } }
codeHTTPWhat to do
invalid_request400Fix the request; message says what's wrong
unauthorized401Check the key; it may be revoked
payment_required402Free interview allowance exhausted; upgrade to Pro at /pricing
not_found404Check the id
conflict409The resource can't do that now (turn running, interview ended, …)
rate_limited429Wait Retry-After seconds
upstream_error502The model or code runner failed. Retry
internal_error500Retry; 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.

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:

{
  "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.

# 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.

# 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"}'
{
  "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": "…" }]
}
# 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").

# 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 server. It speaks Streamable HTTP and takes the same API key.

  • URL: https://jamesperalta.com/api/mcp
  • Auth header: Authorization: Bearer jpk_...
ToolWhat it does
get_resumeThe saved resume
coach_resumeTalk to the Resume Coach (omit sessionId to start a conversation)
decide_resume_proposalAccept or reject the coach's staged edit
list_practice_questionsThe Peralta 75 track, with filters
get_practice_questionPrompt, examples, and starter code
submit_practice_solutionRun a solution against the tests
get_practice_progressAttempted and solved questions
start_coding_interviewStart an interview (optionally from a practice question)
get_coding_interviewStatus, question, code, checklist, transcript
send_interview_messageTalk to the interviewer
submit_interview_codeRun code against the current question's tests
finish_coding_interviewEnd the interview
get_interview_scorecardThe 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

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)

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

OpenAI Responses API

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.