# Raw endpoints

Two tiers share one front door. **Capabilities** are the routed product: one name, normalized input and output, a quote before the run, several providers behind it, waterfall failover. **Raw endpoints** are the long tail: one provider's own endpoint, called with the provider's own request shape, answered with the provider's own payload. No routing, no normalization, no fixed price.

Use a capability when one exists (discover tells you which capability wraps an endpoint). Use a raw endpoint when you need a provider field the capability does not expose, or a job the catalog has not wrapped yet.

## Where they come from

Endpoints are imported from the providers' own catalogs, not hand-written: DataForSEO's OpenAPI specification (every live endpoint plus the free reference data), the Apify store (public actors that allow agentic use and bill per event or per compute, filtered to actors people actually run), and the OpenAPI specifications of TikHub, Exa and Firecrawl. A few providers without a machine-readable catalog have hand-maintained manifests (LeadMagic, Hunter, Tomba, MillionVerifier, PredictLeads, ScrapeCreators).

Some providers are never exposed raw, and inspect says why: fal (its terms prohibit it; use `creative.*`), Apollo (bring-your-own-key only), AgentMail and Name.com (inboxes and domains live in the platform account and are owned per workspace; use `email.*` and `domain.*`).

Each provider page shows how many endpoints it serves. `GET /v1/catalog/providers/{provider}` carries `endpoints.count` and `endpoints.status`: `live` (runs now), `ready` (listed, platform account pending), `excluded`.

## Addressing

```text
provider + endpoint
```

`dataforseo` + `/v3/serp/google/organic/live/advanced`. `apify` + `/apidojo/tweet-scraper`. `tikhub` + `/api/v1/tiktok/app/v3/fetch_video_comments`. The endpoint is the provider's own path; discover and the provider listing return it verbatim.

## Discover

`POST /v1/discover` searches capabilities and endpoints together. Every item has `kind` (`capability` or `endpoint`), a relevance `score` and a `health` verdict; capabilities outrank endpoints on equal relevance, and a live capability that wraps an endpoint always ranks above it. Restrict with `"kind": "endpoint"` or `"provider": "dataforseo"`. Read the response's `hints`.

```bash
routergrowth discover -q "tiktok video comments" --kind endpoint
routergrowth endpoints -p dataforseo -q backlinks     # one provider's list, substring filter
```

## Inspect

```json
{"provider": "dataforseo", "endpoint": "/v3/serp/google/organic/live/advanced"}
```

Returns the provider's contract: `input` with `pathParams`, `queryParams`, `body` (properties, required) and `bodyType`, an `example`, the `cost` model, `health`, `wrapped_by` (capabilities that call this endpoint), a ready-to-run `run.cli`, and `hints`. Apify actors load their input schema from the actor's build on first inspect.

| `input.bodyType` | Meaning |
| --- | --- |
| `task` | DataForSEO: `input` is one task object (or an array of up to 10); the request body is the array |
| `actor_input` | Apify: `input` is the actor's input object |
| `json` | the JSON body; on `GET` endpoints `input` becomes query parameters |

## Run

```json
{
  "provider": "dataforseo",
  "endpoint": "/v3/serp/google/organic/live/advanced",
  "input": {"keyword": "best crm", "location_code": 2840, "language_code": "en"},
  "routing": {"max_cost": "0.05"}
}
```

`path` fills `{placeholders}` in the endpoint (`{"domain": "example.com"}`), `query` adds query parameters. The response has the run's usual shape with `kind: "endpoint"`, `capability: null`, `endpoint` set, and `result.data` holding the provider's payload untouched (`result.raw: true`).

## How a raw run bills

There is no quote. Instead:

1. **Hold.** Before the call, the wallet holds the rate-card estimate x 1.5 when the provider publishes rates, or $1.00 when it does not. `routing.max_cost` lowers the hold; set it on every raw call.
2. **Settle.** After the call, the charge is the provider's measured cost x 1.43 (a 30% margin) when the provider reports its cost (DataForSEO reports every task's cost; Apify reports `usageTotalUsd`), the rate-card estimate otherwise. The charge never exceeds the hold, and never exceeds `max_cost`.
3. **Release.** Provider errors, timeouts and HTTP 4xx/5xx release the hold in full. A measured cost of zero charges nothing.

Apify actors that bill per event receive your cap directly: the hold divided by the markup becomes the run's `maxTotalChargeUsd`, enforced by Apify.

Raw runs never fall back to another provider: there is nothing equivalent to fall back to.

## Controls

The daily spend guard applies to raw runs like any other. A **per-run ceiling** exists for exactly this tier: `POST /v1/spend-guard {"per_run_limit": "1"}` blocks any single run whose hold would exceed $1 (status `blocked`, error `cost_limit_exceeded`, a `controls` list naming the control). It is off unless you enable it; `null` disables it again.

## Health

Every endpoint and every capability offer carries `health`: `healthy` (confirmed within the last hour), `stable` (strong 30-day record), `degraded`, `outage`, or `unknown` (not enough data, common for low-traffic endpoints, not a warning), with the measured success rate, p50 and p95 latency and the sample size. For Apify actors with no measurements of our own, the actor's published 30-day run stats stand in (`source: "provider_reported"`). Use health to break ties between two endpoints that both fit, never to skip one that is `unknown`.

## Promotion

Raw calls that recur are the roadmap: an endpoint many workspaces call gets wrapped into a routed capability with a fixed price and a second provider behind it. `python -m app.ops.endpoints usage` is the report.
