# VendorStacks — full documentation (llms-full.txt)

> One-file markdown mirror of everything under https://vendorstacks.com
/docs. Canonical URLs: https://vendorstacks.com
/docs/ · https://vendorstacks.com
/docs/agents/ · https://vendorstacks.com
/docs/guides/ · https://vendorstacks.com
/docs/api/. Per-page markdown mirrors: /docs.md, /docs/agents.md, /docs/guides.md, /docs/guides/{slug}.md, /docs/api.md.



---

# VendorStacks Documentation

> VendorStacks — the subprocessor disclosure data API. Canonical: https://vendorstacks.com
/docs/


## Introduction

VendorStacks is a data API over public subprocessor and DPA disclosures. Given an entity (a domain) it answers: does this entity publish a disclosure, where does it live, and which vendors are on it — returned as a structured vendor stack across 13 categories with the quoted disclosure row as evidence. Reverse lookup returns the entities that list a given vendor or category. It is built for GTM enrichment pipelines and AI agents; extraction is deterministic — no LLM, no invented vendors.

Base URL: `https://vendorradar-production.up.railway.app
`. All responses are JSON and carry an `X-Request-Id` header.

### Quick start

**1 · Get a key (instant, 25 free credits)**

```
curl -X POST https://vendorradar-production.up.railway.app
/v1/keys \
  -H "Content-Type: application/json" \
  -d '{"email": "you@company.com"}'
```

**2 · Check an entity**

```
curl "https://vendorradar-production.up.railway.app
/v1/check?url=stripe.com" \
  -H "Authorization: Bearer vr_live_..."
```

**3 · Reverse lookup by vendor**

```
curl "https://vendorradar-production.up.railway.app
/v1/prospect?vendor=twilio&category=sms_messaging" \
  -H "Authorization: Bearer vr_live_..."
```

### Agent-first surface

- [vendorradar-production.up.railway.app
/llms.txt](https://vendorradar-production.up.railway.app
/llms.txt) — orientation file for LLMs
- [vendorradar-production.up.railway.app
/openapi.json](https://vendorradar-production.up.railway.app
/openapi.json) — OpenAPI 3.1 spec for codegen and tool generation
- [vendorradar-production.up.railway.app
/skill](https://vendorradar-production.up.railway.app
/skill) — installable SKILL.md for Claude Code and friends
- [vendorradar-production.up.railway.app
/agents](https://vendorradar-production.up.railway.app
/agents) — agent integration guide
- [/llms-full.txt](/llms-full.txt) — every page of these docs as one markdown document

## Authentication

Authenticated endpoints take a Bearer key: `Authorization: Bearer vr_live_…`. Keys are issued instantly — POST an email to [/v1/keys](/docs/api#keys) and the key arrives in the same response. No verification click, which means an agent can self-provision mid-task.

The plaintext key is shown exactly once and never stored — only a SHA-256 hash is kept server-side, so a lost key cannot be recovered, only replaced. Rotate keys from the [dashboard](/app/keys): create a new key, move traffic, revoke the old one. Revocation is immediate.

## Credits & pricing

Everything is prepaid credits, and every lookup costs the same: **1 credit flat**. If the entity is already in the index you get the answer in under a second; if it isn't, the API live-scans it automatically (15–90s) at no extra cost. Credits never expire.

| Operation | Endpoint | Cost | Latency |
| --- | --- | --- | --- |
| Lookup | `GET /v1/check` (live-scans automatically when unindexed) · `GET /v1/company/{domain}` (never scans) | 1 credit | <1s indexed · 15–90s live scan |
| Reverse-lookup page | `GET /v1/prospect` (10 entities) | 1 credit | <1s |

Packs: $10 → 1,100 credits · $50 → 6,000 · $250 → 35,000. New keys start with 25 free credits; the free plan allows 5 live scans per day (indexed lookups are uncapped) — any pack removes the cap. Machine-readable pricing is served at [/v1/pricing](/docs/api#pricing).

When a request would cost more than the key holds, the API answers 402 Payment Required with the exact shortfall and a `topup_url` — nothing is charged, and the retry after top-up is idempotent. See [Errors](#errors) for the full anatomy.

## Usage limits

| Limit | Value | Behavior |
| --- | --- | --- |
| Request rate | 60 requests / minute / key | 429 with a `Retry-After` header (seconds). Never charged. |
| Free-plan live scans | 5 per day | Indexed lookups are uncapped. Exceeding the allowance returns 429 `daily_scan_limit`; any credit pack removes the cap. |
| Concurrent live scans | 3 per key, platform-wide | Additional scans queue. Concurrent requests for the same domain share one scan (and one charge) — per-domain dedup. |
| Key issuance | 3 keys / IP / day on `/v1/keys` | 429 beyond that; existing keys are unaffected. |
| Request body | 32KB max | 413 `payload_too_large`. |

- Set client timeouts to 120s — an unindexed domain triggers an automatic live scan that legitimately takes 15–90 seconds, at the same 1-credit price.
- The index already covers most entities you'll ask about, so most lookups return in under a second — latency, not price, is the only difference when a live scan kicks in.
- Need more than 60 rpm? `rpm_limit` is configurable per key — [contact us](/enterprise) and we'll raise it.

## Errors

Conventional HTTP status codes with a JSON body. No error response ever charges credits. Every response — success or error — carries an `X-Request-Id` header for correlation; quote it in support requests.

### 400 validation_error

```
{ "error": "validation_error",
  "message": "url must be a domain, e.g. acme.com" }
```

Malformed domain, unknown category slug, bad JSON, or missing required parameter.

### 401 unauthorized

```
{ "error": "unauthorized",
  "message": "Missing or invalid API key. Get one instantly: POST https://vendorradar-production.up.railway.app
/v1/keys {\"email\": ...}" }
```

Missing, malformed, or revoked key. The message self-documents key issuance, so an agent hitting 401 can self-provision without reading docs.

### 402 payment_required

```
{ "error": "payment_required",
  "message": "This request costs 1 credit; balance is 0.",
  "remaining_credits": 0,
  "credits_needed": 1,
  "topup_url": "https://vendorstacks.com/app/billing" }
```

Nothing was charged. Agents: surface `topup_url` and the exact shortfall to a human, pause, and retry after top-up — the retry is idempotent. See the [402 guide](/docs/guides/handle-402s) for the full pattern.

### 404 not_found

```
{ "error": "not_found",
  "message": "No cached profile for that domain." }
```

Unknown route, or a `/v1/company` domain that has never been scanned.

### 413 payload_too_large

```
{ "error": "payload_too_large", "message": "Body exceeds 32KB." }
```

### 429 rate_limited / daily_scan_limit

```
{ "error": "rate_limited",
  "message": "60 requests/minute exceeded.",
  "retry_after_seconds": 12 }
```

```
{ "error": "daily_scan_limit",
  "message": "Free plan allows 5 live scans per day. Indexed lookups remain available; any credit pack removes the cap.",
  "retry_after_seconds": 41230 }
```

`rate_limited` is the per-minute request cap (also sent as a `Retry-After` header) — back off and retry. `daily_scan_limit` is the free plan's live-scan allowance (5/day); indexed lookups keep working, and any pack removes the cap. Neither is ever charged.

### 500 internal_error

```
{ "error": "internal_error",
  "message": "Something broke on our side.",
  "request_id": "9f6f4a1e-…" }
```

Include the `request_id` (also in the `X-Request-Id` header) when contacting support — it pins the exact log line.

### 502 scan_failed

```
{ "error": "scan_failed",
  "message": "Could not reach the target's disclosure pages.",
  "charged": 0 }
```

Fairness rule: a lookup whose live scan fails — unreachable target, no disclosure located — always charges 0 credits.

---

# VendorStacks for Agents

> VendorStacks — the subprocessor disclosure data API. Canonical: https://vendorstacks.com
/docs/agents/


## Overview

VendorStacks is built to be operated by agents, not just called by code a human wrote. Four properties make that work:

- **Instant keys** — `POST /v1/keys` with an email returns the key in the same response. No verification click, so an agent can self-provision mid-task.
- **Deterministic output** — no LLM in the extraction path. The same disclosure always parses to the same stack, every claim carries a verbatim quote, and the API never invents a vendor. Your agent's stochastic layer stays the only one to audit.
- **402 handoff** — running out of credits returns a machine-readable paywall with the exact shortfall and a `topup_url`. Nothing is charged; the retry is idempotent.
- **Self-documenting responses** — error messages carry their own fix (401 tells you how to get a key; 402 tells you what it costs), so an agent can recover without a docs lookup.

### Discovery surface

| Resource | URL | What it is |
| --- | --- | --- |
| Site llms.txt | [vendorstacks.com/llms.txt](/llms.txt) | Orientation + link map for this site |
| Everything, one file | [vendorstacks.com/llms-full.txt](/llms-full.txt) | All docs, guides, and API reference as one markdown document |
| Markdown mirrors | `/docs.md` · `/docs/api.md` · `/docs/guides.md` · `/docs/guides/{slug}.md` · `/docs/agents.md` | Every docs page as plain markdown |
| OpenAPI 3.1 | [vendorradar-production.up.railway.app
/openapi.json](https://vendorradar-production.up.railway.app
/openapi.json) | Spec for codegen and tool generation |
| Agent guide | [vendorradar-production.up.railway.app
/agents](https://vendorradar-production.up.railway.app
/agents) | The API's own agent integration guide |
| Skill file | [vendorradar-production.up.railway.app
/skill](https://vendorradar-production.up.railway.app
/skill) | Installable SKILL.md (see below) |

## Claude Code / Claude skills

The API serves an installable skill file — a markdown document written for a model, teaching the endpoints, the credit economics, and the cost-discipline rules in one fetch.

**Install (Claude Code)**

```
mkdir -p .claude/skills/vendorstacks
curl -s https://vendorradar-production.up.railway.app
/skill \
  -o .claude/skills/vendorstacks/SKILL.md
```

Claude Code discovers skills in `.claude/skills/` automatically and loads this one when a task involves vendor stacks, subprocessor disclosures, entity due diligence, or prospecting by vendor — the skill's frontmatter description is what triggers it.

- What the skill teaches: endpoint map with the flat 1-credit pricing, self-provisioning via `/v1/keys`, timeout discipline for live scans, `found:false` semantics, and the 402 relay pattern.
- For other skill-compatible harnesses (or plain system prompts), fetch the same file and paste it into context — it is self-contained markdown.

## OpenAI function calling

**Function schema**

```
{
  "type": "function",
  "function": {
    "name": "vendorstacks_check_entity",
    "description": "Look up whether an entity (by domain) publishes a subprocessor/DPA disclosure and which vendors it names, as a structured vendor stack across 13 categories with quoted evidence. Every lookup costs 1 credit flat: indexed answers return <1s, unindexed domains live-scan automatically (15-90s, same price) — use a 120s timeout. fresh=true forces a re-scan of an indexed domain at the same price. found=false means no disclosure was located, NOT that the entity uses no vendors.",
    "parameters": {
      "type": "object",
      "properties": {
        "url": { "type": "string", "description": "Domain, e.g. acme.com" },
        "fresh": { "type": "boolean", "description": "Force live re-scan. Default false." }
      },
      "required": ["url"]
    }
  }
}
```

**Usage (Responses API)**

```
import OpenAI from "openai";
const client = new OpenAI();

const response = await client.responses.create({
  model: "gpt-5",
  input: "What vendors does notify-hq.com disclose?",
  tools: [vendorstacksTool], // the schema above
});

for (const item of response.output) {
  if (item.type === "function_call" && item.name === "vendorstacks_check_entity") {
    const { url, fresh } = JSON.parse(item.arguments);
    const data = await fetch(
      `https://vendorradar-production.up.railway.app
/v1/check?url=${encodeURIComponent(url)}` + (fresh ? "&fresh=true" : ""),
      { headers: { Authorization: `Bearer ${process.env.VENDORSTACKS_KEY}` } },
    ).then(r => r.json());
    // feed `data` back as the function_call_output item
  }
}
```

The same schema works in the Assistants API and Chat Completions `tools` array unchanged.

## Anthropic tool use

**Tool definition (Anthropic format)**

```
{
  "name": "vendorstacks_check_entity",
  "description": "Look up whether an entity (by domain) publishes a subprocessor/DPA disclosure and which vendors it names, as a structured vendor stack across 13 categories with quoted evidence. Every lookup costs 1 credit flat: indexed answers return <1s, unindexed domains live-scan automatically (15-90s, same price) — use a 120s timeout. fresh=true forces a re-scan of an indexed domain at the same price. found=false means no disclosure was located, NOT that the entity uses no vendors.",
  "input_schema": {
    "type": "object",
    "properties": {
      "url": { "type": "string", "description": "Domain, e.g. acme.com" },
      "fresh": { "type": "boolean", "description": "Force live re-scan. Default false." }
    },
    "required": ["url"]
  }
}
```

**Messages API example**

```
import Anthropic from "@anthropic-ai/sdk";
const anthropic = new Anthropic();

const msg = await anthropic.messages.create({
  model: "claude-sonnet-4-5",
  max_tokens: 1024,
  tools: [vendorstacksTool], // the definition above
  messages: [{ role: "user", content: "Vet notify-hq.com's subprocessors." }],
});

const call = msg.content.find(b => b.type === "tool_use");
if (call) {
  const { url, fresh } = call.input;
  const data = await fetch(/* same fetch as the OpenAI example */);
  // return as { type: "tool_result", tool_use_id: call.id, content: JSON.stringify(data) }
}
```

## MCP

**Status: hosted MCP server — coming.** Not shipped yet; this section describes what is planned, not what exists today.

The hosted server will speak the Model Context Protocol over streamable HTTP and expose the core operations as MCP tools:

- `check_entity` — wraps `GET /v1/check` (1 credit flat, with the timeout guardrails baked into the tool description)
- `prospect` — wraps `GET /v1/prospect` reverse lookup with pagination
- `balance` — wraps `GET /v1/balance` so agents can check spend headroom for free

Until it ships, the practical equivalents are the [skill file](#claude) for Claude-family harnesses and the function-calling definitions above for everything else — they cover the same three operations.

## LangChain / LlamaIndex

**LangChain (python)**

```
import os, requests
from langchain_core.tools import tool

@tool
def vendorstacks_check_entity(url: str, fresh: bool = False) -> dict:
    """Look up whether an entity (by domain) publishes a subprocessor/DPA disclosure and which vendors it names, as a structured vendor stack across 13 categories with quoted evidence. Every lookup costs 1 credit flat: indexed answers return <1s, unindexed domains live-scan automatically (15-90s, same price) — use a 120s timeout. fresh=true forces a re-scan of an indexed domain at the same price. found=false means no disclosure was located, NOT that the entity uses no vendors."""
    r = requests.get(
        "https://vendorradar-production.up.railway.app
/v1/check",
        params={"url": url, **({"fresh": "true"} if fresh else {})},
        headers={"Authorization": f"Bearer {os.environ['VENDORSTACKS_KEY']}"},
        timeout=120 if fresh else 15,
    )
    if r.status_code == 402:
        body = r.json()
        return {"needs_human": True, "topup_url": body["topup_url"],
                "credits_needed": body["credits_needed"]}
    return r.json()
```

**LlamaIndex (python)**

```
import os, requests
from llama_index.core.tools import FunctionTool

def check_entity(url: str, fresh: bool = False) -> dict:
    """Look up whether an entity (by domain) publishes a subprocessor/DPA disclosure and which vendors it names, as a structured vendor stack across 13 categories with quoted evidence. Every lookup costs 1 credit flat: indexed answers return <1s, unindexed domains live-scan automatically (15-90s, same price) — use a 120s timeout. fresh=true forces a re-scan of an indexed domain at the same price. found=false means no disclosure was located, NOT that the entity uses no vendors."""
    r = requests.get(
        "https://vendorradar-production.up.railway.app
/v1/check",
        params={"url": url, **({"fresh": "true"} if fresh else {})},
        headers={"Authorization": f"Bearer {os.environ['VENDORSTACKS_KEY']}"},
        timeout=120 if fresh else 15,
    )
    return r.json()

vendorstacks_tool = FunctionTool.from_defaults(fn=check_entity,
                                              name="vendorstacks_check_entity")
```

## Cost discipline for agents

Agents spend real credits. These rules belong in the tool description or system prompt — the model, not your wrapper code, makes the spending decisions:

- **Flat pricing, variable latency.** Every lookup is 1 credit — scans cost nothing extra. The thing to budget is wall-clock time: unindexed domains take 15–90s. Pass `fresh=true` only when the user explicitly asked for a re-scan (same price, always slow).
- **120-second timeouts.** Any lookup can trigger a live scan; a short timeout burns the wait and re-spends on retry.
- **Bulk lookups cost 1 credit each — budget time, not just credits.** Before looping a large list, quote the credit total (`count × 1`) and warn that unindexed domains add 15–90s each of wall-clock time. On the free plan, live scans are capped at 5/day (429 `daily_scan_limit`); any pack removes the cap.
- **On 402, relay `topup_url` verbatim** with `credits_needed` and stop. Don't retry, don't paraphrase the URL, don't switch keys.
- **Warn below 20 credits.** `GET /v1/balance` is free — check it at the start of long runs and surface a warning before the balance can strand a task mid-list.

Worked examples: [Handle 402s gracefully](/docs/guides/handle-402s) and [the agent guide](/docs/guides/agent-tool).

## Machine payments (Tempo / MPP)

**Status: in development.** Nothing described here is live; no dates are promised. This section exists so agent builders know the direction.

Today, a 402 needs a human with a card. The plan is to remove that hop for autonomous workloads: VendorStacks intends to accept stablecoin micropayments from agents via the Machine Payments Protocol (MPP) on Tempo, a payments-focused L1. The intended flow:

- The 402 response carries machine-readable payment details (amount, asset, destination) alongside the existing `topup_url`.
- The agent settles the micropayment on-chain from its own wallet — no checkout page, no card, no human.
- The agent retries the same request with proof of settlement; the API verifies and serves it.

The existing 402 contract stays unchanged for humans — machine payment details will be additive fields. When this ships it will be announced on the [blog](/blog) and documented here first.

---

# VendorStacks Guides

> VendorStacks — the subprocessor disclosure data API. Canonical: https://vendorstacks.com
/docs/guides/

- [Enrich a list of domains](https://vendorstacks.com
/docs/guides/enrich-a-domain-list/) (markdown: https://vendorstacks.com
/docs/guides/enrich-a-domain-list.md) — Loop a CSV of domains through /v1/check with rate-limit handling, and write the disclosed stack back out as CSV.
- [Build a vendor-segmented prospect list](https://vendorstacks.com
/docs/guides/vendor-segmented-prospect-list/) (markdown: https://vendorstacks.com
/docs/guides/vendor-segmented-prospect-list.md) — Page through /v1/prospect, dedupe across queries, and export a segment of entities that disclose a given vendor.
- [Give your AI agent the VendorStacks tool](https://vendorstacks.com
/docs/guides/agent-tool/) (markdown: https://vendorstacks.com
/docs/guides/agent-tool.md) — Register an entity-due-diligence tool: function-calling definition, cost-discipline rules, and the 402 relay.
- [Handle 402s and top-ups gracefully](https://vendorstacks.com
/docs/guides/handle-402s/) (markdown: https://vendorstacks.com
/docs/guides/handle-402s.md) — The paywall anatomy, why nothing is ever half-charged, and the retry-after-payment pattern for scripts and agents.
- [Plug into Clay / n8n / Zapier](https://vendorstacks.com
/docs/guides/clay-n8n-zapier/) (markdown: https://vendorstacks.com
/docs/guides/clay-n8n-zapier.md) — HTTP-request node configuration for the three most common no-code pipelines: auth header, URL templates, and output mapping.

---

# Enrich a list of domains

> VendorStacks — the subprocessor disclosure data API. Canonical: https://vendorstacks.com
/docs/guides/enrich-a-domain-list/


Loop a CSV of domains through /v1/check with rate-limit handling, and write the disclosed stack back out as CSV.

The most common first job: you have a CSV of accounts, you want each row enriched with the vendors that entity discloses. Every lookup is 1 credit flat, so a 1,000-row list is exactly 1,000 credits (~$9 on the starter pack). Indexed domains answer in under a second; unindexed ones live-scan automatically at the same price — budget extra wall-clock time for those, not extra credits.

### The loop

**enrich.mjs (Node 18+, no dependencies)**

```
import { readFileSync, writeFileSync } from "node:fs";

const KEY = process.env.VENDORSTACKS_KEY;
const domains = readFileSync("accounts.csv", "utf8")
  .split("\n").map(l => l.split(",")[0].trim()).filter(Boolean);

const out = [["domain", "found", "sms_vendor", "email_vendor", "payments_vendor", "evidence_url"]];

for (const domain of domains) {
  const res = await fetch(
    `https://vendorradar-production.up.railway.app
/v1/check?url=${encodeURIComponent(domain)}`,
    { headers: { Authorization: `Bearer ${KEY}` } },
  );

  if (res.status === 429) {
    // Honor Retry-After, then retry the same domain.
    const wait = Number(res.headers.get("retry-after") ?? 2);
    await new Promise(r => setTimeout(r, wait * 1000));
    domains.push(domain);
    continue;
  }
  if (res.status === 402) throw new Error("Out of credits — top up and re-run.");

  const data = await res.json();
  const first = (cat) => data.vendor_stack?.[cat]?.[0];
  out.push([
    domain,
    String(data.found),
    first("sms_messaging")?.vendor ?? "",
    first("email")?.vendor ?? "",
    first("payments")?.vendor ?? "",
    first("sms_messaging")?.evidence?.source ?? "",
  ]);
}

writeFileSync("enriched.csv", out.map(r => r.join(",")).join("\n"));
```

### Details that matter

- The 429 branch re-queues the same domain after `Retry-After` seconds — you lose no rows and no credits (rate-limited calls are never charged).
- `found: false` means no disclosure was located, not "no vendors". Keep those rows and treat them as unknown.
- The script is idempotent from the API's perspective — re-running charges again for each lookup, so checkpoint `out` to disk periodically for very large lists.
- Want today's truth for a handful of hot accounts? Re-run just those with `&fresh=true` — same 1-credit price, just slower (15–90s; set a 120s timeout).

Full parameter reference: [GET /v1/check](/docs/api#check). Rate-limit specifics: [Usage limits](/docs#limits).

---

# Build a vendor-segmented prospect list

> VendorStacks — the subprocessor disclosure data API. Canonical: https://vendorstacks.com
/docs/guides/vendor-segmented-prospect-list/


Page through /v1/prospect, dedupe across queries, and export a segment of entities that disclose a given vendor.

Reverse lookup answers "who discloses vendor X?" in pages of 10 entities, 1 credit per page. A full segment is: page until exhausted, dedupe (an entity can match several queries if you run more than one), export.

### Page until exhausted

**prospect.mjs**

```
const KEY = process.env.VENDORSTACKS_KEY;
const seen = new Map(); // domain -> row

async function segment(params) {
  for (let page = 1; ; page++) {
    const qs = new URLSearchParams({ ...params, page: String(page) });
    const res = await fetch(`https://vendorradar-production.up.railway.app
/v1/prospect?${qs}`, {
      headers: { Authorization: `Bearer ${KEY}` },
    });
    if (res.status === 429) {
      await new Promise(r => setTimeout(r, 1000 * Number(res.headers.get("retry-after") ?? 2)));
      page--; continue;
    }
    const data = await res.json();
    for (const c of data.companies) {
      if (!seen.has(c.domain)) seen.set(c.domain, { ...c, matched: qs.toString() });
    }
    if (page * data.per_page >= data.total) break;
  }
}

// Two queries, one deduped segment:
await segment({ vendor: "twilio", category: "sms_messaging" });
await segment({ vendor: "sinch", category: "sms_messaging" });

console.log([...seen.values()]
  .map(c => [c.domain, c.evidence?.quote?.replaceAll(",", ";"), c.matched].join(","))
  .join("\n"));
```

### Cost math

| Segment size | Pages | Credits | At $10-pack rate |
| --- | --- | --- | --- |
| 200 entities | 20 | 20 | ~$0.18 |
| 1,400 entities | 140 | 140 | ~$1.27 |
| 5,000 entities | 500 | 500 | ~$4.55 |

- Keep the `evidence.quote` column — it's the line your sequence can cite verbatim.
- Total counts come back on every page (`total`), so you can estimate credits before paging far.
- Enrich the top of the deduped list with full profiles via [/v1/company](/docs/api#company) (1 credit each) when you need the whole stack for tiering.

---

# Give your AI agent the VendorStacks tool

> VendorStacks — the subprocessor disclosure data API. Canonical: https://vendorstacks.com
/docs/guides/agent-tool/


Register an entity-due-diligence tool: function-calling definition, cost-discipline rules, and the 402 relay.

VendorStacks is designed to be operated by agents: instant key issuance, deterministic JSON, machine-readable paywalls. The fastest path is the installable skill file at [vendorradar-production.up.railway.app
/skill](https://vendorradar-production.up.railway.app
/skill) — for Claude Code, save it to `.claude/skills/vendorstacks/SKILL.md`. For explicit function-calling, register this tool:

**Tool definition (Anthropic format; for OpenAI rename input_schema → parameters)**

```
{
  "name": "vendorstacks_check_entity",
  "description": "Look up whether an entity (by domain) publishes a subprocessor/DPA disclosure and which vendors it names, as a structured vendor stack with quoted evidence. Every lookup costs 1 credit flat: indexed answers return <1s, unindexed domains live-scan automatically (15-90s, same price) — use a 120s timeout. fresh=true forces a re-scan at the same price. found=false means no disclosure was located, NOT that the entity uses no vendors.",
  "input_schema": {
    "type": "object",
    "properties": {
      "url": { "type": "string", "description": "Domain, e.g. acme.com" },
      "fresh": { "type": "boolean", "description": "Force live re-scan. Default false." }
    },
    "required": ["url"]
  }
}
```

### The two rules that belong in the description

- **Budget time, not just credits.** Every lookup is 1 credit, so the model can't overspend per call — but `fresh=true` and unindexed domains take 15–90s each. The description tells the model to expect the wait and to re-scan only on explicit request.
- **`found: false` ≠ "no vendors".** An agent that reports "this company uses no third parties" off a found:false is confidently wrong. Spell out the correct reading.

### The 402 relay

**Handler with human handoff**

```
async function vendorstacks_check_entity({ url, fresh = false }) {
  const res = await fetch(
    `https://vendorradar-production.up.railway.app
/v1/check?url=${encodeURIComponent(url)}` + (fresh ? "&fresh=true" : ""),
    { headers: { Authorization: `Bearer ${process.env.VENDORSTACKS_KEY}` } },
  );
  if (res.status === 402) {
    const { topup_url, credits_needed, remaining_credits } = await res.json();
    return {
      needs_human: true,
      message: `Need ${credits_needed} credits (have ${remaining_credits}). Top up: ${topup_url}`,
    };
  }
  return await res.json();
}
```

The human gets a ten-second decision instead of a stack trace; nothing was charged and the retry is idempotent. More on the paywall in [Handle 402s gracefully](/docs/guides/handle-402s); install snippets and the OpenAI-format JSON live on the [skills page](/skills).

---

# Handle 402s and top-ups gracefully

> VendorStacks — the subprocessor disclosure data API. Canonical: https://vendorstacks.com
/docs/guides/handle-402s/


The paywall anatomy, why nothing is ever half-charged, and the retry-after-payment pattern for scripts and agents.

Every VendorStacks request is priced before it runs. If the key's balance can't cover it, the API returns `402 payment_required` and charges nothing — there is no partial state to clean up, which makes the retry logic trivial.

### Anatomy

```
HTTP/1.1 402 Payment Required
X-Request-Id: 9f6f4a1e-…

{
  "error": "payment_required",
  "message": "This request costs 1 credit; balance is 0.",
  "remaining_credits": 3,
  "credits_needed": 10,
  "topup_url": "https://vendorstacks.com/app/billing"
}
```

- `credits_needed` is the price of *this* request, not a suggested pack size.
- `topup_url` goes straight to billing — pass it to whoever holds the card.
- The failed request is safe to retry byte-for-byte after top-up.

### Script pattern: pause, poll, resume

**retry-after-payment**

```
async function callWithTopupPause(url, opts, key) {
  for (;;) {
    const res = await fetch(url, opts);
    if (res.status !== 402) return res;

    const { topup_url, credits_needed } = await res.json();
    console.error(`Paused: need ${credits_needed} credits → ${topup_url}`);

    // Poll the free balance endpoint until a top-up lands, then resume.
    for (;;) {
      await new Promise(r => setTimeout(r, 30_000));
      const bal = await fetch("https://vendorradar-production.up.railway.app
/v1/balance", {
        headers: { Authorization: `Bearer ${key}` },
      }).then(r => r.json());
      if (bal.credit_balance >= credits_needed) break;
    }
  }
}
```

`/v1/balance` is free to call, so the poll loop costs nothing. For agents, don't poll — relay `topup_url` to the human and end the turn; see [the agent guide](/docs/guides/agent-tool). For batch jobs, checking [/v1/pricing](/docs/api#pricing) and `/v1/balance` up front lets you fail before doing half the work.

---

# Plug into Clay / n8n / Zapier

> VendorStacks — the subprocessor disclosure data API. Canonical: https://vendorstacks.com
/docs/guides/clay-n8n-zapier/


HTTP-request node configuration for the three most common no-code pipelines: auth header, URL templates, and output mapping.

There's no SDK to install — VendorStacks is plain JSON over HTTPS with one header, which is exactly what no-code HTTP nodes want. The three configs below all do the same thing: enrich a row that has a `domain` column.

### Clay — HTTP API enrichment column

| Field | Value |
| --- | --- |
| Method | `GET` |
| Endpoint | `https://vendorradar-production.up.railway.app
/v1/check?url={{domain}}` |
| Headers | `Authorization`: `Bearer vr_live_…` (store the key as a Clay secret) |
| Output columns | `found` · `vendor_stack.sms_messaging[0].vendor` · `vendor_stack.payments[0].vendor` · `credit_balance` |

Clay retries non-2xx responses automatically; that's safe here because failed calls are never charged. Add a formula column on `vendor_stack` to compute your fit tier in-table.

### n8n — HTTP Request node

**Node settings**

```
Method:            GET
URL:               https://vendorradar-production.up.railway.app
/v1/check
Query parameters:  url = {{ $json.domain }}
Header parameters: Authorization = Bearer {{ $credentials.vendorStacksKey }}
Options → Timeout: 120000   # only matters if you add fresh=true
On error:          Continue (error output) — route 402 to a Slack alert
```

Put the key in an n8n credential (Header Auth type), not inline in the node. For batch runs, add a Wait node of 1s between iterations to stay under 60 rpm without tripping 429 handling.

### Zapier — Webhooks by Zapier (Custom Request)

**Zap step**

```
Action:  GET
URL:     https://vendorradar-production.up.railway.app
/v1/check?url={{domain}}
Headers: Authorization | Bearer vr_live_…
```

Zapier flattens the JSON response into fields you can map into your CRM step — `vendor_stack sms_messaging 1 vendor` becomes a pickable field. Reverse-lookup segments work the same way against [/v1/prospect](/docs/api#prospect); loop pages with the built-in Looping app, 1 credit per page of 10.

---

# VendorStacks API Reference

> VendorStacks — the subprocessor disclosure data API. Canonical: https://vendorstacks.com
/docs/api/


Base URL: https://vendorradar-production.up.railway.app
 — Bearer auth (`Authorization: Bearer vr_live_…`). Machine-readable: https://vendorradar-production.up.railway.app
/openapi.json · https://vendorradar-production.up.railway.app
/llms.txt

## Keys

### POST /v1/keys

Auth: none · Cost: Free

Issue an API key instantly. POST an email, get a key in the same response — no verification click. New keys receive 25 free credits (free plan: 5 live scans/day; indexed lookups uncapped).

| Parameter | In | Required | Description |
| --- | --- | --- | --- |
| email | body | required | Email address to associate with the key. JSON body field. |

**curl**

```bash
curl -X POST https://vendorradar-production.up.railway.app
/v1/keys \
  -H "Content-Type: application/json" \
  -d '{"email": "you@company.com"}'
```

**JavaScript**

```js
const res = await fetch("https://vendorradar-production.up.railway.app
/v1/keys", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ email: "you@company.com" }),
});
const { api_key, credit_balance } = await res.json();
```

**Response**

```
HTTP/1.1 201 Created

{
  "api_key": "vr_live_8f4b2c91d3e7a650",
  "email": "you@company.com",
  "credit_balance": 25,
  "free_tier": true,
  "note": "Key shown once. Store it now."
}
```

- The key is returned exactly once and cannot be recovered — store it immediately.
- 400 if the email is malformed; 429 if too many keys were requested from the same source recently.

## Lookups

### GET /v1/check?url={domain}

Auth: Bearer key · Cost: 1 credit flat (scans automatically when unindexed)

Check an entity: does this domain publish a subprocessor/DPA disclosure, where does it live, and which vendors are on it. 1 credit flat — indexed answers return <1s; unindexed domains live-scan automatically (15–90s) at the same price.

| Parameter | In | Required | Description |
| --- | --- | --- | --- |
| url | query | required | Domain or URL of the entity to check, e.g. acme.com. Scheme and path are normalized away. |
| fresh | query | optional | true forces a live re-scan even when the entity is indexed. Same 1-credit price, just slower (15–90s). |
| category | query | optional | Restrict the response to one category slug (see /v1/vendors), e.g. sms_messaging. |
| Authorization | header | required | Bearer vr_live_… API key. |

**curl**

```bash
curl "https://vendorradar-production.up.railway.app
/v1/check?url=notify-hq.com" \
  -H "Authorization: Bearer $VENDORSTACKS_KEY"
```

**JavaScript**

```js
const res = await fetch(
  "https://vendorradar-production.up.railway.app
/v1/check?url=" + encodeURIComponent("notify-hq.com"),
  { headers: { Authorization: `Bearer ${process.env.VENDORSTACKS_KEY}` } },
);
const data = await res.json();
// data.found, data.vendor_stack, data.credits_used
```

**Response**

```
HTTP/1.1 200 OK

{
  "domain": "notify-hq.com",
  "found": true,
  "cache": "hit",
  "scanned_at": "2026-07-21T09:14:02Z",
  "disclosure_url": "https://notify-hq.com/legal/subprocessors",
  "vendor_stack": {
    "sms_messaging": [
      {
        "vendor": "twilio",
        "confidence": "vendor_keyword",
        "evidence": {
          "source": "https://notify-hq.com/legal/subprocessors",
          "quote": "Twilio Inc. — SMS and voice message delivery"
        }
      }
    ],
    "email": [
      {
        "vendor": "sendgrid",
        "confidence": "vendor",
        "evidence": {
          "source": "https://notify-hq.com/legal/subprocessors",
          "quote": "SendGrid (Twilio Inc.) — transactional email"
        }
      }
    ],
    "payments": [
      {
        "vendor": "stripe",
        "confidence": "vendor_keyword",
        "evidence": {
          "source": "https://notify-hq.com/legal/dpa",
          "quote": "Stripe, Inc. — payment processing services"
        }
      }
    ]
  },
  "sends_sms": true,
  "credits_used": 1,
  "credit_balance": 24
}
```

- found: false means no disclosure page was located for the entity — it does not mean the entity uses no vendors. See confidence tiers below.
- Fairness rule: if a live scan fails to locate a disclosure page, credits_used is 0.

### GET /v1/company/{domain}

Auth: Bearer key · Cost: 1 credit, never scans

Full profile read for an entity: complete vendor stack across all 13 categories, disclosure sources, and scan history. Guaranteed instant — this endpoint never triggers a scan.

| Parameter | In | Required | Description |
| --- | --- | --- | --- |
| domain | path | required | The entity's domain, e.g. notify-hq.com. |
| Authorization | header | required | Bearer vr_live_… API key. |

**curl**

```bash
curl "https://vendorradar-production.up.railway.app
/v1/company/notify-hq.com" \
  -H "Authorization: Bearer $VENDORSTACKS_KEY"
```

**JavaScript**

```js
const res = await fetch("https://vendorradar-production.up.railway.app
/v1/company/notify-hq.com", {
  headers: { Authorization: `Bearer ${process.env.VENDORSTACKS_KEY}` },
});
const profile = await res.json();
```

**Response**

```
HTTP/1.1 200 OK

{
  "domain": "notify-hq.com",
  "found": true,
  "first_seen": "2026-03-02T11:40:19Z",
  "scanned_at": "2026-07-21T09:14:02Z",
  "disclosure_sources": [
    "https://notify-hq.com/legal/subprocessors",
    "https://notify-hq.com/legal/dpa"
  ],
  "vendor_stack": {
    "sms_messaging": [{ "vendor": "twilio", "confidence": "vendor_keyword", "evidence": { "...": "..." } }],
    "email": [{ "vendor": "sendgrid", "confidence": "vendor", "evidence": { "...": "..." } }],
    "payments": [{ "vendor": "stripe", "confidence": "vendor_keyword", "evidence": { "...": "..." } }],
    "cloud_infra": [{ "vendor": "aws", "confidence": "vendor", "evidence": { "...": "..." } }]
  },
  "sends_sms": true,
  "credits_used": 1,
  "credit_balance": 23
}
```

- Returns 404 with found: false (0 credits) if the domain has never been scanned — use /v1/check, which live-scans unindexed domains automatically.

## Prospecting

### GET /v1/prospect?vendor=&category=&page=

Auth: Bearer key · Cost: 1 credit / page

Reverse lookup: list entities by vendor or category. Returns every entity in the index whose disclosure names the given vendor, optionally narrowed by category — 10 entities per page, each with its quoted evidence.

| Parameter | In | Required | Description |
| --- | --- | --- | --- |
| vendor | query | optional | Canonical vendor slug, e.g. twilio. At least one of vendor or category is required. |
| category | query | optional | Category slug, e.g. sms_messaging. Combine with vendor to narrow. |
| page | query | optional | 1-based page number. Default 1. Each page of 10 entities costs 1 credit. |
| Authorization | header | required | Bearer vr_live_… API key. |

**curl**

```bash
curl "https://vendorradar-production.up.railway.app
/v1/prospect?vendor=twilio&category=sms_messaging&page=1" \
  -H "Authorization: Bearer $VENDORSTACKS_KEY"
```

**JavaScript**

```js
const params = new URLSearchParams({
  vendor: "twilio",
  category: "sms_messaging",
  page: "1",
});
const res = await fetch("https://vendorradar-production.up.railway.app
/v1/prospect?" + params, {
  headers: { Authorization: `Bearer ${process.env.VENDORSTACKS_KEY}` },
});
const { companies, total } = await res.json();
```

**Response**

```
HTTP/1.1 200 OK

{
  "vendor": "twilio",
  "category": "sms_messaging",
  "page": 1,
  "per_page": 10,
  "total": 1417,
  "companies": [
    {
      "domain": "notify-hq.com",
      "confidence": "vendor_keyword",
      "evidence": {
        "source": "https://notify-hq.com/legal/subprocessors",
        "quote": "Twilio Inc. — SMS and voice message delivery"
      },
      "scanned_at": "2026-07-21T09:14:02Z"
    },
    { "domain": "shipfast.io", "confidence": "vendor", "evidence": { "...": "..." } }
  ],
  "credits_used": 1,
  "credit_balance": 22
}
```

- Each page of 10 entities costs 1 credit. Page numbering starts at 1.

## Account

### GET /v1/balance

Auth: Bearer key · Cost: Free

Current credit balance and lifetime usage for the calling key.

| Parameter | In | Required | Description |
| --- | --- | --- | --- |
| Authorization | header | required | Bearer vr_live_… API key. |

**curl**

```bash
curl "https://vendorradar-production.up.railway.app
/v1/balance" \
  -H "Authorization: Bearer $VENDORSTACKS_KEY"
```

**JavaScript**

```js
const res = await fetch("https://vendorradar-production.up.railway.app
/v1/balance", {
  headers: { Authorization: `Bearer ${process.env.VENDORSTACKS_KEY}` },
});
const { credit_balance } = await res.json();
```

**Response**

```
HTTP/1.1 200 OK

{
  "credit_balance": 22,
  "free_tier": true,
  "lifetime_credits_used": 3,
  "topup_url": "https://vendorstacks.com/pricing"
}
```

## Reference

### GET /v1/pricing

Auth: none · Cost: Free

Machine-readable pricing: operation costs and available credit packs. Point your agent here before it spends.

**curl**

```bash
curl "https://vendorradar-production.up.railway.app
/v1/pricing"
```

**JavaScript**

```js
const res = await fetch("https://vendorradar-production.up.railway.app
/v1/pricing");
const { operations, packs } = await res.json();
```

**Response**

```
HTTP/1.1 200 OK

{
  "operations": {
    "lookup": { "credits": 1, "note": "flat; live-scans automatically when unindexed" },
    "prospect_page": { "credits": 1, "note": "per page of 10 entities" }
  },
  "packs": [
    { "usd": 10,  "credits": 1100 },
    { "usd": 50,  "credits": 6000 },
    { "usd": 250, "credits": 35000 }
  ],
  "free_tier": { "credits": 25, "live_scans_per_day": 5 }
}
```

### GET /v1/vendors

Auth: none · Cost: Free

The vendor taxonomy: all 13 categories and the canonical vendor slugs recognized in each, for building reverse-lookup queries.

**curl**

```bash
curl "https://vendorradar-production.up.railway.app
/v1/vendors"
```

**JavaScript**

```js
const res = await fetch("https://vendorradar-production.up.railway.app
/v1/vendors");
const { categories } = await res.json();
// categories.sms_messaging -> ["twilio", "sinch", ...]
```

**Response**

```
HTTP/1.1 200 OK

{
  "categories": {
    "sms_messaging": ["twilio", "sinch", "messagebird", "vonage", "plivo"],
    "email": ["sendgrid", "mailgun", "postmark", "ses", "iterable"],
    "payments": ["stripe", "adyen", "braintree", "checkout_com"],
    "...": ["..."]
  }
}
```

### GET /healthz

Auth: none · Cost: Free

Liveness probe. Returns 200 when the API is up. No auth, never rate limited.

**curl**

```bash
curl https://vendorradar-production.up.railway.app
/healthz
```

**Response**

```
HTTP/1.1 200 OK

{ "ok": true }
```

## Confidence tiers
| Tier | Strength | Meaning |
| --- | --- | --- |
| vendor_keyword | Strongest | The disclosure row names the vendor and describes the matching category function (e.g. “Twilio Inc. — SMS delivery”). Both signals present in the quoted text. |
| vendor | Strong | The disclosure row names the vendor; the category is assigned from the vendor's canonical classification rather than the row's own wording. |
| vendor_inferred_sms | Inferred | No explicit SMS vendor row, but the disclosure's wording indicates SMS capability (e.g. a messaging aggregator listed for “customer communications”). Only used for the sends_sms flag, never for vendor_stack entries. |

`found: false` means no disclosure page could be located — not that the entity uses no vendors.