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

# HTTP Targets

> Register an externally-hosted agent endpoint so a dataset run can call it directly, without instrumenting it with the Zespan SDK.

## What it is

An **HTTP Target** is a registered, externally-hosted endpoint — a deployed Amazon Bedrock or Glean agent, or any chatbot/agent API you don't control or can't add the Zespan SDK to. Once registered, the ["Run over dataset"](/dashboard/datasets#running-against-an-http-endpoint) flow can POST each dataset item directly to that endpoint and capture its raw response as a trace, without your own pipeline code in the loop.

Use this when you want to evaluate an agent you can only reach over HTTP — you don't own its deployment, or it runs on a stack the SDK doesn't wrap — but you still want it scored against a dataset like any other run.

<Note>
  If you *can* add the Zespan SDK to the service being tested, prefer that: a normal [dataset run](/dashboard/datasets#how-dataset-runs-work) captures your pipeline's real trace tree (retrieval spans, tool calls, sub-agent hops), not just a single request/response pair. HTTP Targets exist for the case where that isn't possible.
</Note>

## Where to register one

Go to **Project Settings → HTTP Targets**.

## Registering a target

<Steps>
  <Step title="Name and URL">
    Give the target a name (shown wherever you pick it for a run) and the endpoint's full URL, e.g. `https://agent.example.com/invoke`.
  </Step>

  <Step title="Auth mode">
    Choose how Zespan authenticates to the endpoint:

    | Auth mode          | What it sends                                                    |
    | ------------------ | ---------------------------------------------------------------- |
    | **No auth**        | No credential attached.                                          |
    | **Bearer token**   | An `Authorization: Bearer <token>` header.                       |
    | **API key header** | Your credential in a header you name, e.g. `X-Api-Key: <value>`. |

    Like an [LLM Connection](/platform/llm-connections), the credential is encrypted at rest and **write-only** — once saved, it's never returned by the API or shown in the UI again; the list only shows whether a credential is set.
  </Step>

  <Step title="Request template">
    Write the JSON body Zespan should POST to the endpoint, with the literal placeholder `{{input}}` wherever a dataset item's input should be substituted, e.g.:

    ```json theme={"theme":{"light":"github-light","dark":"one-dark-pro"}}
    {"message": "{{input}}"}
    ```

    `{{input}}` can also appear in a header value or in the URL itself. At run time it's replaced with the dataset item's `input` (JSON-escaped so the result is always valid JSON when it sits inside a body string).
  </Step>
</Steps>

<Note>
  Editing a target's auth mode or rotating its credential always requires supplying a fresh credential value — a stored secret is never re-readable to pre-fill an edit, the same as an [LLM Connection](/platform/llm-connections).
</Note>

## Security: URL validation

A target URL must be `http`/`https` and must resolve to a public address. It's validated server-side before it's saved, and validated **again** on the *hydrated* URL immediately before every outbound call — so a dataset item's input can't smuggle a request toward an internal address even if `{{input}}` is substituted into the URL itself. A blocked call is recorded as a failed trace rather than silently skipped or allowed through.

These ranges are rejected:

| Range                                             | What it covers                                                                                        |
| ------------------------------------------------- | ----------------------------------------------------------------------------------------------------- |
| `127.0.0.0/8`, `::1`, `0.0.0.0`                   | Loopback and unspecified addresses                                                                    |
| `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`   | RFC 1918 private networks                                                                             |
| `169.254.0.0/16`, `fe80::/10`                     | Link-local, including the `169.254.169.254` cloud instance-metadata address                           |
| `fc00::/7`                                        | IPv6 unique local addresses                                                                           |
| `100.64.0.0/10`                                   | RFC 6598 carrier-grade NAT shared address space — **this includes Tailscale's default address range** |
| `224.0.0.0/4` and above                           | Multicast and reserved space                                                                          |
| `localhost`, any `.internal` or `.local` hostname | Non-public hostnames                                                                                  |

IPv4-mapped-IPv6 (`::ffff:a.b.c.d`) and NAT64 (`64:ff9b::a.b.c.d`) spellings of a blocked address are rejected too, so a blocked range can't be reached by rewriting it as IPv6.

<Warning>
  There is no allowlist or override for HTTP Targets — a target must be reachable at a public address. An agent that's only reachable over a private network or a Tailscale/CGNAT address (`100.64.x.x`–`100.127.x.x`) cannot be registered as an HTTP Target. Expose it at a public hostname, or instrument the service with the [Zespan SDK](/quickstart) and run a normal [dataset run](/dashboard/datasets#how-dataset-runs-work) instead.
</Warning>

## How a run differs from a prompt-version run

Running against an HTTP Target is a distinct execution mode from running against a registered [prompt version](/dashboard/prompts#the-quality-gate):

* **Prompt version:** Zespan calls an LLM provider directly, using your prompt template and an [LLM Connection](/platform/llm-connections).
* **HTTP Target:** Zespan POSTs your hydrated request template straight to the endpoint you registered — no LLM connection is used or required — and records the raw response as a trace tagged `sdk_name: "zespan-http-endpoint"`, so it's easy to tell apart from traces your own SDK-instrumented code produced.

Each outbound call retries on a network error or a `429`/`5xx` response (with backoff), and never follows redirects — a `3xx` response is recorded as a terminal failure rather than followed, since a target agent has no legitimate reason to redirect a dataset-run request.

## Trace propagation

Every outbound call carries a W3C `traceparent` header built from a freshly generated trace/span id pair — the same id pair the resulting Zespan trace is stored under. If the target agent is itself instrumented with its own OpenTelemetry SDK and configured to export to a Zespan-reachable endpoint, and it extracts and continues that incoming trace context, its own spans can land under the identical trace id — giving you a combined view even though Zespan didn't call the agent's code directly.

## Deleting a target

Deleting a target removes it immediately. Dataset runs already created against it keep their captured traces; new runs can no longer select it.

## Next steps

* [Datasets](/dashboard/datasets#running-against-an-http-endpoint) — start a dataset run against a registered HTTP Target
* [LLM Connections](/platform/llm-connections) — the closest analog for how credentials are stored and masked
* [Prompt version runs & the quality gate](/dashboard/prompts#the-quality-gate) — the alternative "Run over dataset" mode that calls an LLM directly
