> ## Documentation Index
> Fetch the complete documentation index at: https://brezelscraper.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Results and exports

> Fetch paginated results, download CSV output, and inspect job costs.

## `GET /api/v1/jobs/{id}/results`

Fetch the rich result payload for a single job.

### Query parameters

| Parameter      | Type    | Default | Notes                                                                                                                                                                |
| -------------- | ------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `page`         | integer | `1`     | Must be positive                                                                                                                                                     |
| `limit`        | integer | `50`    | Max `100`                                                                                                                                                            |
| `web_presence` | string  | empty   | Filter results by web presence. Comma-separated list of tiers and/or group aliases (empty = no filter). See [Filtering by web presence](#filtering-by-web-presence). |

### Example response

```json theme={null}
{
  "results": [
    {
      "id": 1,
      "user_id": "user_123",
      "job_id": "d8d8a24e-cef2-4b02-8396-abc290b6f299",
      "title": "Cafe Example",
      "category": "Cafe",
      "address": "Example Street 1, Berlin",
      "website": "https://example.com",
      "web_presence": "has",
      "phone": "+49 30 123456",
      "review_count": 128,
      "rating": 4.6,
      "open_hours": {
        "monday": ["08:00-18:00"]
      },
      "images": [],
      "user_reviews": [],
      "emails": "hello@example.com",
      "created_at": "2026-04-10T09:05:00Z"
    }
  ],
  "total": 1,
  "page": 1,
  "limit": 50,
  "has_more": false,
  "web_presence_counts": {
    "none": 12,
    "social": 23,
    "directory": 4,
    "builder": 6,
    "booking": 3,
    "has": 39
  }
}
```

Useful fields include:

* `title`, `category`, `address`, `website`, `phone`
* `web_presence` — the business's web-presence tier (see [Filtering by web presence](#filtering-by-web-presence))
* `review_count`, `rating`, `reviews_link`
* `open_hours`, `popular_times`, `reviews_per_rating`
* `images`, `reservations`, `order_online`, `menu`, `owner`
* `user_reviews`, `user_reviews_extended`
* `emails`

<Note>
  The example above is abbreviated. The full response includes additional fields such as `latitude`, `longitude`, `description`, `timezone`, `price_range`, `popular_times`, `reviews_per_rating`, `complete_address`, `about`, `reservations`, `order_online`, `menu`, `owner`, `user_reviews`, and `images` when available. Optional fields are omitted from the response when empty.
</Note>

### Filtering by web presence

Every result is classified into a **web-presence tier** from its scraped `website`. Pass `web_presence` to return only the tiers you want. The top-level `web_presence_counts` always reports the **unfiltered** breakdown, so you can show totals like "42 of 200 have no website" regardless of the active filter.

**Tiers** — the value of the `web_presence` field on each result:

| Tier        | Meaning                                                                      |
| ----------- | ---------------------------------------------------------------------------- |
| `none`      | No website listed at all                                                     |
| `social`    | Only a social network or link-in-bio page (Facebook, Instagram, Linktree, …) |
| `directory` | Only a directory/aggregator listing (Yelp, Yellow Pages, …)                  |
| `builder`   | Only a free website-builder page (`*.wixsite.com`, `*.business.site`, …)     |
| `booking`   | Only a booking/ordering-platform page (OpenTable, Booksy, DoorDash, …)       |
| `has`       | A real, owned website                                                        |

**Group aliases.** `web_presence` also accepts the same coarse groups as the scrape-time [`website_filter`](/docs/api-reference/jobs#website-filter), so one vocabulary works end to end:

| Alias         | Expands to                                          |
| ------------- | --------------------------------------------------- |
| `all`         | No filter (every tier)                              |
| `no_website`  | `none`, `social`, `directory`, `builder`, `booking` |
| `has_website` | `has`                                               |

Pass tiers, aliases, or a mix — values are de-duplicated and matching is case-insensitive. An unrecognized value returns `400 Bad Request`.

```bash theme={null}
# Businesses with no real website (group alias)
curl "https://api.brezelscraper.com/api/v1/jobs/YOUR_JOB_ID/results?web_presence=no_website" \
  -H "Authorization: Bearer bscraper_your_key_here"

# Equivalent, using explicit tiers
curl ".../results?web_presence=none,social,directory,builder,booking" ...

# Only social-only and directory-only leads (fine-grained)
curl ".../results?web_presence=social,directory" ...
```

<Note>
  This read-time filter is **free** and runs over already-scraped data, so you can re-segment a finished job as often as you like. To avoid scraping and paying for unwanted businesses in the first place, set [`website_filter`](/docs/api-reference/jobs#website-filter) when you create the job.
</Note>

If the job is in `failed` status, this endpoint returns `402 Payment Required` with a billing failure message.

## `GET /api/v1/results`

Fetch a flat result list across all of your jobs.

### Query parameters

| Parameter | Type    | Default | Notes                |
| --------- | ------- | ------- | -------------------- |
| `limit`   | integer | `50`    | Max `100`            |
| `offset`  | integer | `0`     | Must be non-negative |

This endpoint returns a plain JSON array of result objects rather than a paginated envelope.

## `GET /api/v1/jobs/{id}/download`

Download the CSV file for a job.

Current behavior:

* Response content type is `text/csv`
* Response includes `Content-Disposition` with the generated file name
* Invalid UUID values return `422 Unprocessable Entity`
* Missing or unavailable CSV output returns `404 Not Found`
* Failed jobs return `402 Payment Required`

### Example request

```bash theme={null}
curl -L "https://api.brezelscraper.com/api/v1/jobs/YOUR_JOB_ID/download" \
  -H "Authorization: Bearer bscraper_your_key_here" \
  -o results.csv
```

## `GET /api/v1/jobs/{id}/costs`

Read the cost breakdown for one job.

### Example response

```json theme={null}
{
  "job_id": "d8d8a24e-cef2-4b02-8396-abc290b6f299",
  "items": [
    {
      "event_type": "job_start",
      "quantity": 1,
      "cost_credits": "0.007000"
    },
    {
      "event_type": "place_scraped",
      "quantity": 400,
      "cost_credits": "1.200000"
    }
  ],
  "total_credits": "1.207000",
  "total_rounded": 1
}
```

Each `event_type` represents a billing line item:

| Event type        | Meaning                                                                                                                |
| ----------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `job_start`       | One-time setup fee when the job starts                                                                                 |
| `place_scraped`   | Per-place fee for each result collected                                                                                |
| `contact_details` | Per-place fee when email extraction is enabled                                                                         |
| `review`          | Per-review fee when reviews are collected                                                                              |
| `image`           | Per-image fee when images are collected                                                                                |
| `filters_applied` | Per-matching-place fee when a `no_website`/`has_website` [`website_filter`](/docs/api-reference/jobs#website-filter) is set |

Cost totals use the pricing rules that were active when the job ran. See [Credits and billing](/docs/api-reference/credits-and-billing) for per-event prices.

## `POST /api/v1/jobs/costs/batch`

Read the cost breakdown for multiple jobs in one request.

### Request body

```json theme={null}
{
  "job_ids": [
    "d8d8a24e-cef2-4b02-8396-abc290b6f299",
    "4b454a90-cd30-42a5-8401-1d8ebd911234"
  ]
}
```

Notes:

* Maximum `100` job IDs per request
* Invalid UUID values return `422 Unprocessable Entity`
* Jobs you do not own are silently omitted from the `costs` map

### Example response

```json theme={null}
{
  "costs": {
    "d8d8a24e-cef2-4b02-8396-abc290b6f299": {
      "job_id": "d8d8a24e-cef2-4b02-8396-abc290b6f299",
      "items": [
        {
          "event_type": "job_start",
          "quantity": 1,
          "cost_credits": "0.007000"
        },
        {
          "event_type": "place_scraped",
          "quantity": 400,
          "cost_credits": "1.200000"
        }
      ],
      "total_credits": "1.207000",
      "total_rounded": 1
    }
  }
}
```
