Upwork Leads
API

Three on-ramps. Same data.

Ounie accounts hit REST endpoints with a bearer token. AI assistants connect via MCP and get a native tool surface. One-off scripts can skip the account and pay per call via x402 in USDC. All three return the same normalized Upwork job rows.

POST/api/scrape
auth: Bearer
Start a search job
Returns a job_id immediately. The actual run completes async (1–3 minutes); poll /api/jobs/{id} or wait on the webhook.
Body
{
  "mode": "query",                    // query | url
  "query": "react native developer",  // query mode
  "raw_url": "https://www.upwork.com/nx/search/jobs/?q=...",  // url mode
  "max_jobs": 200,
  "job_type": ["hourly"],             // fixed | hourly
  "experience_level": ["intermediate", "expert"],
  "client_history": ["1to9Hires", "10+Hires"],
  "payment_verified": true,
  "hourly_min": 40, "hourly_max": 90,
  "fixed_min": 500, "fixed_max": 5000,
  "max_job_age_hours": 24,            // fresh-first
  "sort": "newest",                   // newest | relevance
  "webhook_url": "https://your-host.com/hook"   // optional
}
Response
{ "ok": true, "job_id": "job_01HX...", "credits_reserved": 200 }
GET/api/jobs/{id}
auth: Bearer
Check job status
Returns the job row plus a 10-minute signed download URL once succeeded.
Response
{
  "job": { "status": "succeeded", "results_count": 187, "mode": "query", "query": "react native developer", ... },
  "preview_url": "https://...presigned..."
}
GET/api/jobs/{id}/export?format=csv|json
auth: Bearer
Download the dataset
Streams the file directly. CSV is the default; JSON returns the full normalized shape with a nested client object.
POST/api/x402/scrape
auth: none · x402
Pay-per-call (agents)
No account, no API key. Returns 402 with payment terms; sign and retry with X-Payment header. Synchronous; capped at 200 jobs per call.
Body
{
  "mode": "query",
  "query": "shopify developer",
  "max_jobs": 50
}
Response
// 402 Payment Required (initial)
{ "x402Version": 1, "accepts": [{ "scheme":"exact", "network":"eip155:8453", "maxAmountRequired": "600000", ... }] }

// 200 OK (after settled X-Payment header)
{ "ok": true, "count": 47, "jobs": [...], "payment": { "tx_hash": "0x..." } }
HTTP/api/mcp
MCP · Streamable HTTP + SSE
Model Context Protocol server
Drop our tool surface into any MCP host (Cursor, Claude desktop, ChatGPT GPTs, the Ounie AI Team, the AI SDK). Seven tools: scrape_upwork_jobs, get_job_status, export_job_results, list_jobs, get_credit_balance, get_pricing, whoami. Auth: Bearer API key minted in the dashboard — or append ?api_key=upw_live_… to the URL if your MCP client can't set headers. Calls draw your Ounie credits; when the balance runs short the run is refused with a top-up link, never overdrawn.
Body
// .cursor/mcp.json (Cursor / any Streamable HTTP host)
{
  "mcpServers": {
    "upwork-leads": {
      "url": "https://upwork.ounie.com/api/mcp",
      "headers": { "Authorization": "Bearer upw_live_..." }
    }
  }
}
Response
// Tool call: scrape_upwork_jobs
{
  "mode": "query",
  "query": "webflow designer",
  "max_jobs": 250,
  "payment_verified": true
}
→ { "ok": true, "job_id": "...", "credits_reserved": 250, "poll_with": { ... } }
// One request. Async job.
const res = await fetch("https://upwork.ounie.com/api/scrape", {
  method: "POST",
  headers: {
    "Authorization": "Bearer ${API_KEY}",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    mode: "query",
    query: "react native developer",
    max_jobs: 200,
    job_type: ["hourly"],
    experience_level: ["intermediate", "expert"],
    payment_verified: true,
    max_job_age_hours: 24,
  }),
});

const { job_id } = await res.json();
// → poll /api/jobs/${job_id} or wait on the webhook
Schema
Every job row.
{
  "id": "01HX92...",
  "url": "https://www.upwork.com/jobs/~01...",
  "title": "Senior React Native engineer",
  "description": "We're building a fintech app...",
  "budget": "$50-75/hr",
  "jobType": "Hourly",
  "experienceLevel": "Expert",
  "postedAt": "2026-07-08T14:12:00Z",
  "postedRelative": "Posted 3 minutes ago",
  "proposals": 4,
  "tags": ["React Native", "TypeScript", "iOS"],
  "allowedCountries": [],
  "screeningQuestions": ["Share a recent app you shipped"],
  "client": {
    "name": "Jordan",
    "location": "United States",
    "paymentVerified": true,
    "rating": 4.9,
    "hireRatePercent": 82,
    "totalSpent": 142000,
    "avgHourlyRate": 58,
    "hasHired": false
  }
}
Availability
What happens when the search engine is down.

A scheduled probe checks the search engine. While it is unavailable we refuse up front and charge nothing: no quote is issued, no credits are reserved, and no job is created.

You get 503 with {"error":"upstream_unavailable"} and a Retry-After header. On the x402 rail this deliberately carries no payment offer, so an agent never signs an authorization for work that cannot complete. Service resumes on its own.

Rate limits
  • · REST: 60 jobs / min / key
  • · x402: 5 req / sec / payer address
  • · Max jobs per run: 1,000 (REST) · 200 (x402)