# Best Friends Animal Society Scraper (`crawlerbros/best-friends-scraper`) Actor

Scrape adoptable pets from Best Friends Animal Society (bestfriends.org) - the largest US animal-welfare organization. Browse adoptable dogs and cats by location, search by name, or fetch individual pet profiles with full metadata, photos, and adoption links.

- **URL**: https://apify.com/crawlerbros/best-friends-scraper.md
- **Developed by:** [Crawler Bros](https://apify.com/crawlerbros) (community)
- **Categories:** Automation, Developer tools, Other
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.00 / 1,000 results

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

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

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Best Friends Animal Society Scraper

Scrape **Best Friends Animal Society** (bestfriends.org) — the largest animal-welfare organization in the United States. Browse adoptable dogs and cats at any of the five Best Friends adoption centers, search adoptable pets by name across all locations, or fetch individual pet profiles with full metadata, photo galleries, and adoption links. Pure HTTP (SSR Drupal site). No auth, no API key, no proxy required.

### What this actor does

- **Four modes:** `byLocation`, `search`, `byIds`, `byUrls`
- **Five adoption centers:** Salt Lake City, Los Angeles, New York City, Houston, Northwest Arkansas
- **Filters:** species (Dog / Cat), age (Baby / Young / Adult / Senior), sex, size (Small / Medium / Large / X-Large)
- **Sorting:** A–Z, newest arrivals, longest stay, featured
- **Full profiles:** name, species, breeds, birthdate, color, size, status, description, photo gallery, shelter adoption link
- **Typed error records** for unresolvable IDs / URLs (recordType `error`)
- **Empty fields are omitted**

### Data Source

This actor is a **replacement** for the slot originally occupied by the RescueGroups (rescuegroups.org) scraper.

- **Original slot:** RescueGroups (rescuegroups.org). Its API requires a registration API key — requests without a key return `401 Unauthorized`. That violates the free-plan constraint (no actor may require user-supplied credentials), so the slot was replaced.
- **Replacement:** Best Friends Animal Society (bestfriends.org) — the largest US animal-welfare organization, in the same pet-adoption category.
- **Data tier:** the site is a server-side-rendered Drupal 11 application; listing pages (`/<location>/adopt/adoptable-pets`) and animal profile pages (`/<location>/adopt/<id>/<slug>`) are plain SSR HTML that resolves from datacenter IPs.
- **Proxy behavior:** none required; works with no proxy. `proxyConfiguration` is intentionally not required. Requests retry with exponential backoff and rotate user agents on 403/429.
- **Images** are served from `available-animals.bestfriends.org` (S3/CloudFront) and resolve with `HTTP 200` from a clean public client — no Referer header, cookies, or signing required.

### Output per animal

- `animalId` — Best Friends animal ID (numeric, from the profile URL)
- `name` — pet name
- `species` — Dog / Cat / Bird / etc. (from the profile's "looks like" label)
- `primaryBreed`, `secondaryBreed` — breed(s), e.g. `Terrier, Pit Bull` + `Mix`
- `age` — Baby / Young / Adult / Senior
- `birthDate` — estimated birthdate (ISO `YYYY-MM-DD`, when published)
- `sex` — Male / Female
- `color` — coat color
- `size` — Small / Medium / Large / X-Large (when published)
- `location` — adoption center slug (e.g. `salt-lake-city`)
- `status` — e.g. "In foster", "I'm at a local event; come say hello."
- `description` — full adoption description text
- `imageUrl`, `imageUrls[]` — profile photo + gallery (up to 10)
- `adoptUrl` — shelter adoption-survey link
- `sourceUrl` — canonical profile URL
- `recordType: "animal"`, `scrapedAt`

When **Fetch full profile details** is off, records contain card-level fields only: `animalId`, `name`, `location`, `status`, `imageUrl`, `primaryBreed`, `secondaryBreed`, `age`, `sex`, `sourceUrl`.

#### Error records

Unresolvable IDs / URLs emit a typed record with `recordType: "error"`, the failed `sourceUrl` (or `animalId`), and an `error` message. Normal records never contain those fields.

### Input

| Field | Type | Default | Description |
|---|---|---|---|
| `mode` | string | `byLocation` | `byLocation` / `search` / `byIds` / `byUrls` |
| `location` | string | `salt-lake-city` | Adoption center (mode=byLocation, mode=byIds) |
| `searchQuery` | string | – | Pet name or partial name (mode=search; also narrows mode=byLocation) |
| `animalIds` | array | – | Numeric animal IDs, e.g. `214192932` (mode=byIds) |
| `startUrls` | array | – | Profile URLs, e.g. `https://bestfriends.org/salt-lake-city/adopt/214157435/amaranth` (mode=byUrls) |
| `species` | string | All | `Dog` / `Cat` |
| `age` | string | All | `Baby` / `Young` / `Adult` / `Senior` |
| `sex` | string | All | `Female` / `Male` |
| `size` | string | All | `Small` / `Medium` / `Large` / `X-Large` |
| `sortBy` | string | `title` | `title` / `newest` / `longestStay` / `featured` |
| `enrichDetails` | boolean | `true` | Fetch each pet's full profile page |
| `maxItems` | int | `50` | Hard cap (1–1000) |

#### Example: senior dogs in Los Angeles, longest stay first

```json
{
  "mode": "byLocation",
  "location": "los-angeles",
  "species": "Dog",
  "age": "Senior",
  "sortBy": "longestStay",
  "maxItems": 20
}
```

#### Example: name search across all locations

```json
{
  "mode": "search",
  "searchQuery": "amaranth",
  "species": "Cat",
  "maxItems": 10
}
```

#### Example: lookup by animal ID

```json
{
  "mode": "byIds",
  "location": "salt-lake-city",
  "animalIds": ["214192932", "214157435"]
}
```

#### Example: fetch from profile URLs

```json
{
  "mode": "byUrls",
  "startUrls": [
    { "url": "https://bestfriends.org/salt-lake-city/adopt/214157435/amaranth" },
    { "url": "https://bestfriends.org/houston/adopt/213734892/scampi" }
  ]
}
```

### Use cases

- **Adoption agencies & rescues** — monitor adoptable inventory at Best Friends centers for transfer opportunities
- **Pet marketplaces** — syndicate live adoptable listings into adoption feeds
- **Animal-welfare research** — analyze breed / age / stay-duration patterns across the largest US shelter network
- **Nonprofit dashboards** — daily snapshots of available pets per location for advocacy reporting
- **Content teams** — pull names, photos, and bios for adoption-promotion campaigns

### FAQ

**What is the data source?**
Best Friends Animal Society's public website (bestfriends.org), a server-side-rendered Drupal site. No API key or registration is required.

**Why is this actor replacing "RescueGroups"?**
The RescueGroups API requires a registered API key — without it every request returns `401 Unauthorized`. Best Friends publishes the same category of data (adoptable pets) with zero credentials required.

**How fresh is the data?**
Live. Every run fetches the current pages directly; the actor never caches listings.

**How many locations are covered?**
Five Best Friends adoption centers: Salt Lake City (UT), Los Angeles (CA), New York City (NY), Houston (TX), and Northwest Arkansas. Best Friends Sanctuary (Kanab, UT) animals can be fetched by URL or ID from `startUrls`, but the sanctuary has no filterable listing page.

**What do the filter dropdowns mean?**
`species`, `age`, `sex`, and `size` map 1:1 to Best Friends' own listing filters. `sortBy` maps to the site's sort options (A–Z, newest arrivals, longest stay, featured).

**Why are some fields sometimes missing (e.g. size, birthdate)?**
Best Friends only publishes certain fields (size, birthdate, description) on some profiles. Missing fields are omitted from the record entirely — no `null` placeholders.

**Why don't I see species when profile enrichment is off?**
Listing cards do not carry the species marker; species is only available from the profile page. Enable **Fetch full profile details** to include it.

**Are the photo URLs accessible?**
Yes. Images are served from an S3/CloudFront CDN and return `HTTP 200` from any public client with no special headers.

**Is this affiliated with Best Friends Animal Society?**
No. This is an independent third-party actor that reads publicly available data from their website.

**What happens if the site rate-limits me?**
The actor retries with exponential backoff and rotates user agents on 403/429 responses; persistent failures surface as typed error records instead of crashing.

# Actor input Schema

## `mode` (type: `string`):

What to fetch.

## `location` (type: `string`):

Best Friends adoption center (mode=byLocation, mode=byIds).

## `searchQuery` (type: `string`):

Pet name or partial name (mode=search; also narrows mode=byLocation). Case-insensitive, e.g. `amaranth`. Leave empty for all pets.

## `animalIds` (type: `array`):

Best Friends animal IDs from pet profile URLs, e.g. `214192932`.

## `startUrls` (type: `array`):

Full pet profile URLs, e.g. `https://bestfriends.org/salt-lake-city/adopt/214192932/alfalfa`.

## `species` (type: `string`):

Only emit this species.

## `age` (type: `string`):

Only emit pets in this age group.

## `sex` (type: `string`):

Only emit pets of this sex.

## `size` (type: `string`):

Only emit pets of this size (mode=byLocation, mode=search).

## `sortBy` (type: `string`):

Order of the listing (mode=byLocation, mode=search).

## `enrichDetails` (type: `boolean`):

Fetch each pet's profile page for species, description, photos, birthdate, adoption link. When off, records contain card-level data only.

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

Hard cap on emitted records.

## Actor input object example

```json
{
  "mode": "byLocation",
  "location": "salt-lake-city",
  "searchQuery": "",
  "animalIds": [],
  "startUrls": [],
  "species": "",
  "age": "",
  "sex": "",
  "size": "",
  "sortBy": "title",
  "enrichDetails": true,
  "maxItems": 20
}
```

# Actor output Schema

## `animals` (type: `string`):

Dataset containing all scraped Best Friends adoptable pet records.

# 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 = {
    "mode": "byLocation",
    "location": "salt-lake-city",
    "searchQuery": "",
    "animalIds": [],
    "startUrls": [],
    "species": "",
    "age": "",
    "sex": "",
    "size": "",
    "sortBy": "title",
    "enrichDetails": true,
    "maxItems": 20
};

// Run the Actor and wait for it to finish
const run = await client.actor("crawlerbros/best-friends-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 = {
    "mode": "byLocation",
    "location": "salt-lake-city",
    "searchQuery": "",
    "animalIds": [],
    "startUrls": [],
    "species": "",
    "age": "",
    "sex": "",
    "size": "",
    "sortBy": "title",
    "enrichDetails": True,
    "maxItems": 20,
}

# Run the Actor and wait for it to finish
run = client.actor("crawlerbros/best-friends-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 '{
  "mode": "byLocation",
  "location": "salt-lake-city",
  "searchQuery": "",
  "animalIds": [],
  "startUrls": [],
  "species": "",
  "age": "",
  "sex": "",
  "size": "",
  "sortBy": "title",
  "enrichDetails": true,
  "maxItems": 20
}' |
apify call crawlerbros/best-friends-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,crawlerbros/best-friends-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/6jfIBXNstoXaTnB0b/builds/WXs6KwLRsgm9ucimY/openapi.json
