# Providers

You end up on this page for one of two reasons: the page you fetched didn't convert to markdown on its own, or you want better output or higher rate limits than the default gives you for free.

## When you need a provider

A provider only runs as the last resort in [how furl resolves a URL](/how-it-works) — most fetches never reach one. jina is the default provider and needs no key, so a fresh install already works without touching this page.

## Choosing between the three

| Provider | Key | Output |
| --- | --- | --- |
| `jina` (default) | Optional — raises rate limits | Markdown, after a metadata preamble (`Title:`, `URL Source:`, `Published Time:`, then `Markdown Content:`) |
| `exa` | Required | Plain extracted text, not markdown |
| `firecrawl` | Required | Markdown |

jina's metadata prefix means its output isn't a bare document — worth knowing if something downstream expects only the page content.

exa is the one to skip if you need structured markdown: it returns clean extracted text regardless of the source page. firecrawl returns markdown.

## Setting a key

`furl providers` is interactive — no flags, no non-interactive mode.

```bash
furl providers
```

```text
? Select a provider ›
❯ jina  (default · keyless)
  exa  (not set)
  firecrawl  (not set)
  Exit
```

Each label reflects live state — whether a key is set for that provider, and which one is currently the default.

Picking `jina` with no key stored offers `Set as default`, `Set key`, or `Skip`:

```text
? Jina is keyless by default. Manage an optional key for higher limits? ›
❯ Set as default
  Set key
  Skip
```

Saving a jina key does not change the default provider — jina can hold a key purely for rate limits while another provider stays active.

Picking `exa` or `firecrawl` with no key stored goes straight to `Enter <provider> API key:`. Saving one prints `Saved <provider> key.` followed by `<provider> is now your default provider.` — setting a key for either of these two always makes it the default, unlike jina.

If a key is already stored, the provider gets a `Manage <provider>` menu instead, with `Set as default`, `Replace key`, `Delete key`, and `Cancel`. Deleting the key of the provider currently set as default resets the default back to jina and prints `jina is now your default provider because the active provider key was deleted.` Deleting a jina key never touches the default.

Keys are stored in the OS keychain via `Bun.secrets`, under the service name `com.fdarian.furl`. They are never written to disk in plaintext.

## Key resolution

A provider key resolves from exactly two places, in order:

1. An environment variable — `JINA_API_KEY`, `EXA_API_KEY`, or `FIRECRAWL_API_KEY`
2. The keychain entry saved by `furl providers`

If neither is set, jina still runs keyless. exa and firecrawl fail instead, with:

```text
No API key configured for exa. Run `furl providers`.
```

Environment variables are also the way to supply a key where the keychain isn't available, such as CI or a container.

## Choosing the default provider

Which provider actually runs resolves in order:

1. The [`--provider` flag](/fetching), for one invocation
2. `provider` in the config file
3. `jina`

The config file lives at `~/.config/furl/config.json`, built from `$HOME` — `XDG_CONFIG_HOME` is ignored. It's only ever created by `furl providers`; a plain fetch never writes it. The only field is `provider`, and the only valid values are the three provider names — anything else is a config error.

```json [~/.config/furl/config.json]
{
  "provider": "firecrawl"
}
```

## Provider reference

**jina**

* `GET https://r.jina.ai/<url>`
* Always sends `X-Return-Format: markdown` and `Accept: text/markdown`; adds `Authorization: Bearer <key>` when a key is present
* Markdown is the full response body

**exa**

* `POST https://api.exa.ai/contents`
* Auth header: `x-api-key: <key>`
* Body: `{ urls: [url], text: true }`
* Text is read from `results[0].text`
* Failure messages: "Exa returned no results", "Exa response is missing results\[0].text"

**firecrawl**

* `POST https://api.firecrawl.dev/v1/scrape`
* Auth header: `Authorization: Bearer <key>`
* Body: `{ url: url, formats: ['markdown'] }`
* Markdown is read from `data.markdown`
* Failure message: "Firecrawl response is missing data.markdown"
