{
  "openapi": "3.0.1",
  "info": {
    "title": "Upwork Jobs Scraper | 30+ Filters & Duplicate-Free Runs",
    "description": "Scrape Upwork jobs with title, FULL description, budget, skills and project length. 30+ filters: budget floors across hourly AND fixed, keyword include/exclude, client country, experience level, freshness window. Monitoring mode skips jobs earlier runs returned, so you never pay twice.",
    "version": "0.1",
    "x-build-id": "qMSQ7Yuohegjc7wyU"
  },
  "servers": [
    {
      "url": "https://api.apify.com/v2"
    }
  ],
  "paths": {
    "/acts/tqm~upwork-jobs-scraper/run-sync-get-dataset-items": {
      "post": {
        "operationId": "run-sync-get-dataset-items-tqm-upwork-jobs-scraper",
        "x-openai-isConsequential": false,
        "summary": "Executes an Actor, waits for its completion, and returns Actor's dataset items in response.",
        "tags": [
          "Run Actor"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/inputSchema"
              }
            }
          }
        },
        "parameters": [
          {
            "name": "token",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Enter your Apify token here"
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    },
    "/acts/tqm~upwork-jobs-scraper/runs": {
      "post": {
        "operationId": "runs-sync-tqm-upwork-jobs-scraper",
        "x-openai-isConsequential": false,
        "summary": "Executes an Actor and returns information about the initiated run in response.",
        "tags": [
          "Run Actor"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/inputSchema"
              }
            }
          }
        },
        "parameters": [
          {
            "name": "token",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Enter your Apify token here"
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/runsResponseSchema"
                }
              }
            }
          }
        }
      }
    },
    "/acts/tqm~upwork-jobs-scraper/run-sync": {
      "post": {
        "operationId": "run-sync-tqm-upwork-jobs-scraper",
        "x-openai-isConsequential": false,
        "summary": "Executes an Actor, waits for completion, and returns the OUTPUT from Key-value store in response.",
        "tags": [
          "Run Actor"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/inputSchema"
              }
            }
          }
        },
        "parameters": [
          {
            "name": "token",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Enter your Apify token here"
          }
        ],
        "responses": {
          "200": {
            "description": "OK"
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "inputSchema": {
        "type": "object",
        "properties": {
          "searchQueries": {
            "title": "Search queries",
            "type": "array",
            "description": "What to search for on Upwork. One run can cover several queries; results are deduplicated across all of them.",
            "items": {
              "type": "string"
            }
          },
          "startUrls": {
            "title": "Start URLs",
            "type": "array",
            "description": "Upwork job-search URLs to scrape as-is. Use this to paste a URL you built in the Upwork UI with filters this actor does not expose. Combined with the search queries above.",
            "items": {
              "type": "object",
              "required": [
                "url"
              ],
              "properties": {
                "url": {
                  "type": "string",
                  "title": "URL of a web page",
                  "format": "uri"
                }
              }
            }
          },
          "maxItems": {
            "title": "Max results",
            "minimum": 1,
            "type": "integer",
            "description": "Hard cap on how many job postings the run returns in total, across every query. The run stops as soon as it is reached.",
            "default": 100
          },
          "maxPagesPerQuery": {
            "title": "Max pages per query",
            "minimum": 1,
            "maximum": 50,
            "type": "integer",
            "description": "How deep to paginate each search. Upwork shows roughly 10 jobs per page.",
            "default": 3
          },
          "jsonSearch": {
            "title": "Fast JSON search (recommended)",
            "type": "boolean",
            "description": "Fetch search results through Upwork's own JSON endpoint instead of rendering search pages in a browser. Dramatically faster and cheaper - measured at 50 results in 0.5s against ~5 minutes of page rendering - and it returns the FULL job description and complete skill list, which the browser path only gets by opening each job page. Falls back to the browser automatically if the JSON endpoint is unavailable. Client data (spend, hires, payment-verified) still requires 'Open each job page for full details' either way. Leave this on unless you are debugging.",
            "default": true
          },
          "requireJsonSearch": {
            "title": "Fail if the fast JSON search is unavailable",
            "type": "boolean",
            "description": "Stops the run instead of falling back to the browser when Upwork will not issue a visitor token. The fallback still returns a full result set, so by default it is used - but it is roughly 400x more traffic per row, and a run that quietly takes it looks identical to one that did not. Turn this on if you chose this Actor for its cost profile and would rather fail than be silently upgraded to the expensive path. Check jsonFallbacks in RUN_STATS either way.",
            "default": false
          },
          "sortBy": {
            "title": "Sort by",
            "enum": [
              "recency",
              "relevance",
              "clientSpend"
            ],
            "type": "string",
            "description": "Upwork's own result ordering. 'Most recent' is the right choice for monitoring runs.",
            "default": "recency"
          },
          "jobType": {
            "title": "Job type",
            "type": "array",
            "description": "Applied by Upwork before results are returned, so it also makes the run cheaper.",
            "items": {
              "type": "string",
              "enum": [
                "hourly",
                "fixed"
              ],
              "enumTitles": [
                "Hourly",
                "Fixed price"
              ]
            }
          },
          "experienceLevel": {
            "title": "Experience level",
            "type": "array",
            "description": "Contractor tier the client is asking for.",
            "items": {
              "type": "string",
              "enum": [
                "entry",
                "intermediate",
                "expert"
              ],
              "enumTitles": [
                "Entry level",
                "Intermediate",
                "Expert"
              ]
            }
          },
          "projectLength": {
            "title": "Project length",
            "type": "array",
            "description": "Expected engagement duration as stated by the client. IMPORTANT: Upwork applies this to HOURLY jobs only - fixed-price results come back unfiltered (verified 2026-08-14: the same search with 'less than 1 month' and 'more than 6 months' returned identical fixed-price jobs). It cannot be applied afterwards either, because fixed-price listings carry no duration at all. Set Job type to Hourly if you need this enforced.",
            "items": {
              "type": "string",
              "enum": [
                "lessThanOneMonth",
                "oneToThreeMonths",
                "threeToSixMonths",
                "moreThanSixMonths"
              ],
              "enumTitles": [
                "Less than 1 month",
                "1 to 3 months",
                "3 to 6 months",
                "More than 6 months"
              ]
            }
          },
          "hourlyRateMin": {
            "title": "Min hourly rate (USD)",
            "minimum": 0,
            "type": "integer",
            "description": "Lower bound of the hourly rate window. Upwork matches jobs whose advertised range OVERLAPS your window rather than jobs contained inside it, so a $25-100/hr job matches a minimum of 60. That is Upwork's own search behaviour, verified 2026-08-14, and it is passed through unchanged."
          },
          "hourlyRateMax": {
            "title": "Max hourly rate (USD)",
            "minimum": 0,
            "type": "integer",
            "description": "Upper bound of the hourly rate window. As with the minimum, Upwork matches OVERLAPPING ranges: a $15-40/hr job matches a maximum of 20, because its lower end falls inside the window."
          },
          "fixedBudgetMin": {
            "title": "Min fixed budget (USD)",
            "minimum": 0,
            "type": "integer",
            "description": "Only fixed-price jobs at or above this budget. Verified exact 2026-08-14, unlike the hourly rate bounds: a 1000-5000 window returned $1000, $1750 and $2500 jobs and excluded a $10 one."
          },
          "fixedBudgetMax": {
            "title": "Max fixed budget (USD)",
            "minimum": 0,
            "type": "integer",
            "description": "Only fixed-price jobs at or below this budget. Verified exact 2026-08-14 - an upper bound of 200 returned $10, $30, $50 and $100 jobs."
          },
          "includeKeywords": {
            "title": "Must contain any of these keywords",
            "type": "array",
            "description": "A job is kept only if its title, description, skills or category contains at least one of these. Matched on word boundaries, so 'ops' does not match 'operations'. Leave empty to keep everything.",
            "items": {
              "type": "string"
            }
          },
          "excludeKeywords": {
            "title": "Must not contain any of these keywords",
            "type": "array",
            "description": "A job is dropped if any of these appears in its text. Useful for stripping out the adjacent niches a broad query drags in.",
            "items": {
              "type": "string"
            }
          },
          "requireAllIncludeKeywords": {
            "title": "Require ALL include keywords",
            "type": "boolean",
            "description": "Switch the include list from 'any of' to 'all of'.",
            "default": false
          },
          "minBudgetMonthlyUsd": {
            "title": "Min budget (USD per month)",
            "minimum": 0,
            "type": "integer",
            "description": "Compares hourly and fixed-price jobs on one axis: an hourly rate is projected to a month using the billable-hours setting below, a fixed budget is taken as one month's spend."
          },
          "keepUnknownBudget": {
            "title": "Keep jobs with no stated budget",
            "type": "boolean",
            "description": "Upwork hides the budget on a fair share of postings. Turn this off to drop them rather than let them through the minimum-budget filter.",
            "default": true
          },
          "clientCountries": {
            "title": "Client countries",
            "type": "array",
            "description": "Keep only clients located in these countries. Applied by Upwork's own search before results are fetched, so it genuinely narrows the run and costs less - unlike every other client filter, which needs the detail page. Full country NAMES only - \"US\" returns nothing, \"United States\" works. An unrecognised name returns zero results rather than being ignored, so a typo fails loudly.",
            "items": {
              "type": "string"
            }
          },
          "ongoingOnly": {
            "title": "Ongoing / retainer work only",
            "type": "boolean",
            "description": "Keep only postings whose text reads like recurring work - 'ongoing', 'long-term', 'retainer', 'monthly', 'maintenance' and similar.",
            "default": false
          },
          "ongoingKeywords": {
            "title": "Custom ongoing-work keywords",
            "type": "array",
            "description": "Override the built-in vocabulary used by the filter above.",
            "items": {
              "type": "string"
            }
          },
          "maxProposals": {
            "title": "Max proposals so far",
            "minimum": 0,
            "type": "integer",
            "description": "Drop jobs that already have more than this many proposals. Low-competition postings are worth far more to anyone actually bidding."
          },
          "postedWithinHours": {
            "title": "Only jobs posted in the last N hours",
            "minimum": 1,
            "type": "integer",
            "description": "Drop postings older than this many hours. Leave empty for no limit. Pair with a scheduled run to get only what appeared since you last looked - sortBy: recency orders results but does not bound them. Postings whose age cannot be determined are KEPT, never dropped."
          },
          "deduplicateAgainstPreviousRuns": {
            "title": "Skip jobs already returned by earlier runs (monitoring mode)",
            "type": "boolean",
            "description": "Remember which jobs previous runs returned and skip them, so a scheduled run only ever charges you for postings you have not seen. Off by default - a one-off scrape should return everything it finds. The ledger is scoped to this search (queries + Upwork-side filters); changing maxItems or a post-fetch filter does NOT reset it. Zero results in this mode means 'nothing new', which is a valid outcome, and the run says so.",
            "default": false
          },
          "seenExpiryDays": {
            "title": "Forget seen jobs after N days",
            "minimum": 1,
            "maximum": 365,
            "type": "integer",
            "description": "How long a job stays in the monitoring ledger. Upwork postings go stale fast, so 30 days is plenty; a longer window just makes the ledger bigger. Only used when monitoring mode is on.",
            "default": 30
          },
          "seenStateKey": {
            "title": "Monitoring ledger key (advanced)",
            "type": "string",
            "description": "Override the automatic ledger id. Set the same value on two schedules to make them share one 'already seen' list, or different values to keep them separate. Leave empty unless you know you need it."
          },
          "proxyConfiguration": {
            "title": "Proxy configuration",
            "type": "object",
            "description": "Upwork sits behind Cloudflare and blocks datacenter traffic hard. Residential proxies are strongly recommended - without them most runs return nothing.",
            "default": {
              "useApifyProxy": true,
              "apifyProxyGroups": [
                "RESIDENTIAL"
              ]
            }
          },
          "hoursPerMonth": {
            "title": "Billable hours per month",
            "minimum": 1,
            "maximum": 744,
            "type": "integer",
            "description": "Used to project an hourly rate onto the monthly figure the budget filter and the budgetMonthlyUsd field use.",
            "default": 160
          },
          "minDelaySeconds": {
            "title": "Min delay between requests",
            "minimum": 0,
            "maximum": 60,
            "type": "integer",
            "description": "Politeness delay floor. Raising it lowers the block rate at the cost of run time.",
            "default": 1
          },
          "maxDelaySeconds": {
            "title": "Max delay between requests",
            "minimum": 0,
            "maximum": 120,
            "type": "integer",
            "description": "Politeness delay ceiling. Each wait is randomized between the floor and this.",
            "default": 3
          },
          "maxRetriesPerPage": {
            "title": "Retries per page",
            "minimum": 0,
            "maximum": 5,
            "type": "integer",
            "description": "How many times to retry a page that came back blocked, each on a fresh proxy session.",
            "default": 2
          },
          "blockImagesAndStyles": {
            "title": "Block images, fonts and stylesheets",
            "type": "boolean",
            "description": "Skip downloading images, fonts, media and CSS. None of it is parsed, and it is a large share of Upwork's page weight, so this makes runs meaningfully faster and cheaper. Turn it off only if a page stops rendering correctly.",
            "default": true
          },
          "failOnZeroResults": {
            "title": "Fail the run when it returns no jobs",
            "type": "boolean",
            "description": "Marks the run FAILED, with a status message naming the cause, when it finishes with an empty dataset. A run that returns nothing and still reports SUCCEEDED is indistinguishable from a broken actor, so this is on by default. It does NOT apply to monitoring mode: when deduplicateAgainstPreviousRuns is on and every posting was already returned by an earlier run, that is a valid empty result and the run still succeeds. Turn this off if you want an empty result to succeed in every case. It does not change what is scraped, and it does not change what you are charged: result charges are per result, so a run that returns nothing incurs none. Every run also carries a nominal $0.00001 Actor-start charge regardless of its outcome.",
            "default": true
          }
        }
      },
      "runsResponseSchema": {
        "type": "object",
        "properties": {
          "data": {
            "type": "object",
            "properties": {
              "id": {
                "type": "string"
              },
              "actId": {
                "type": "string"
              },
              "userId": {
                "type": "string"
              },
              "startedAt": {
                "type": "string",
                "format": "date-time",
                "example": "2025-01-08T00:00:00.000Z"
              },
              "finishedAt": {
                "type": "string",
                "format": "date-time",
                "example": "2025-01-08T00:00:00.000Z"
              },
              "status": {
                "type": "string",
                "example": "READY"
              },
              "meta": {
                "type": "object",
                "properties": {
                  "origin": {
                    "type": "string",
                    "example": "API"
                  },
                  "userAgent": {
                    "type": "string"
                  }
                }
              },
              "stats": {
                "type": "object",
                "properties": {
                  "inputBodyLen": {
                    "type": "integer",
                    "example": 2000
                  },
                  "rebootCount": {
                    "type": "integer",
                    "example": 0
                  },
                  "restartCount": {
                    "type": "integer",
                    "example": 0
                  },
                  "resurrectCount": {
                    "type": "integer",
                    "example": 0
                  },
                  "computeUnits": {
                    "type": "integer",
                    "example": 0
                  }
                }
              },
              "options": {
                "type": "object",
                "properties": {
                  "build": {
                    "type": "string",
                    "example": "latest"
                  },
                  "timeoutSecs": {
                    "type": "integer",
                    "example": 300
                  },
                  "memoryMbytes": {
                    "type": "integer",
                    "example": 1024
                  },
                  "diskMbytes": {
                    "type": "integer",
                    "example": 2048
                  }
                }
              },
              "buildId": {
                "type": "string"
              },
              "defaultKeyValueStoreId": {
                "type": "string"
              },
              "defaultDatasetId": {
                "type": "string"
              },
              "defaultRequestQueueId": {
                "type": "string"
              },
              "buildNumber": {
                "type": "string",
                "example": "1.0.0"
              },
              "containerUrl": {
                "type": "string"
              },
              "usage": {
                "type": "object",
                "properties": {
                  "ACTOR_COMPUTE_UNITS": {
                    "type": "integer",
                    "example": 0
                  },
                  "DATASET_READS": {
                    "type": "integer",
                    "example": 0
                  },
                  "DATASET_WRITES": {
                    "type": "integer",
                    "example": 0
                  },
                  "KEY_VALUE_STORE_READS": {
                    "type": "integer",
                    "example": 0
                  },
                  "KEY_VALUE_STORE_WRITES": {
                    "type": "integer",
                    "example": 1
                  },
                  "KEY_VALUE_STORE_LISTS": {
                    "type": "integer",
                    "example": 0
                  },
                  "REQUEST_QUEUE_READS": {
                    "type": "integer",
                    "example": 0
                  },
                  "REQUEST_QUEUE_WRITES": {
                    "type": "integer",
                    "example": 0
                  },
                  "DATA_TRANSFER_INTERNAL_GBYTES": {
                    "type": "integer",
                    "example": 0
                  },
                  "DATA_TRANSFER_EXTERNAL_GBYTES": {
                    "type": "integer",
                    "example": 0
                  },
                  "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                    "type": "integer",
                    "example": 0
                  },
                  "PROXY_SERPS": {
                    "type": "integer",
                    "example": 0
                  }
                }
              },
              "usageTotalUsd": {
                "type": "number",
                "example": 0.00005
              },
              "usageUsd": {
                "type": "object",
                "properties": {
                  "ACTOR_COMPUTE_UNITS": {
                    "type": "integer",
                    "example": 0
                  },
                  "DATASET_READS": {
                    "type": "integer",
                    "example": 0
                  },
                  "DATASET_WRITES": {
                    "type": "integer",
                    "example": 0
                  },
                  "KEY_VALUE_STORE_READS": {
                    "type": "integer",
                    "example": 0
                  },
                  "KEY_VALUE_STORE_WRITES": {
                    "type": "number",
                    "example": 0.00005
                  },
                  "KEY_VALUE_STORE_LISTS": {
                    "type": "integer",
                    "example": 0
                  },
                  "REQUEST_QUEUE_READS": {
                    "type": "integer",
                    "example": 0
                  },
                  "REQUEST_QUEUE_WRITES": {
                    "type": "integer",
                    "example": 0
                  },
                  "DATA_TRANSFER_INTERNAL_GBYTES": {
                    "type": "integer",
                    "example": 0
                  },
                  "DATA_TRANSFER_EXTERNAL_GBYTES": {
                    "type": "integer",
                    "example": 0
                  },
                  "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                    "type": "integer",
                    "example": 0
                  },
                  "PROXY_SERPS": {
                    "type": "integer",
                    "example": 0
                  }
                }
              }
            }
          }
        }
      }
    }
  }
}