Query recent Upwork job listings with AI quality scores built in. Every result includes scores — no extra calls required.
Pass your API key as a Bearer token on every request. Generate keys in Settings → API Keys.
Authorization: Bearer vw_your_api_key_here
Responses are always { data: [...], count: N } on success, or { error: '...' } with an appropriate HTTP status on failure. Free accounts also include quotaRemaining, freshnessDelayHours, newerJobsMatched and note in each response (see freshness).count is the number of jobs in this response, not the total number of jobs matching the filters.
https://kttkatrmvlzsepgprqqd.supabase.co/functions/v1/public-jobsReturns recent Upwork job listings matching your filters. Jobs are continuously ingested and scored as they are posted, and are usually available within 1–2 minutes of going live on Upwork. Paid keys see them at that point; free keys only see jobs older than 24 hours (see freshness).
Example request
curl https://kttkatrmvlzsepgprqqd.supabase.co/functions/v1/public-jobs \ -H "Authorization: Bearer vw_your_key" \ -G \ -d keywords=react \ -d jobType=fixed \ -d minQuickWin=7 \ -d sort=quick_win \ -d limit=20
Example response (free account)
{
"data": [
{
"id": "vw_b797267068b246bfa8da8722",
"title": "Build a Next.js dashboard with Supabase backend",
"category": "Web Development",
"jobType": "fixed",
"budget": 800,
"budgetMax": null,
"experienceLevel": "Intermediate",
"duration": "Less than 1 month",
"connects": 11,
"hoursPerWeek": null,
"skills": ["Next.js", "TypeScript", "Supabase", "Tailwind CSS"],
"clientLocation": "United States",
"clientPaymentVerified": true,
"clientTotalSpent": 42000,
"clientHireRate": 78,
"clientRating": 4.9,
"clientAvgRate": null,
"description": "Build a clean admin dashboard...",
"upworkUrl": "https://www.upwork.com/jobs/~01abc...",
"scores": {
"quickWin": 8,
"scopeClarity": 9,
"redFlags": 10,
"effortHours": 14
},
"postedAt": "2026-06-08T09:31:00+00:00",
"receivedAt": "2026-06-08T09:32:14.000+00:00"
}
],
"count": 1,
"quotaRemaining": 99,
"freshnessDelayHours": 24,
"newerJobsMatched": 37,
"note": "Fresh jobs (<24h old) are limited to paid users only. 37 newer jobs matched your filters ..."
}keywordsstringFilter by job title keyword (e.g. react, shopify). Case-insensitive partial match.categorystringFilter by the exact category string returned on a job, for example Web Development, AI & Machine Learning, or QA & Testing. Categories originate upstream and may expand over time.jobTypefixed | hourlyFilter by contract type.experienceLevelEntry Level | Intermediate | ExpertFilter by the client's requested experience level.minBudgetnumberMinimum budget in USD. Uses the project budget for fixed-price jobs and the minimum hourly rate for hourly jobs. Hourly jobs that do not publish a rate have no budget value at all, so they are excluded whenever this is set.minQuickWininteger 0–10Only return jobs at or above this Quick Win score. Set to 7 to filter for high-quality quick wins only.sortnewest | quick_win | budgetSort order. Default: newest. budget sorts highest first, but jobs with no published budget sort ahead of the rest, so pair it with minBudget to skip those.limitinteger 1–100Maximum number of results to return. Default: 50. Free accounts are charged only for jobs actually returned.Every job returned by this endpoint has been scored by an LLM. The scores object is included on every result. Individual fields may be null when the source data or estimate is unavailable.
scores.quickWininteger 0–10Fixed price + clear bounded scope + completable in one session. Higher = faster money with less negotiation risk.scores.scopeClarityinteger 0–10How precisely the deliverable is defined. Low scores indicate vague requirements and scope creep risk.scores.redFlagsinteger 0–10Absence of bad signals. 10 = clean client with no red flags. Low scores mean proceed with caution.scores.effortHoursnumber | nullRealistic estimated hours to complete the job.Jobs are ingested and scored continuously and are usually queryable within 1–2 minutes of going live on Upwork. How much of that speed your key gets depends on the plan.
Paid keys: every job as soon as it is scored, with no delay.
Free keys: only jobs more than 24 hours old. Anything newer is withheld regardless of your filters.
Free responses carry three extra fields so the delay is visible rather than silent: freshnessDelayHours (currently 24), newerJobsMatched, the number of jobs that matched your filters inside the delay window and were held back, and a plain-language note. An empty data array alongside a non-zero newerJobsMatched means your filters are fine and the matches are simply too recent for a free key.
Anything time-sensitive, bidding early or alerting, needs a paid key. Polling more often on a free key cannot get inside the 24-hour window; a webhook is the fastest path once you are on a paid plan.
| Free | Paid | |
|---|---|---|
| Job freshness | Older than 24h only | Real time |
| Results per day | 100 | Unlimited |
| Max per request | 100 | 100 |
| Per-minute limit | None | None |
| Quota reset | Midnight UTC | — |
| API keys | Multiple | Multiple |
The daily quota is per account, not per key — creating additional API keys does not increase your limit. There is no application-level per-minute throttle, but clients should avoid unnecessary high-frequency polling.
The live API uses an approximately seven-day rolling job window. It currently returns only the first matching page (up to 100 jobs); date cursors, pagination, and access to the cold archive are not available through this endpoint.
Rather than polling the API, webhooks push a job payload to your endpoint when a matching job is ingested — usually within 1–2 minutes of it posting on Upwork. Available on paid plans. Webhooks are configured per filter: open a filter's alert settings in the dashboard, enable the Webhook channel, and paste your endpoint URL. Use Preview alert to send a sample payload immediately.
Payload shape
{
"event": "job.matched",
"matchedAt": "2026-06-08T09:32:14.000Z",
"filterName": "React fixed-price",
"match": {
"scoreQuickWin": 8,
"scoreScopeClarity": 9,
"scoreRedFlags": 10,
"effortEstimateHours": 14,
"reasoning": "Clear bounded deliverable..."
},
"job": {
"id": "vw_b797267068b246bfa8da8722",
"title": "Build a Next.js dashboard",
"description": "Build a clean admin dashboard...",
"url": "https://www.upwork.com/jobs/~01abc...",
"type": "fixed",
"contractType": null,
"budget": { "min": 800, "max": null, "currency": "USD", "display": "$800" },
"duration": "Less than 1 month",
"hoursPerWeek": null,
"experienceLevel": "Intermediate",
"connectsRequired": 11,
"skills": ["Next.js", "TypeScript"],
"categories": ["Web Development"],
"postedAt": "2026-06-08T09:31:00+00:00",
"questions": ["Share a similar dashboard you've built", "What's your availability this week?"]
},
"client": {
"location": "United States",
"locationRestriction": null,
"paymentVerified": true,
"totalSpent": 42000,
"jobsPosted": 31,
"hires": 24,
"hireRate": 78,
"rating": 4.9,
"reviewCount": 19,
"avgHourlyRate": null,
"rankScore": 5,
"rankLabel": "Excellent"
}
}job.questions and client.locationRestriction are frequently empty — not every job carries screening questions or a hiring-location restriction, and coverage varies by source. Treat both as "present when available," not guaranteed.
Requests carry the X-Vibeworker-Event: job.matched header. Preview deliveries include "preview": true in the payload. Delivery is currently a single best-effort POST with no automatic retries or signature. Your endpoint should respond quickly, be idempotent, and treat the URL as a secret. Anyone who knows it can send requests to it, so ignore payloads that don't match this shape.