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

# Swift

> Official Swift SDK for Spidra — scrape pages, run browser actions, batch-process URLs, and crawl entire sites using modern Swift Concurrency.

The official Swift SDK for Spidra uses modern `async/await` concurrency throughout. All results come back as structured data ready to feed into your iOS, macOS, or server-side Swift applications.

## Installation

### Swift Package Manager

Add Spidra to your `Package.swift` dependencies:

```swift theme={null}
dependencies: [
    .package(url: "https://github.com/spidra-io/spidra-swift.git", from: "1.0.0")
]
```

Or add it directly via Xcode: **File → Add Packages...** and paste the repository URL.

<Note>
  Get your API key from [app.spidra.io](https://app.spidra.io) under **API Keys**.
  Never hardcode it in source files — use an environment variable instead.
</Note>

## Requirements

* Swift 5.9+
* iOS 15.0+ / macOS 12.0+ / tvOS 15.0+ / watchOS 8.0+
* A Spidra API key ([sign up free](https://spidra.io))

## Getting started

```swift theme={null}
import Spidra

let spidra = SpidraClient(apiKey: "spd_YOUR_API_KEY")
```

From here you access everything through `spidra.scrape`, `spidra.batch`, `spidra.crawl`, `spidra.logs`, and `spidra.usage`.

## Quick start

```swift theme={null}
import Spidra

Task {
    do {
        let spidra = SpidraClient(apiKey: "spd_YOUR_API_KEY")

        let params = ScrapeParams(
            urls: [ScrapeUrl(url: "https://news.ycombinator.com")],
            prompt: "List the top 5 stories with title, points, and comment count",
            output: "json"
        )

        let job = try await spidra.scrape.run(params)

        if let content = job.result?.content?.value {
            print(content)
        }
    } catch {
        print("Error: \(error.localizedDescription)")
    }
}
```

## Scraping

All scrape jobs run asynchronously using Swift's `async/await`. The `run()` method submits a job and polls until it finishes. Up to 3 URLs can be passed per request and they are processed in parallel.

### Basic scrape

```swift theme={null}
let params = ScrapeParams(
    urls: [ScrapeUrl(url: "https://example.com/pricing")],
    prompt: "Extract all pricing plans with name, price, and included features",
    output: "json"
)

let job = try await spidra.scrape.run(params)
print(job.result?.content?.value ?? "No data")
```

**Parameters**

| Parameter | Type | Description |
| - | - | - |
| `urls` | `[ScrapeUrl]` | Up to 3 URLs, each with optional per-URL browser actions |
| `prompt` | `String` | AI extraction instruction |
| `output` | `String` | `"markdown"` (default) or `"json"` |
| `schema` | `AnyCodable?` | JSON Schema for guaranteed output shape |
| `useProxy` | `Bool` | Route through a residential proxy |
| `proxyCountry` | `String?` | Two-letter country code, e.g. `"us"`, `"de"`, `"jp"` |
| `extractContentOnly` | `Bool` | Strip navigation, ads, and boilerplate before AI extraction |
| `screenshot` | `Bool` | Capture a screenshot of the page |
| `fullPageScreenshot` | `Bool` | Capture a full-page (scrolled) screenshot |
| `cookies` | `String?` | Raw `Cookie` header string for authenticated pages |

### Fire-and-forget approach

Use `submit()` and `get()` when you want to manage polling yourself.

```swift theme={null}
// Submit a job immediately
let queued = try await spidra.scrape.submit(ScrapeParams(
    urls: [ScrapeUrl(url: "https://example.com")],
    prompt: "Extract the main headline"
))

// Check status later
let status = try await spidra.scrape.get(queued.jobId)
if status.status == "completed" {
    print(status.result?.content?.value ?? "")
}
```

**Job statuses:** `waiting` · `active` · `completed` · `failed`

### Structured JSON output

Pass a `schema` to enforce an exact output shape. Missing fields come back as `null` rather than hallucinated values.

```swift theme={null}
let schemaDict: [String: Any] = [
    "type": "object",
    "required": ["title", "company", "remote"],
    "properties": [
        "title":   ["type": "string"],
        "company": ["type": "string"],
        "remote":  ["type": ["boolean", "null"]]
    ]
]

let params = ScrapeParams(
    urls: [ScrapeUrl(url: "https://jobs.example.com/senior-engineer")],
    prompt: "Extract the job listing details",
    output: "json",
    schema: AnyCodable(schemaDict)
)

let job = try await spidra.scrape.run(params)
```

### Geo-targeted scraping

Pass `useProxy: true` and a `proxyCountry` code to route through a residential IP in that country.

```swift theme={null}
let params = ScrapeParams(
    urls: [ScrapeUrl(url: "https://www.amazon.de/gp/bestsellers")],
    prompt: "List the top 10 products with name and price",
    useProxy: true,
    proxyCountry: "de"
)
```

Supported codes include `us`, `gb`, `de`, `fr`, `jp`, `au`, `ca`, `br`, `in`, `nl`, and [40+ more](/features/stealth-mode#country-targeting). Use `"global"` or `"eu"` for regional routing.

### Authenticated pages

Pass cookies as a string to scrape pages that require a login session.

```swift theme={null}
let params = ScrapeParams(
    urls: [ScrapeUrl(url: "https://app.example.com/dashboard")],
    prompt: "Extract the monthly revenue and active user count",
    cookies: "session=abc123; auth_token=xyz789"
)
```

### Browser actions

Actions let you interact with the page before the scrape runs. They execute in order.

```swift theme={null}
let url = ScrapeUrl(
    url: "https://example.com/products",
    actions: [
        .click(selector: "#accept-cookies", value: nil),
        .wait(duration: 1000),
        .scroll(to: "80%")
    ]
)

let params = ScrapeParams(urls: [url], prompt: "Extract product names and prices")
let job = try await spidra.scrape.run(params)
```

**Available actions**

| Action | Description |
| - | - |
| `.click(selector:value:)` | Click a button, link, or any element |
| `.type(selector:value:)` | Type text into an input or textarea |
| `.check(selector:value:)` | Check a checkbox |
| `.uncheck(selector:value:)` | Uncheck a checkbox |
| `.wait(duration:)` | Pause for a set number of milliseconds |
| `.scroll(to:)` | Scroll to a percentage of the page height |
| `.forEach(observe:mode:...)` | Loop over every matched element and process each |

### forEach — loop over every element

`forEach` finds a set of elements and processes each individually. Best used when dealing with pagination, clicking into detail pages, or looping over long lists.

```swift theme={null}
let forEachAction = BrowserAction.forEach(
    observe: "Find all book cards in the product grid",
    mode: "inline",
    captureSelector: "article.product_pod",
    maxItems: 20,
    itemPrompt: "Extract title, price, and star rating. Return as JSON",
    waitAfterClick: nil,
    actions: nil,
    pagination: nil
)

let url = ScrapeUrl(
    url: "https://books.toscrape.com/",
    actions: [forEachAction]
)
```

**Modes:**

* **`inline`** — Read element content directly without navigating away.
* **`navigate`** — Follow each element's link to its destination page and capture content there.
* **`click`** — Click each element, capture the content that appears (e.g., a modal), then move on.

You can also use pagination to navigate through multiple pages automatically:

```swift theme={null}
let pagination = BrowserActionPagination(nextSelector: "li.next > a", maxPages: 3)
```

### Poll options

Override default polling intervals via `PollOptions`:

```swift theme={null}
let options = PollOptions(pollInterval: 2.0, timeout: 60.0)
let job = try await spidra.scrape.run(params, options: options)
```

The same options work on `batch.run()` and `crawl.run()`.

## Batch scraping

Submit up to 50 URLs in a single request. All URLs are processed in parallel. Each URL is a plain string.

```swift theme={null}
let params = BatchScrapeParams(
    urls: [
        "https://shop.example.com/product/1",
        "https://shop.example.com/product/2",
        "https://shop.example.com/product/3"
    ],
    prompt: "Extract product name, price, and availability",
    output: "json",
    useProxy: true
)

let batch = try await spidra.batch.run(params)

for item in batch.items {
    if item.status == "completed" {
        print("Completed: \(item.url)")
    } else if item.status == "failed" {
        print("Failed: \(item.error ?? "Unknown")")
    }
}
```

**Item statuses:** `pending` · `running` · `completed` · `failed`

**Batch statuses:** `pending` · `running` · `completed` · `failed` · `cancelled`

You can also `list()`, `retry()`, or `cancel()` batches using the same pattern as `scrape`.

## Crawling

Given a starting URL, Spidra discovers pages automatically according to your instruction and extracts structured data from each one.

```swift theme={null}
let params = CrawlParams(
    baseUrl: "https://competitor.com/blog",
    crawlInstruction: "Find all blog posts published in 2024",
    transformInstruction: "Extract the title, author, and publish date",
    maxPages: 30
)

let job = try await spidra.crawl.run(params)

if let pages = job.result {
    for page in pages {
        print(page.url, page.data?.value ?? "No Data")
    }
}
```

**Parameters**

| Parameter | Type | Description |
| - | - | - |
| `baseUrl` | `String` | Starting URL for the crawl |
| `crawlInstruction` | `String` | Which links to follow and which to skip |
| `transformInstruction` | `String` | What to extract from each page |
| `maxPages` | `Int` | Maximum number of pages to crawl |
| `useProxy` | `Bool` | Route through a residential proxy |
| `proxyCountry` | `String?` | Two-letter country code, e.g. `"us"` |
| `cookies` | `String?` | Raw `Cookie` header string for authenticated sites |

### Download crawled content

Fetch signed download URLs for HTML and Markdown for all crawled pages. Links expire after **1 hour**.

```swift theme={null}
let response = try await spidra.crawl.pages(job.jobId)
```

## Logs

Every API scrape job is logged automatically.

```swift theme={null}
let params = ScrapeLogsParams(status: "failed", limit: 20)
let response = try await spidra.logs.list(params)

for log in response.logs {
    print("Log: \(log.uuid) - Status: \(log.status) - Credits: \(log.creditsUsed)")
}

// Get full extraction result for a specific log
let detail = try await spidra.logs.get("log-uuid")
```

## Usage statistics

Returns credit and request usage broken down by day or week.

```swift theme={null}
let rows = try await spidra.usage.get("30d") // "7d" | "30d" | "weekly"

for row in rows {
    print("Date: \(row.date) - Requests: \(row.requests) - Credits: \(row.credits)")
}
```

| Range | Description |
| - | - |
| `"7d"` | Last 7 days, one row per day |
| `"30d"` | Last 30 days, one row per day |
| `"weekly"` | Last 7 weeks, one row per week |

## Error handling

Every API error throws a `SpidraError`. Catch the specific case you care about.

```swift theme={null}
do {
    let job = try await spidra.scrape.run(params)
} catch SpidraError.authenticationError(let msg) {
    // 401: API key is missing or invalid
    print("Check your API key: \(msg)")
} catch SpidraError.insufficientCreditsError(let msg) {
    // 403: Monthly credit limit reached
    print("Out of credits: \(msg)")
} catch SpidraError.rateLimitError(let msg) {
    // 429: Too many requests
    print("Rate limited: \(msg)")
} catch SpidraError.serverError(let msg) {
    // 500: Server error
    print("Server error: \(msg)")
} catch {
    // Decoding errors, network timeouts, etc.
    print("Other error: \(error.localizedDescription)")
}
```

| Error case | Status | When |
| - | - | - |
| `.authenticationError` | 401 | API key is missing or invalid |
| `.insufficientCreditsError` | 403 | No credits remaining |
| `.rateLimitError` | 429 | Too many requests — back off |
| `.serverError` | 500 | Unexpected server-side error |

<CardGroup cols={2}>
  <Card title=".NET" icon="microsoft" href="/sdks/dotnet">
    Official .NET SDK — fully async, typed exceptions, JSON schema support. Requires .NET 8+.
  </Card>

  <Card title="Java" icon="java" href="/sdks/java">
    Official Java SDK — CompletableFuture-based, builder pattern, no extra HTTP dependencies.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.