PasteApply

Remote Streamable HTTP MCP สำหรับการปรับแต่งเรซูเม่/จดหมายสมัครงานอย่างตรงไปตรงมา ฟรีสำหรับการสร้าง; ปลดล็อกด้วย Stripe สำหรับการดาวน์โหลด MCP: https://pasteapply.com/mcp

เอกสาร

PasteApply for agents

Product: https://pasteapply.com

Listed on mcpservers.org

MCP happy path (bots: copy this loop)

Connect Remote MCP https://pasteapply.com/mcp (Streamable HTTP). Tools: status, store_resume, generate, unlock_link, confirm_payment, get_generation. Prefer MCP over HTTP twins and over driving the website.

  1. status — readiness + prices. Confirm Stripe/OpenAI are up. 1b. store_resume(resume) once (optional) → resumeId rs_1.<uuid> — on later generate, pass resumeId (prefer resumeId over re-pasting the full resume; pasted resume still wins if both are sent). TTL ~30 days. File uploads: HTTP POST /api/resumes multipart file (pdf/docx/doc/txt/md, max ~8MB).
  2. generate(jobPosting or jobUrl, resume or resumeId) — seconds later: teaser only when paid:false — coverage / gaps, fit (recommend: skip|weak|apply + mustHavesMissing / impliedOnly + gap when a must-have is missing — exactly one missing must-have ⇒ weak with gap "Missing must-have: …"; two or more ⇒ skip), recommendUnlock (false when fit.recommend is skip; true for weak|apply; optional unlockAdvice when false), proof (bulletsRephrased = model-reworded pairs only, diff kind rephrased; pairs with kind matched + requirement are unchanged source bullets labeled with the posting requirement they support, counted in proof.bulletsMatched (present when >0), never as rephrased; a rephrased pair may also carry requirement when the bullet was reworded and labeled ("Written communication: …") — it still counts once, in bulletsRephrased; visible pair count = bulletsRephrased + bulletsMatched), whatChanged / diff pairs, redacted sample. files.ready is false — no files.pdf / files.docx / files.coverPdf. No full letter, no full resume. Prefer pasted resume if both resume and resumeId are sent.
  3. Check recommendUnlock / fit.recommend before unlock. If recommendUnlock is false (or fit.recommend is skip), do not call unlock_link — missing must-haves cannot be invented. Otherwise unlock_link(price) — Stripe Checkout URL → hand to a HUMAN only. Never scrape Checkout. Prices: monthly=$3/1 · export=$5/5 (default) · pro=$10/50+Pro · power=$20/up to 200 unlocks+Pro extras (within 2,350 signed-in requests a month).
  4. poll get_generation(id) until paid:true — the Stripe webhook marks the draft paid and persists it; no cs_ ritual required on the happy path. Use confirm_payment(cs_ session id) only if the poll stays unpaid (slow webhook).
  5. When paid:true: full coverLetter, structured resume JSON, and paid files — files.ready, files.pdf, files.docx, files.coverPdf, files.expiresAt, files.coverLetter, files.resume. files.pdf / files.docx / files.coverPdf are absolute ?dl= URLs that last the UTC billing month (expire at UTC end of the current calendar month). Refresh anytime in-month via get_generation / confirm / paid generate (same generation still paid). If a file GET returns 403 (month ended), re-call get_generation for a fresh token (may 404/402 if the paid generation itself is past month-end TTL). Download Content-Disposition is LastCompany_Role.pdf / .docx and LastCompany_Role_cover.pdf (posting company+role, else latest experience; fallback Resume.pdf / Cover_Letter.pdf). files.coverPdf is /api/pdf/:id?kind=cover&dl=; alias GET /api/cover-pdf/:id.

Paid files object (machine shape):

{
  "ready": true,
  "pdf": "https://pasteapply.com/api/pdf/<id>?dl=<token>",
  "docx": "https://pasteapply.com/api/docx/<id>?dl=<token>",
  "coverPdf": "https://pasteapply.com/api/pdf/<id>?kind=cover&dl=<token>",
  "expiresAt": "2026-10-01T00:00:00.000Z",
  "coverLetter": "…full letter…",
  "resume": { "schemaVersion": 1, "name": "…", "contact": { "email": "…" }, "summary": "…", "skills": [], "experience": [{ "employer": "…", "title": "…", "start": "…", "end": "…", "location": "", "bullets": [] }], "education": [] }
}

Paid resume JSON: ATS schema v1 — schemaVersion:1, structured contact {email?, phone?, location?, links?} (not one blob), and every experience row has required employer|title|start|end|location|bullets (location "" if unknown; dates/employers frozen from source).

Honesty: never invent employers/dates/skills/metrics. If the posting wants something not on the resume, omit it. Guarantee: https://pasteapply.com/honesty · https://pasteapply.com/honesty.md

Quotas (UTC calendar month): $3/mo = 1 unlock · $5/mo Export = 5 unlocks · $10/mo Pro = 50 unlocks + hire-fit / variants · $20/mo = up to 200 unlocks + same Pro extras, within its 2,350 signed-in requests a month (every plan's monthly request allowance: see the request cap below). No public promo codes.

Site ad unlock (humans only — AppLixir S2S): The website may offer Unlock free with ads after an unpaid generate when recommendUnlock is true (not skip), when AD_UNLOCK_ENABLED + AppLixir API key are live. The human watches AppLixir rewarded ads (SDK v6.1.0); client complete is optimistic UI only. The server grants only after AppLixir S2S GET /api/ad/applixir (MD5+TID signature, dedupe on tid) or generic HMAC POST /api/ad/reward, then marks that generation paid:true with the same artifacts as Stripe. MCP / bots / Bearer pa_… cannot ad-unlock — stay on Stripe: unlock_link → poll get_generation. No client-trust / AdSense grants; no LLM call on unlock.

Paid bots: Authorization: Bearer pa_… from confirm_payment / POST /api/confirm (apiKey) or POST /api/agent-key { "sessionId": "cs_…" } (confirm is fallback when you need an apiKey or the webhook is slow). Status/generate return unlocksUsed, unlocksRemaining, period (YYYY-MM); at cap generate → 402 code:"unlock_quota" with remaining:0 / unlocksRemaining:0 (not retry-later); free daily cap → 429 code:"free_cap"; bogus Bearer → 401 (no silent teaser).

Cursor mcp.json (hosted default — no provider key):

{
  "mcpServers": {
    "pasteapply": {
      "url": "https://pasteapply.com/mcp"
    }
  }
}

Heavy agents can BYOK through a secret connection header, never a tool argument or chat paste. Use an env-var placeholder — never a literal key. Rotate any key previously pasted into tool args or chat.

{
  "mcpServers": {
    "pasteapply": {
      "url": "https://pasteapply.com/mcp",
      "headers": {
        "X-PasteApply-BYOK-Key": "${OPENAI_API_KEY}"
      }
    }
  }
}

Some Cursor builds want "type": "http" next to the URL. Claude / other connectors: paste https://pasteapply.com/mcp as the remote MCP URL. Referral ?ref=CODE works on the site (see Referral); the MCP URL itself does not need ?ref=.

HTTP twins (secondary)

Base: https://pasteapply.com — status/generate/checkout work without auth (teaser). Paid bots use Authorization: Bearer pa_…. Same contract as MCP.

  • GET /.well-known/mcp.json → directory listing (name, mcp, docs, honesty)
  • GET /api → machine index (paths + prices)
  • GET /api/status → readiness JSON (openaiConfigured, stripeConfigured, googleAuthConfigured, demoMode, prices, byokSupported, byokProviders; with Bearer pa_… also plan/unlocks*/period/agentHints)
  • POST /api/resumes { "resume": "..." } or multipart file (pdf/docx/doc/txt/md, max ~8MB) → { "resumeId": "rs_1.<uuid>" }. Stores extracted text only (~30 day TTL). Cap ~20 stores/hour/IP. No listing of stored resumes.
  • POST /api/generate { "jobPosting", "resume" } or { "jobUrl", "resumeId" } or { "useSample": true } → generation.id + truncated teaser when paid:false. If both jobPosting and jobUrl, prefer pasted jobPosting. If both resume and resumeId, prefer pasted resume. Unknown/expired resumeId → 400. jobUrl must be public http(s); bad/blocked/failed fetch → 400 (not 500). Full bodies only after unlock. Optional proPasses for Pro. Optional BYOK: modelProvider (hosted default | openai | anthropic) in JSON; provider key only via header X-PasteApply-BYOK-Key (never in JSON body / tool args; body apiKey → HTTP 400). Hosted ignores the header. Honesty/fact-lock still applies on every path (vault text is source — never invent employers).
  • POST /api/checkout { "price": "monthly"|"export"|"pro"|"power", "generationId"?: "..." } → { "url" } — hand to HUMAN only (twin of unlock_link)
  • POST /api/confirm { "sessionId": "cs_...", "claimToken": "…" } → fallback unlock + apiKey if poll stays unpaid (twin of confirm_payment)
  • POST /api/agent-key { "sessionId": "cs_...", "claimToken": "…" } → { apiKey, plan, unlocksRemaining, unlocksUsed, period } for paid bots
  • Purchaser only (Sep 27 2026): POST /api/checkout / unlock_link return a claimToken (browsers also get an httpOnly cookie). Confirm / agent-key link the plan and return apiKey only to the purchaser: the same signed-in Google account that started Checkout, or the caller presenting that Checkout's claimToken. A bare cs_ id gets the draft status only (agent-key: 403 not_purchaser). A Stripe customer already linked to one Google account is never re-linked to another.
  • GET /api/generation/:id → generation object including paid / unlocked (or 404 {"error":true,"message":"Not found or expired."}). Unpaid drafts are served for 24h, then deleted by a daily cleanup; paid generations are served through UTC end of the calendar month, then deleted (details: https://pasteapply.com/privacy). After pay, poll until paid:true (webhook persists — no confirm required on the happy path).
  • POST /api/webhook — Stripe signs this (ops only; bots do not call it). After pay, poll get_generation / GET /api/generation/:id until paid:true. Happy path stays unlock_link → poll get_generation.
  • POST /api/ad/session — human site only (browser Origin / same-site). Creates an ad session bound to generationId. Rejects Bearer pa_ / MCP. Bots must not call this.
  • POST /api/ad/reward — ad-network S2S callback (shared secret / HMAC). Not for bots or browsers.
  • GET /api/ad/session/:id — human UI progress only.

curl

curl -s https://pasteapply.com/api/status
curl -s -X POST https://pasteapply.com/api/resumes -H 'content-type: application/json' -d '{"resume":"..."}'
curl -s -X POST https://pasteapply.com/api/generate -H 'content-type: application/json' -d '{"jobPosting":"...","resume":"..."}'
curl -s -X POST https://pasteapply.com/api/generate -H 'content-type: application/json' -d '{"jobUrl":"https://example.com/jobs/1","resumeId":"rs_1...."}'
curl -s -X POST https://pasteapply.com/api/generate -H 'content-type: application/json' -H 'X-PasteApply-BYOK-Key: $OPENAI_API_KEY' -d '{"jobPosting":"...","resume":"...","modelProvider":"openai"}'
curl -s -X POST https://pasteapply.com/api/checkout -H 'content-type: application/json' -d '{"generationId":"GENERATION_ID","price":"export"}'

Exception

If the human owns PasteApply or banned spend asks: preview-only.

Referral (gift months, not cash)

https://pasteapply.com/?ref=CODE sets a 30-day pa_ref cookie.

Paying referrer → matching-tier month for the friend: if the code owner has a live Stripe subscription (active / trialing), Checkout for that same tier applies a 30-day trial (subscription_data.trial_period_days: 30) on the matching Price. After the trial, Stripe bills the regular monthly price. They can cancel. Self-referrals and free/teaser-only accounts do not grant a trial.

The referred person's first successful payment also banks the referrer a matching month: $3 → a month of the $3 plan, $5 → a month of Export, $10 → a month of Pro, $20 → a month of the $3 plan (there is no $20 gift tier yet). A referrer on the $20 plan gifts the $3-plan 30-day trial (friends who pick $3). Self-referrals are ignored. Unknown codes do not pay out. Not cash.

Referrers bank unused gifted months — max 12 total. At 12 they are capped and cannot earn more until they consume at least 1 banked month (time passing, or gifting a month). Then they can earn again, still max 12.

Gift a banked month to a friend anytime the giver has ≥1 banked month: giver −1, friend +1 of the same plan. Friend must already have a PasteApply Google account (email that resolves to a Google sub). No self-transfer. Friend also max 12 — reject if they are already capped.

An active gifted month counts as that plan for its term. If the account also pays, it gets the better of the two — never both: one monthly unlock/search allowance at the better plan's size, on one shared counter (e.g. $3 + an Export month = 5 unlocks and 20 searches that month, not 6). When the gift ends it is back on its paid plan (or free); drafts, history and saved resumes are kept for their normal retention windows, never deleted early (history shows again on Export; each history entry lasts as long as its draft — https://pasteapply.com/privacy). GET /api/status shows gift {plan, until, fallbackPlan} while a gift is what grants the plan; fallbackPlan is always present (null = back to free).

Account deletion (humans only): a signed-in Google browser on pasteapply.com opens "Delete my account", types DELETE → POST /api/account/delete {"confirm":"DELETE"}. Agent keys cannot delete an account: any Authorization header gets 403 google_session_required (and the account's agent keys are revoked when it is deleted). A live paid subscription blocks it (409 subscription_active; cancel in the billing portal first). What is deleted vs kept: https://pasteapply.com/privacy#deleting-your-account

Checkout and unlock_link still stamp referralCode / referredBy. Signed-in humans gift from the Share panel (POST /api/referral/gift).

Endpoints (detail)

GET /api/status

Returns JSON: openaiConfigured, stripeConfigured, googleAuthConfigured, demoMode, subscriptionActive, proActive, prices ($3/mo · 1 unlock, $5/mo · 5 unlocks, $10/mo Pro · 50 unlocks, $20/mo · up to 200 unlocks · 2,350 requests/mo), byokSupported, byokProviders (openai, anthropic). With Authorization: Bearer pa_…: also plan, unlocksUsed, unlocksRemaining, period (YYYY-MM), agentHints (e.g. reuse resumeId — don't re-paste the full resume). Bogus Bearer → 401. No secrets. CORS * (no credentials).

POST /api/resumes

Content-Type: application/json or multipart/form-data

{ "resume": "Jane Doe\nAcme 2020-2024 Operations Analyst\nSQL" }

Or multipart field file (.pdf / .docx / .doc / .txt / .md, max ~8MB) — we extract text via the same path as /api/extract-resume and store text only (no polish / no invented employers).

Response: { "resumeId": "rs_1.<uuid>" }. Reuse resumeId on generate. Soft TTL ~30 days (unknown/expired → generate 400). Cap ~20 stores/hour/IP. ResumeId is a capability (unguessable) — there is no list endpoint.

POST /api/generate

Content-Type: application/json

{
  "jobPosting": "...",
  "jobUrl": "https://example.com/jobs/1",
  "resume": "...",
  "resumeId": "rs_1.<uuid>",
  "useSample": false,
  "proPasses": [],
  "modelProvider": "hosted",
  "model": ""
}

Header (BYOK only): X-PasteApply-BYOK-Key: ${OPENAI_API_KEY} — env-var placeholder, never a literal key in docs or chat.

useSample: true loads the public demo. Identical job+resume (same fingerprint) returns the same generation.id and debits at most one unlock / free daily consume. proPasses only applies for Pro members. Pass jobPosting text and/or jobUrl (public http(s) only; we fetch and strip HTML; prefer pasted jobPosting if both). Pass resume text and/or resumeId from POST /api/resumes / MCP store_resume (prefer pasted resume if both). Unknown/expired resumeId → HTTP 400. Bad/blocked URL, fetch failure, empty/non-HTML body → HTTP 400 with a clear message (not 500). Counts toward the free daily generate cap like a pasted job.

Optional BYOK (server-side only): hosted is default. Heavy agents set modelProvider to openai or anthropic and send the provider key only in X-PasteApply-BYOK-Key (HTTP header / MCP connection headers). Never place provider keys in tool arguments, JSON body, or chat. JSON body apiKey is rejected (HTTP 400). Hosted ignores the header and never lets the client override our model (cost lock). BYOK uses the user's tokens and may set model. The key is never logged, persisted, hashed, echoed, or returned. Rotate a key if it was previously pasted. Fact-lock still runs on every path and fails closed (HTTP 422). Pricing ladder unchanged ($3=1 / $5=5 / $10=50 / $20=200).

Free vs paid payload

Unpaid (paid: false, unlocked: false, preview: true): incomplete teaser only. Keep coverage, fit, recommendUnlock (and unlockAdvice when skip), proof / factLock (bulletsRephrased = reworded pairs only — diff kind rephrased; kind matched pairs carry requirement, are the unchanged bullet labeled "Requirement: …" (matched to requirement, not reworded) and are counted in proof.bulletsMatched when >0; a kind rephrased pair carries requirement too when it was reworded and labeled with a requirement — still counted only in bulletsRephrased; visible pair count = bulletsRephrased + bulletsMatched), and whatChanged / diff with every original → tailored pair (original is the source line; tailored may be shortened). Redacted sample (coverLetter is the first sentence with [redacted]; resume keeps name + first role header + 1–2 redacted bullets). No full cover letter, no full tailored resume, no snapshot. Unpaid files.ready is false — no files.pdf / files.docx / files.coverPdf.

Paid (paid: true, unlocked: true, preview: false) after Checkout confirm or an active plan still under its monthly unlock quota: full coverLetter, full resume, clean files, and snapshot. Paid files: files.ready (true), files.pdf, files.docx, files.coverPdf, files.expiresAt, files.coverLetter, files.resume. Download tokens last the UTC billing month (HMAC ?dl= expires at UTC end of the calendar month); refresh via get_generation anytime in-month. Paid generations are kept to UTC month end (unpaid teasers 24h), then deleted by the daily cleanup. On file GET 403, re-call MCP get_generation / GET /api/generation/:id for a fresh token. Unpaid keeps files.ready false and omits pdf/docx/coverPdf. Pro extras (hire-fit, variants, interview, scorecard) on $10 Pro or $20/mo (up to 200 unlocks, within 2,350 signed-in requests a month).

MCP generate is the same contract. Do not stitch the teaser into a file. Check recommendUnlock / fit.recommend first — if skip / recommendUnlock:false, do not unlock. Otherwise call MCP unlock_link (default $5 Export = 5 unlocks/mo) and hand the URL to a human.

Stable fields: generation.id, coverLetter, resume, coverage, fit (always: recommend skip|weak|apply, mustHavesMissing, impliedOnly, and gap whenever a must-have is missing — gaps only, never invents; one missing must-have ⇒ weak, two or more ⇒ skip), recommendUnlock (false when fit.recommend===skip; true for weak|apply; optional unlockAdvice when false), whatChanged (same as diff), proof, factLock (human line), quality, paid, unlocked, preview, estimate (~10s), files. CORS * (no credentials). Prefer server-side fetch.

Machine-readable honesty (also on MCP generate and GET /api/generation/:id):

{
  "proof": {
    "factLock": true,
    "inventedClaims": 0,
    "bulletsRephrased": 4,
    "skillsLed": 2,
    "promise": "We only rearrange what is already true."
  },
  "coverage": [
    { "skill": "SQL", "status": "present", "note": "Already on the source resume. We can lead with this." },
    { "skill": "Salesforce admin", "status": "implied", "note": "Salesforce is on the resume; the exact admin title is not." },
    { "skill": "Kubernetes", "status": "missing", "note": "Kubernetes is not on the resume. Lead with SQL — do not invent Kubernetes." }
  ]
}

Interview chance: generation.interviewChanceBefore / interviewChanceAfter (percent) are an estimate of resume↔posting fit, never a guarantee, and are never shown above 95% (no draft is a sure thing). generation.interviewChanceChange (delta, surfaced, lost, note) explains the difference honestly: the fact lock never adds qualifications, so before and after are often the same, and a rise only means an implied qualification is now stated from facts already on the resume. coverage[].status is present | implied | missing. Every generate also returns fit: deterministic skip|weak|apply from coverage (exactly one missing must-have ⇒ weak, named in fit.gap "Missing must-have: …"; two or more ⇒ skip, fit.gap "Missing must-haves: …"; nothing on the resume matching at all ⇒ skip; honesty still strips invented skills separately). The interview % is computed inside non-overlapping bands that follow the verdict — apply 70–95, weak 45–69, skip 0–44 — so a weak never shows more than an apply and a skip never shows 80%+ (same bands for Pro hireFitPercent). interviewChanceAfter is scored on the tailored draft text itself; it rises only when the draft now states a requirement the resume already supports (e.g. an implied must-have), otherwise interviewChanceChange.note says "No change". Agent views also include recommendUnlock (false on skip; true on weak|apply) and optional unlockAdvice when false — check before unlock_link. Present = named on the source resume. Implied = a related tool or title is on the resume, not the exact word. Missing = not on the resume — we show the gap and do not invent it. If inventedClaims would be >0, generate fails (HTTP 422) instead of returning fiction. Guarantee: https://pasteapply.com/honesty

POST /api/checkout

{ "price": "export", "generationId": "<uuid>" }

price: monthly | export | pro | power — required. $3/mo = 1 unlock (price: "monthly"). $5/mo = 5 unlocks (price: "export", default). $10/mo Pro = 50 unlocks + extras (price: "pro"). $20/mo = up to 200 unlocks (within 2,350 signed-in requests a month) + same Pro extras (price: "power"). Empty body, missing price, or bogus price → HTTP 400. generationId optional. Website checkout type for $20 is power ($20=200).

Response: { "url": "https://checkout.stripe.com/..." } — give this link to a human.

POST /api/confirm

{ "sessionId": "cs_live_..." }

Fallback unlock after Checkout when get_generation still shows paid:false (slow webhook). On success includes apiKey (pa_1.…) for paid-bot Bearer auth. Twin: POST /api/agent-key with the same sessionId. Happy path: poll GET /api/generation/:id / MCP get_generation until paid:true — no cs_ required.

GET /api/generation/:id

Returns the generation object plus paid / unlocked, proof, and coverage, or 404 if expired or unknown. Unpaid drafts are served for 24h; paid generations through UTC end of the created calendar month. After that they are deleted by a daily cleanup (see /privacy). Poll this (or MCP get_generation) right after generate; you should get the teaser, not 404. Unpaid responses stay teasers. After the human pays, the Stripe webhook marks the draft paid and persists it — poll until paid: true for the full payload (confirm only if the poll stays unpaid).

Rate limits

Public teaser path needs no key. Paid bots use Authorization: Bearer pa_…. Free teasers are capped per IP and per browser session (UTC day). Paid plans are capped by UTC calendar-month unlocks: $3 = 1, $5 = 5, $10 = 50, $20 = 200. At cap, generate returns HTTP 402 with a human upsell message.

  • Real job+resume teasers: 10 / day
  • Sample (useSample: true): 60 / day

Exceed free cap: HTTP 429, Retry-After (seconds until the next UTC midnight), and JSON:

{ "error": true, "code": "free_cap", "message": "Daily free generate limit reached (10 real job+resume teasers per UTC day). Resets at 00:00 UTC (in 12h; retryAfter = seconds until then). Or unlock with $3/mo (1 unlock), $5 Export (5), $10 Pro (50), or $20/mo (200) this calendar month (UTC).", "retryAfter": 43200 }

Show message to the human. Honor retryAfter / Retry-After. If you get 429, 502, or 504, wait (or backoff 1s, 2s, 4s on 502/504). Do not hammer generate. Prefer one generate at a time per client.

Paid unlock month cap (not retry-later): HTTP 402 and JSON:

{ "error": true, "code": "unlock_quota", "message": "…", "remaining": 0, "unlocksRemaining": 0, "unlocksUsed": 1, "period": "2026-09", "plan": "starter" }

Do not treat 402 / unlock_quota as wait-and-retry — upsell or stop.

Signed-in / agent-key request cap: every request that carries a Google session or a Bearer pa_… key counts toward a monthly (UTC) request allowance by plan for that Google account or that key's billing customer (counted separately; all keys of one customer share one count): 300 free; 300 on $3 / $5 or with an Export gift (including the one-month welcome gift, given at first Google sign-in only to accounts with no existing billing customer); 3,150 on $10 (pro); 2,350 requests per UTC month on $20 (power). A gift of a plan gets that plan's allowance. Large reads are charged by cost: a request's draft, resume-file, job and job-index reads are priced in units of one storage read plus 32 KB transferred (a 32 KB draft = 1 unit, 100 KB = 3, 512 KB = 13); the first unit is part of the request and each further unit counts as one more request. A read whose size is not reported is measured (never counted as 0). If a read needs more requests than are left, it is refused, the extra requests it took are given back, and the request itself still counts once (the route answers an error). One exception: POST /api/confirm records a Checkout purchase before it reads the draft, so if that read is refused the purchase and its unlock stay recorded (the draft opens on a later request, or signed out with the claim token). If the plan cannot be verified when the account passes 300 (Stripe / storage error), a general 800 limit applies, labelled tier:"unverified". Past it: HTTP 429 code:"account_request_cap" with tier ("free", "monthly", "export", "pro", "power" or "unverified"; gift:true when a gift grants it) and cap, Retry-After = seconds until 00:00 UTC on the 1st, nothing changed. Not counted: signed-out requests, Next.js link prefetch / RSC requests for the /jobs list, the GET /api/generate warm-up probe and the POST /api/src beacon (none reads account data). Every cap 429 also carries resetsOn:"1st of each month, 00:00 UTC", resetsAt, unlockDebited:false (the cap never spends an unlock) and unlocksRollOver:false (unlocks left at the end of the UTC month expire; they do not carry over). Since Oct 1 2026 this count is kept in each server instance's memory plus a signed pa_acct cookie (no storage call per request), so it is a per-instance soft cap: a browser that drops the cookie and lands on a fresh instance, or a pa_ key, restarts that instance's count. Unlock and outside-search debits never run while their own counters are unavailable: generate answers 503 code:"unlock_quota_unavailable" with unlockDebited:false (nothing generated, unlocked or used; retry after retryAfter).

MCP batching: /mcp takes ONE JSON-RPC message per HTTP request. A JSON-RPC batch (array body) is refused with HTTP 400 and JSON-RPC error -32600, and no tool runs, so every tool call is one counted request.

Errors and retries

JSON uses human-readable message (or error string on checkout/confirm) — show that text to the human; do not invent status copy.

  • 400 — bad JSON, empty job/resume (unless useSample or valid resumeId), unknown/expired resumeId, bad/blocked jobUrl / fetch fail, or confirm with a bogus/unknown Checkout sessionId (not 502). Example: "Paste a job posting (or jobUrl) and your current resume." / "Unknown or expired resumeId…" / "That job URL is blocked (local or private network). Paste the posting text instead." / "That Checkout session was not found."
  • 422 — honesty lock: invented claims. Fix the source resume; do not retry the same invention.
  • 429 — code:"account_request_cap": this account's or key's billing customer's counted requests for the UTC month are used (by plan: 300 free, 300 $3 / $5 / Export gift, 3,150 $10, 2,350 $20; tier says which; wait for Retry-After; nothing changed). Or free generate daily cap (code:"free_cap"; 10 real / 60 sample per IP and session per UTC day, reset at 00:00 UTC; the message states the real time left; since Oct 1 2026 counted per server instance in memory plus the pa_rl cookie, so a soft cap, with a per-instance daily cap on real free previews). Wait for retryAfter / Retry-After (seconds until UTC midnight). Never treat as a paid-wall.
  • 403 — download ?dl= token expired (UTC billing month ended). Re-call MCP get_generation / GET /api/generation/:id for a fresh files.pdf / files.docx / files.coverPdf URL; do not DIY the file.
  • 402 — unpaid PDF/DOCX/cover download (code:"payment_required", unlockDebited:false; a plan unlocks only drafts made in its own signed-in account: the first POST download of such a draft uses one unlock, later downloads are free, and a plain GET never spends one — code:"owner_unlock_needs_post"), or paid plan at monthly unlock cap (code:"unlock_quota", remaining:0 / unlocksRemaining:0, plus used/period/plan; LIVE-UNVERIFIED). Show message. Upsell the next tier; do not retry-later (402 ≠ 429). Do not invent a promo code.
  • 404 — generation expired (unpaid 24h; paid UTC month end; then deleted) or unknown id — one identical code:"not_found" body for all of these. File routes (/api/pdf, /api/docx, /api/cover-pdf) send Cache-Control: no-store, X-Robots-Tag: noindex and Referrer-Policy: no-referrer; about 20 unknown ids from one IP within 10 minutes get 429 code:"not_found_rate_limited" with Retry-After
  • 504 — generate took too long; try again in a moment
  • 503 code:"storage_unavailable" — file storage is down, so drafts and files cannot be saved or reopened. Generate (HTTP and MCP) and outside job search answer this BEFORE any model call: nothing generated, no free preview counted, no unlock used (unlockDebited:false, freePreviewCounted:false). Retry after Retry-After. While storage is down, purchases are paused too: POST /api/checkout / MCP unlock_link and Promote checkout answer 503 code:"purchases_paused" ("Purchases are paused while we restore file storage; you have not been charged"), so nobody pays for files they cannot get. If storage fails between the check and the save, generate answers 503 code:"draft_not_saved" instead of a draft that could never be reopened: the free preview and any reserved unlock are given back and the message says exactly what was (unlockDebited, freePreviewRefunded). Check GET /api/status → storage ("ok" | "unavailable" | "local" | "unchecked" = this server has not checked yet), storageHealth ("healthy" | "unhealthy" | "unknown"; the status call runs the storage check itself, at most once a minute per server, and reuses a passing result for up to 6 hours on Blob; healthy needs two passing checks in a row; storage not configured = "unhealthy"), storageBackend, storageCheckedAt, purchasesPaused (it may also be paused by hand while storage is restored). Automatic pauses clear by themselves once a real write-and-read of storage succeeds again.
  • Standard generate aborts at ~20s; Pro rewrite ~28s. estimate is ~10s.

Input size

Job posting max 20,000 characters. Resume max 40,000 characters. Longer bodies return 400. On the $20 plan, hosted Pro accepts at most 8,950 tokens (o200k tokenizer; roughly 35k–50k characters depending on the text — it is a token cap, not a character cap) of resume + job posting combined; over that returns 400 with no truncation (Standard and BYOK keep the full character limits).

MCP tool schemas

Remote MCP https://pasteapply.com/mcp (Streamable HTTP). Tools:

  • status — no args. Readiness + prices (+ plan/unlocks when inbound Authorization: Bearer pa_…).
  • store_resume — resume text → { resumeId } (rs_1.<uuid>). Store once; prefer resumeId on later generate over re-pasting. Files: HTTP multipart POST /api/resumes (not this tool).
  • generate — jobPosting?, jobUrl?, resume?, resumeId?, useSample?, proPasses?, optional BYOK modelProvider? / model? (no apiKey in tool args). Provider key only via connection header X-PasteApply-BYOK-Key. Prefer resumeId over re-pasting after store_resume; pasted resume / jobPosting still win if both pairs sent. Forwards inbound Bearer + BYOK header. Unpaid: teaser JSON + proof + recommendUnlock/fit (not the full letter). Paid key with remaining>0: full cover + resume + files (consumes 1 unlock). Check recommendUnlock before unlock_link.
  • unlock_link — only when recommendUnlock is true (skip when fit.recommend is skip). generationId?, price? (monthly = $3/mo · 1 unlock | export = $5/mo · 5 unlocks, default | pro = $10/mo · 50 unlocks + Pro | power = $20/mo · up to 200 unlocks (within 2,350 signed-in requests a month) + Pro extras). Stripe Checkout URL for a human. After pay, poll get_generation until paid:true for paid files.* (ready, pdf, docx, coverPdf, expiresAt, coverLetter, resume). confirm_payment only if the poll stays unpaid.
  • confirm_payment — sessionId (cs_...) + claimToken from unlock_link. Fallback when webhook is slow. After success, payload is the full artifact + paid files + apiKey.
  • get_generation — id or generationId (same uuid). Happy path after pay: poll until paid:true. Teaser until paid. When paid, returns full cover + resume + files (ready, pdf, docx, coverPdf, expiresAt, coverLetter, resume) with tokens lasting the UTC billing month. On file 403, re-call for a fresh token.

Browser GET Accept: text/html on /mcp returns a short hint page linking here. MCP clients should send JSON / event-stream, not HTML.

Jobs board (humans)

Human site surface: /jobs (search; /jobs?q=…&location=…&remote=1 also filters server-side without JavaScript), /jobs/{id} (detail), /jobs/employers (post/manage). Owned PasteApply listings always. Outside results (qualifying paid plans only: $5 Export 20 searches/mo, $10 Pro and $20 75/mo per account, UTC month): GET /api/jobs?q=…&location=…&web=1 with a signed-in paid session runs the OpenAI Responses web_search tool restricted to public ATS job boards (Greenhouse, Lever, Ashby, Workday, SmartRecruiters, Workable, Jobvite, iCIMS and similar); every URL is re-validated server-side and only real source/citation listing URLs are returned (source:"web", external:true, applyUrl). Cached by normalized query+location (cache hits don't count). Outside results are link-checked before they are shown and cached for up to 12h, so a listing can still close within that window. Each outside search runs one hosted search; one whose results exceed 20,000 input tokens counts as ceil(tokens / 20,000) searches (never more than are left that month). Free/$3 → owned posts + one upsell line (webSearch.upsell). No competitor scraping: never Indeed/LinkedIn/Glassdoor/Google scrapes, no SerpAPI. Interview % for an outside result: POST /api/jobs/{ws_id}/interview-chance with {resume, jobUrl: applyUrl}, or tailor via /?jobUrl={applyUrl} for before/after. Posting needs a Google sign-in (a browser session on pasteapply.com; there is no API key or token for employers, and the QA header does not authorize). Create: POST /api/jobs (draft). Edit: PATCH /api/jobs/{id} with the fields to change. Optional applicantLocation (remote jobs only, up to 120 chars, what the poster types, e.g. "United States"; null clears) is the only source for JobPosting applicantLocationRequirements; blank means none is published. Close: PATCH /api/jobs/{id} with {"status":"closed"} (owner only). DELETE /api/jobs/{id} is not supported: it returns 405 with Allow: GET, PATCH, OPTIONS and a JSON pointer to the close PATCH. Closed and expired jobs leave the board; the record is deleted 30 days after it expires. Search: GET /api/jobs?q=… filters PasteApply posts (every keyword must appear in title, company, location, description or salary; any order) and echoes the applied filters in query with count. Error and signed-in responses are Cache-Control: no-store. Free posts: signed-in Google employers draft → Publish (POST /api/jobs/{id}/publish) marks open + expiresAt = now+30d — no Stripe, no 402. Rate-limited per employer UTC day; open-seat cap still applies. Optional Promote: $30 one-time for 30 days featured (Stripe Checkout). Checkout metadata.type=job_promote → webhook sets promotedUntil; public board sorts featured first + badge. Unpromoted open jobs still listed. MCP stays tailor tools (status / store_resume / generate / unlock_link / …). Do not invent a search_jobs MCP tool until a real inventory API exists. Bridge: job detail → /?jobUrl=https://pasteapply.com/jobs/{id} (generate resolves owned URLs from Blob; honesty lock + unlock ladder unchanged).

Version

Contract v0. Changelog: site ad-funded unlock (humans only; bots stay Stripe); BYOK provider keys move to X-PasteApply-BYOK-Key (never tool args / JSON body); public $20 plan id is power ($20=200); resume vault store_resume / POST /api/resumes → resumeId rs_1.<uuid> (~30d); generate accepts resumeId; paid-bot Authorization: Bearer pa_… (confirm / /api/agent-key); UTC-month unlock quotas $3=1 / $5=5 / $10=50 / $20=200 + Pro extras; at cap generate is 402 + unlock_quota; free daily cap is 429 + free_cap; starter unlock is $3/month (price: "monthly"); default human unlock remains $5 Export; free generate daily caps (10 real / 60 sample per UTC day; was 15, then 12; 2026-09-26) return 429 with the real reset time; nice-to-have gaps never force fit.recommend: skip; free previews clip model input by o200k tokens (full text still drives coverage / fit / fact lock); a reworded bullet that shortens, renames or drops a tool or proper noun from its source line reverts to the source line; unpaid generate is a truncated teaser; paid unlock returns the full artifact + clean files; honesty proof + coverage (present / implied / missing); referral gift months (max 12); Drive save; remote MCP happy path status → optional store_resume → generate → unlock_link → poll get_generation until paid:true (confirm_payment fallback only). No public promo codes.

Rules for operators

  • Prefer MCP (https://pasteapply.com/mcp) over HTTP twins; prefer either over driving the website.
  • Never invent employers, dates, skills, or metrics.
  • Never complete Stripe Checkout as the user.
  • Never call /api/ad/* as a bot — site ad unlock is humans only; bots stay Stripe paid.
  • Site for humans: https://pasteapply.com