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 - This guide as Markdown:
/developers/api.md
Authentication
- Sign in at jamesperalta.com/developers and create a
key. It starts with
jpk_and is shown once. - 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-Idheader. 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, andPOST /resume-coach/sessionsaccept anIdempotency-Keyheader. 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;GETthe 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_…" } }
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.
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 (
questionIdgiven): one question, no clock. - Standard (no
questionId): two questions picked forlevel, 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:
stepsis the current question's checklist:approach(you explained your plan),passingRun(a submission passed every test), andcomplexity(you stated time and space). When all three aretrue, the interviewer wraps up the question. It isnullonce the interview has ended.remainingStep({ id, label, detail }) highlights the one step left once you've made progress, withidapproach,passing_run, orcomplexity. It isnullbefore you start and after you finish; usestepsfor 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
codewith 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.indexandstatusafter each turn.POST /finishends 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_...
| 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
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.