# Gupy Jobs Scraper (`scrapyx/gupy-jobs-scraper`) Actor

Scrapes live job vacancies from Gupy — Brazil's largest recruitment platform, where thousands of companies host their careers pages (~81,500 open roles). Filter by search term, city, state, workplace type or employer; every row carries the full description, company, location and apply URL.

- **URL**: https://apify.com/scrapyx/gupy-jobs-scraper.md
- **Developed by:** [Ibnu Adzim](https://apify.com/scrapyx) (community)
- **Categories:** Jobs, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.84 / 1,000 results

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Gupy Jobs Scraper (Brazil)

Scrapes live vacancies from **[Gupy](https://portal.gupy.io)** — Brazil's
largest recruitment/ATS platform. Thousands of Brazilian companies host their
careers pages on Gupy, and `portal.gupy.io` is the unified board across all of
them: **~81,500 open roles** at the time of writing.

Public data only. No login, no cookies, no browser, **no bot challenge of any
kind** — the underlying API answers 200 on every TLS profile tested, including
a request sent with no headers at all.

### What makes this one unusual

Almost every other target in this portfolio has some flavour of *silent
wrongness* — a bad filter that quietly returns unfiltered results. **Gupy does
the opposite.** Its API rejects what it does not understand:

| You send | Gupy answers |
| --- | --- |
| an unknown parameter name | **HTTP 400** Bad Request |
| `workplaceType=BOGUS` | HTTP 200 with an honest `total: 0` |
| `pageSize` above 100 | **HTTP 400** |
| a page past the 10,000-row window | **HTTP 400** |

So you cannot get quietly-wrong data out of this actor. Bad inputs are
refused before the run starts, with a message saying why.

#### The one thing it *does* get wrong

`pagination.total` is only truthful while the page size is 10 or less. From
11 upward it returns a hardcoded **100**, whatever the real count — verified
across filters and offsets. Since the efficient page size is 100, a naive
client would report `totalMatches: 100` for a search really matching 81,545.

This actor takes the total from one dedicated small probe per query, then
pages at whatever size you asked for. That is the extra request you will see
in `requestsMade`.

### What you get

Two record types share one dataset, told apart by `recordType`.

#### `JOB` — one row per vacancy

Upstream's object passed through verbatim: job title, **full HTML
description**, company name and logo, `companyId`, careers-page URL, direct
apply URL, city/state/country, workplace type (remote / on-site / hybrid),
contract type, published date, application deadline, accessibility badges and
skills.

**There is no "fetch details" toggle, and that is deliberate.** Gupy's
per-job endpoint (`/api/v1/jobs/{id}`) returns exactly the same keys as the
search row — a detail pass would double the request count to re-fetch what is
already in hand.

#### `SEARCH_SUMMARY` — one row per search term

The true match total, how many rows this run pulled, how many requests it
took, the filters actually sent, and whether the query hit the 10,000-row
window.

### Input

| Field | What it does |
| --- | --- |
| **Search terms** | one search each; leave empty to crawl the whole board |
| **Workplace type** | remote / on-site / hybrid |
| **City** | Brazilian city, accents included (`São Paulo`) |
| **State** | `SP` or `São Paulo` — the code is translated for you |
| **Country** | usually `Brasil` |
| **Company ID** | track one employer's hiring over time |
| **Max jobs per search term** | `0` = unlimited, up to the 10,000 window |

#### Example

```json
{
  "searchTerms": ["desenvolvedor", "analista de dados"],
  "workplaceType": "remote",
  "state": "SP",
  "maxItems": 500,
  "pageSize": 100
}
```

### Notes on reliability

- **No WAF.** `chrome124`, `chrome120`, `chrome131`, `safari17_0`,
  `safari18_0`, `chrome99_android`, `edge101` and `firefox133` all return
  200, as does a bare request with no headers.
- **`robots.txt` is `Disallow:`** — an empty disallow, i.e. allow everything.
- **HTTP 400 is treated as fatal, not retried.** It means "your query is
  wrong", so retrying it four times just reaches the same answer slower.
- **Residential proxy pinned to Brazil** is the cloud default.
- Pagination stops *before* crossing the result window, so the run ends with
  a clean summary rather than an error row.

### Known limits

- **10,000 rows per query**, whatever the total says — an Elasticsearch-style
  result window. Narrow by city, state, workplace type or a more specific
  search term to reach the rest.
- `state` matches only the full accented Brazilian name upstream; the actor
  translates two-letter codes, but an unrecognised state string returns an
  honest zero rather than an error.
- Descriptions are raw HTML as the employer wrote them, not cleaned text.

# Actor input Schema

## `searchTerms` (type: `array`):

One search per entry, each with its own SEARCH\_SUMMARY row — e.g. `desenvolvedor`, `enfermeiro`, `analista de dados`. Matches against the job title.

Leave empty to crawl the whole board (~81,500 live vacancies) under whatever location/workplace filters you set.

Because the API pages through at most 10,000 rows per query, several narrow terms return far more data than one broad one.

## `workplaceType` (type: `string`):

Remote, on-site or hybrid. An unrecognised value returns a clean zero-result search rather than an error, so anything outside this list is refused before the run starts.

## `city` (type: `string`):

Brazilian city name, accents included — e.g. `São Paulo`, `Belo Horizonte`, `Curitiba`.

## `state` (type: `string`):

Brazilian state. Accepts either the two-letter code (`SP`, `RJ`, `MG`) or the full accented name (`São Paulo`) — the code is translated for you, because Gupy itself only matches the full name and would answer a bare `SP` with zero results.

## `country` (type: `string`):

Usually `Brasil`. Gupy is Brazil-first but a small share of listings are elsewhere, so this is left free-text rather than forced.

## `companyId` (type: `integer`):

Restrict to a single employer, using the `companyId` value that appears on every job row. Handy for tracking one company's hiring over time.

## `maxItems` (type: `integer`):

Stop after this many jobs per term. Set to 0 for unlimited — note the API itself pages through at most 10,000 rows per query however many matches it reports. Narrow by city, state or a more specific term to reach the rest.

## `pageSize` (type: `integer`):

How many jobs to pull per API call. 100 is the maximum Gupy accepts (it returns HTTP 400 above that) and means fewer requests for the same data.

## `maxConcurrency` (type: `integer`):

Upper bound on requests in flight at once, across all search terms.

## `proxyConfiguration` (type: `object`):

Gupy's API is fully open — no auth, no token, no bot challenge on any TLS profile tested. Residential pinned to Brazil is still the cloud default, since container egress is a different posture than a home connection and this is a Brazil-only job board.

## Actor input object example

```json
{
  "searchTerms": [
    "desenvolvedor"
  ],
  "workplaceType": "",
  "city": "",
  "state": "",
  "country": "",
  "companyId": 0,
  "maxItems": 100,
  "pageSize": 100,
  "maxConcurrency": 4,
  "proxyConfiguration": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "BR"
  }
}
```

# Actor output Schema

## `items` (type: `string`):

One row per scraped record. See the dataset's default view for field definitions.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "searchTerms": [
        "desenvolvedor"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("scrapyx/gupy-jobs-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "searchTerms": ["desenvolvedor"] }

# Run the Actor and wait for it to finish
run = client.actor("scrapyx/gupy-jobs-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "searchTerms": [
    "desenvolvedor"
  ]
}' |
apify call scrapyx/gupy-jobs-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,scrapyx/gupy-jobs-scraper"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/KEoJIVMznZM9nsZlm/builds/FAFA9CswdYfPa7WfF/openapi.json
