# SpiderIQ — Full Content > Web Scraping & Lead Intelligence at Scale > Full Markdown of all published, indexable content on this site, for LLM crawlers. ## Readability Test Source: https://spideriq.ai/readability-test ## Test Headline Test Description [Click Me Now](/) ## Leeds Plumbing Co Source: https://spideriq.ai/leeds-plumbing ## Expert Plumbing in Leeds Fast, reliable, and affordable plumbing services in Leeds. Available 24/7. ## SpiderIQ Source: https://spideriq.ai/home Start your agency ## SpiderIQ — Built for {business} Source: https://spideriq.ai/invite Start your agency ## We told every agent to send a parameter our own API rejected Source: https://spideriq.ai/blog/format-parameter-every-endpoint SpiderIQ tells every agent that connects to it to send `?format=llm`. The parameter turns on a guidance envelope: alongside the data, the response explains what the resource is for, what it is *not* for, and what to call next. We document `?format=json` as the way to turn it back off. For most of this year, on most of our endpoints, both of those values came back `422 Unprocessable Entity`. ## Following the documentation was the way to break the call The failure had a shape worth describing precisely, because the obvious reading of it is wrong. ![The documentation on one side, the refusal on the other](https://media.cdn.spideriq.ai/content/cli_5z6zs9k9po75hd4s/blog/1788103045-f212fa47.webp) A middleware reads `format` off the raw query string and decides whether to splice in the guidance block. It never rewrites the query string. So an agent that sent no `format` parameter at all was never affected: it got the envelope, and it got a 200. The 422 was reserved for callers who did what we told them to do. Send the value from our own documentation, and the request was refused at the door. That is a worse failure than a broken endpoint, because it punishes exactly the reader who paid attention. ## Fifteen files each declared their own list Underneath, there was no single definition of what `format` meant. Fifteen separate files each hand-wrote their own allowed list as a regular expression, most of them some variant of `^(yaml|md)$`. Each one was written by someone solving the endpoint in front of them, and each one was individually reasonable. ![Fifteen files, each with its own list of allowed formats](https://media.cdn.spideriq.ai/content/cli_5z6zs9k9po75hd4s/blog/1788103046-71f4e2e0.webp) Collectively they were a contract nobody owned. `llm` and `json` were absent from most of them, so most of the API rejected them. Nothing enforced consistency, which meant every new endpoint was free to invent a sixteenth vocabulary, and several had. The census is the part that surprised us. Counting by hand, or by grep, gives the wrong answer in both directions: two files declare the pattern through a shared constant that a text search never sees, and FastAPI encodes an optional constrained parameter in a JSON shape that a naive reader scores as *unconstrained*, which is to say *healthy*. Taking the count from the generated API schema instead, with those two shapes resolved properly: **114 endpoints accept a **`**format**`** parameter, and 94 of them refused **`**llm**`**, **`**json**`**, or both.** ## One definition owns every format parameter The fix is unglamorous. One module now owns every `format` vocabulary in the API. The ordinary case is a single four-value enum: `json` and `llm` return JSON, `yaml` returns `text/yaml`, `md` returns `text/markdown`. ![One shared definition, enforced by a build check](https://media.cdn.spideriq.ai/content/cli_5z6zs9k9po75hd4s/blog/1788103047-d1349c79.webp) Three endpoints genuinely differ, and they are declared rather than improvised. Two render Markdown but have no YAML renderer, so `yaml` is a deliberate 422 there. Two are exports whose natural default is CSV. Encoding those as named types, in the same module as the ordinary case, is what stops the next author from hand-rolling a sixteenth pattern. A build check enforces it. Any route that declares `format` with a raw regular expression now fails the build. The rule is no longer a convention that a reviewer has to remember. Widening what is accepted is not the same as ignoring what is sent. `?format=bogus` is still a 422, carrying a structured error that names the parameter. An unsupported value is refused rather than quietly answered in JSON, which was never the bug. ## The same script reported 94 on the old image and 0 on the new A census that reports zero problems is worth nothing until you have watched it report a non-zero number. A checker that resolves nothing compares nothing to nothing and passes, and it looks identical to a clean bill of health. ![The same census script reporting 94 on the old image and 0 on the new](https://media.cdn.spideriq.ai/content/cli_5z6zs9k9po75hd4s/blog/1788103048-a5f13345.webp) So before trusting the result, we booted the previously deployed image and ran the byte-identical script against both: ``` endpoints refusing llm or json previous image 114 94 current image 114 0 ``` Same denominator on both sides. That the script can and does report 94 is what makes the 0 a measurement rather than an assumption. The same discipline was applied to the guard itself: before trusting it, one endpoint was deliberately reverted to the old pattern to confirm the build actually went red and named the offending routes. ## What changed for you If you send no `format` parameter, nothing about your calls has changed. If you followed our documentation and sent `?format=llm` or `?format=json`, the call that used to return 422 now returns 200, with the content type the value asks for. `yaml` and `md` behave exactly as they did. ## Your leads were always a source. We finally pointed at them. Source: https://spideriq.ai/blog/enrich-the-leads-you-already-own Every lead source we had ever built asked the same question: where do we go and buy these people? Outscraper and Apify buy them from Google Maps. A CSV upload skips the purchase but still starts from a file somebody handed you. Sortlist reads a public directory. Four sources, four ways of pointing outward. Meanwhile a customer of ours had 205,923 businesses sitting in their own account, and no way to do anything with them in bulk. That is the thing this release fixes. `internal` is the fifth source, and it points the other way. ## The corpus was always there Every campaign you run leaves leads behind. Some got their websites crawled and never got their emails verified. Some were sourced in a run where you deliberately turned everything off because you only wanted the addresses that day. Some are eighteen months old and have grown a website since. They are all still there, in your account, already paid for. Until this week the only way to act on them in bulk was to buy them again from the same provider you bought them from the first time, and let deduplication sort it out. Now you pick them. ## Two ways to point at your own leads The first is the one people ask for by name. **Pick a past run.** You remember the campaign, you remember what it did and did not do, and you want to finish it. Choose the campaign, or one job inside it, and it goes. The second is the one that turned out to matter more. **Query the corpus.** A condition-and-group filter over 84 fields, the shape anyone who has used Airtable already knows: where `has website` is not empty, and `has email` is empty, and city is any of Berlin or Hamburg. Add a condition. Add a group. Change `and` to `or`. Both end in the same place, and it is worth saying what that place is, because it is the part I would get wrong if I were reading about it instead of building it. ## The filter is what gets saved. Not the leads. There is a button that says *select all 10,008 in these results*, and behind it is the only interesting engineering decision in the whole feature. The obvious implementation is that the browser collects ten thousand row ids and posts them. It is obvious and it is wrong twice. It is wrong on correctness, because the list was assembled at 14:02 and the run starts at 14:09, and in between your other jobs finished and changed what the answer should be. It is wrong on isolation, because a request body carrying ten thousand ids is a request body that could carry someone else's ten thousand ids. So the browser sends the question, never the answer. The filter goes to the server, gets validated against a closed list of fields, and is stored as a selection. At submit time the server re-runs that same validated question against your corpus, applies the gates, and takes what it finds. The only thing that crosses the boundary is an id the server already issued to you. ![Select-all sends the question, not the rows: the browser posts a filter, the server re-runs it at dispatch](https://media.cdn.spideriq.ai/content/cli_5z6zs9k9po75hd4s/general/1787572163-9e5ed108.webp) The visible consequence is small and honest: your count can move between when you look at it and when you run it, and we say so on the screen. The invisible consequence is that there has never been a code path where your run could read another tenant's leads, because no request on this surface has ever contained a lead. ## Two numbers, and the second one is the product Every screen here shows you two counts, and the gap between them is the whole reason to build this rather than just re-buying. **Matched** is how many leads your question describes. **Eligible** is how many of those would actually gain something from the stages you ticked. A business whose site was crawled last Tuesday gains nothing from crawling it again. It matches your filter perfectly and it is not in the run. On one live account, `has no email` matches 9,887 leads and only 986 of them are eligible once you switch on verification, because the other 8,901 have already been through it. Selecting five thousand and enriching two hundred is not a bug report. It is what a mature corpus looks like, and the number that matters is the small one, because that is what gets billed, that is what the ceiling is checked against, and that is what determines how long you wait. ![Matched is what your filter describes; eligible is what would actually gain from the stages you chose](https://media.cdn.spideriq.ai/content/cli_5z6zs9k9po75hd4s/general/1787572164-162774cb.webp) The corollary catches people. Eligibility is measured against the stages, so the moment you tick one more stage the number moves. I watched it happen while writing this: a campaign showing six matched and five eligible went to six and six the instant I added the site-crawl stage, because the lead that had nothing to gain from verification had plenty to gain from a crawl. There is no such thing as "how many eligible leads do I have". Only "how many for these stages". ## The chat box we did not build The original plan had a natural-language box. Type *"restaurants in Berlin with a website but no email"*, we parse it, we build the filter. We dropped it, and the reason is the more interesting half of this release. Your agent is already better at this than any parser we would ship. So instead of writing a language model into the product, we made the API legible to the ones you already have. The field catalogue endpoint returns the 84 fields **and the grammar** — every operator each type accepts, which operators take no value, which take a list, the aliases that normalise, all fourteen refusal codes, and a complete worked example with its English translation sitting next to it. A Claude or Cursor session reads that once and writes valid filters. It does not guess a column name, because an unlisted field is refused and never reaches the query. It does not need us to have anticipated the sentence. ![The API serves its own grammar, so the agent writes the filter and no parser sits in the product](https://media.cdn.spideriq.ai/content/cli_5z6zs9k9po75hd4s/general/1787572165-75e047f5.webp) Six CLI commands and seven MCP tools cover the same path, and there is a skill on the marketplace that documents the recipe. The natural-language layer is real. We just do not host it. ## The number I did not expect One thing worth recording, because it nearly shipped broken. Counting eligible leads means asking "which of these have no crawl yet", which in SQL is a `NOT EXISTS`. Written in the `WHERE` clause, Postgres plans it as an anti-join and it is fast. Moved into a `count(*) FILTER (...)` — same answer, tidier-looking code — it becomes a correlated subplan that runs once per candidate row. On the test account that took the count from 42.9 ms to 10,934 ms, past the statement timeout, into a 500 on the confirmation screen. On the largest account, 205,923 businesses, it was not slow, it was never. ![The same question, two query plans: one pass across the set, or the same work repeated once per row](https://media.cdn.spideriq.ai/content/cli_5z6zs9k9po75hd4s/general/1787572167-fa9f29a4.webp) The fix restored the anti-join and the numbers came back: 20 ms for a single stage on the test account, 450 ms on the large one. What makes it worth writing down is that a plan-shape test already existed and passed the entire time, because it explained a different query than the one that regressed. A test that asserts on a query plan has to name which builder's output it is explaining. Ours did not, so it guarded the wrong statement while the right one got 255 times slower. ## What it costs Nothing at the source, because nothing is bought. But free at the source is not free: the enrichment still runs, once per eligible lead, at the same per-stage price as any other run. The screen prices it on eligible, not matched, which is why the distinction gets the space it does above. And one thing to leave switched off unless you mean it. VayaPin publishes a permanent public profile page for every lead in the run, and deleting the run does not delete those pages. --- Live now on the dashboard under **Flows → Bulk Lead Sourcing → Your existing leads**, on the API, and through the CLI, the MCP tools and the skill. The [how-to is here](https://spideriq.ai/docs/lead-generation/enrich-leads-you-already-have) and the [API reference is here](https://spideriq.ai/docs/lead-generation/bulk-lead-sourcing-api). ## Your leads do not have to come from us Source: https://spideriq.ai/blog/bring-your-own-leads Your leads do not have to come from us. That sounds like a strange thing to put on our own blog. It is the most useful thing I can tell you. SpiderIQ is two products wearing one name. The first finds businesses. The second turns a name and a website into something you can actually sell to. Most people meet the first half and assume that is the whole thing. It is the second half that does the work. And the second half does not care where the lead came from. ## You already have leads Almost everyone does. A list you bought. An export from a tool you are leaving. A conference badge scan. A spreadsheet a colleague keeps in their downloads folder. Rows and rows of company names, and maybe a website, and maybe an email that bounced four months ago. That list is not useless. It is unfinished. ![Chalk sketch: a sparse single line labelled BARE ROW, an arrow, and a filled rectangle labelled FINISHED LEAD with one gold dot on its edge.](https://media.cdn.spideriq.ai/content/cli_5z6zs9k9po75hd4s/blog/1786388083-a924ab04.webp) There are plenty of places to buy raw business records. If you already have rows, buying them again is not the answer. What you need is everything that turns a row into a conversation. ## What enrichment actually means here Hand SpiderIQ a lead and it goes to work on it. Not once. In stages. **It reads the website.** Give it a URL and it crawls the site, pulls the emails, the phone numbers and the social links, and comes back with what is actually published rather than what a database remembers. **It verifies every email.** An address that exists is not the same as an address that receives. Verification checks deliverability before the address is stored, so you find out now instead of finding out from your bounce rate. **It recovers what is missing.** A business with a Facebook page and no website is not a dead end. Social enrichment reads the public profiles and pulls back the email, the phone or the real site the listing never had. **It reads people, not just companies.** A LinkedIn URL comes back as a profile. A company name comes back as an account brief: the domain, the registry filing, the team, the verified addresses. **Then it thinks about what it found.** This is the part I like most. The enriched lead goes through a language model that reads the site and writes the analysis: what this company does, who they sell to, where you fit. You get a paragraph a human would write, on every row, without a human writing it. ![Chalk sketch: a horizontal track with three circles labelled READ SITE, VERIFY EMAIL and WRITE ANALYSIS, and a gold dot travelling past the third.](https://media.cdn.spideriq.ai/content/cli_5z6zs9k9po75hd4s/blog/1786388102-0e93b4c8.webp) That is the difference between a list and a pipeline. A list tells you a company exists. A pipeline tells you why to call them. ## Both worlds Here is the shape of it. You bring leads from wherever you like. They land in your SpiderIQ CRM alongside the ones we found. Every one of them, whatever door it came through, gets the same treatment: read, verified, recovered, analysed. You do not have to choose a side. You do not have to abandon a source you are happy with, and you do not have to accept a raw row as finished work. ## Open by construction, not by promise Any company can say it is open. Here is what it means in our code, which is a more honest place to look. A lead source in SpiderIQ is an adapter behind a five-method contract: submit, poll, fetch, parse, usage. That is the entire surface. Registering a new source is one line, and nothing downstream changes. There is a test in the suite whose whole job is to assert exactly that: a newly registered source becomes available with no other code change. ![Chalk sketch: a small square labelled PLUGS IN aligned with a matching notch cut into a tall rectangle labelled ONE CONTRACT, one gold dot between them.](https://media.cdn.spideriq.ai/content/cli_5z6zs9k9po75hd4s/blog/1786388113-6858f254.webp) The enrichment chain never learns where a lead came from. The website crawler does not know. The email verifier does not know. Your CRM does not know. The result comes back in the same shape either way, which means every export, every filter and every report you already built keeps working. ![Chalk sketch: a solid square and a dashed square labelled TWO DOORS, both arrowed to a single gold hexagon labelled SAME SHAPE.](https://media.cdn.spideriq.ai/content/cli_5z6zs9k9po75hd4s/blog/1786388134-fc94de3f.webp) That is what open looks like when it is structural. Not a promise about the roadmap. A seam that is already there. ## Where we are honest When this post first went up, one source was wired and it read Google Maps. The others were seams, not switches, and I would rather have written that sentence than let you discover it. That is no longer where we are. All four are live now, and the section below says what they are. Deduplication runs across everything inside a single run, on an exact key: the map place id first, then the website domain, then the phone, then the name and town together. Across two separate runs it does not deduplicate. A business you pulled last week can arrive again this week. And enrichment is not free. Every stage you turn on is work someone pays for. That is why the confirmation screen does the arithmetic out loud before anything runs, and why a run that gets refused costs nothing at all. ## What changed since I wrote this *Updated 13 August 2026.* Two of the four sources buy Google Maps records: Outscraper, and Apify, which is priced per record, so the confirmation screen can show you a real figure rather than only a record count. The other two are what this post was actually about. CSV upload and JSON upload take a file you already have and run every row through the same chain. Nothing is bought at the source, so the confirmation screen stops doing purchase arithmetic and starts doing enrichment arithmetic: leads times the stages you switched on. A spreadsheet can carry any headers, so we do not guess at them. We propose a mapping for the columns we recognise, you confirm or correct it, and the run stays blocked until at least one column identifies the business. There is deliberately no column you can map onto an email address. Contact data is something the pipeline produces, never something the file supplies. ## Start with one You do not need a migration plan. Take one lead you already have, hand us the website, and look at what comes back. If the answer is more than you had, bring the rest. ## Your AI Agents Can Now Run Email — One Inbox or a Thousand Source: https://spideriq.ai/blog/ai-agents-can-now-run-your-email Email is where most "AI employee" demos quietly fall apart. Reading one tidy message in a slide is easy. Running a real inbox — hundreds of threads, replies that have to land in the right conversation, a sender that has to stay out of the spam folder — is a different job. Today that job gets a lot easier. **Your AI agents can now run email. One inbox, or a thousand.** ## The same skill, whatever your scale Whether you point a single agent at a single mailbox or a whole fleet at hundreds of them, it's the same capability underneath. Your agent reads what came in, understands the full conversation, replies in your voice, and keeps everything tidy — without a human in the loop for the routine 90%. It already had the tools. What it was missing was the *know-how* — the difference between "technically able to send an email" and "handles your inbox like someone who's done it for years." That's what we just shipped: a playbook of **recipes** your agent follows, so it does the right thing by default. ## What that looks like in practice - **Triage at machine speed.** Master inbox across every mailbox, full-text search, whole-thread context — then mark, label, snooze, or clear a hundred messages in one move. - **Replies that sound like you.** Your agent writes in plain language; we turn it into a polished, on-brand email — with reusable templates and signatures it previews before sending. - **Outreach that actually lands.** Connected to Smartlead, lemlist, or Instantly? Your agent watches warmup and deliverability across every sending account, so your cold email reaches inboxes instead of spam folders. - **Sends you can trust.** No "I think it sent" — the agent confirms delivery before it reports back. ## Built for one agent or an army of them This isn't a feature bolted onto a chatbot. It's a first-class skill that travels everywhere your agents already work — Claude, Cursor, and the OPVS marketplace — so the agent you hire tomorrow speaks email on day one, exactly like the one you have today. Point an agent at your mail and let it work. Then point ten more at it. *Available now. Ask your SpiderIQ agent to check your inbox — and watch it actually handle it.* ## Stop paying Cloudinary, Vimeo, and Mux — it's all in SpiderIQ now (and it's AI-native) Source: https://spideriq.ai/blog/media-library-dashboard ![One AI-native media platform replacing Cloudinary, Vimeo and Mux](https://media.cdn.spideriq.ai/content/cli_vwpm8o5er5qnnb4w/blog/1779880168-787e56c0.webp) If you run a modern web presence, your media has probably been living across three separate bills: **Cloudinary** to host and transform images, **Vimeo** to host and stream video, and **Mux** to get analytics on how those videos perform. Three vendors, three logins, three invoices — for what is really one job. Meet **SpiderMedia**. It turns everything we already host for you — every image and video across all of your sites — into one managed Library you can browse, search, upload to, organize, and measure. It is already included in your SpiderIQ account, and it was built AI-native from the ground up. ## Everything Cloudinary does — image hosting + on-the-fly transforms Host every image, then get any version of it on demand. Need a 16:9 thumbnail for a card, a web-optimized hero, a square avatar, or a modern format like WebP or AVIF? Ask for it by URL. SpiderMedia derives that exact variant from your original, caches it on our CDN, and serves it — no re-uploading, no separate image CDN to wire up, and no per-transform invoice at the end of the month. ## Everything Vimeo does — video hosting + adaptive streaming Upload a file straight from the Library, or import a video by URL from YouTube, Vimeo, or a direct link. We host it, transcode it into an adaptive quality ladder up to 1080p, and stream it from our own player — your videos, on your domain, with no third-party logo and no upload caps to babysit. ## Everything Mux does — analytics, populated from day one Because we run the player ourselves, every video shows **views and viewers from day one** — no SDK to install, no separate analytics product to bolt on. Track a 7-, 30-, or 90-day trend, see how a video compares to the platform average, and dig into watch-time and retention. The numbers are simply there, the moment a video goes live. ## One Library for your team Open the new **Media** tab in your dashboard to see every asset across every storage tier in one place. Filter by type or status, search by name or tag, drag-and-drop or batch-upload new files, import a video by URL, edit folders, tags, and alt-text, copy a public URL in one click, and delete cleanly — with no orphaned files left behind. ![The SpiderMedia Library in action — browse, upload, and per-video analytics](https://media.cdn.spideriq.ai/content/cli_vwpm8o5er5qnnb4w/blog/spidermedia-demo-1781137994.gif) ## And it's AI-native — the same media, for every agent This is the part no other media host gives you. The entire Library is a first-class surface for automation, over three channels that all read and write the **same catalog** — no fork, no glue code: - **AI automations** call the REST API (Bearer auth) to upload, search, transform, and pull analytics programmatically. - **Claude Code** and any terminal workflow drive it through the `spideriq media` CLI. - **Your OPVS, Hermes, and OpenClaw agents** use the MCP tools and skills — derive an image, search the catalog, import a video, read a video's stats — natively, with no integration work. What a person sees in the Media tab is exactly what an agent sees over MCP. One source of truth for your whole team, human and machine. ## Why AI-native matters Media is no longer something only people touch. Your agents build pages, generate the right-sized image for each layout, import and caption video, and report on what is working — all against the same catalog, with the same permissions, in real time. A traditional DAM bolted onto three vendors can't do that. SpiderMedia was designed for it. ## It's already in your account There is no new vendor to sign up for and nothing to migrate. Open the **Media** tab to browse your library, upload your first asset, or check how a video is performing. And point your agents at the API, CLI, and MCP — they can start building with your media today. This is AI-native media hosting: hosting, streaming, transforms, and analytics, on one catalog, wired for your agents from day one. ## SpiderVerify: 99.2% Accuracy on 10M Emails Source: https://spideriq.ai/blog/spiderverify-accuracy Email verification is table stakes for outbound. But accuracy claims are rarely backed by rigorous benchmarks. We decided to change that. ## The Benchmark We took 10 million real email addresses from agency campaigns (anonymized) and verified them against ZeroBounce, NeverBounce, and our own SpiderVerify engine. ## Results SpiderVerify achieved 99.2% accuracy — the highest in our benchmark. False positive rate was 0.3%, compared to 1.1% for the next best competitor. ## How We Did It Our secret is a multi-signal approach: MX record validation, SMTP handshake simulation, mailbox existence probing, and a proprietary deliverability model trained on 500M+ historical send outcomes. ## Running Celery at Scale: Lessons from 2M Daily Tasks Source: https://spideriq.ai/blog/celery-at-scale Celery powers all async work in SpiderIQ — from email sending to web scraping to report generation. At 2 million tasks per day, we have learned a few things. ## Worker Pool Tuning We run prefork workers with a concurrency of 8 per node. Gevent was tempting for I/O-bound tasks, but the debugging complexity was not worth the throughput gain. ## The Retry Storm In March 2026, a Redis failover caused 200K tasks to retry simultaneously. The resulting thundering herd took down our entire worker fleet. We now use exponential backoff with jitter and circuit breakers. ## Redis Memory Management Celery result backends can eat Redis memory fast. We set result_expires to 3600 seconds and use Redis Cluster with 6 shards to distribute the load. ## Self-Hosting SpiderIQ: Docker Compose on a Single €9 VPS Source: https://spideriq.ai/blog/self-hosting-guide Some agencies can't use cloud SaaS — regulatory requirements, data residency, or just strong opinions about ownership. That's why SpiderIQ offers a fully self-hosted option. Here's how to get it running on a single €9/month Hetzner VPS. ## Prerequisites • A VPS with at least 4GB RAM and 2 vCPUs (Hetzner CX22 works perfectly) • Ubuntu 24.04 LTS • A domain pointed to your server's IP • Docker and Docker Compose v2 installed ## The Stack Our Docker Compose file spins up six services: • PostgreSQL 16 — primary data store • Redis 7 (cluster mode) — caching, rate limiting, job queues • Celery workers (x2) — async task execution for scraping and email delivery • FastAPI gateway — the API layer that MCP tools call • Caddy — automatic TLS via Let's Encrypt, reverse proxy • SpiderIQ Agent — the orchestration engine ## Quick Start ```bash git clone https://github.com/spideriq/self-hosted.git cd self-hosted cp .env.example .env # Edit .env with your domain and license key docker compose up -d ``` The first boot takes about 90 seconds. Caddy will automatically provision your TLS certificate. Once healthy, your agent is available at https://your-domain.com/agent. ## Performance On the €9 CX22, we benchmarked: 12 concurrent MCP tool calls, 500 email verifications/minute, and 3 simultaneous scraping jobs. For teams under 5 users, this is more than enough. ## Backups We include a daily backup cron that dumps PostgreSQL and uploads to S3-compatible storage. Retention: 14 days by default, configurable via BACKUP_RETENTION_DAYS. Need help? Join our self-hosted Discord channel or email infra@spideriq.ai. ## From Zero to 500-Lead Campaign in Under 4 Minutes Source: https://spideriq.ai/blog/zero-to-campaign Agency founders ask us the same question every week: "How fast can I actually go from zero to a live outbound campaign?" The answer: under 4 minutes. No exaggeration. Let me show you. ## Step 1: The Prompt (0:00) Open your SpiderIQ agent and type: "Find 500 Italian restaurants in Berlin with websites and email addresses. Verify the emails. Create an outreach campaign introducing our restaurant marketing package." That's it. One sentence. The agent takes over from here. ## Step 2: SpiderMaps Scraping (0:05 – 1:30) The agent calls SpiderMaps' search_places tool with category=restaurant, cuisine=italian, city=Berlin. It pages through results automatically, collecting business names, addresses, websites, phone numbers, and any publicly listed emails. Within 90 seconds, you have 500+ raw leads with location data. ## Step 3: SpiderVerify Validation (1:30 – 2:45) Raw emails are unreliable. The agent pipes every address through SpiderVerify's bulk_validate tool, which checks MX records, SMTP handshake, and our proprietary deliverability model. Invalid and risky addresses get flagged. Result: ~380 verified, deliverable email addresses. A 76% hit rate from raw scraping is excellent. ## Step 4: SpiderMail Campaign (2:45 – 3:50) The agent drafts a 3-email sequence (intro, value prop, soft CTA), creates a campaign in SpiderMail, attaches the verified list, and schedules sends with smart throttling (50/hour to protect deliverability). You review the drafts — one click to approve, and the campaign goes live. ## Total Time: 3 minutes, 50 seconds No CSV exports. No dashboard hopping. No Zapier glue. Just a prompt and an agent that knows your entire toolstack. Try it yourself — sign up at app.spideriq.ai and run your first campaign today. ## 157 MCP Tools: How We Built the Largest Agent-Callable Surface in Martech Source: https://spideriq.ai/blog/157-mcp-tools When we launched SpiderIQ, we had 12 tools. Today, we ship 157 MCP tools across 5 dedicated servers — the largest agent-callable surface in martech. Here's how we got here, and why MCP is the right bet. ## Why Model Context Protocol? Most automation platforms expose REST APIs. Agents can call them, sure — but every integration requires bespoke glue code: auth wrappers, pagination handling, error mapping. MCP eliminates this entirely by providing a standardized tool-calling interface that any LLM agent understands natively. We evaluated GraphQL federation, gRPC, and OpenAPI-based approaches. MCP won because it optimizes for the agent's mental model, not the developer's. A tool is a function with typed parameters and a structured response — nothing more. ## The Five-Server Architecture Our 157 tools are distributed across five MCP servers, each owning a bounded domain: • SpiderMaps (42 tools) — Location intelligence, POI scraping, geo-enrichment • SpiderVerify (31 tools) — Email/phone validation, deliverability scoring • SpiderMail (28 tools) — Campaign orchestration, template management, sending • SpiderCRM (34 tools) — Contact management, deal pipeline, activity tracking • SpiderAdmin (22 tools) — Billing, usage analytics, team management ## Multi-Tenant Isolation Every MCP call passes through our isolation layer. The agent's session token is scoped to a specific tenant, and tool execution happens inside a sandboxed worker pool. No tenant can ever read another tenant's data — even if the agent hallucinates a cross-tenant ID. ### Schema Validation Each tool's input schema is validated at the gateway level before execution begins. We use JSON Schema draft-2020 with strict mode enabled, rejecting any undeclared properties. This catches malformed agent calls before they hit business logic. ## What's Next We're working on tool composition — letting agents chain multiple tools into atomic transactions. Think: "scrape 50 restaurants, verify all emails, create a campaign, and schedule it" as a single, rollback-safe operation. If you're building agent infrastructure, we'd love to compare notes. Reach out at engineering@spideriq.ai. ## Your Agent's Identity: OPVS Address, One Account, Avatar & Rename Source: https://spideriq.ai/docs/agent-identity Every AI agent you connect to SpiderIQ is now a **first-class account** — a stable identity you can name, recognize, and reach, instead of an anonymous key that multiplies every time it opens a new folder. This applies across SpiderIQ, SpiderPublish, SpiderGate, and SpiderMail — they share the same agent authentication, so your agent's identity is the same everywhere. ## What's new ### Every agent has its own OPVS email address When you create an API key or PAT for an agent, it's now paired with a free **OPVS email address** (for example `witty-otter-35@opvs.run`) that your agent keeps. You can change the handle at any time. ### Agents can message each other Because every agent has a real address, agents can send messages to one another — across tools and platforms. Your Claude Desktop session can message your Antigravity session; your ChatGPT can drop a note to your OPVS agent. ### One agent, one identity — no more token sprawl Connecting the same agent from a new project or machine no longer spawns a brand-new token. It is one identity you can see, rename, rotate the key on, and message — without losing its history. ### Give your agent an avatar Add a display name and an avatar image to each agent, so when you review reports and dashboards — *who did what* — you recognize your agent at a glance instead of squinting at an "api key name." ## Where to find it Open **Dashboard → Team → Agents**. Each agent shows its name, avatar, OPVS address, scopes, and recent activity, with actions to rotate or revoke its key, rename its handle, and open a message thread. ## Renaming an agent's handle From the Agents list, click the pencil on an agent's OPVS address. Type a new handle — availability is checked live — and choose what happens to the old address: - **Alias (recommended)** — the old address keeps routing to the agent for a grace period, then expires. Nothing in flight breaks. - **Reserve** — the old address is retired and cannot be taken by anyone else. - **Release** — the old address is freed immediately. Your agent's key does not change when you rename it. ## FAQ **Do I have to do anything?** No. Existing keys keep working, and every agent already has an address. **Can I change the address?** Yes — rename it any time from the Agents page. **Does renaming break messages already sent to the old address?** Not if you choose *Alias* — the old address keeps delivering during the grace period. ## Jobs Source: https://spideriq.ai/docs/api-reference/jobs ## `POST` /api/v1/jobs/submit **Submit Job** Submit a new scraping job to SpiderIQ distributed scraping platform ## Supported Job Types ### 1. SpiderMaps (`type: "spiderMaps"`) Scrape business listings from maps services. - **Required**: Either `url` OR `search_query` in payload - **AI Usage**: None (0 tokens) - **Processing Time**: 30-90 seconds for 20 results - **Features**: Reviews, photos, multi-language support ### 2. SpiderSite (`type: "spiderSite"`) Intelligent website crawling with AI-powered lead generation. - **Required**: `url` in payload - **AI Usage**: Opt-in (0 tokens by default, ~500-3,800 tokens if AI enabled) - **Processing Time**: 5-60 seconds depending on pages and AI features - **Features**: - v2.7.0: AI Context Engine (smart markdown compendiums + R2 storage) - v2.4.0: SPA auto-detection with Playwright - v2.3.0: Sitemap-first crawling - v2.2.1: AI opt-in defaults (zero cost unless enabled) - v2.1.0: Multilingual (36+ European languages) ## SpiderIQ Features - **Automatic deduplication**: Returns cached job if submitted within 24 hours - **Distributed processing**: Jobs queued to RabbitMQ, processed by workers across multiple VPS servers - **Priority support**: 0-10 (higher number = processed first) - **Async processing**: Returns job ID immediately (<100ms), poll for results ## Authentication Requires Bearer token: `Authorization: Bearer ::` Contact admin to register a client and receive credentials. ## Queue Limits - SpiderMaps queue: 10,000 jobs maximum - SpiderSite queue: 5,000 jobs maximum ## Returns Job ID and initial status. Use `/jobs/{job_id}/status` to check progress. **YAML Input (v2.60.0)**: Accepts Content-Type: text/yaml for AI agent submissions. **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/jobs/submit' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "type": "spiderMaps", "payload": {}, "priority": 0 }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/jobs/submit", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"type": "spiderMaps", "payload": {}, "priority": 0}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/jobs/submit", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"type": "spiderMaps", "payload": {}, "priority": 0}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"type": "spiderMaps", "payload": {}, "priority": 0}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/jobs/submit", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `201` — Successful Response - `422` — Validation Error ## `GET` /api/v1/jobs/{job_id}/status **Get status of a single job** Retrieve queue/processing/completion status for a job owned by the authenticated client. Does NOT return results — use `/jobs/{job_id}/results` for completed data. Supports `?format=json|yaml|md|llm` for AI-agent-friendly output. **Parameters** - `job_id` (path, string, required) - `format` (query, any, optional) — Response format. `json` (default) and `llm` return JSON; `yaml` returns text/yaml; `md` returns text/markdown. Any other value is a 422. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/jobs/{job_id}/status' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/jobs/{job_id}/status", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/jobs/{job_id}/status", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/jobs/{job_id}/status", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Job status retrieved successfully - `400` — Invalid job ID format - `401` — Authentication failed - `403` — Client account is inactive - `404` — Job not found, or it doesn't belong to the authenticated client. If the id you sent is this client's INTERNAL `jobs.id` rather than the public `jobs.job_id`, the response names the public id in `error.public_job_id` -- the internal id is never accepted on a client route. A job belonging to another client returns the plain form, with no hint. - `422` — Validation error in query parameters - `429` — Rate limit exceeded ## `GET` /api/v1/jobs/{job_id}/results **Get results for a completed job** Retrieve the results payload for a job owned by the authenticated client. Returns 200 with `data` when the job is `completed`, 202 while `queued`/`processing` (poll again), 410 when `failed` or `cancelled`. Response shape is flat (v2.7.6+) — social platforms live at `data.linkedin`, `data.twitter`, etc. Supports `?format=json|yaml|md|llm` for AI-agent-friendly output. **Parameters** - `job_id` (path, string, required) - `format` (query, any, optional) — Response format. `json` (default) and `llm` return JSON; `yaml` returns text/yaml; `md` returns text/markdown. Any other value is a 422. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/jobs/{job_id}/results' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/jobs/{job_id}/results", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/jobs/{job_id}/results", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/jobs/{job_id}/results", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Job completed successfully - results are available - `202` — Job is queued or processing - results not ready yet. Poll this endpoint to check for completion. - `400` — Invalid job ID format - `401` — Authentication failed - `403` — Client account is inactive - `404` — Job not found, or it doesn't belong to the authenticated client. If the id you sent is this client's INTERNAL `jobs.id` rather than the public `jobs.job_id`, the response names the public id in `error.public_job_id` -- the internal id is never accepted on a client route. A job belonging to another client returns the plain form, with no hint. - `410` — Job failed or was cancelled - no results available - `422` — Internal validation error - Response serialization failed. This should NOT occur in normal operation. If you encounter this error, please contact support as it indicates a bug in the API. - `429` — Rate limit exceeded ## `GET` /api/v1/jobs/{job_id}/workflow-results **Get workflow results for SpiderMaps job** Get complete workflow results for a SpiderMaps job submitted with workflow configuration. Returns aggregated data from SpiderMaps → SpiderSite → SpiderVerify chain. **Query Parameters:** - `wait=true` (default): Block until all businesses complete or timeout - `wait=false`: Return current status immediately (for polling) **Note:** Only works for SpiderMaps jobs submitted with `workflow` configuration. For jobs without workflow, use the regular `/jobs/{job_id}/results` endpoint. **Timeouts:** - SpiderSite: 5 minutes per business - SpiderVerify: 2 minutes per business - Maximum total: 10 minutes **Formats:** `?format=json` (default) / `llm` -> JSON; `yaml` -> text/yaml; `md` -> text/markdown. Any other value is a 422. Before SDS-26 this route declared no `format` at all, so FastAPI DROPPED it silently. **Parameters** - `job_id` (path, string, required) - `wait` (query, boolean, optional) — Wait for completion (blocking). If false, returns current state immediately. - `format` (query, any, optional) — Response format. `json` (default) and `llm` return JSON; `yaml` returns text/yaml; `md` returns text/markdown. Any other value is a 422. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/jobs/{job_id}/workflow-results' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/jobs/{job_id}/workflow-results", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/jobs/{job_id}/workflow-results", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/jobs/{job_id}/workflow-results", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/jobs/list **List jobs for the authenticated client** Return a paginated list of jobs belonging to the authenticated client. Filter by `status_filter` (queued/processing/completed/failed/cancelled) and/or `type_filter` (spiderMaps/spiderSite/...). Supports `?format=json|yaml|md|llm` for AI-agent-friendly output. `status` / `type` / `per_page` are accepted as DEPRECATED aliases of `status_filter` / `type_filter` / `page_size` — see the handler docstring. **Parameters** - `status_filter` (query, any, optional) — Filter by job status - `type_filter` (query, any, optional) — Filter by job type - `page` (query, integer, optional) — Page number - `page_size` (query, any, optional) — Items per page (default 50) - `format` (query, any, optional) — Response format. `json` (default) and `llm` return JSON; `yaml` returns text/yaml; `md` returns text/markdown. Any other value is a 422. - `status` (query, any, optional) — DEPRECATED alias of `status_filter`. - `type` (query, any, optional) — DEPRECATED alias of `type_filter`. - `per_page` (query, any, optional) — DEPRECATED alias of `page_size`. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/jobs/list' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/jobs/list", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/jobs/list", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/jobs/list", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Paginated list of jobs for the authenticated client - `401` — Authentication failed - `403` — Client account is inactive - `422` — Invalid query parameters (status, type, page, page_size, format) - `429` — Rate limit exceeded ## `DELETE` /api/v1/jobs/{job_id} **Cancel a queued or processing job** Cancel a job owned by the authenticated client. Only jobs in `queued` or `processing` state can be cancelled; `completed`, `failed`, or already-`cancelled` jobs return 400. Cancellation is best-effort — a worker that has already claimed the job may still complete it before seeing the flag. **Parameters** - `job_id` (path, string, required) :::codegroup ```bash curl -X DELETE 'https://spideriq.ai/api/v1/jobs/{job_id}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.delete( "https://spideriq.ai/api/v1/jobs/{job_id}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/jobs/{job_id}", { method: "DELETE", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("DELETE", "https://spideriq.ai/api/v1/jobs/{job_id}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Job cancelled successfully - `400` — Invalid job ID format or job cannot be cancelled in its current state - `401` — Authentication failed - `403` — Client account is inactive - `404` — Job not found, or it doesn't belong to the authenticated client. If the id you sent is this client's INTERNAL `jobs.id` rather than the public `jobs.job_id`, the response names the public id in `error.public_job_id` -- the internal id is never accepted on a client route. A job belonging to another client returns the plain form, with no hint. - `429` — Rate limit exceeded - `422` — Validation Error ## `POST` /api/v1/jobs/spiderCompanyData/submit **Submit company registry lookup job** Look up company records in US (SEC EDGAR), UK (Companies House), and EU business registries. Returns officers, filings, registered address, and structured metadata. Returns a `job_id` — poll `/api/v1/jobs/{job_id}/results` for the aggregated registry payload. **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/jobs/spiderCompanyData/submit' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "payload": { "mode": "search", "name": "Apple Inc", "identifier": "0000320193", "country": "US", "vat_number": "GB123456789", "limit": 10, "include_financials": false, "financials_mode": "url_only", "test": false }, "priority": 5 }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/jobs/spiderCompanyData/submit", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"payload": {"mode": "search", "name": "Apple Inc", "identifier": "0000320193", "country": "US", "vat_number": "GB123456789", "limit": 10, "include_financials": false, "financials_mode": "url_only", "test": false}, "priority": 5}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/jobs/spiderCompanyData/submit", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"payload": {"mode": "search", "name": "Apple Inc", "identifier": "0000320193", "country": "US", "vat_number": "GB123456789", "limit": 10, "include_financials": false, "financials_mode": "url_only", "test": false}, "priority": 5}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"payload": {"mode": "search", "name": "Apple Inc", "identifier": "0000320193", "country": "US", "vat_number": "GB123456789", "limit": 10, "include_financials": false, "financials_mode": "url_only", "test": false}, "priority": 5}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/jobs/spiderCompanyData/submit", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `201` — Job created and queued successfully - `401` — Authentication failed - `403` — Client account is inactive - `422` — Validation error - Invalid payload - `429` — Rate limit exceeded - `500` — Internal server error - `503` — Queue service unavailable ## `POST` /api/v1/jobs/spiderVayapin/submit **Submit VayaPin profile / outreach job** Create or update VayaPin business profiles from extracted leads. Triggers the VayaPin automation stack (profile enrichment, outreach scheduling). Returns a `job_id` — poll `/api/v1/jobs/{job_id}/results` for per-profile outcomes. **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/jobs/spiderVayapin/submit' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "payload": { "business_name": "Restaurant Tight", "country_code": "DK", "gmaps_coordinates": { "latitude": 0.0, "longitude": 0.0 }, "place_id": "string", "markdown_url": "https://media.spideriq.ai/crawls/abc123.md", "markdown_compendium": "# Company Name\n\nDescription of the business...", "business_phone": "+4533116996", "business_address": "string", "original_website": "https://restauranttight.dk", "domain": "restauranttight.dk", "street": "string", "city": "string", "postal_code": "string", "state": "string", "country": "string", "emails_verified": [ { "email": {}, "status": {}, "is_deliverable": {} } ], "facebook": "string", "instagram": "string", "linkedin": "string", "twitter": "string" }, "priority": 5 }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/jobs/spiderVayapin/submit", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"payload": {"business_name": "Restaurant Tight", "country_code": "DK", "gmaps_coordinates": {"latitude": 0.0, "longitude": 0.0}, "place_id": "string", "markdown_url": "https://media.spideriq.ai/crawls/abc123.md", "markdown_compendium": "# Company Name\n\nDescription of the business...", "business_phone": "+4533116996", "business_address": "string", "original_website": "https://restauranttight.dk", "domain": "restauranttight.dk", "street": "string", "city": "string", "postal_code": "string", "state": "string", "country": "string", "emails_verified": [{"email": {}, "status": {}, "is_deliverable": {}}], "facebook": "string", "instagram": "string", "linkedin": "string", "twitter": "string"}, "priority": 5}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/jobs/spiderVayapin/submit", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"payload": {"business_name": "Restaurant Tight", "country_code": "DK", "gmaps_coordinates": {"latitude": 0.0, "longitude": 0.0}, "place_id": "string", "markdown_url": "https://media.spideriq.ai/crawls/abc123.md", "markdown_compendium": "# Company Name\n\nDescription of the business...", "business_phone": "+4533116996", "business_address": "string", "original_website": "https://restauranttight.dk", "domain": "restauranttight.dk", "street": "string", "city": "string", "postal_code": "string", "state": "string", "country": "string", "emails_verified": [{"email": {}, "status": {}, "is_deliverable": {}}], "facebook": "string", "instagram": "string", "linkedin": "string", "twitter": "string"}, "priority": 5}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"payload": {"business_name": "Restaurant Tight", "country_code": "DK", "gmaps_coordinates": {"latitude": 0.0, "longitude": 0.0}, "place_id": "string", "markdown_url": "https://media.spideriq.ai/crawls/abc123.md", "markdown_compendium": "# Company Name\n\nDescription of the business...", "business_phone": "+4533116996", "business_address": "string", "original_website": "https://restauranttight.dk", "domain": "restauranttight.dk", "street": "string", "city": "string", "postal_code": "string", "state": "string", "country": "string", "emails_verified": [{"email": {}, "status": {}, "is_deliverable": {}}], "facebook": "string", "instagram": "string", "linkedin": "string", "twitter": "string"}, "priority": 5}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/jobs/spiderVayapin/submit", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `201` — Job created and queued successfully - `401` — Authentication failed - `403` — Client account is inactive - `422` — Validation error - Invalid payload - `429` — Rate limit exceeded - `500` — Internal server error - `503` — Queue service unavailable ## `POST` /api/v1/jobs/spiderMail/submit **Submit mail send / reply job** Send an email or reply to an existing thread through the SpiderMail worker. Delivery goes through the client's configured mailbox (SMTP or provider API). Returns a `job_id` — poll `/api/v1/jobs/{job_id}/results` for the delivery outcome and message ID. **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/jobs/spiderMail/submit' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "payload": { "action": "send", "from_email": "alice@company.com", "to": [ "bob@prospect.com" ], "cc": [ "string" ], "subject": "Quick question about your services", "body_text": "string", "body_html": "string", "attachments": [ { "filename": {}, "content_base64": {}, "mime_type": {} } ], "reply_to_message_id": 0, "reply_all": false, "test": false }, "priority": 0 }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/jobs/spiderMail/submit", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"payload": {"action": "send", "from_email": "alice@company.com", "to": ["bob@prospect.com"], "cc": ["string"], "subject": "Quick question about your services", "body_text": "string", "body_html": "string", "attachments": [{"filename": {}, "content_base64": {}, "mime_type": {}}], "reply_to_message_id": 0, "reply_all": false, "test": false}, "priority": 0}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/jobs/spiderMail/submit", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"payload": {"action": "send", "from_email": "alice@company.com", "to": ["bob@prospect.com"], "cc": ["string"], "subject": "Quick question about your services", "body_text": "string", "body_html": "string", "attachments": [{"filename": {}, "content_base64": {}, "mime_type": {}}], "reply_to_message_id": 0, "reply_all": false, "test": false}, "priority": 0}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"payload": {"action": "send", "from_email": "alice@company.com", "to": ["bob@prospect.com"], "cc": ["string"], "subject": "Quick question about your services", "body_text": "string", "body_html": "string", "attachments": [{"filename": {}, "content_base64": {}, "mime_type": {}}], "reply_to_message_id": 0, "reply_all": false, "test": false}, "priority": 0}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/jobs/spiderMail/submit", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `201` — Job created and queued successfully - `401` — Authentication failed - `403` — Client account is inactive - `422` — Validation error - Invalid payload - `429` — Rate limit exceeded - `500` — Internal server error - `503` — Queue service unavailable ## `POST` /api/v1/jobs/spiderMaps/submit **Submit Google Maps scraping job** Scrape Google Maps business listings by search query or direct Maps URL. Optionally enrich via an attached `workflow` (chain SpiderSite + SpiderVerify). Returns a `job_id` — poll `/api/v1/jobs/{job_id}/results` for the flat response. Per-VPS rate limit applies (10 jobs/min). **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/jobs/spiderMaps/submit' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "payload": { "url": "https://www.google.com/maps/place/...", "search_query": "coffee shops in San Francisco", "max_results": 20, "extract_reviews": false, "extract_photos": false, "lang": "en", "country": "string", "headless": true, "test": false, "store_images": true, "validate_phones": true, "fuzziq_enabled": true, "fuzziq_unique_only": true, "skip_proxy": false, "maps_source": "maps", "workflow": { "spidersite": {}, "spiderverify": {}, "vayapin": {}, "social_media_enrichment": {}, "smartlead": {}, "filter_social_media": {}, "filter_review_sites": {}, "filter_directories": {}, "filter_maps": {} } }, "priority": 0 }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/jobs/spiderMaps/submit", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"payload": {"url": "https://www.google.com/maps/place/...", "search_query": "coffee shops in San Francisco", "max_results": 20, "extract_reviews": false, "extract_photos": false, "lang": "en", "country": "string", "headless": true, "test": false, "store_images": true, "validate_phones": true, "fuzziq_enabled": true, "fuzziq_unique_only": true, "skip_proxy": false, "maps_source": "maps", "workflow": {"spidersite": {}, "spiderverify": {}, "vayapin": {}, "social_media_enrichment": {}, "smartlead": {}, "filter_social_media": {}, "filter_review_sites": {}, "filter_directories": {}, "filter_maps": {}}}, "priority": 0}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/jobs/spiderMaps/submit", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"payload": {"url": "https://www.google.com/maps/place/...", "search_query": "coffee shops in San Francisco", "max_results": 20, "extract_reviews": false, "extract_photos": false, "lang": "en", "country": "string", "headless": true, "test": false, "store_images": true, "validate_phones": true, "fuzziq_enabled": true, "fuzziq_unique_only": true, "skip_proxy": false, "maps_source": "maps", "workflow": {"spidersite": {}, "spiderverify": {}, "vayapin": {}, "social_media_enrichment": {}, "smartlead": {}, "filter_social_media": {}, "filter_review_sites": {}, "filter_directories": {}, "filter_maps": {}}}, "priority": 0}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"payload": {"url": "https://www.google.com/maps/place/...", "search_query": "coffee shops in San Francisco", "max_results": 20, "extract_reviews": false, "extract_photos": false, "lang": "en", "country": "string", "headless": true, "test": false, "store_images": true, "validate_phones": true, "fuzziq_enabled": true, "fuzziq_unique_only": true, "skip_proxy": false, "maps_source": "maps", "workflow": {"spidersite": {}, "spiderverify": {}, "vayapin": {}, "social_media_enrichment": {}, "smartlead": {}, "filter_social_media": {}, "filter_review_sites": {}, "filter_directories": {}, "filter_maps": {}}}, "priority": 0}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/jobs/spiderMaps/submit", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `201` — Job created and queued successfully - `401` — Authentication failed - `403` — Client account is inactive - `422` — Validation error - Invalid payload - `429` — Rate limit exceeded - `500` — Internal server error - `503` — Queue service unavailable ## `POST` /api/v1/jobs/spiderSite/submit **Submit website crawl job** Crawl a website and extract contact information, company vitals, social links, and AI-enriched lead data. Supports single-URL and multi-page crawls (best-first / BFS / DFS). Returns a `job_id` — poll `/api/v1/jobs/{job_id}/results` for the flat (v2.7.6+) response. **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/jobs/spiderSite/submit' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "payload": { "url": "https://example.com", "mode": "contacts", "overrides": {}, "max_pages": 10, "crawl_strategy": "bestfirst", "target_pages": [ "contact", "about", "team", "news", "blog" ], "enable_spa": true, "spa_timeout": 30, "extract_team": false, "extract_company_info": false, "extract_pain_points": false, "product_description": "string", "icp_description": "string", "timeout": 30, "compendium": { "enabled": {}, "max_chars": {}, "cleanup_level": {}, "separator": {}, "include_in_response": {}, "remove_duplicates": {}, "priority_sections": {} }, "custom_ai_prompt": { "enabled": {}, "system_prompt": {}, "user_prompt": {}, "json_schema": {}, "output_field_name": {}, "model": {}, "temperature": {}, "max_tokens": {} }, "test": false, "fuzziq_enabled": true, "fuzziq_unique_only": true, "extraction": {} }, "priority": 0 }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/jobs/spiderSite/submit", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"payload": {"url": "https://example.com", "mode": "contacts", "overrides": {}, "max_pages": 10, "crawl_strategy": "bestfirst", "target_pages": ["contact", "about", "team", "news", "blog"], "enable_spa": true, "spa_timeout": 30, "extract_team": false, "extract_company_info": false, "extract_pain_points": false, "product_description": "string", "icp_description": "string", "timeout": 30, "compendium": {"enabled": {}, "max_chars": {}, "cleanup_level": {}, "separator": {}, "include_in_response": {}, "remove_duplicates": {}, "priority_sections": {}}, "custom_ai_prompt": {"enabled": {}, "system_prompt": {}, "user_prompt": {}, "json_schema": {}, "output_field_name": {}, "model": {}, "temperature": {}, "max_tokens": {}}, "test": false, "fuzziq_enabled": true, "fuzziq_unique_only": true, "extraction": {}}, "priority": 0}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/jobs/spiderSite/submit", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"payload": {"url": "https://example.com", "mode": "contacts", "overrides": {}, "max_pages": 10, "crawl_strategy": "bestfirst", "target_pages": ["contact", "about", "team", "news", "blog"], "enable_spa": true, "spa_timeout": 30, "extract_team": false, "extract_company_info": false, "extract_pain_points": false, "product_description": "string", "icp_description": "string", "timeout": 30, "compendium": {"enabled": {}, "max_chars": {}, "cleanup_level": {}, "separator": {}, "include_in_response": {}, "remove_duplicates": {}, "priority_sections": {}}, "custom_ai_prompt": {"enabled": {}, "system_prompt": {}, "user_prompt": {}, "json_schema": {}, "output_field_name": {}, "model": {}, "temperature": {}, "max_tokens": {}}, "test": false, "fuzziq_enabled": true, "fuzziq_unique_only": true, "extraction": {}}, "priority": 0}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"payload": {"url": "https://example.com", "mode": "contacts", "overrides": {}, "max_pages": 10, "crawl_strategy": "bestfirst", "target_pages": ["contact", "about", "team", "news", "blog"], "enable_spa": true, "spa_timeout": 30, "extract_team": false, "extract_company_info": false, "extract_pain_points": false, "product_description": "string", "icp_description": "string", "timeout": 30, "compendium": {"enabled": {}, "max_chars": {}, "cleanup_level": {}, "separator": {}, "include_in_response": {}, "remove_duplicates": {}, "priority_sections": {}}, "custom_ai_prompt": {"enabled": {}, "system_prompt": {}, "user_prompt": {}, "json_schema": {}, "output_field_name": {}, "model": {}, "temperature": {}, "max_tokens": {}}, "test": false, "fuzziq_enabled": true, "fuzziq_unique_only": true, "extraction": {}}, "priority": 0}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/jobs/spiderSite/submit", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `201` — Job created and queued successfully - `401` — Authentication failed - `403` — Client account is inactive - `422` — Validation error - Invalid payload - `429` — Rate limit exceeded - `500` — Internal server error - `503` — Queue service unavailable ## `POST` /api/v1/jobs/spiderVerify/submit **Submit email verification job** Verify one or more email addresses against SMTP, MX, DNSBL, disposable-domain, catch-all, and Gravatar checks. Accepts `email` (single) or `emails` (bulk, up to payload limit). Returns a `job_id` — poll `/api/v1/jobs/{job_id}/results` for per-address verdicts. **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/jobs/spiderVerify/submit' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "payload": { "email": "john@example.com", "emails": [ "john@example.com", "jane@example.com" ], "from_email": "string", "hello_name": "string", "check_gravatar": false, "smtp_timeout_secs": 45, "check_dnsbl": false, "test": false, "fuzziq_enabled": true, "fuzziq_unique_only": true }, "priority": 0 }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/jobs/spiderVerify/submit", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"payload": {"email": "john@example.com", "emails": ["john@example.com", "jane@example.com"], "from_email": "string", "hello_name": "string", "check_gravatar": false, "smtp_timeout_secs": 45, "check_dnsbl": false, "test": false, "fuzziq_enabled": true, "fuzziq_unique_only": true}, "priority": 0}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/jobs/spiderVerify/submit", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"payload": {"email": "john@example.com", "emails": ["john@example.com", "jane@example.com"], "from_email": "string", "hello_name": "string", "check_gravatar": false, "smtp_timeout_secs": 45, "check_dnsbl": false, "test": false, "fuzziq_enabled": true, "fuzziq_unique_only": true}, "priority": 0}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"payload": {"email": "john@example.com", "emails": ["john@example.com", "jane@example.com"], "from_email": "string", "hello_name": "string", "check_gravatar": false, "smtp_timeout_secs": 45, "check_dnsbl": false, "test": false, "fuzziq_enabled": true, "fuzziq_unique_only": true}, "priority": 0}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/jobs/spiderVerify/submit", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `201` — Job created and queued successfully - `401` — Authentication failed - `403` — Client account is inactive - `422` — Validation error - Invalid payload - `429` — Rate limit exceeded - `500` — Internal server error - `503` — Queue service unavailable ## `POST` /api/v1/jobs/spiderPeople/submit **Submit people-search / enrichment job** Search for people by name, title, or company, or enrich an existing lead with email/phone/social. Operates in `search` or `enrich` mode depending on the payload. Returns a `job_id` — poll `/api/v1/jobs/{job_id}/results` for aggregated people records. **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/jobs/spiderPeople/submit' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "payload": { "mode": "profile", "linkedin_url": "https://www.linkedin.com/in/john-doe", "search_query": "5 AI engineers in Israel", "search_limit": 10, "country_code": "string", "company_url": "https://www.linkedin.com/company/pleo", "max_employees": 100, "profile_mode": "short", "person_name": "string", "test": false, "fuzziq_enabled": true, "fuzziq_unique_only": true }, "priority": 0 }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/jobs/spiderPeople/submit", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"payload": {"mode": "profile", "linkedin_url": "https://www.linkedin.com/in/john-doe", "search_query": "5 AI engineers in Israel", "search_limit": 10, "country_code": "string", "company_url": "https://www.linkedin.com/company/pleo", "max_employees": 100, "profile_mode": "short", "person_name": "string", "test": false, "fuzziq_enabled": true, "fuzziq_unique_only": true}, "priority": 0}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/jobs/spiderPeople/submit", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"payload": {"mode": "profile", "linkedin_url": "https://www.linkedin.com/in/john-doe", "search_query": "5 AI engineers in Israel", "search_limit": 10, "country_code": "string", "company_url": "https://www.linkedin.com/company/pleo", "max_employees": 100, "profile_mode": "short", "person_name": "string", "test": false, "fuzziq_enabled": true, "fuzziq_unique_only": true}, "priority": 0}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"payload": {"mode": "profile", "linkedin_url": "https://www.linkedin.com/in/john-doe", "search_query": "5 AI engineers in Israel", "search_limit": 10, "country_code": "string", "company_url": "https://www.linkedin.com/company/pleo", "max_employees": 100, "profile_mode": "short", "person_name": "string", "test": false, "fuzziq_enabled": true, "fuzziq_unique_only": true}, "priority": 0}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/jobs/spiderPeople/submit", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `201` — Job created and queued successfully - `401` — Authentication failed - `403` — Client account is inactive - `422` — Validation error - Invalid payload - `429` — Rate limit exceeded - `500` — Internal server error - `503` — Queue service unavailable ## `POST` /api/v1/jobs/spiderPhone/submit **Submit phone lookup / outreach job** Look up phone numbers from Google Maps listings or trigger phone-based outreach actions via the iPhone bridge. `platform` and `action` in the payload select the behaviour. Returns a `job_id` — poll `/api/v1/jobs/{job_id}/results` for the result set. **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/jobs/spiderPhone/submit' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "payload": { "platform": "linkedin", "action": "string", "params": {}, "config": { "action_timeout": 30.0, "max_retries": 3, "max_items": 100, "capture_screenshots": true, "behavior": {} }, "test": false }, "priority": 0 }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/jobs/spiderPhone/submit", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"payload": {"platform": "linkedin", "action": "string", "params": {}, "config": {"action_timeout": 30.0, "max_retries": 3, "max_items": 100, "capture_screenshots": true, "behavior": {}}, "test": false}, "priority": 0}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/jobs/spiderPhone/submit", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"payload": {"platform": "linkedin", "action": "string", "params": {}, "config": {"action_timeout": 30.0, "max_retries": 3, "max_items": 100, "capture_screenshots": true, "behavior": {}}, "test": false}, "priority": 0}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"payload": {"platform": "linkedin", "action": "string", "params": {}, "config": {"action_timeout": 30.0, "max_retries": 3, "max_items": 100, "capture_screenshots": true, "behavior": {}}, "test": false}, "priority": 0}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/jobs/spiderPhone/submit", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `201` — Job created and queued successfully - `401` — Authentication failed - `403` — Client account is inactive - `422` — Validation error - Invalid payload - `429` — Rate limit exceeded - `500` — Internal server error - `503` — Queue service unavailable ## `POST` /api/v1/jobs/spiderMapsEnrich/submit **Submit Google Maps business-data enrichment job** Enrich an existing Google Maps business (by place_id or search) with reviews, photos, opening hours, and extended attributes. Use when `/spiderMaps` results need deeper detail. Returns a `job_id` — poll `/api/v1/jobs/{job_id}/results` for the enriched record. **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/jobs/spiderMapsEnrich/submit' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "payload": { "google_place_id": "string", "google_cid": "string", "place_url": "string", "original_data": {}, "exclude_vps": [ "string" ], "use_proxy": true, "enrich_options": { "reviews": {}, "photos": {}, "popular_times": true, "store_images": true }, "snowball": { "enabled": false, "max_depth": 2, "max_places_per_seed": 10 }, "test": false }, "priority": 5 }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/jobs/spiderMapsEnrich/submit", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"payload": {"google_place_id": "string", "google_cid": "string", "place_url": "string", "original_data": {}, "exclude_vps": ["string"], "use_proxy": true, "enrich_options": {"reviews": {}, "photos": {}, "popular_times": true, "store_images": true}, "snowball": {"enabled": false, "max_depth": 2, "max_places_per_seed": 10}, "test": false}, "priority": 5}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/jobs/spiderMapsEnrich/submit", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"payload": {"google_place_id": "string", "google_cid": "string", "place_url": "string", "original_data": {}, "exclude_vps": ["string"], "use_proxy": true, "enrich_options": {"reviews": {}, "photos": {}, "popular_times": true, "store_images": true}, "snowball": {"enabled": false, "max_depth": 2, "max_places_per_seed": 10}, "test": false}, "priority": 5}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"payload": {"google_place_id": "string", "google_cid": "string", "place_url": "string", "original_data": {}, "exclude_vps": ["string"], "use_proxy": true, "enrich_options": {"reviews": {}, "photos": {}, "popular_times": true, "store_images": true}, "snowball": {"enabled": false, "max_depth": 2, "max_places_per_seed": 10}, "test": false}, "priority": 5}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/jobs/spiderMapsEnrich/submit", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `201` — Job created and queued successfully - `401` — Authentication failed - `403` — Client account is inactive - `422` — Validation error - Invalid payload - `429` — Rate limit exceeded - `500` — Internal server error - `503` — Queue service unavailable ## `POST` /api/v1/jobs/spiderFacebookPage/submit **Submit Facebook page scraping job** Scrape a public Facebook Page for posts, about info, contact details, and engagement metadata. Accepts the page URL or numeric page ID. Returns a `job_id` — poll `/api/v1/jobs/{job_id}/results` for the structured page payload. **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/jobs/spiderFacebookPage/submit' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "payload": { "url": "https://www.facebook.com/instagram", "test": false }, "priority": 0 }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/jobs/spiderFacebookPage/submit", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"payload": {"url": "https://www.facebook.com/instagram", "test": false}, "priority": 0}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/jobs/spiderFacebookPage/submit", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"payload": {"url": "https://www.facebook.com/instagram", "test": false}, "priority": 0}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"payload": {"url": "https://www.facebook.com/instagram", "test": false}, "priority": 0}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/jobs/spiderFacebookPage/submit", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `201` — Job created and queued successfully - `401` — Authentication failed - `403` — Client account is inactive - `422` — Validation error - Invalid payload - `429` — Rate limit exceeded - `500` — Internal server error - `503` — Queue service unavailable ## `POST` /api/v1/jobs/spiderPublicInstagram/submit **Submit Instagram profile scraping job** Scrape a public Instagram profile for bio, recent posts, follower counts, and contact fields exposed via the `business_contact` schema. Accepts the profile URL or handle. Returns a `job_id` — poll `/api/v1/jobs/{job_id}/results` for the structured profile payload. **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/jobs/spiderPublicInstagram/submit' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "payload": { "username": "natgeo", "instagram_url": "https://www.instagram.com/natgeo/", "extract_contact_from_bio": true, "store_profile_image": true, "test": false }, "priority": 0 }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/jobs/spiderPublicInstagram/submit", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"payload": {"username": "natgeo", "instagram_url": "https://www.instagram.com/natgeo/", "extract_contact_from_bio": true, "store_profile_image": true, "test": false}, "priority": 0}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/jobs/spiderPublicInstagram/submit", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"payload": {"username": "natgeo", "instagram_url": "https://www.instagram.com/natgeo/", "extract_contact_from_bio": true, "store_profile_image": true, "test": false}, "priority": 0}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"payload": {"username": "natgeo", "instagram_url": "https://www.instagram.com/natgeo/", "extract_contact_from_bio": true, "store_profile_image": true, "test": false}, "priority": 0}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/jobs/spiderPublicInstagram/submit", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `201` — Job created and queued successfully - `401` — Authentication failed - `403` — Client account is inactive - `422` — Validation error - Invalid payload - `429` — Rate limit exceeded - `500` — Internal server error - `503` — Queue service unavailable ## `POST` /api/v1/jobs/spiderPublicLinkedin/submit **Submit LinkedIn scraping job** Scrape LinkedIn profiles and companies using Voyager API. Requires mobile proxy. **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/jobs/spiderPublicLinkedin/submit' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "payload": { "mode": "get_company", "public_id": "microsoft", "linkedin_url": "https://www.linkedin.com/company/microsoft/", "keywords": "AI startup funding", "max_results": 10, "location": "string", "company": "string", "title": "string", "industry": "string", "store_logo": true, "include_posts": false, "test": false, "skip_proxy": false }, "priority": 0 }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/jobs/spiderPublicLinkedin/submit", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"payload": {"mode": "get_company", "public_id": "microsoft", "linkedin_url": "https://www.linkedin.com/company/microsoft/", "keywords": "AI startup funding", "max_results": 10, "location": "string", "company": "string", "title": "string", "industry": "string", "store_logo": true, "include_posts": false, "test": false, "skip_proxy": false}, "priority": 0}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/jobs/spiderPublicLinkedin/submit", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"payload": {"mode": "get_company", "public_id": "microsoft", "linkedin_url": "https://www.linkedin.com/company/microsoft/", "keywords": "AI startup funding", "max_results": 10, "location": "string", "company": "string", "title": "string", "industry": "string", "store_logo": true, "include_posts": false, "test": false, "skip_proxy": false}, "priority": 0}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"payload": {"mode": "get_company", "public_id": "microsoft", "linkedin_url": "https://www.linkedin.com/company/microsoft/", "keywords": "AI startup funding", "max_results": 10, "location": "string", "company": "string", "title": "string", "industry": "string", "store_logo": true, "include_posts": false, "test": false, "skip_proxy": false}, "priority": 0}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/jobs/spiderPublicLinkedin/submit", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `201` — Job created and queued successfully - `401` — Authentication failed - `403` — Client account is inactive - `422` — Validation error - Invalid payload - `429` — Rate limit exceeded - `500` — Internal server error - `503` — Queue service unavailable ## `POST` /api/v1/jobs/spiderLanding/submit **Submit landing page capture job** Capture landing pages with screenshots, HTML bundles, and AI-extracted marketing content. **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/jobs/spiderLanding/submit' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "payload": { "url": "https://tracking.example.com/redirect?ad_id=123", "ad_id": "string", "options": { "capture_screenshot": true, "capture_full_page": true, "capture_html_bundle": true, "extract_content": true, "dismiss_popups": true, "scroll_for_lazy_load": true, "viewport": {}, "timeout_seconds": 60, "max_redirects": 10 }, "test": false }, "priority": 0 }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/jobs/spiderLanding/submit", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"payload": {"url": "https://tracking.example.com/redirect?ad_id=123", "ad_id": "string", "options": {"capture_screenshot": true, "capture_full_page": true, "capture_html_bundle": true, "extract_content": true, "dismiss_popups": true, "scroll_for_lazy_load": true, "viewport": {}, "timeout_seconds": 60, "max_redirects": 10}, "test": false}, "priority": 0}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/jobs/spiderLanding/submit", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"payload": {"url": "https://tracking.example.com/redirect?ad_id=123", "ad_id": "string", "options": {"capture_screenshot": true, "capture_full_page": true, "capture_html_bundle": true, "extract_content": true, "dismiss_popups": true, "scroll_for_lazy_load": true, "viewport": {}, "timeout_seconds": 60, "max_redirects": 10}, "test": false}, "priority": 0}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"payload": {"url": "https://tracking.example.com/redirect?ad_id=123", "ad_id": "string", "options": {"capture_screenshot": true, "capture_full_page": true, "capture_html_bundle": true, "extract_content": true, "dismiss_popups": true, "scroll_for_lazy_load": true, "viewport": {}, "timeout_seconds": 60, "max_redirects": 10}, "test": false}, "priority": 0}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/jobs/spiderLanding/submit", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `201` — Job created and queued successfully - `401` — Authentication failed - `403` — Client account is inactive - `422` — Validation error - Invalid payload - `429` — Rate limit exceeded - `500` — Internal server error - `503` — Queue service unavailable ## `POST` /api/v1/jobs/spiderVideo/submit **Submit video stitching job** Stitch AI-generated video scenes into final video using Remotion. **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/jobs/spiderVideo/submit' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "payload": { "projectName": "string", "aspectRatio": "9:16", "scenes": [ { "videoUrl": "string", "durationInSeconds": 0.0 } ], "transitionDurationInFrames": 15, "musicUrl": "string", "musicVolume": 0.3, "preprocess": { "enabled": false }, "upload": { "enabled": false }, "test": false }, "priority": 5 }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/jobs/spiderVideo/submit", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"payload": {"projectName": "string", "aspectRatio": "9:16", "scenes": [{"videoUrl": "string", "durationInSeconds": 0.0}], "transitionDurationInFrames": 15, "musicUrl": "string", "musicVolume": 0.3, "preprocess": {"enabled": false}, "upload": {"enabled": false}, "test": false}, "priority": 5}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/jobs/spiderVideo/submit", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"payload": {"projectName": "string", "aspectRatio": "9:16", "scenes": [{"videoUrl": "string", "durationInSeconds": 0.0}], "transitionDurationInFrames": 15, "musicUrl": "string", "musicVolume": 0.3, "preprocess": {"enabled": false}, "upload": {"enabled": false}, "test": false}, "priority": 5}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"payload": {"projectName": "string", "aspectRatio": "9:16", "scenes": [{"videoUrl": "string", "durationInSeconds": 0.0}], "transitionDurationInFrames": 15, "musicUrl": "string", "musicVolume": 0.3, "preprocess": {"enabled": false}, "upload": {"enabled": false}, "test": false}, "priority": 5}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/jobs/spiderVideo/submit", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `201` — Job created and queued successfully - `401` — Authentication failed - `403` — Client account is inactive - `422` — Validation error - Invalid payload - `429` — Rate limit exceeded - `500` — Internal server error - `503` — Queue service unavailable ## `POST` /api/v1/jobs/spiderVideo/extract-frames/submit **Extract numbered image sequence from a video (scroll-sequence pipeline)** Run ffmpeg against a source video to produce a numbered sequence of web-optimized WebP/JPEG frames suitable for canvas + GSAP ScrollTrigger scroll-linked image sequences. Output manifest plugs directly into the `sys-scroll-sequence` system component as `{base_url, pattern, count}`. **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/jobs/spiderVideo/extract-frames/submit' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "payload": { "aspectRatio": "9:16", "musicUrl": "https://example.com/music.mp3", "musicVolume": 0.3, "projectName": "my-video", "scenes": [ { "durationInSeconds": 5, "videoUrl": "https://example.com/scene1.mp4" }, { "durationInSeconds": 3, "videoUrl": "https://example.com/scene2.mp4" } ], "transitionDurationInFrames": 15, "upload": { "enabled": true } }, "priority": 5 }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/jobs/spiderVideo/extract-frames/submit", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"payload": {"aspectRatio": "9:16", "musicUrl": "https://example.com/music.mp3", "musicVolume": 0.3, "projectName": "my-video", "scenes": [{"durationInSeconds": 5, "videoUrl": "https://example.com/scene1.mp4"}, {"durationInSeconds": 3, "videoUrl": "https://example.com/scene2.mp4"}], "transitionDurationInFrames": 15, "upload": {"enabled": true}}, "priority": 5}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/jobs/spiderVideo/extract-frames/submit", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"payload": {"aspectRatio": "9:16", "musicUrl": "https://example.com/music.mp3", "musicVolume": 0.3, "projectName": "my-video", "scenes": [{"durationInSeconds": 5, "videoUrl": "https://example.com/scene1.mp4"}, {"durationInSeconds": 3, "videoUrl": "https://example.com/scene2.mp4"}], "transitionDurationInFrames": 15, "upload": {"enabled": true}}, "priority": 5}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"payload": {"aspectRatio": "9:16", "musicUrl": "https://example.com/music.mp3", "musicVolume": 0.3, "projectName": "my-video", "scenes": [{"durationInSeconds": 5, "videoUrl": "https://example.com/scene1.mp4"}, {"durationInSeconds": 3, "videoUrl": "https://example.com/scene2.mp4"}], "transitionDurationInFrames": 15, "upload": {"enabled": true}}, "priority": 5}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/jobs/spiderVideo/extract-frames/submit", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `201` — Job created and queued successfully - `401` — Authentication failed - `403` — Client account is inactive - `422` — Validation error - Invalid payload - `429` — Rate limit exceeded - `500` — Internal server error - `503` — Queue service unavailable ## `POST` /api/v1/jobs/spiderSocial/submit **Submit a Social Media Enrichment (contact recovery) job** Recover a missing email, phone, real website, or social links for ONE business from its known social handles. If the business already has a usable email, the job self-skips. Returns a `job_id` — poll `/api/v1/jobs/{job_id}/results` for the recovered fields (or the skip reason). Access is entitlement-gated (Social Media Enrichment plan). **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/jobs/spiderSocial/submit' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "payload": { "social_media": {}, "website": "string", "email": "string", "phone": "string", "place_id": "string", "campaign_id": "string", "business_name": "string", "country_code": "string", "test": false }, "priority": 0 }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/jobs/spiderSocial/submit", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"payload": {"social_media": {}, "website": "string", "email": "string", "phone": "string", "place_id": "string", "campaign_id": "string", "business_name": "string", "country_code": "string", "test": false}, "priority": 0}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/jobs/spiderSocial/submit", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"payload": {"social_media": {}, "website": "string", "email": "string", "phone": "string", "place_id": "string", "campaign_id": "string", "business_name": "string", "country_code": "string", "test": false}, "priority": 0}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"payload": {"social_media": {}, "website": "string", "email": "string", "phone": "string", "place_id": "string", "campaign_id": "string", "business_name": "string", "country_code": "string", "test": false}, "priority": 0}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/jobs/spiderSocial/submit", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `201` — Job created and queued successfully - `401` — Authentication failed - `403` — Client account is inactive - `422` — Validation error - Invalid payload - `429` — Rate limit exceeded - `500` — Internal server error - `503` — Queue service unavailable ## `POST` /api/v1/jobs/spiderPR/submit **Submit a press-release wire-distribution job** Distribute a press release over the newswire. Submit a release (title + body, plus optional summary/category/tags/contact); returns a `job_id` — poll `/api/v1/jobs/{job_id}/results` for the published URL and wire report once the release goes live. **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/jobs/spiderPR/submit' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "payload": { "title": "Acme Corp launches AI-powered widget", "body": "string", "summary": "string", "category": "string", "tags": [ "string" ], "contact": { "name": "Jane Doe", "email": "press@example.com", "phone": "+1-555-123-4567" }, "provider": "prnow", "scheduled_release_at": "2026-01-01T00:00:00Z", "test": false }, "priority": 5 }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/jobs/spiderPR/submit", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"payload": {"title": "Acme Corp launches AI-powered widget", "body": "string", "summary": "string", "category": "string", "tags": ["string"], "contact": {"name": "Jane Doe", "email": "press@example.com", "phone": "+1-555-123-4567"}, "provider": "prnow", "scheduled_release_at": "2026-01-01T00:00:00Z", "test": false}, "priority": 5}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/jobs/spiderPR/submit", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"payload": {"title": "Acme Corp launches AI-powered widget", "body": "string", "summary": "string", "category": "string", "tags": ["string"], "contact": {"name": "Jane Doe", "email": "press@example.com", "phone": "+1-555-123-4567"}, "provider": "prnow", "scheduled_release_at": "2026-01-01T00:00:00Z", "test": false}, "priority": 5}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"payload": {"title": "Acme Corp launches AI-powered widget", "body": "string", "summary": "string", "category": "string", "tags": ["string"], "contact": {"name": "Jane Doe", "email": "press@example.com", "phone": "+1-555-123-4567"}, "provider": "prnow", "scheduled_release_at": "2026-01-01T00:00:00Z", "test": false}, "priority": 5}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/jobs/spiderPR/submit", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `201` — Job created and queued successfully - `401` — Authentication failed - `403` — Client account is inactive - `422` — Validation error - Invalid payload - `429` — Rate limit exceeded - `500` — Internal server error - `503` — Queue service unavailable ## `POST` /api/v1/jobs/spiderConvert/submit **Convert a document to markdown (async)** Convert PDF / DOCX / XLSX / PPTX / CSV / HTML / image documents to markdown. Returns `202` + a `job_id` — poll `GET /api/v1/jobs/{job_id}/results` for the conversion. **This endpoint is asynchronous by design.** Extraction runs OCR and office-suite conversion that can take minutes on large scanned documents; it is not a request-cycle operation. Submit, then poll. Supply exactly one of `media_id` (a SpiderMedia upload) or `content_base64` (inline, small files). To POST a file directly, use `/api/v1/jobs/spiderConvert/upload`. **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/jobs/spiderConvert/submit' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "payload": { "media_id": "med_01J9X2QK7B8", "content_base64": "string", "filename": "safety-data-sheet.pdf", "mime_type": "application/pdf", "full_text": false, "ocr": "auto", "test": false }, "priority": 0 }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/jobs/spiderConvert/submit", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"payload": {"media_id": "med_01J9X2QK7B8", "content_base64": "string", "filename": "safety-data-sheet.pdf", "mime_type": "application/pdf", "full_text": false, "ocr": "auto", "test": false}, "priority": 0}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/jobs/spiderConvert/submit", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"payload": {"media_id": "med_01J9X2QK7B8", "content_base64": "string", "filename": "safety-data-sheet.pdf", "mime_type": "application/pdf", "full_text": false, "ocr": "auto", "test": false}, "priority": 0}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"payload": {"media_id": "med_01J9X2QK7B8", "content_base64": "string", "filename": "safety-data-sheet.pdf", "mime_type": "application/pdf", "full_text": false, "ocr": "auto", "test": false}, "priority": 0}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/jobs/spiderConvert/submit", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `202` — Successful Response - `201` — Job created and queued successfully - `401` — Authentication failed - `403` — Client account is inactive - `422` — Validation error - Invalid payload - `429` — Rate limit exceeded - `500` — Internal server error - `503` — Queue service unavailable ## `POST` /api/v1/jobs/spiderConvert/upload **Upload a document for markdown conversion (async, multipart)** Multipart variant of `/spiderConvert/submit` — POST the file itself. Returns `202` + a `job_id`. **Maximum 10 MB.** This is the nginx `client_max_body_size` for `/api/v1/`, not an arbitrary application choice; a larger body is rejected by the proxy before it reaches the API. For bigger documents, upload to SpiderMedia and submit the `media_id` instead. **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/jobs/spiderConvert/upload' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "file": "string", "full_text": false, "ocr": "auto", "test": false }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/jobs/spiderConvert/upload", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"file": "string", "full_text": false, "ocr": "auto", "test": false}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/jobs/spiderConvert/upload", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"file": "string", "full_text": false, "ocr": "auto", "test": false}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"file": "string", "full_text": false, "ocr": "auto", "test": false}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/jobs/spiderConvert/upload", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `202` — Successful Response - `201` — Job created and queued successfully - `401` — Authentication failed - `403` — Client account is inactive - `422` — Validation error - Invalid payload - `429` — Rate limit exceeded - `500` — Internal server error - `503` — Queue service unavailable ## `GET` /api/v1/jobs/spiderConvert/{job_id}/content **Fetch the full markdown of a conversion stored in object storage** When a conversion's markdown exceeds the inline threshold (**256 KB** by default — `DOCUMENT_STORAGE_THRESHOLD_KB`) it is written to object storage and the job result carries a `storage_key` instead of a body (SpiderMail LEARNINGS §11). This is where you follow that key. 🔑 **There is only a key to follow if the conversion was submitted with **`**full_text=true**`**.** That flag is evaluated before the size threshold, so an over-threshold document requested without it is never stored and this route 404s — correctly. Check `truncation_notice` on the result before reading a 404 here as a storage failure. Returns `text/markdown`, not JSON — the payload is by definition large and wrapping it in a JSON string helps nobody. **This is not a way around **`**extraction_truncated**`**.** If that flag is true the missing text was never extracted, so it is not here either. **Parameters** - `job_id` (path, string, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/jobs/spiderConvert/{job_id}/content' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/jobs/spiderConvert/{job_id}/content", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/jobs/spiderConvert/{job_id}/content", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/jobs/spiderConvert/{job_id}/content", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — The complete markdown. - `404` — No such job for this client, or it has no stored content. - `503` — Object storage is unreachable. - `422` — Validation Error ## Response formats Source: https://spideriq.ai/docs/api-reference/response-formats Most read endpoints in the SpiderIQ API can answer in four different formats. You pick one with a `format` query parameter. This page explains what each value does, when to reach for it, and what happens when you send something the endpoint cannot serve. ## The four values | Value | You get back | Content type | | --- | --- | --- | | `json` | the ordinary JSON body. This is the default when you send no `format` at all | `application/json` | | `llm` | the same JSON body, plus a `guidance` block describing what the resource is for, what to call next, and the mistakes callers actually make | `application/json` | | `yaml` | the same data serialised as YAML, which is meaningfully cheaper to read | `text/yaml` | | `md` | a human-readable Markdown rendering | `text/markdown` | ```bash curl "https://spideriq.ai/api/v1/jobs/{job_id}/results?format=yaml" \ -H "Authorization: Bearer $TOKEN" ``` ## Which one you want **If you are writing code, send nothing.** The default is JSON and it is the format every client library expects. **If you are an AI agent, send **`**format=llm**`**.** The guidance block is the reason it exists: alongside the data you get a short description of what this endpoint is for, what it is explicitly *not* for, and what to call next. That saves a round trip through the documentation on every call. Bearer and PAT callers are opted into guidance by default, so you often do not need to send anything. Sending `format=llm` makes the intent explicit and is safe either way. **If you want to turn guidance off for one request, send **`**format=json**`**.** An explicit format always beats the Bearer default. **If you are reading a response into a context window, try **`**format=yaml**`**.** It carries the same fields with markedly less punctuation, which is worth 40 to 60 percent of the tokens on a large body. You can also set `SPIDERIQ_FORMAT=yaml` once in your MCP configuration instead of adding the parameter to every call. ## What a rejected value looks like An unsupported value is refused rather than quietly answered in JSON: ```json { "error": { "code": "SCHEMA_VALIDATION_FAILED", "errors": [{ "type": "enum", "loc": ["query", "format"] }] } } ``` That refusal is deliberate. Accepting a format the endpoint cannot render, and answering in JSON anyway, would mean a Markdown request silently returning something that is not Markdown. ## The endpoints that accept less than four A small number of endpoints genuinely serve a narrower set, and they refuse the rest for the same reason: - **Press releases and doc exports** render Markdown but have no YAML renderer, so `format=yaml` returns a 422 there. - **Lead export** defaults to `csv` rather than JSON, because a spreadsheet is what callers of that endpoint want. It also accepts `json` and `llm`. - **Page export** adds `archive`, which returns a ZIP laid out the way the VSCode extension stores a site locally. If you are unsure what a specific endpoint accepts, the API reference lists the permitted values for every one of them, and an invalid value is always a 422 rather than a surprise. ## Buy a lead list in bulk Source: https://spideriq.ai/docs/lead-generation/bulk-lead-sourcing **Who this is for:** Anyone who needs leads across many search terms and many towns at once, and would rather make one purchase than set up a campaign per location. Already holding a list? You do not have to buy it again. See [Enrich a list you already have](bulk-lead-sourcing-upload). **What this does:** Buys one list covering every search term in every location you name, removes the duplicates, then runs each remaining business through the same website scrape, email verification and VayaPin steps a campaign uses. ## Before you begin - Your plan includes bulk lead sourcing. If the source dropdown is empty, it does not. - You know roughly how many records you want. Bulk spends in one go, and a run cannot be stopped once the provider accepts it. - You have decided whether you want VayaPin profiles. Turning VayaPin on publishes one permanent public page per lead. ## Steps 1. Open **Flows** and choose **Bulk Lead Sourcing**. 2. Under **Source**, pick where the records come from. Two providers sell Google Maps records: **Outscraper** and **Apify**. Apify is billed per record and shows you a real figure in the confirmation panel; Outscraper has no unit price configured, so its cost reads **not priced**. The green **LIVE** badge means the source is available on your plan. The two upload options in the same list, **CSV upload** and **JSON upload**, take a file instead of buying anything and are covered in [Enrich a list you already have](bulk-lead-sourcing-upload). 3. Under **Search terms**, add each thing you are looking for. Use **Add term** for more. Every term runs against every location, so three terms and four towns is twelve searches, not seven. 4. Under **Locations**, add each place and give each one a **label** such as `Berlin, Germany`. The country code beside it does not place the search, it is only a language hint. A location with a country code and no label buys a nationwide list of real businesses that nobody asked for, and nothing about the result will look wrong. 5. Under **Limits**, set **Max records per search**. Leave it blank and each search buys **500** records. Set **Max records total** if you want a hard stop for the whole run. 6. Click **Review & run**. Nothing is bought yet. 7. Read the confirmation panel. It multiplies your terms by your locations by your records per search and shows the total as **Records purchased, at most**. Check that against **Your ceiling**. If the cost reads **not priced**, no unit price is configured for your account: that does not mean the run is free, and the record count is the size of the purchase. 8. Tick **I am authorising a provider purchase**, then click **Start bulk run**. Steps 1 to 5 happen on this form. Note the record limit, which is the field that decides what you spend: ![The Bulk Lead Sourcing form showing the source selector, search terms, locations and a max-records-per-search field defaulting to 500.](https://media.spideriq.ai/client-cli-vwpm8o5er5qnnb4w/docs/bulk-lead-sourcing/20260810_175755_s1-form.png) Step 7 is this panel. It is the last point at which nothing has been bought: ![The Confirm bulk run modal showing 1 search term times 1 location times 100 records per search equals 100 records purchased at most, against a 25,000 ceiling.](https://media.spideriq.ai/client-cli-vwpm8o5er5qnnb4w/docs/bulk-lead-sourcing/20260810_175756_s2-confirm.png) ## Verify it worked Open the **Runs** tab. A new run appears and moves through submitted, polling, parsing and fanning out, then sits at **enriching** while the pipeline works. Polling for a few hours is normal while the provider works through your searches. `enriching` is not finished. It means the records were bought and split into one job per lead, and the scraping and verification are still running. A run that enriched only some of its leads reports **partial** and carries an enriched and a not-enriched count; one that enriched none reports **failed** rather than completed. The run has genuinely finished the pipeline, not just the purchase, when leads carry **verified email addresses**. Contact details are never requested from the provider, so an email on a lead can only have come from the website scrape, and a verified one can only have come from email verification. An empty run is not automatically a broken one. A run can buy records, reject every one of them against your own filters, and finish correctly with nothing to enrich. The results now say which of the two happened: a **kept** count of zero sitting beside a reason such as "too few reviews" is your filter working, while a run that never got as far as screening reports no counts at all. Ask your developer to read [the results endpoint](bulk-lead-sourcing-api), which returns those numbers today; the Runs tab does not show them yet. ## Troubleshoot - **The source dropdown is empty or says "Not available yet".** Bulk lead sourcing is not enabled on your plan. Talk to your account contact. - **"Start bulk run" stays greyed out.** You have not ticked the authorisation checkbox, or you have no search term or no location yet. - **The run was refused because it is too large.** Your estimate is above your per-job record ceiling, 25,000 by default. Reduce **Max records per search**, or cut locations or terms, and review again. Resubmitting the same run will fail the same way every time. - **The run was refused on spend.** Your projected spend for the last 24 hours would cross your cost limit. Wait for the window to roll, or make the run smaller. - **You got far more records than you expected.** You almost certainly left **Max records per search** blank, which buys 500 per search. Count records, not searches. - **The same business appears in two different runs.** Duplicates are removed within a run, never across runs. A business you bought last week will be bought again this week. - **The run says completed but there are no leads at all.** Records were probably bought and then rejected by your own screening, which is a working run rather than a failed one. The results carry a kept count and a reason for every record dropped. If there are no counts at all, the run stopped before it screened anything and that is the real fault. - **The cost reads "not priced" on one provider and a real figure on another.** Outscraper has no unit price configured on your account, so only the record count bounds the run. Apify is priced per record, so its figure is a real estimate. "Not priced" never means free. ## Related - [Enrich a list you already have](bulk-lead-sourcing-upload) — the same pipeline, starting from your own CSV or JSON file. - [Enrich leads you already have](enrich-leads-you-already-have) — skip the purchase entirely and re-run the stages over leads already in your account. - [Bulk Lead Sourcing API](bulk-lead-sourcing-api) — the same capability over HTTP, the CLI and MCP. ## System Source: https://spideriq.ai/docs/api-reference/system ## `GET` /api/v1/system/health **Health Check** Health check endpoint for monitoring Checks: - API server status - PostgreSQL connection - Redis connection - RabbitMQ connection Returns: Service status information :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/system/health' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/system/health", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/system/health", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/system/health", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response ## `GET` /api/v1/system/ready **Readiness Check** Engine-honest readiness probe — the blue-green / Traefik health gate. Unlike ``/health`` and ``/api/gate/v1/models`` (which return 200 *before* the litellm Router is built — LEARNINGS #37), this returns **503 until** the SpiderGate engine has fully initialized AND a Router was built with at least one deployment AND Postgres answers a live query. Traefik adds a new (green) container to the pool ONLY when this is 200, so the ~27 s engine boot happens on a slot no user is hitting yet — eliminating the deploy 502 window (see docs/services/Infrastructure/blue-green-traefik-proxy.md §6.3). NOTE: this is the api-gateway readiness gate. content-api shares this image but runs a minimal lifespan that does NOT build the Router, so it would report not_ready here — its blue-green gate (phase 3) needs a content probe. Returns 200 ``{"status": "ready", ...}`` when serveable; 503 ``{"status": "not_ready", ...}`` (Retry-After: 5) while booting. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/system/ready' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/system/ready", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/system/ready", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/system/ready", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response ## `GET` /api/v1/system/queue-stats **Get Queue Stats** Get queue statistics Returns: Current queue depths for all job types :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/system/queue-stats' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/system/queue-stats", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/system/queue-stats", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/system/queue-stats", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response ## `GET` /api/v1/system/credential-health **Credential Health** Active clients whose server-side Bearer forge is broken — in BOTH ways. A tenant has two credential representations and they must agree: api_key_hash / api_secret_hash ← what `middleware/auth.py` VERIFIES an inbound Bearer against encrypted_api_key / _api_secret ← what `tenant_bearer.forge_…` decrypts to MINT a Bearer for WindMill dispatch `missing_encrypted_credentials` counts the pair being ABSENT. Necessary, but it was also the ONLY check anywhere — here, in `stalled_campaign_monitor`, and in `backfill_missing_encrypted_credentials`, all three testing `IS NULL`. `desynced_credentials` counts the pair being PRESENT AND WRONG: it decrypts cleanly but no longer matches the hash, so the forge mints a superseded triplet and every downstream flow 401s while the client's own token keeps working perfectly. Invisible to a NULL check, silent in every log, and it cost 18 consecutive bulk runs and 221 flow failures before anyone looked (card 46c537cc — root cause was a rotation path that wrote only the hashes, fixed in `dashboard.py`). BOTH must stay at 0. Remediation differs: - missing → `python -m scripts.backfill_missing_encrypted_credentials` - desynced → the plaintext is unrecoverable from a bcrypt hash, so the only repair is a fresh rotation through a path that writes all four columns (dashboard "Regenerate credentials", or the admin brands rotate endpoint). This issues the tenant a NEW token. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/system/credential-health' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/system/credential-health", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/system/credential-health", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/system/credential-health", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response ## `GET` /api/v1/system/cal-health **Cal Health** Probe the self-hosted Cal.com stack (SpiderBook P1.3). Cal.com v6.2.0 does not ship a public ``/api/health`` endpoint — that lives on the separate NestJS v2 API image. We mirror the upstream container healthcheck and probe the Next.js root, following the normal redirect chain until we hit a 2xx. Same logic as ``scripts/cal_health_check.sh``. Returns ``{"status": "healthy"}`` on success. Raises 503 with a ``Retry-After: 30`` header if Cal.com is unreachable or returns an error — callers should back off at least 30s before retrying. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/system/cal-health' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/system/cal-health", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/system/cal-health", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/system/cal-health", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response ## `GET` /api/v1/system/info **Get System Info** Get system information Returns: API configuration and version information :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/system/info' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/system/info", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/system/info", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/system/info", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response ## Enrich a list you already have Source: https://spideriq.ai/docs/lead-generation/bulk-lead-sourcing-upload **Who this is for:** Anyone holding a list of businesses already, in a spreadsheet or an export, who wants websites scraped and email addresses verified without buying the list again. **What this does:** Takes a CSV or JSON file you upload, reads your columns, and runs each row through the same website scrape, email verification and VayaPin steps a purchased list goes through. Nothing is bought at the source. ## Before you begin - Your file has one business per row and a header row naming the columns. The headers can be anything; you will map them in step 5. - Your file is under 64 MB. Split a larger one. - At least one column identifies the business: a name, a website, a phone number, a `place_id`, or a `google_place_id`. Without one of those, every row looks identical, deduplication collapses the file to a single lead, and there is nothing for the enrichment steps to look up. - You have decided whether you want VayaPin profiles. Turning VayaPin on publishes one permanent public page per lead. ## Steps 1. Open **Flows** and choose **Bulk Lead Sourcing**. 2. Under **Source**, pick **CSV upload** or **JSON upload**. The Search terms, Locations and Limits fields disappear, because an upload has nothing to search for. 3. Click the file picker and choose your file. It uploads immediately. Uploading is not running: nothing is queued and nothing is spent yet, and an unused file is discarded after 7 days. 4. Check the record count that comes back. It is counted by reading the stored file back, so it is the real number of rows we hold, not the number your spreadsheet showed. 5. Work through the **column mapping** panel. We propose a mapping for the columns we recognise and leave the rest blank. Confirm each proposal or change it, and set anything we missed. Leave a field on **— not in my file —** if you do not have it. ![The column mapping panel showing our proposed mapping for a file with non-obvious headers, with each field selectable against a column from the file.](https://media.cdn.spideriq.ai/content/cli_5z6zs9k9po75hd4s/general/1786625329-9374d3b9.webp) 1. Under **Enrichment stages**, switch on the steps you want. This is the part you pay for, once per lead per stage. 2. Click **Review & run**, then read the confirmation panel. It shows the cost of the records as **free** and the enrichment volume as leads times stages. Tick the acknowledgement and click through to start. ![The confirmation modal for an uploaded list, showing the cost of the records as free and the enrichment run sized as leads times stages.](https://media.cdn.spideriq.ai/content/cli_5z6zs9k9po75hd4s/general/1786625330-14f9b48b.webp) ## Verify it worked Open the **Runs** tab. The run moves through parsing and fanning out, then sits at **enriching** while the pipeline works. `enriching` is not finished: sourcing succeeded, enrichment has not. The run has genuinely finished when it reports **completed** and the results carry **verified email addresses**. We never accept contact details from your file, and there is no column you can map onto an email field, so an email on a lead can only have come from the website scrape and a verified one only from email verification. That makes an email the one piece of evidence your own upload could not have produced. If some leads enriched and others did not, the run reports **partial** and the results carry an enriched and a not-enriched count. If none enriched, it reports **failed** rather than completed. ## Troubleshoot - **CSV upload and JSON upload are greyed out.** Bulk lead sourcing is not enabled on your plan. Talk to your account contact. - **"That CSV has no usable header row."** The first line is blank or only separators, such as `;;;;;`. Delete it. We refuse rather than guess, because adopting your first business as the header would lose that lead and mislabel every column. - **"That file has a header row but no data rows."** The file is headers only. Nothing was stored and nothing was charged. - **The file is rejected as too large.** The limit is 64 MB. A much larger file is refused earlier by the edge and comes back as a plain web page rather than a readable error; either way, split the file. - **"Review & run" stays greyed out.** You have not mapped an identifying column. Map a name, website, phone, `place_id` or `google_place_id` and it re-enables. - **Accented characters look wrong in the preview.** We detect the file's encoding and say which one we used under the mapping panel. If it reads as the wrong one, re-save the file as UTF-8 and upload again. - **Everything collapsed into one lead.** You mapped no identifying column, so every row deduplicated onto the same key. Map one and re-upload. - **You have to upload again after a failure.** A file is claimed by the run that consumes it. If that run fails before anything is spent the claim is released and the same file can be submitted again, but a file consumed by a run that got as far as spending is used up. ## Related - [Buy a lead list in bulk](bulk-lead-sourcing) — the same pipeline, with the records purchased instead of uploaded. - [Bulk Lead Sourcing API](bulk-lead-sourcing-api) — the upload and submit endpoints over HTTP. ## booking Source: https://spideriq.ai/docs/api-reference/booking ## `POST` /api/v1/booking/{flow_id}/hold-slot **Reserve a booking slot for 10 minutes** Create a 10-minute hold for `(flow_id, slot_start, staff_id)`. Concurrency: `create_hold()` serializes through a per-slot advisory lock, so two simultaneous calls for the same slot resolve deterministically — first wins with 201, second gets 409. **Parameters** - `flow_id` (path, string, required) **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/booking/{flow_id}/hold-slot' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "slot_start": "2026-01-01T00:00:00Z", "slot_end": "2026-01-01T00:00:00Z", "service_id": "00000000-0000-0000-0000-000000000000", "staff_id": "00000000-0000-0000-0000-000000000000" }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/booking/{flow_id}/hold-slot", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"slot_start": "2026-01-01T00:00:00Z", "slot_end": "2026-01-01T00:00:00Z", "service_id": "00000000-0000-0000-0000-000000000000", "staff_id": "00000000-0000-0000-0000-000000000000"}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/booking/{flow_id}/hold-slot", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"slot_start": "2026-01-01T00:00:00Z", "slot_end": "2026-01-01T00:00:00Z", "service_id": "00000000-0000-0000-0000-000000000000", "staff_id": "00000000-0000-0000-0000-000000000000"}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"slot_start": "2026-01-01T00:00:00Z", "slot_end": "2026-01-01T00:00:00Z", "service_id": "00000000-0000-0000-0000-000000000000", "staff_id": "00000000-0000-0000-0000-000000000000"}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/booking/{flow_id}/hold-slot", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `201` — Successful Response - `409` — An active hold already exists for this slot. - `404` — Flow not found or inactive. - `429` — IP rate limit exceeded. - `422` — Validation Error ## `POST` /api/v1/booking/holds/{hold_id}/release **Release a slot hold (internal)** Idempotent release. Called from the P3.3 public submit endpoint after a successful Cal.com booking-create — returning the slot to availability immediately instead of waiting for the 10-minute TTL or the cleanup cron. **Parameters** - `hold_id` (path, string, required) :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/booking/holds/{hold_id}/release' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/booking/holds/{hold_id}/release", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/booking/holds/{hold_id}/release", { method: "POST", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/booking/holds/{hold_id}/release", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `404` — Hold not found. - `422` — Validation Error ## Bulk Lead Sourcing API Source: https://spideriq.ai/docs/lead-generation/bulk-lead-sourcing-api Source leads in bulk, then run every lead through the same enrichment pipeline a campaign uses. The records can be **bought** from a data provider in one job covering many search terms across many locations, **uploaded** from a file you already have, or **selected from the leads already in your account**. Use this instead of a campaign when you want breadth in a single purchase. A campaign runs one Google Maps search per location; bulk buys every `query x location` combination at once. ``` POST /bulk-lead-sourcing/submit -> 202 (manifest written, provider NOT called) | bulk worker submit -> poll -> fetch -> parse -> dedup -> fan-out | one job per lead SpiderSite -> SpiderVerify -> VayaPin (identical to a campaign) ``` The provider is not contacted during this request. A manifest row is created and `202 Accepted` returns immediately with `status: "pending"`. The bulk worker drives the run. A 202 means accepted and gated, not bought. ## Where records come from Six sources are registered. Two buy records, two read a file you upload, and two cost nothing at the source. | `provider` | Kind | `source_kinds` | Costs money at the source | | --- | --- | --- | --- | | `outscraper` | Provider job | `google_maps` | Yes. No unit price is configured, so `estimated_cost_usd` is `null`. | | `apify` | Provider job | `google_maps` | Yes, billed per record. `estimated_cost_usd` returns a real number. | | `csv` | Upload | `google_maps` | No. | | `json` | Upload | `google_maps` | No. | | `sortlist` | Public directory | `sortlist_agency` | No. Agencies from a public B2B directory. Not eligible for the vayapin stage. | | `internal` | Your own corpus | `unenriched_run`, `corpus_query` | No. Selects leads you already have; see below. | Do not hardcode this list. `GET /sources` resolves it from the adapter registry at request time, so a source added on the backend appears without a client change. An upload is free **at the source**, which is not the same as free. You still pay for each enrichment stage you switch on, once per lead. Branch your cost messaging on `source_is_free`, never on `has_cost` — an unpriced provider like `outscraper` looks identical to a free upload on every money field. ## Two traps that cost real money Neither of these errors. Both look like a clean success. They apply to the **provider** sources only; an upload has no queries and no geo. 1. **Omitting **`**limits.max_records_per_query**`** buys 500 records per search.** Two queries across three cities reads as "6 searches" and is a 3,000-record purchase. The platform default is 500 records per expanded query. Quote yourself records, never searches. 2. `**country_code**`** does not place a search.** It is a locale hint. Only `geo[].label`, appended as `"{query}, {label}"`, or an explicit `latitude` and `longitude` steers where the provider looks. A target carrying only `{"country_code": "US"}` buys a nationwide set of genuinely valid businesses that nobody asked for. ## `GET /api/v1/dashboard/bulk-lead-sourcing/sources` Every source, with availability resolved live from the adapter registry. ```bash curl "https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/sources" \ -H "Authorization: Bearer $CLIENT_ID:$API_KEY:$API_SECRET" ``` **Response** ```json { "sources": [ { "provider": "csv", "label": "CSV upload", "description": "Bring your own records. No provider, no spend at the source.", "source_kinds": ["google_maps"], "available": true, "unavailable_reason": null, "requires_upload": true } ] } ``` | Field | Meaning | | --- | --- | | `available` | Whether this source can be selected right now. | | `unavailable_reason` | Why not, when `available` is `false`. `null` otherwise. | | `requires_upload` | `true` means you must upload a file first and submit its `upload_id`. `false` means you send `queries` and `geo`. | **Errors:** | Status | Reason code | When | How to resolve | | --- | --- | --- | --- | | `401` | unauthenticated | Credentials are missing or wrong. | Send a valid Bearer triple, PAT, or session cookie. | | `403` | not tenant scoped | Authenticated, but the caller resolves to no tenant. | Use a client PAT, or select a brand on the session. | This route has no other failure mode. An adapter that vanishes between listing and lookup is skipped rather than erroring, so the list is always answerable. ## `POST /api/v1/dashboard/bulk-lead-sourcing/upload` Store a records file and read back what is in it. **This is not a submit.** Nothing is enriched, nothing is queued, nothing is spent. The file sits in storage for 7 days and you can abandon it. `multipart/form-data` with three parts: `provider` (`csv` or `json`), `source_kind`, and `file`. ```bash curl -X POST "https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/upload" \ -H "Authorization: Bearer $CLIENT_ID:$API_KEY:$API_SECRET" \ -F "provider=csv" \ -F "source_kind=google_maps" \ -F "file=@my-leads.csv" ``` **Response** `201 Created` ```json { "upload_id": "97a3669f-3c2e-4a1b-9d55-0f2b7c8e4d10", "provider": "csv", "source_kind": "google_maps", "filename": "my-leads.csv", "size_bytes": 4182, "sha256": "bae968a5...f3712", "record_count": 3, "expires_at": "2026-08-20T09:14:00Z", "columns": { "columns": ["Firma", "Str.", "Ort", "PLZ", "Tel", "Homepage"], "proposed_mapping": { "phone": "Tel", "website": "Homepage" }, "unmapped_columns": ["Firma", "Str.", "Ort", "PLZ"], "delimiter": ";", "encoding": "cp1252", "mappable_fields": ["name", "place_id", "google_place_id", "address", "city", "..."], "identifying_fields": ["name", "place_id", "google_place_id", "website", "phone"] }, "message": "Stored bulk upload" } ``` | Field | Meaning | | --- | --- | | `record_count` | Rows counted by reading the artifact back **out of storage**, so a truncated write is caught here rather than as an unexplained short run. This is what the record ceiling is applied to. | | `expires_at` | 7 days from upload. An unconsumed file is cleaned up. | | `columns` | Present for `csv`, absent for `json`. See below. | **Errors:** | Status | Reason code | When | How to resolve | | --- | --- | --- | --- | | `413` | `UPLOAD_TOO_LARGE` | The file is over 64 MiB. | Split the file. A file over roughly 96 MB is refused earlier still, by the edge, as raw HTML rather than JSON. | | `422` | `UPLOAD_UNPARSEABLE` | The file did not parse, or parsed to zero records: a CSV with a header and no data rows, a first line of only separators, or JSON in a shape with no record array. | Fix the file. No row is written and the stored object is deleted, so a failed upload leaves nothing behind. | | `422` | `UNKNOWN_UPLOAD_SOURCE` | `provider` is not a registered upload source. | Use `csv` or `json`. Check `GET /sources`. | | `401` | unauthenticated | Credentials are missing or wrong. | Send a valid Bearer triple, PAT, or session cookie. | ## Reading the column proposal, and why the mapping is yours to confirm A CSV has arbitrary headers, so the pipeline cannot assume a shape. The upload response reports the header row in file order, the detected delimiter and text encoding, and a **proposed** mapping. The proposal in `columns` is advisory: it is a guess shown for confirmation, and the mapping that actually runs is the one you send back at submit time in `source.filters.column_mapping`. `mappable_fields` and `identifying_fields` are the exception: they are not a guess, they are the server's own vocabulary, spliced in at response time. Render the mapping panel from `mappable_fields` rather than from a list kept by hand in the client, or the panel silently loses every field the server gains later. An empty `identifying_fields` means the server predates the field, not that nothing identifies a business. A run needs at least one **identifying** column mapped: `name`, `place_id`, `google_place_id`, `website`, or `phone`. With none of them every row is an anonymous bag of attributes, deduplication collapses the file to a single lead, and the enrichment stages have nothing to look up. The mapping is refused at the door, before anything is spent. There is deliberately **no email or contact target** to map onto. A client can name a column anything, so a deny-list cannot work; the guarantee is that no mapping target for contact data exists to select. Contact data is produced by the verification stages, never accepted from the file. ## Enriching leads you already own The third origin. `internal` sources no records at all — it selects leads already in your account and sends them through the enrichment pipeline again, so a lead you paid to find last month can be crawled, verified or pinned this month without being bought a second time. It has two `source_kinds`, and they answer different questions: | `source_kind` | The question | Narrowed by | | --- | --- | --- | | `unenriched_run` | "That campaign I ran in June never got its websites crawled." | `campaign_id`, or a single `job_id` inside it | | `corpus_query` | "Every lead with a website but no email." | a `filter` AST over 84 fields | Both resolve through one code path, and both end at the same place: a **selection**, a saved, tenant-scoped, seven-day snapshot of a question. You submit the selection's id. You never submit the question. ``` GET corpus/fields -> the 84 fields AND the filter grammar POST corpus/count -> matched_leads + eligible_leads (free, fires on every keystroke) POST selections -> 201 { selection_id } (saves the question) POST submit -> { source: { selection: { selection_id } } } ``` ## Matched is not eligible, and the difference is what you pay for Every counting route returns two numbers, and reading the wrong one is the single most expensive mistake on this surface. | | Means | | --- | --- | | `matched_leads` | How many leads your question describes. | | `eligible_leads` | How many of those would actually **gain** something from the stages you enabled. | A lead whose website is already crawled gains nothing from another crawl. On a mature corpus the gap is not a rounding error — a live tenant returns 9,887 matched and 986 eligible for `has.email is_empty` with a verify stage enabled. **Selecting 5,000 and enriching 200 is the ordinary reading, not a bug.** `eligible_leads` is what the record ceiling is checked against, what the run costs, and what it will take to finish. Quote it. `matched_leads` is context and nothing in the system acts on it. Eligibility is scoped to `stages`, which is why `stages` is required on every counting route and never defaults. A count taken for one stage set is meaningless for another — never carry a number across. ## `GET /api/v1/dashboard/bulk-lead-sourcing/past-runs` Past campaigns and jobs that still have something to gain, for the `unenriched_run` kind. **Parameters** | Field | Type | Required | Description | | --- | --- | --- | --- | | `stages` | string[] | yes | The stages the new run would enable. Eligibility is measured against these. | | `kind` | string | no | `campaign` or `job`. Defaults to campaign-level rollups. | | `campaign_id` | string | no | List the jobs inside one campaign instead of all campaigns. | | `exclude_without_website` | boolean | no | Drop leads with no website at all, which a crawl cannot help. Defaults to `true`. | | `limit` | integer | no | Maximum runs to return. | ```bash curl -G "https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/past-runs" \ -H "Authorization: Bearer $CLIENT_ID:$API_KEY:$API_SECRET" \ --data-urlencode "stages=spidersite" \ --data-urlencode "stages=spiderverify" ``` **Response** Each run carries its own two counts plus a label describing what that run originally ran, derived from the campaign's stored workflow config. ```json { "runs": [ { "campaign_id": "camp_us_dentists_20260614...", "label": "Dentists, 12 locations", "matched_leads": 1204, "eligible_leads": 205, "ran_stages": ["spidermaps"] } ] } ``` A campaign's stage config is **opt-out**: a campaign stored with no workflow config ran the full pipeline, not an empty one. The label reflects that, so a run you remember as "maps only" may correctly report that it also verified. **Errors:** | Status | Reason code | When | How to resolve | | --- | --- | --- | --- | | `401` | unauthenticated | Credentials missing or wrong. | Send a valid Bearer triple, PAT, or session cookie. | | `422` | validation error | `stages` omitted, or a stage name that is not gateable. | Send at least one of `spidersite`, `spiderverify`, `vayapin`, `social_media_enrichment`. | ## `GET /api/v1/dashboard/bulk-lead-sourcing/corpus/fields` The field catalogue **and the filter grammar**, in one response. This is the route an agent reads before it writes anything. There is no natural-language endpoint on this surface and that is deliberate. The agent is the natural-language layer: it reads this catalogue, translates the user's sentence into a filter tree, and posts the tree. Nothing on the server parses English. ```bash curl "https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/corpus/fields" \ -H "Authorization: Bearer $CLIENT_ID:$API_KEY:$API_SECRET" ``` **Response** Four top-level keys: `fields`, `limits`, `presets`, `ast`. ```json { "fields": [ { "key": "business.city", "label": "City", "type": "text", "field_class": "direct", "operators": ["is", "is_not", "contains", "does_not_contain", "starts_with", "is_empty", "is_not_empty", "is_any_of"] } ], "limits": { "max_conditions": 20, "max_depth": 3, "max_groups": 5, "max_values_per_list": 100 }, "presets": [ { "key": "never_crawled", "label": "Never crawled", "help": "Has a website, but SpiderSite has never run on it.", "filter": { "op": "and", "conditions": [ { "field": "has.website", "operator": "is_not_empty" }, { "field": "has.crawl", "operator": "is_empty" } ] } } ], "ast": { } } ``` The `ast` block carries eleven keys and is the whole contract for writing a filter: | Key | What it gives you | | --- | --- | | `grammar` | The tree shape: a root group, groups nesting groups or conditions, mixed freely. | | `boolean_operators` | `and`, `or`. | | `operators_by_type` | Which operators each field type accepts. Agrees with every field's own `operators` list. | | `valueless_operators` | `is_empty`, `is_not_empty`. Sending a `value` with these is refused. | | `list_operators` | `has_all_of`, `has_any_of`, `has_none_of`, `is_any_of`, `is_none_of`. These take an array. | | `operator_aliases` | Convenience spellings that normalise, such as `=` to `eq` and `has any of` to `has_any_of`. | | `value_rules` | Per-type value constraints. | | `limits` | The same budget as the top-level `limits`. | | `error_codes` | All fourteen refusal codes, listed below. | | `example` | A complete, valid filter you can post as-is. | | `example_reads_as` | That example in English, so you can check your translation against it. | **The example, shipped in the response and runnable unchanged:** ```json { "op": "and", "conditions": [ { "field": "has.website", "operator": "is_not_empty" }, { "field": "has.email", "operator": "is_empty" }, { "op": "or", "conditions": [ { "field": "business.city", "operator": "is_any_of", "value": ["Berlin", "Hamburg"] }, { "field": "business.city", "operator": "starts_with", "value": "Mün" } ] } ] } ``` Reads as: has a website AND has no email yet AND (is in Berlin or Hamburg OR its city starts with `Mün`). **Build from the catalogue, never from a column name.** `field` must be a `key` from the `fields` array in the same response. An unlisted key is refused as `unknown_field` and is never interpolated into SQL, not even to report the error. **Errors:** | Status | Reason code | When | How to resolve | | --- | --- | --- | --- | | `401` | unauthenticated | Credentials missing or wrong. | Send a valid Bearer triple, PAT, or session cookie. | The catalogue itself cannot fail on input. It takes no parameters. ## `GET /api/v1/dashboard/bulk-lead-sourcing/corpus/values` Distinct values for one field, for populating a picker. **Parameters** | Field | Type | Required | Description | | --- | --- | --- | --- | | `field` | string | yes | A `key` from the catalogue. | | `q` | string | no | Substring filter on the returned values. | ```bash curl -G "https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/corpus/values" \ -H "Authorization: Bearer $CLIENT_ID:$API_KEY:$API_SECRET" \ --data-urlencode "field=business.city" \ --data-urlencode "q=ber" ``` **Response** ```json { "field": "business.city", "values": [ { "value": "Berlin", "count": 1204 }, { "value": "Bergamo", "count": 87 } ] } ``` Feed a `value` straight back into a condition using `is` or `is_any_of`. **Errors:** | Status | Reason code | When | How to resolve | | --- | --- | --- | --- | | `401` | unauthenticated | Credentials missing or wrong. | Send a valid Bearer triple, PAT, or session cookie. | | `422` | `unknown_field` | `field` is not a catalogue key. | Read `fields[].key` from the catalogue. | ## `POST /api/v1/dashboard/bulk-lead-sourcing/corpus/count` Both counts for a filter, saving nothing. Free, and cheap enough to fire while the user is still typing. **Parameters** | Field | Type | Required | Description | | --- | --- | --- | --- | | `stages` | string[] | yes | The stages the run would enable. `eligible_leads` is measured against these. | | `source_kind` | string | no | `corpus_query`, the default, or `unenriched_run`. | | `filter` | object | no | The filter AST. Omit it to count the whole corpus. | | `campaign_id` | string | no | For `unenriched_run`. | | `job_id` | string | no | For `unenriched_run`, one job inside a campaign. | | `exclude_without_website` | boolean | no | Defaults to `true`. | ```bash curl -X POST "https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/corpus/count" \ -H "Authorization: Bearer $CLIENT_ID:$API_KEY:$API_SECRET" \ -H "Content-Type: application/json" \ -d '{ "source_kind": "corpus_query", "stages": ["spidersite"], "filter": { "op": "and", "conditions": [ { "field": "has.website", "operator": "is_not_empty" }, { "field": "has.crawl", "operator": "is_empty" } ] } }' ``` **Response** ```json { "matched_leads": 10064, "eligible_leads": 9862 } ``` Both numbers come from one statement against one snapshot of the corpus, so they always describe the same instant. **Errors:** | Status | Reason code | When | How to resolve | | --- | --- | --- | --- | | `401` | unauthenticated | Credentials missing or wrong. | Send a valid Bearer triple, PAT, or session cookie. | | `422` | `unknown_field` | `field` is not a catalogue key. | Read `fields[].key` from the catalogue. | | `422` | `invalid_operator` | The operator is not one this field's type accepts. | Check `ast.operators_by_type`, or the field's own `operators`. | | `422` | `operator_not_allowed` | A valid operator, wrong field type. Comparing a city with `>`. | Use an operator from that field's list. | | `422` | `unexpected_value` | A `value` was sent with `is_empty` or `is_not_empty`. | Drop the `value`. | | `422` | `missing_value` | An operator that needs a value did not get one. | Supply `value`. | | `422` | `invalid_value` | The value does not match the field's type, such as a non-uuid for a uuid field. | Check `ast.value_rules`. | | `422` | `value_too_long` | A single value exceeds the length cap. | Shorten it. | | `422` | `list_too_long` | More than 100 values in one list operator. | Split the condition. | | `422` | `too_many_conditions` | More than 20 conditions. | Simplify, or run two selections. | | `422` | `too_many_groups` | More than 5 groups. | Flatten a level. | | `422` | `too_deep` | Nesting past depth 3. | Flatten a level. | | `422` | `invalid_group_operator` | A group `op` that is not `and` or `or`. | Use `and` or `or`. | | `422` | `malformed_node` | A node that is neither a group nor a condition. `filter: {}` is refused for this reason. | Send a group with an `op` and a `conditions` array, or omit `filter`. | | `422` | `unknown_condition_key` | An unrecognised key on a condition object. | A condition takes `field`, `operator` and `value` only. | | `422` | validation error | `stages` omitted. | `stages` is required and never defaults. | ## `POST /api/v1/dashboard/bulk-lead-sourcing/corpus/leads` A page of the leads a filter matches, so the user can see what they are about to enrich. Same body as `corpus/count`, plus paging. **Parameters** | Field | Type | Required | Description | | --- | --- | --- | --- | | `cursor` | string | no | Keyset cursor from the previous page. Not an offset. | | `sort` | string | no | Sort order for the page. | | `limit` | integer | no | Page size. | ```bash curl -X POST "https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/corpus/leads?limit=2" \ -H "Authorization: Bearer $CLIENT_ID:$API_KEY:$API_SECRET" \ -H "Content-Type: application/json" \ -d '{ "source_kind": "corpus_query", "stages": ["spidersite"], "filter": { "op": "and", "conditions": [ { "field": "business.city", "operator": "is", "value": "Berlin" } ] } }' ``` **Response** ```json { "leads": [ { "business_id": "b7f1...", "name": "Cafe Kranzler", "city": "Berlin", "website": "https://cafe-kranzler.example" } ], "next_cursor": "eyJpZCI6..." } ``` Page forward by sending `next_cursor` back as `cursor`. Paging is keyset, not offset, so a corpus that grows while you page will not hand you the same row twice. Rows carry business identity only. **No email, contact or phone field is ever returned here**, even though the eligibility gates read those columns to decide what still needs work. **Errors:** the same fourteen filter refusals as `corpus/count`, plus `401`. ## `POST /api/v1/dashboard/bulk-lead-sourcing/selections` Save a question. Returns a `selection_id` you can submit. **Parameters** | Field | Type | Required | Description | | --- | --- | --- | --- | | `source_kind` | string | yes | `unenriched_run` or `corpus_query`. | | `stages` | string[] | yes | The stages the new run will enable. Eligibility is scoped to these. | | `campaign_id` | string | conditional | Required for `unenriched_run` unless `job_id` is given. | | `job_id` | string | no | One job, one location, inside a campaign. | | `filter` | object | no | The filter AST, for `corpus_query`. Same shape and same validator as `corpus/count`. | | `exclude_without_website` | boolean | no | Defaults to `true`. | ```bash curl -X POST "https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/selections" \ -H "Authorization: Bearer $CLIENT_ID:$API_KEY:$API_SECRET" \ -H "Content-Type: application/json" \ -d '{ "source_kind": "corpus_query", "stages": ["spidersite"], "filter": { "op": "and", "conditions": [ { "field": "has.website", "operator": "is_not_empty" }, { "field": "has.crawl", "operator": "is_empty" } ] } }' ``` **Response** — `201 Created` ```json { "selection_id": "8db896d0-...", "source_kind": "corpus_query", "matched_leads": 10064, "eligible_leads": 9862, "expires_at": "2026-08-30T12:00:00Z" } ``` A selection expires after seven days and is scoped to the tenant that created it. **Errors:** | Status | Reason code | When | How to resolve | | --- | --- | --- | --- | | `401` | unauthenticated | Credentials missing or wrong. | Send a valid Bearer triple, PAT, or session cookie. | | `404` | not found | The `selection_id` belongs to another tenant, or does not exist. | Both cases return the same body on purpose. A `403` would confirm the id exists. | | `422` | filter refusal | Any of the fourteen codes in the `corpus/count` table. | Same resolutions. | | `422` | validation error | `source_kind` is `unenriched_run` with neither `campaign_id` nor `job_id`. | Supply one. | ## Submitting an internal run The selection travels alone. `source.selection` is an id and nothing else, and the third arm is mutually exclusive with `queries` and `upload`. ```bash curl -X POST "https://spideriq.ai/api/v1/bulk-lead-sourcing/submit" \ -H "Authorization: Bearer $CLIENT_ID:$API_KEY:$API_SECRET" \ -H "Content-Type: application/json" \ -d '{ "source": { "provider": "internal", "source_kind": "corpus_query", "queries": [], "selection": { "selection_id": "8db896d0-..." } }, "settings": { "workflow": { "spidersite": { "enabled": true }, "vayapin": { "enabled": false } } } }' ``` **The filter is never accepted on submit.** Sending one is a `422` with `extra_forbidden`. This is not tidiness: a body that could carry a predicate could carry someone else's predicate, so the only thing that crosses the submit boundary is an id the server already validated and already scoped to your tenant. The run resolves the selection at dispatch, snapshots the eligible leads, and fans out one job per lead into the same pipeline every other source uses. The result envelope is byte-identical to a campaign's. ## `POST /api/v1/dashboard/bulk-lead-sourcing/estimate` Size a run **without committing to it**. Writes nothing, contacts no provider, consumes no quota. Asking is not buying, which is exactly why this is a separate route from submit: submit returns its estimate only after the manifest is written. Takes the same body as submit. ```bash curl -X POST "https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/estimate" \ -H "Authorization: Bearer $CLIENT_ID:$API_KEY:$API_SECRET" \ -H "Content-Type: application/json" \ -d '{ "source": { "provider": "csv", "source_kind": "google_maps", "queries": [], "upload": { "upload_id": "97a3669f-3c2e-4a1b-9d55-0f2b7c8e4d10" } }, "settings": { "workflow": { "spidersite": { "enabled": true }, "spiderverify": { "enabled": true } } } }' ``` **Response** ```json { "provider": "csv", "source_kind": "google_maps", "estimated_queries": 0, "estimated_records": 3, "estimated_cost_usd": null, "has_cost": false, "records_cap": 25000, "would_exceed_cap": false, "is_upload": true, "source_is_free": true, "record_count": 3, "record_count_known": true, "enrichment_stages": ["spidersite", "spiderverify"], "enrichment_jobs": 6, "upload_filename": "my-leads.csv" } ``` | Field | Meaning | | --- | --- | | `is_upload` | The source is a file, not a provider job. | | `source_is_free` | Nothing is spent acquiring the records. Use this, not `has_cost`, to decide whether to show a purchase confirmation. | | `record_count_known` | The exact record count is known ahead of the run. True for uploads; false for a provider job, where `estimated_records` is an upper bound. | | `enrichment_jobs` | Leads times enabled stages. This is what an upload actually costs you. | | `would_exceed_cap` | Advisory. The authoritative refusal is still the dispatcher's at submit; this runs the same comparison early so the user finds out before the confirm button rather than after it. | **Errors:** | Status | Reason code | When | How to resolve | | --- | --- | --- | --- | | `401` | unauthenticated | Credentials are missing or wrong. | Send a valid Bearer triple, PAT, or session cookie. | | `422` | schema validation | A field failed validation, identical to submit. | The response names the field. Fix it and call again. | | `422` | stage ineligible | `settings.workflow` enabled a stage this `source_kind` cannot serve. | Disable that stage, or change `source_kind`. | | `404` | unknown upload | `source.upload.upload_id` does not exist, is expired, or belongs to another tenant. | Upload the file again and use the new `upload_id`. | There is no `429` here. Nothing is reserved and nothing is bought, so there is nothing to refuse. `would_exceed_cap` reports what submit *would* do rather than refusing on its behalf. ## `POST /api/v1/bulk-lead-sourcing/submit` Submit one bulk buy order. Returns `202 Accepted` with a manifest handle; the purchase happens afterwards, in the worker. Authenticate with `Authorization: Bearer ::`. A dashboard-scoped twin lives at `POST /api/v1/dashboard/bulk-lead-sourcing/submit`. It accepts a session cookie or a PAT and runs the **same gate** through the same `execute_submission` call. Two doors onto one irreversible spend do not mean two gates. **Parameters** | Field | Type | Required | Description | | --- | --- | --- | --- | | `source` | object | yes | The provider-neutral buy order. | | `source.provider` | string | yes | Registered lead-source adapter, 1 to 64 characters: `outscraper`, `apify`, `csv`, `json`, `sortlist`, `internal`. Resolved against the registry at request time, so it is deliberately not an enum. | | `source.source_kind` | string | yes | What shape of source to ask for: `google_maps`, `linkedin_company`, `sortlist_agency`, `unenriched_run` or `corpus_query`. Bounds which downstream stages are eligible. | | `source.queries` | string[] | yes | Bare search terms, 1 to 1,000 entries. The effective search list is `queries x geo labels`. Send `[]` for an upload or internal source, which has no searches. | | `source.upload` | object | no | `{ "upload_id": "..." }` from the upload route. Required for `csv` and `json`; forbidden for a provider. | | `source.selection` | object | no | `{ "selection_id": "..." }` from the selections route. Required for `internal`; forbidden elsewhere. Mutually exclusive with `queries` and `upload`. The filter itself is never accepted here. | | `source.geo` | object[] | no | Geographic targets, up to 1,000 entries. Each carries `label`, `latitude`, `longitude`, `country_code`, `region`. A labelled entry multiplies the query list; a coordinate-only entry steers the search centre. Not used by an upload. | | `source.limits.max_records_per_query` | integer | no | Records per expanded query, 1 to 100,000. Omitted falls back to 500. | | `source.limits.max_total_records` | integer | no | Ceiling on total records for the whole job, 1 to 1,000,000. | | `source.filters` | object | no | Provider-specific knobs. For `csv` this carries `column_mapping`, an object of `field -> your header`. The adapter refuses any key that would overwrite a field this request already owns. | | `source.language` | string | no | Result language, 2 to 8 characters. Defaults to `en`. | | `settings.workflow` | object | no | Which downstream stages each sourced lead runs: `spidersite`, `spiderverify`, `vayapin`, `social_media_enrichment`, `smartlead`. This is the same `WorkflowConfig` a campaign uses, reused verbatim. | | `priority` | integer | no | Queue priority for the fanned-out leads, 0 to 10. Defaults to 5. | | `test` | boolean | no | Route to test queues; no production side effects. Defaults to `false`. | **Example — a provider job** ```ts import { SpiderIQClient } from "@spideriq/core"; const client = new SpiderIQClient({ token: process.env.SPIDERIQ_PAT }); const run = await client.bulkLeadSourcing({ source: { provider: "apify", source_kind: "google_maps", queries: ["restaurants", "cafes"], geo: [ { label: "Atlanta, Georgia, USA", country_code: "US" }, { label: "Savannah, Georgia, USA", country_code: "US" }, ], limits: { max_records_per_query: 100 }, }, settings: { workflow: { spidersite: { enabled: true }, spiderverify: { enabled: true }, vayapin: { enabled: false }, }, }, }); console.log(run.estimated_records); // → 400, across 4 expanded queries console.log(run.estimated_cost_usd); // → 1.6 on apify; null on outscraper // a 429 here means a guard refused the run BEFORE anything was bought ``` **Example — an uploaded CSV** Upload first, confirm the mapping, then submit the `upload_id` with the mapping you confirmed. ```bash curl -X POST "https://spideriq.ai/api/v1/bulk-lead-sourcing/submit" \ -H "Authorization: Bearer $CLIENT_ID:$API_KEY:$API_SECRET" \ -H "Content-Type: application/json" \ -d '{ "source": { "provider": "csv", "source_kind": "google_maps", "queries": [], "upload": { "upload_id": "97a3669f-3c2e-4a1b-9d55-0f2b7c8e4d10" }, "filters": { "column_mapping": { "name": "Firma", "address": "Str.", "city": "Ort", "postal_code": "PLZ", "phone": "Tel", "website": "Homepage" } } }, "settings": { "workflow": { "spidersite": { "enabled": true } } } }' ``` The mapping keys are our field names; the values are your headers, byte for byte as the upload reported them. Do not normalise a header before sending it back. From the CLI: ```bash spideriq bulk-source submit \ -q restaurants -q cafes \ -g "Atlanta, Georgia, USA" -g "Savannah, Georgia, USA" \ --max-records 100 ``` From an MCP client: ``` submit_bulk_lead_sourcing( provider="apify", source_kind="google_maps", queries=["restaurants", "cafes"], geo=[{"label": "Atlanta, Georgia, USA"}, {"label": "Savannah, Georgia, USA"}], max_records_per_query=100 ) ``` The CLI and MCP surfaces cover the **provider** sources. An upload needs the multipart call above first, which the dashboard drives. **Response** ```json { "bulk_job_id": "051c62ab-ef51-470a-a8f0-4abdd6b14f90", "job_id": "7ae3b20d-a9ee-4601-a6ed-317885f6a3aa", "provider": "apify", "source_kind": "google_maps", "status": "pending", "estimated_queries": 4, "estimated_records": 400, "estimated_cost_usd": 1.6, "message": "Bulk lead sourcing accepted" } ``` | Field | Meaning | | --- | --- | | `bulk_job_id` | The manifest handle: provenance, counts, artifact digest. | | `job_id` | The parent job. Poll this for progress. | | `estimated_queries` | Concrete search strings after `queries x geo` expansion. `0` for an upload. | | `estimated_records` | Upper-bound record count the budget guard was evaluated against. For an upload this is the exact counted total. | | `estimated_cost_usd` | `estimated_records` times the provider unit cost. A `null` means no unit cost is configured, which **does not mean free** — the record ceiling is then the only guard on the run. For an upload it is `null` because the source genuinely costs nothing. | **Errors:** | Status | Reason code | When | How to resolve | | --- | --- | --- | --- | | `422` | schema validation | A field failed validation, including an upload source sent without `source.upload`, or a provider sent with one. | The response names the field. Fix it and resubmit. | | `422` | expansion ceiling | `queries x geo` expanded past 1,000 concrete queries. | Split the run into several submissions. | | `422` | stage ineligible | `settings.workflow` enabled a stage this `source_kind` cannot serve. | Disable that stage, or change `source_kind`. See the eligibility table below. | | `422` | column mapping | The mapping named a column not present in the file, or resolved no identifying column. | Re-read `columns` from the upload response and map at least one of `name`, `place_id`, `google_place_id`, `website`, `phone`. | | `429` | `bulk_records_per_job_exceeded` | Estimated records above your per-job ceiling, 25,000 by default. | Narrow the run. This response carries no `Retry-After` by design: the run is too big and stays too big, so retrying it unchanged fails identically forever. | | `429` | `bulk_estimated_spend_exceeded` | Projected 24-hour spend would cross your cost ceiling. | Wait, or narrow the run. This one does carry `Retry-After`, because the window rolls. | Both `429`s are evaluated before the manifest is written and before anything is purchased, so a denial leaves nothing behind and charges nothing. The record ceiling applies to an upload exactly as it does to a purchase, because an upload's records are counted at upload time. ## Checking progress There is no bulk-specific status route. Poll the parent job with the `job_id` the submit returned: ```bash curl "https://spideriq.ai/api/v1/jobs/7ae3b20d-a9ee-4601-a6ed-317885f6a3aa/status" \ -H "Authorization: Bearer $CLIENT_ID:$API_KEY:$API_SECRET" ``` The manifest walks these states: ``` pending -> submitted -> polling -> ready -> fetching -> parsing -> fanning_out -> enriching -> completed (or partial, or failed, or cancelled) ``` A `polling` state lasting hours is normal: the provider is running your searches. `enriching` means the leads are fanned out and the downstream pipeline is still working. It is **not** terminal, and this is the point: sourcing successfully is not the same as enriching successfully. The four terminal states are `completed`, `partial`, `failed` and `cancelled`. The results envelope carries `enriched_count` and `not_enriched_count`. A run where some leads enriched and some did not is `partial`; a run where none did is `failed`, not `completed`. Results do **not** come back in the same envelope as a campaign. A bulk run fans out one job per kept lead, so the parent job carries the run's funnel and the leads live on its children. Reading a bulk run is two steps, and the parent hands you the child ids. ## `GET /api/v1/jobs/{job_id}/results` `GET /api/v1/jobs/{job_id}/results` on the parent returns what the run *did*: how many records the provider delivered, how many survived your filters and why the rest did not, what the run spent, and the id of every fanned-out child. It never carries a `businesses` array. **Step 1 — the parent, for the funnel.** ```bash curl "https://spideriq.ai/api/v1/jobs/a582d8e1-5873-4be9-aca9-668a290b412d/results" \ -H "Authorization: Bearer $CLIENT_ID:$API_KEY:$API_SECRET" ``` Response, abridged to the three blocks this endpoint added: ```json { "success": true, "type": "bulkLeadSourcing", "status": "completed", "data": { "screening": { "provider_delivered": 2, "kept": 2, "dropped": 0, "drop_reasons": {} }, "cost": { "cost_usd": 0.0, "currency": "USD", "source_is_free": true, "provider_records": 2, "provider_queries": 1, "estimated_records": 2, "estimated_cost_usd": null }, "children": { "campaign_id": "bulk_dca5160507cf4a698d9f7d80c5320c2c", "fanned_out_count": 2, "job_ids": [ "1d169a58-ee06-470c-97a1-6415611de072", "97c44c7b-1394-4fa7-b7da-25723462f705" ], "job_ids_truncated": false, "results_endpoint_template": "/api/v1/jobs/{job_id}/results", "workflow_results_endpoint": "/api/v1/jobs/spiderMaps/campaigns/bulk_dca5160507cf4a698d9f7d80c5320c2c/workflow-results" }, "delivered_count": 2, "deduped_count": 2, "fanned_out_count": 2, "enriched_count": 2, "not_enriched_count": 0, "requested_count": 2, "provider": "sortlist", "source_kind": "google_maps", "stages_enabled": ["spidersite"], "enrichment_requested": true, "bulk_job_id": "…", "campaign_id": "bulk_dca5160507cf4a698d9f7d80c5320c2c", "flow_path": "…" } } ``` **Step 2 — each child, for the leads.** Substitute a `children.job_ids` entry into `children.results_endpoint_template`: ```bash curl "https://spideriq.ai/api/v1/jobs/1d169a58-ee06-470c-97a1-6415611de072/results" \ -H "Authorization: Bearer $CLIENT_ID:$API_KEY:$API_SECRET" ``` A child answers with the ordinary per-lead envelope: `data.businesses`, `data.metadata`, `data.query`, `data.results_count`. That is where the campaign-shaped result you are looking for actually lives. The same two steps from the client library: ```ts import { SpiderIQClient } from "@spideriq/core"; const client = new SpiderIQClient({ token: process.env.SPIDERIQ_PAT }); const parent = await client.getJobResults("a582d8e1-5873-4be9-aca9-668a290b412d"); console.log(parent.data.screening.kept); // 2 console.log(parent.data.cost.cost_usd); // 0 const leads = []; for (const childId of parent.data.children.job_ids) { const child = await client.getJobResults(childId); leads.push(...child.data.businesses); } console.log(leads.length); // 2 ``` **What the parent's fields mean.** | Field | Meaning | | --- | --- | | `screening.provider_delivered` | Records the provider actually returned, before any filtering of yours. | | `screening.kept` | Records that survived your filters and were fanned out for enrichment. | | `screening.dropped` | Records rejected before fan-out. | | `screening.drop_reasons` | Reason to count, e.g. `{"too_few_reviews": 20}`. | | `cost.cost_usd` | What the run spent at the source. | | `cost.source_is_free` | Nothing was bought to obtain these records. Branch your cost messaging on this, never on a null figure. | | `cost.estimated_cost_usd` | `null` means no unit price is configured for the provider. It does not mean free. | | `children.job_ids` | One job id per kept lead. Read each one through `results_endpoint_template`. | | `children.job_ids_truncated` | `true` when the run fanned out more than 100 leads and the list was capped. Use `workflow_results_endpoint` for the whole run. | | `children.workflow_results_endpoint` | A per-stage roll-up across every child, including `workflow_progress.sites_completed`. | | `enriched_count` / `not_enriched_count` | Leads that finished the enrichment pipeline, and leads that did not. | **Zero kept is an answer, not a failure.** A completed run reporting `kept: 0` with a populated `drop_reasons` is a run that worked: the provider delivered records and **your own** screening rejected them. The run below delivered 20 and kept none because every record fell under the review floor. ```json "screening": { "provider_delivered": 20, "kept": 0, "dropped": 20, "drop_reasons": {"too_few_reviews": 20} } ``` `screening` **absent entirely** is what a broken run looks like. A zero with a reason beside it is a working filter, and `fanned_out_count: 0` follows from it rather than indicating a fault. **Errors:** | Status | Reason code | When | How to resolve | | --- | --- | --- | --- | | 404 | job not found | The `job_id` is wrong, or the job belongs to another tenant. | Use the `job_id` the submit returned. A `bulk_job_id` is not a job id and has no job route. | | 401 | unauthenticated | Credentials are missing or wrong. | Send a valid Bearer triple, PAT, or session cookie. | | 422 | string_pattern_mismatch | You sent `?format=json`. | JSON is the default. Send no `format` param at all; only `yaml` and `md` are accepted values. | | 200, `data.screening` absent | — | The run failed before it stored a summary. | Read `GET /jobs/{job_id}/status`; a failed run carries an `error_message`. | > **Do not list a bulk run's children through **`**GET /api/v1/jobs/spiderMaps/campaigns/{campaign_id}/jobs**`**.** It answers `200` with a correct `total` and an empty `jobs` array for every bulk campaign, because a bulk child is a per-lead job and carries no location. Use `children.job_ids`, which is why it is inlined. ## Stage eligibility depends on the source kind | Source kind | Eligible stages | | --- | --- | | `google_maps` | All: site, verify, vayapin, social enrichment, smartlead. | | `linkedin_company` | Site, verify, social enrichment, smartlead. Never vayapin. | | `sortlist_agency` | Site, verify, social enrichment, smartlead. Never vayapin. | | `unenriched_run` | All, bounded by what each lead still lacks. | | `corpus_query` | All, bounded by what each lead still lacks. | A LinkedIn company has no street address and no `place_id`, so a map pin would be garbage. Enabling an ineligible stage is a `422` at submit, deliberately not a silent skip: a run that quietly did less looks like a success. `linkedin_company` appears in the eligibility table although no adapter serves it yet. That is deliberate. The rule is a property of the source kind, not of the adapter, so a future LinkedIn adapter inherits the vayapin exclusion instead of rediscovering it against live data. ## How deduplication works The flat result set is deduplicated on an exact canonical key, in this precedence: `place_id`, then website domain, then phone, then name plus locality. There is no fuzzy matching. For an uploaded file the key is derived from the columns **you mapped**, so mapping a website or phone column gives you meaningfully distinct leads and mapping none of them collapses the file. The key is scoped to the pair of `bulk_job_id` and canonical key, so deduplication is per run and does not span runs. A business you sourced last week will be sourced again this week. ## How bulk differs from a campaign | | Campaign | Bulk | | --- | --- | --- | | Purchases | One search per location. | One provider job for the whole set, or no purchase at all for an upload. | | Deduplication | Per location. | Across the whole result set. | | Retry | Per-location retry on a thin result. | None. A thin result is re-bought, not retried. | | Stopping mid-flight | Supported. | Not supported. | | Result envelope | Per-lead results on the campaign job. | A funnel summary on the parent; the per-lead results sit on the fanned-out children, reachable through `data.children.job_ids`. | A **child** job's envelope is byte-identical to a campaign job's, so every parser and export that already reads campaign output reads a bulk child unchanged. What differs is the entry point: you reach those children through the parent's `data.children.job_ids` instead of through a campaign id. An uploaded record arrives as its own result rather than being re-searched, so its `metadata.query` is `null`. ## Related - `POST /api/v1/campaigns/submit` — the per-location alternative. - `GET /api/v1/jobs/{job_id}/status` and `GET /api/v1/jobs/{job_id}/results`. ## SpiderBook Source: https://spideriq.ai/docs/api-reference/spiderbook ## `POST` /api/v1/booking/{flow_id}/submit **Public submit — confirm a booking OR persist a form submission (no auth)** Submit against an active flow. SpiderFlow P1.W (2026-05-13) + P2.1 (2026-05-14) — the body schema is dispatched at runtime based on the flow's ``kind`` discriminator (mig 233): - ``kind='booking'`` / ``'commerce'`` — body must satisfy :class:`BookingAnswers` (slot_start / slot_end / hold_id / contact / consent + optional service_id / staff_id / extra). The Cal.com booking path + Turnstile + GDPR-booking-consent gates run as before. - ``kind='form'`` — body is a flat ``{field_id: value}`` map (or nested ``{step_id: {field_id: value}}``); validated against the flow's FormStep fields. Cal.com / hold / Turnstile / booking-consent gates are NOT applied (forms are not bookings). The submission is persisted to ``public.results`` with ``worker_type='booking', phase='final', data.kind='form'`` so analytics / CRM-sync handlers can pick it up without a new migration. - ``kind='funnel'`` (P2.1) — the public /submit endpoint is not applicable. Funnels sequence SpiderPublish pages and don't carry a single terminal submission event. Callers receive a structured 422 ``submit_not_applicable_for_funnel`` envelope. To collect data from a funnel visitor, embed a form sub-flow whose own ``/submit`` persists the data. Route-level dependencies are intentionally light; every check happens inside the handler so one handler serves the whole flow (G2→G15) and tests can exercise the full chain without wrangling FastAPI's dependency tree. **Parameters** - `flow_id` (path, string, required) :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/booking/{flow_id}/submit' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/booking/{flow_id}/submit", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/booking/{flow_id}/submit", { method: "POST", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/booking/{flow_id}/submit", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `400` — Consent missing, invalid payload, or unknown / missing required form field. - `403` — Turnstile verification failed. - `404` — Flow not found or inactive. - `409` — Slot hold expired or Cal.com reported slot taken. - `422` — Body fails BookingAnswers validation (kind='booking' / 'commerce' only) OR kind='funnel' submit is not applicable. - `429` — IP rate limit exceeded. - `503` — Cal.com temporarily unavailable — retry. ## `GET` /api/v1/booking/{flow_id} **Fetch a booking flow descriptor (agent-facing)** Returns a minimal {flow_id, kind, name, status} descriptor for the given flow_id. Intended for agents (Claude Code, MCP tools) to discover a flow's kind before driving the full /render endpoint. On wrong kind, returns 409 with a suggested_url pointing at the right endpoint. On unknown id, returns 404 RESOURCE_NOT_FOUND. **Parameters** - `flow_id` (path, string, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/booking/{flow_id}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/booking/{flow_id}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/booking/{flow_id}", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/booking/{flow_id}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `404` — Flow not found (RESOURCE_NOT_FOUND envelope) - `409` — Flow is not a booking (WRONG_FLOW_KIND envelope) - `422` — Validation Error ## `GET` /api/v1/booking/{flow_id}/render **Fetch the public flow payload (localized via Accept-Language)** Returns the flow JSON the booking component mounts. Honours Accept-Language: labels/descriptions/button_label are swapped via translations[locale] with fallback to English. Unknown flows → 404. **Parameters** - `flow_id` (path, string, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/booking/{flow_id}/render' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/booking/{flow_id}/render", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/booking/{flow_id}/render", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/booking/{flow_id}/render", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/booking/{flow_id}/render **Record a flow-render event (public, no auth)** Fire-and-forget analytics write called by the booking component on mount and step transitions. Never returns 5xx — analytics is non-critical. **Parameters** - `flow_id` (path, string, required) **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/booking/{flow_id}/render' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "step_reached": "string" }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/booking/{flow_id}/render", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"step_reached": "string"}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/booking/{flow_id}/render", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"step_reached": "string"}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"step_reached": "string"}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/booking/{flow_id}/render", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `202` — Successful Response - `422` — Validation Error ## `GET` /api/v1/booking/manage/{token} **Customer self-service — load booking details (no auth; signed token)** Return booking details for the holder of a valid signed token. **Parameters** - `token` (path, string, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/booking/manage/{token}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/booking/manage/{token}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/booking/manage/{token}", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/booking/manage/{token}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `403` — Token invalid, expired, or tenant mismatch. - `422` — Validation Error ## `POST` /api/v1/booking/manage/{token}/reschedule **Customer self-service — reschedule booking (no auth; signed token)** Reschedule a booking. Calls Cal.com, appends a ``rescheduled`` row, and sends the customer a follow-up email. **Parameters** - `token` (path, string, required) **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/booking/manage/{token}/reschedule' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "new_slot_start": "2026-01-01T00:00:00Z", "new_slot_end": "2026-01-01T00:00:00Z", "new_staff_id": "00000000-0000-0000-0000-000000000000" }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/booking/manage/{token}/reschedule", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"new_slot_start": "2026-01-01T00:00:00Z", "new_slot_end": "2026-01-01T00:00:00Z", "new_staff_id": "00000000-0000-0000-0000-000000000000"}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/booking/manage/{token}/reschedule", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"new_slot_start": "2026-01-01T00:00:00Z", "new_slot_end": "2026-01-01T00:00:00Z", "new_staff_id": "00000000-0000-0000-0000-000000000000"}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"new_slot_start": "2026-01-01T00:00:00Z", "new_slot_end": "2026-01-01T00:00:00Z", "new_staff_id": "00000000-0000-0000-0000-000000000000"}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/booking/manage/{token}/reschedule", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `400` — Invalid payload or slot_start in the past. - `403` — Token invalid, expired, or tenant mismatch. - `409` — Booking is already cancelled or in a terminal state. - `503` — Cal.com temporarily unavailable — retry. - `422` — Validation Error ## `POST` /api/v1/booking/manage/{token}/cancel **Customer self-service — cancel booking (no auth; signed token)** Cancel a booking. Calls Cal.com, appends a ``cancelled`` row, and sends the customer a confirmation of cancellation. **Parameters** - `token` (path, string, required) **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/booking/manage/{token}/cancel' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "reason": "string" }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/booking/manage/{token}/cancel", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"reason": "string"}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/booking/manage/{token}/cancel", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"reason": "string"}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"reason": "string"}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/booking/manage/{token}/cancel", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `403` — Token invalid, expired, or tenant mismatch. - `409` — Booking is already cancelled or in a terminal state. - `503` — Cal.com temporarily unavailable — retry. - `422` — Validation Error ## `POST` /api/v1/booking/{flow_id}/upload-presign **Get a signed URL to upload a form file_upload field's file** Issue a presigned SeaweedFS PUT URL for one ``file_upload`` field. Validation order: rate limit → flow exists+active → ``kind='form'`` → field exists and is ``file_upload`` → content-type allowed (skipped when the field has no ``accept`` whitelist) → size within the field's ``max_size_mb`` → presign. **Parameters** - `flow_id` (path, string, required) **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/booking/{flow_id}/upload-presign' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "field_id": "string", "content_type": "string", "file_size_bytes": 0, "filename": "string", "session_id": "00000000-0000-0000-0000-000000000000" }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/booking/{flow_id}/upload-presign", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"field_id": "string", "content_type": "string", "file_size_bytes": 0, "filename": "string", "session_id": "00000000-0000-0000-0000-000000000000"}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/booking/{flow_id}/upload-presign", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"field_id": "string", "content_type": "string", "file_size_bytes": 0, "filename": "string", "session_id": "00000000-0000-0000-0000-000000000000"}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"field_id": "string", "content_type": "string", "file_size_bytes": 0, "filename": "string", "session_id": "00000000-0000-0000-0000-000000000000"}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/booking/{flow_id}/upload-presign", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `404` — Flow not found, inactive, or draft. - `422` — Validation failed — non-form flow, unknown / wrong field, content-type or size rejected. - `429` — IP rate limit exceeded. - `503` — File uploads not available for this form. ## `GET` /api/v1/booking/feature-status **Get Feature Status** :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/booking/feature-status' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/booking/feature-status", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/booking/feature-status", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/booking/feature-status", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response ## Enrich leads you already have Source: https://spideriq.ai/docs/lead-generation/enrich-leads-you-already-have **Who this is for:** Anyone with leads already in their SpiderIQ account who wants more detail on them, without buying the same businesses a second time. **What this does:** Runs the enrichment stages you choose over leads you already own. There is no purchase at the source, so you pay only for the enrichment, and only for the leads it can actually help. ## Before you begin - You have run at least one campaign, bulk run or lead search, so there are leads in your account. - You know which stages you want: website scrape, email verification, social media enrichment, or a VayaPin profile. - Take a moment on VayaPin. It publishes a permanent public profile page for every lead in the run, and deleting the run does not remove those pages. Leave it off unless you mean it. ## Two ways to choose the leads Both live under the same source, and you pick between them once you have selected it. | Choose | When | | --- | --- | | **A past run** | You remember the campaign. "The dentists I pulled in June never got their sites crawled." | | **Query the corpus** | You do not care which run they came from. "Everything with a website but no email." | ## Steps 1. Open **Flows** and select **Bulk Lead Sourcing**. 2. Under **Source**, open the picker and choose **Your existing leads**. The badge reads `NO SOURCE COST`, because nothing is being bought. 3. Tick the **enrichment stages** you want, before you pick any leads. The counts you are about to see are measured against these stages, so changing them changes the answer. 4. Choose **A past run** to work from one campaign, or **Query the corpus** to filter across everything. For a past run, you get a list of your campaigns with a label showing what each one already ran, and two numbers per row. ![The past-run picker, listing campaigns with what each already ran and both lead counts](https://media.cdn.spideriq.ai/content/cli_5z6zs9k9po75hd4s/general/1787571132-7e1e7ad8.webp) For a corpus query, start from a preset or add your own conditions. A preset writes its conditions into the builder where you can edit or delete them, rather than hiding them. ![The corpus query builder, with a preset condition and both live counts](https://media.cdn.spideriq.ai/content/cli_5z6zs9k9po75hd4s/general/1787571134-88e62d68.webp) 1. Read both numbers before you continue. They are not the same thing, and the second one is the one that matters. See below. 2. Click **Review & run**, check the confirmation screen, and start the run. ## The two numbers, and why they differ > Note: This is the single thing worth understanding on this screen. **Match** is how many leads your question describes. **Eligible** is how many of those would actually gain something from the stages you ticked. A lead whose website was crawled last month gains nothing from another crawl, so it is counted as matched but not eligible. On a corpus you have been building for a while the gap can be large, and that is the system working. Selecting 5,000 leads and enriching 200 of them is an ordinary result, not an error. Eligible is the number your run is sized and priced on. It is also the number that moves when you change the stages: tick one more stage and leads that had nothing to gain suddenly do. ## Verify it worked Open the **Runs** tab on the flow. Your run appears with the eligible count you were shown, and the leads move through the stages one job at a time. Open any finished child job and you will see the same result shape a campaign produces, so anything that already reads your campaign output reads this too. If you queried the corpus, the filter is what was saved, not the list of leads. The filter is re-run when the run starts, so a count taken a day earlier can move if your leads changed in between. ## Troubleshoot - **Eligible says 0, but match is large.** Every lead your question found already has everything the ticked stages produce. Add a stage, or widen the filter. - **The eligible number changed when I did not touch the filter.** You changed the stages. Eligibility is measured against the stages, so the two always move together. - **A campaign I remember as maps-only says it already ran more than that.** A campaign saved without an explicit stage configuration ran the full pipeline, not an empty one. The label reflects what really ran. - **I want the leads themselves, not a run.** This screen enriches. To read the data you already hold, use the lead data API instead. - **The run is taking hours.** Fan-out is paced against your hourly job allowance rather than dispatched all at once. A large run genuinely takes a long time; it is not stuck. ## Related - [Buy a lead list in bulk](bulk-lead-sourcing) - [Enrich a list you already have](bulk-lead-sourcing-upload) - [Bulk Lead Sourcing API](bulk-lead-sourcing-api) ## SpiderFlow Forms Source: https://spideriq.ai/docs/api-reference/spiderflow-forms ## `POST` /api/v1/forms/places-autocomplete **Google Places Autocomplete proxy** Forward the typeahead query to Google Places and return normalised predictions. **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/forms/places-autocomplete' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "query": "string", "session_token": "string", "country_bias": "string", "types": [ "string" ] }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/forms/places-autocomplete", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"query": "string", "session_token": "string", "country_bias": "string", "types": ["string"]}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/forms/places-autocomplete", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"query": "string", "session_token": "string", "country_bias": "string", "types": ["string"]}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"query": "string", "session_token": "string", "country_bias": "string", "types": ["string"]}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/forms/places-autocomplete", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `429` — Per-IP rate limit exceeded. - `502` — Upstream Places API error. - `503` — Places integration not configured for this deployment. - `422` — Validation Error ## `POST` /api/v1/forms/places-details **Google Places Details proxy** Fetch full Place Details for the given place_id. Called by the renderer when a user picks a suggestion. **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/forms/places-details' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "place_id": "string", "session_token": "string" }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/forms/places-details", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"place_id": "string", "session_token": "string"}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/forms/places-details", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"place_id": "string", "session_token": "string"}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"place_id": "string", "session_token": "string"}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/forms/places-details", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `404` — Place id not found. - `429` — Per-IP rate limit exceeded. - `502` — Upstream Places API error. - `503` — Places integration not configured for this deployment. - `422` — Validation Error ## `GET` /api/v1/forms/{flow_id} **Fetch a form flow descriptor (agent-facing)** Returns a minimal {flow_id, kind, name, status} descriptor for the given flow_id. Intended for agents (Claude Code, MCP tools) to discover a flow's kind before driving form-specific tooling. On wrong kind, returns 409 with suggested_url=/api/v1/booking/. On unknown id, returns 404 RESOURCE_NOT_FOUND. **Parameters** - `flow_id` (path, string, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/forms/{flow_id}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/forms/{flow_id}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/forms/{flow_id}", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/forms/{flow_id}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `404` — Flow not found (RESOURCE_NOT_FOUND envelope) - `409` — Flow is not a form (WRONG_FLOW_KIND envelope) - `422` — Validation Error ## MCP — Visual Check Source: https://spideriq.ai/docs/api-reference/mcp-visual-check ## `POST` /api/v1/mcp/visual-check **Open a URL in a real browser and report what an end-user sees.** Proxies to the visual-check Playwright sidecar. Returns screenshot URL (R2-hosted), final URL, status code, DOM info, console errors, failed requests, and per-assertion results. URL must be on the allowlist (spideriq.ai subdomains + tenant verified domains). 20 req/min/client. 30s default timeout, 60s hard cap. **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/mcp/visual-check' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "page_url": "string", "viewport": "desktop", "wait_for_text": "string", "expected_text": [ "string" ], "expected_no_text": [ "string" ], "timeout_ms": 30000, "tenant_id": "string" }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/mcp/visual-check", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"page_url": "string", "viewport": "desktop", "wait_for_text": "string", "expected_text": ["string"], "expected_no_text": ["string"], "timeout_ms": 30000, "tenant_id": "string"}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/mcp/visual-check", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"page_url": "string", "viewport": "desktop", "wait_for_text": "string", "expected_text": ["string"], "expected_no_text": ["string"], "timeout_ms": 30000, "tenant_id": "string"}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"page_url": "string", "viewport": "desktop", "wait_for_text": "string", "expected_text": ["string"], "expected_no_text": ["string"], "timeout_ms": 30000, "tenant_id": "string"}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/mcp/visual-check", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## notifications:public Source: https://spideriq.ai/docs/api-reference/notifications-public ## `GET` /u/{token} **Unsubscribe Confirm** Confirmation landing page. NEVER mutates state — link-scanners that prefetch the URL must see only a page. **Parameters** - `token` (path, string, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/u/{token}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/u/{token}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/u/{token}", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/u/{token}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /u/{token} **Unsubscribe Apply** Apply the unsubscribe and record an audit row. Idempotent: re-POSTing the same token flips already-FALSE rows to FALSE (no-op) and inserts a second audit row. Audit rows reflect user *intents*, not state transitions. All DB work happens in ONE transaction so the audit row and the preference UPSERTs commit together. **Parameters** - `token` (path, string, required) :::codegroup ```bash curl -X POST 'https://spideriq.ai/u/{token}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/u/{token}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/u/{token}", { method: "POST", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("POST", "https://spideriq.ai/u/{token}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## notifications Source: https://spideriq.ai/docs/api-reference/notifications ## `GET` /api/v1/notifications/preferences **Get Notification Preferences** Return the per-(user, brand) subscription matrix, grouped by section. Sections + events come from the in-process catalog (1.1's `services.notifications.catalog.CATALOG`); per-event values are the user's overrides where set, otherwise the catalog defaults. **Parameters** - `brand_id` (query, integer, required) — Brand to load preferences for :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/notifications/preferences' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/notifications/preferences", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/notifications/preferences", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/notifications/preferences", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `PATCH` /api/v1/notifications/preferences **Patch Notification Preferences** Bulk UPSERT partial overrides for one (user, brand). Request body: {"updates": [ {"event_key": "campaign.terminal", "email": false}, {"event_key": "job.failed", "delivery": "digest-daily"} ]} Fields absent from a per-event update preserve the existing row's value (or fall back to the catalog default if no row exists yet). `standalone_only=True` events force `delivery='standalone'` even if the caller sends another value — the catalog wins. Unknown `event_key` or invalid `delivery` → 400. Non-member brand → 403. **Parameters** - `brand_id` (query, integer, required) — Brand the preferences apply to **Request body** :::codegroup ```bash curl -X PATCH 'https://spideriq.ai/api/v1/notifications/preferences' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "updates": [ { "event_key": "string", "email": true, "in_app": true, "delivery": "string" } ] }' ``` ```python import httpx resp = httpx.patch( "https://spideriq.ai/api/v1/notifications/preferences", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"updates": [{"event_key": "string", "email": true, "in_app": true, "delivery": "string"}]}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/notifications/preferences", { method: "PATCH", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"updates": [{"event_key": "string", "email": true, "in_app": true, "delivery": "string"}]}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"updates": [{"event_key": "string", "email": true, "in_app": true, "delivery": "string"}]}`) req, _ := http.NewRequest("PATCH", "https://spideriq.ai/api/v1/notifications/preferences", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/notifications/feed **Get Notification Feed** Return the bell dropdown payload for ``(user, brand)``. The unread branch is always top-N most recent; the read branch is keyset-paginated by ``(created_at, id)`` for the rare case the user scrolls past 20. **Parameters** - `brand_id` (query, integer, required) — Brand to load the bell feed for - `unread_only` (query, boolean, optional) — Skip the read mix - `unread_limit` (query, integer, optional) — Cap on unread rows returned - `read_limit` (query, integer, optional) — Cap on read rows returned (0 disables) - `before_created_at` (query, any, optional) — Keyset cursor (created_at half) — pass the last read row's created_at to page deeper - `before_id` (query, any, optional) — Keyset cursor (id half) — pass alongside before_created_at :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/notifications/feed' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/notifications/feed", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/notifications/feed", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/notifications/feed", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/notifications/unread-count **Get Notification Unread Count** Single-number response for the bell badge. **Parameters** - `brand_id` (query, integer, required) — Brand to count unread for :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/notifications/unread-count' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/notifications/unread-count", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/notifications/unread-count", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/notifications/unread-count", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/notifications/{notification_id}/read **Mark Notification Read** Flip ``read_at`` on one row. Idempotent: re-marking a read row is a no-op (the COALESCE in feed_service keeps the original timestamp). 404 if the row doesn't exist OR doesn't belong to ``(user, brand)`` — same response shape for both because we never reveal which. **Parameters** - `notification_id` (path, integer, required) - `brand_id` (query, integer, required) — Brand the notification belongs to - `created_at` (query, string, required) — Row's created_at (from the feed response) — required for partition pruning on the partitioned notification_log :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/notifications/{notification_id}/read' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/notifications/{notification_id}/read", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/notifications/{notification_id}/read", { method: "POST", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/notifications/{notification_id}/read", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/notifications/read-all **Mark All Notifications Read** Bulk-clear unread for ``(user, brand)``. Returns the count flipped. **Parameters** - `brand_id` (query, integer, required) — Brand to clear unread for :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/notifications/read-all' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/notifications/read-all", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/notifications/read-all", { method: "POST", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/notifications/read-all", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## Auto-Search Source: https://spideriq.ai/docs/api-reference/auto-search ## `GET` /api/v1/auto-search **List auto-search jobs** Get paginated list of all auto-search jobs for the authenticated client **Parameters** - `page` (query, integer, optional) — Page number (starts at 1) - `page_size` (query, integer, optional) — Items per page (max 100) - `status_filter` (query, any, optional) — Filter by status :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/auto-search' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/auto-search", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/auto-search", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/auto-search", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/auto-search **Submit new auto-search job** Submit a new auto-search job to automatically collect unique business results across a geographic area. The system will: 1. Generate location-specific queries based on the provided geography 2. Submit queries to SpiderMaps workers in batches 3. Deduplicate results by place_id 4. Stop when the total_limit is reached or all locations are exhausted 5. Upload results to R2 CDN **Examples:** - Search entire US: `{"keyword": "dentist", "country": "US"}` - Search California: `{"keyword": "pizza", "country": "US", "state": "CA"}` - Search Los Angeles: `{"keyword": "hotel", "country": "US", "state": "CA", "city": "Los Angeles"}` **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/auto-search' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "city": "Los Angeles", "country": "US", "keyword": "pizza restaurant", "state": "CA", "total_limit": 500 }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/auto-search", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"city": "Los Angeles", "country": "US", "keyword": "pizza restaurant", "state": "CA", "total_limit": 500}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/auto-search", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"city": "Los Angeles", "country": "US", "keyword": "pizza restaurant", "state": "CA", "total_limit": 500}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"city": "Los Angeles", "country": "US", "keyword": "pizza restaurant", "state": "CA", "total_limit": 500}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/auto-search", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `201` — Successful Response - `422` — Validation Error ## `GET` /api/v1/auto-search/{auto_search_id} **Get auto-search status** Get detailed status and progress information for an auto-search job **Parameters** - `auto_search_id` (path, string, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/auto-search/{auto_search_id}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/auto-search/{auto_search_id}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/auto-search/{auto_search_id}", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/auto-search/{auto_search_id}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `DELETE` /api/v1/auto-search/{auto_search_id} **Cancel auto-search job** Cancel a running auto-search job. This will: 1. Delete the job-specific RabbitMQ queues (cancels pending queries) 2. Update the job status to 'cancelled' 3. Clean up Redis tracking data Already completed queries will remain in the results. **Parameters** - `auto_search_id` (path, string, required) :::codegroup ```bash curl -X DELETE 'https://spideriq.ai/api/v1/auto-search/{auto_search_id}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.delete( "https://spideriq.ai/api/v1/auto-search/{auto_search_id}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/auto-search/{auto_search_id}", { method: "DELETE", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("DELETE", "https://spideriq.ai/api/v1/auto-search/{auto_search_id}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/auto-search/{auto_search_id}/results **Get auto-search results** Get download URL and metadata for auto-search results **Parameters** - `auto_search_id` (path, string, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/auto-search/{auto_search_id}/results' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/auto-search/{auto_search_id}/results", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/auto-search/{auto_search_id}/results", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/auto-search/{auto_search_id}/results", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## Locations Source: https://spideriq.ai/docs/api-reference/locations ## `POST` /api/v1/locations/import **Import locations** Import multiple locations into the database. **Use Cases:** - Add postcodes for a specific city (e.g., Paris postcodes 75001-75020) - Add custom locations for a region - Bulk import city data **Example - Add Paris postcodes:** ```json { "country_code": "FR", "parent_city": "Paris", "location_type": "postcode", "locations": [ {"search_string": "75001, France", "display_name": "Paris 1er", "latitude": 48.86, "longitude": 2.34}, {"search_string": "75002, France", "display_name": "Paris 2ème", "latitude": 48.87, "longitude": 2.34} ] } ``` **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/locations/import' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "country_code": "string", "country_name": "string", "location_type": "city", "parent_city": "string", "locations": [ { "search_string": "string", "display_name": "string", "latitude": 0.0, "longitude": 0.0, "admin_region": "string", "population": 0 } ] }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/locations/import", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"country_code": "string", "country_name": "string", "location_type": "city", "parent_city": "string", "locations": [{"search_string": "string", "display_name": "string", "latitude": 0.0, "longitude": 0.0, "admin_region": "string", "population": 0}]}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/locations/import", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"country_code": "string", "country_name": "string", "location_type": "city", "parent_city": "string", "locations": [{"search_string": "string", "display_name": "string", "latitude": 0.0, "longitude": 0.0, "admin_region": "string", "population": 0}]}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"country_code": "string", "country_name": "string", "location_type": "city", "parent_city": "string", "locations": [{"search_string": "string", "display_name": "string", "latitude": 0.0, "longitude": 0.0, "admin_region": "string", "population": 0}]}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/locations/import", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `201` — Successful Response - `422` — Validation Error ## `GET` /api/v1/locations **List locations** List locations with filtering and pagination. **Filter Options:** - `country_code`: Filter by ISO 2-letter country code (e.g., FR, DE, US) - `location_type`: Filter by type (city or postcode) - `parent_city`: Filter postcodes by parent city - `admin_region`: Filter by state/province/region - `min_population` / `max_population`: Filter by population range - `needs_postcodes`: Filter big cities that need postcode breakdown - `search`: Search in display_name or search_string **Parameters** - `country_code` (query, any, optional) — ISO 2-letter country code - `location_type` (query, any, optional) — Location type - `parent_city` (query, any, optional) — Parent city (for postcodes) - `admin_region` (query, any, optional) — State/Province/Region - `min_population` (query, any, optional) — Minimum population - `max_population` (query, any, optional) — Maximum population - `needs_postcodes` (query, any, optional) — Filter big cities needing postcodes - `search` (query, any, optional) — Search in display_name or search_string - `page` (query, integer, optional) — Page number - `page_size` (query, integer, optional) — Items per page :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/locations' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/locations", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/locations", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/locations", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/locations/countries **List available countries** Get list of all countries with location counts. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/locations/countries' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/locations/countries", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/locations/countries", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/locations/countries", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response ## `GET` /api/v1/locations/regions **List admin regions (states) for a country** List the admin regions (US states / provinces) of a country, each with its city/postcode split. For US ZIP campaigns the state is the selection unit — pick one region and run its postcodes (each ZIP is its own Maps search). 'All of US' is not selectable for a ZIP campaign (the volume guard 422s ~46K locations / >10K cap). **Parameters** - `country_code` (query, string, required) — ISO country code, e.g. US :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/locations/regions' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/locations/regions", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/locations/regions", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/locations/regions", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/locations/selectable-units **List selectable geo units (countries + US states)** Flat, alphabetically merged list of selectable geo units for a typeahead picker. Each unit is a whole country (kind='country') or a US state (kind='state'). With states_as_units=true (default) the US row is dropped and its 50 states appear as top-level units — so an agent targets 'Florida' the same way it targets 'Germany', and 'all of USA' cannot be picked for a ZIP campaign. Set states_as_units=false for plain countries (US included) when the country is the correct unit. **Parameters** - `states_as_units` (query, boolean, optional) — When true (default), replace US with its 50 states as top-level units. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/locations/selectable-units' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/locations/selectable-units", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/locations/selectable-units", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/locations/selectable-units", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/locations/stats **Get location statistics** Get overall location database statistics. Returns: - Total locations, countries, cities, postcodes - Count of cities needing postcode breakdown - Top 20 countries by location count :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/locations/stats' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/locations/stats", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/locations/stats", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/locations/stats", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response ## `GET` /api/v1/locations/{location_id} **Get location by ID** Get details of a specific location. **Parameters** - `location_id` (path, integer, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/locations/{location_id}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/locations/{location_id}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/locations/{location_id}", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/locations/{location_id}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## SpiderMaps Campaigns Source: https://spideriq.ai/docs/api-reference/spidermaps-campaigns ## `POST` /api/v1/jobs/spiderMaps/campaigns/submit **Create a new SpiderMaps campaign** Create a new SpiderMaps scraping campaign for a specific country and query. **What happens:** 1. Campaign is created with specified filters 2. Matching locations are pre-computed and stored 3. Campaign is ready for `/next` calls **Filter Modes:** - `all` - All locations in the country (default) - `population` - Filter by min/max population - `cities_only` - Only cities, no postcodes - `custom` - Specific location_ids - `regions` - Specific admin regions **Example - Scrape restaurants in France:** ```json { "query": "restaurants", "country_code": "FR", "name": "France Restaurants 2024", "filter": { "mode": "population", "min_population": 50000 } } ``` **Returns:** - `campaign_id` - Use this for subsequent calls - `total_locations` - Number of locations to scrape - `next_location_id` - First location to be processed **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/jobs/spiderMaps/campaigns/submit' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "search_query": "string", "query": "string", "country_code": "string", "name": "string", "filter": { "mode": "all", "min_population": 0, "max_population": 0, "location_ids": [ 0 ], "admin_regions": [ "string" ], "parent_city": "string", "include_postcodes": false, "exhaustive": false }, "max_results": 100, "extract_reviews": false, "extract_photos": false, "lang": "en", "store_images": true, "validate_phones": true, "fuzziq_enabled": true, "fuzziq_unique_only": true, "skip_proxy": false, "test": false, "workflow": { "spidersite": { "enabled": false, "mode": {}, "max_pages": 25, "crawl_strategy": "bestfirst", "target_pages": [ "contact", "about", "team", "news", "blog" ], "enable_spa": true, "spa_timeout": 30, "extract_team": false, "extract_company_info": false, "extract_pain_points": false, "product_description": {}, "icp_description": {}, "compendium": {}, "timeout": 30 }, "spiderverify": { "enabled": false, "check_gravatar": false, "check_dnsbl": false, "smtp_timeout_secs": 45, "max_emails_per_business": 10 }, "vayapin": { "enabled": false }, "social_media_enrichment": { "enabled": true }, "smartlead": { "enabled": false, "connection_id": {}, "remote_campaign_id": {}, "remote_campaign_name": {}, "limit": {}, "only_with_vayapin_seo": false, "only_with_vayapin_pin": false, "field_map": {} }, "filter_social_media": true, "filter_review_sites": true, "filter_directories": true, "filter_maps": true } }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/jobs/spiderMaps/campaigns/submit", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"search_query": "string", "query": "string", "country_code": "string", "name": "string", "filter": {"mode": "all", "min_population": 0, "max_population": 0, "location_ids": [0], "admin_regions": ["string"], "parent_city": "string", "include_postcodes": false, "exhaustive": false}, "max_results": 100, "extract_reviews": false, "extract_photos": false, "lang": "en", "store_images": true, "validate_phones": true, "fuzziq_enabled": true, "fuzziq_unique_only": true, "skip_proxy": false, "test": false, "workflow": {"spidersite": {"enabled": false, "mode": {}, "max_pages": 25, "crawl_strategy": "bestfirst", "target_pages": ["contact", "about", "team", "news", "blog"], "enable_spa": true, "spa_timeout": 30, "extract_team": false, "extract_company_info": false, "extract_pain_points": false, "product_description": {}, "icp_description": {}, "compendium": {}, "timeout": 30}, "spiderverify": {"enabled": false, "check_gravatar": false, "check_dnsbl": false, "smtp_timeout_secs": 45, "max_emails_per_business": 10}, "vayapin": {"enabled": false}, "social_media_enrichment": {"enabled": true}, "smartlead": {"enabled": false, "connection_id": {}, "remote_campaign_id": {}, "remote_campaign_name": {}, "limit": {}, "only_with_vayapin_seo": false, "only_with_vayapin_pin": false, "field_map": {}}, "filter_social_media": true, "filter_review_sites": true, "filter_directories": true, "filter_maps": true}}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/jobs/spiderMaps/campaigns/submit", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"search_query": "string", "query": "string", "country_code": "string", "name": "string", "filter": {"mode": "all", "min_population": 0, "max_population": 0, "location_ids": [0], "admin_regions": ["string"], "parent_city": "string", "include_postcodes": false, "exhaustive": false}, "max_results": 100, "extract_reviews": false, "extract_photos": false, "lang": "en", "store_images": true, "validate_phones": true, "fuzziq_enabled": true, "fuzziq_unique_only": true, "skip_proxy": false, "test": false, "workflow": {"spidersite": {"enabled": false, "mode": {}, "max_pages": 25, "crawl_strategy": "bestfirst", "target_pages": ["contact", "about", "team", "news", "blog"], "enable_spa": true, "spa_timeout": 30, "extract_team": false, "extract_company_info": false, "extract_pain_points": false, "product_description": {}, "icp_description": {}, "compendium": {}, "timeout": 30}, "spiderverify": {"enabled": false, "check_gravatar": false, "check_dnsbl": false, "smtp_timeout_secs": 45, "max_emails_per_business": 10}, "vayapin": {"enabled": false}, "social_media_enrichment": {"enabled": true}, "smartlead": {"enabled": false, "connection_id": {}, "remote_campaign_id": {}, "remote_campaign_name": {}, "limit": {}, "only_with_vayapin_seo": false, "only_with_vayapin_pin": false, "field_map": {}}, "filter_social_media": true, "filter_review_sites": true, "filter_directories": true, "filter_maps": true}}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"search_query": "string", "query": "string", "country_code": "string", "name": "string", "filter": {"mode": "all", "min_population": 0, "max_population": 0, "location_ids": [0], "admin_regions": ["string"], "parent_city": "string", "include_postcodes": false, "exhaustive": false}, "max_results": 100, "extract_reviews": false, "extract_photos": false, "lang": "en", "store_images": true, "validate_phones": true, "fuzziq_enabled": true, "fuzziq_unique_only": true, "skip_proxy": false, "test": false, "workflow": {"spidersite": {"enabled": false, "mode": {}, "max_pages": 25, "crawl_strategy": "bestfirst", "target_pages": ["contact", "about", "team", "news", "blog"], "enable_spa": true, "spa_timeout": 30, "extract_team": false, "extract_company_info": false, "extract_pain_points": false, "product_description": {}, "icp_description": {}, "compendium": {}, "timeout": 30}, "spiderverify": {"enabled": false, "check_gravatar": false, "check_dnsbl": false, "smtp_timeout_secs": 45, "max_emails_per_business": 10}, "vayapin": {"enabled": false}, "social_media_enrichment": {"enabled": true}, "smartlead": {"enabled": false, "connection_id": {}, "remote_campaign_id": {}, "remote_campaign_name": {}, "limit": {}, "only_with_vayapin_seo": false, "only_with_vayapin_pin": false, "field_map": {}}, "filter_social_media": true, "filter_review_sites": true, "filter_directories": true, "filter_maps": true}}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/jobs/spiderMaps/campaigns/submit", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `201` — Successful Response - `422` — Validation Error ## `POST` /api/v1/jobs/spiderMaps/campaigns/estimate **Pre-flight campaign estimate (locations + per-stage billable jobs)** Outscraper-style pre-flight estimate for a campaign — **writes no rows**. Returns the number of locations the campaign would fan out to plus the estimated billable-job COUNTS per stage (spiderMaps / spiderSite / spiderVerify / spiderVayapin) so the UI can show a Proceed/Upgrade decision before the campaign is created. **Cost is expressed as job counts, not USD** — there is no locally-queryable per-job USD price. Downstream stage counts use documented hit-rate assumptions (see the `assumptions` field); spiderMaps is exactly one job per location. `requires_upgrade` is True when the location volume exceeds the global per-campaign backstop. This is a pure READ — it is **not** a metered dispatcher submission. **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/jobs/spiderMaps/campaigns/estimate' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "search_query": "string", "country_code": "string", "filter": { "mode": "all", "min_population": 0, "max_population": 0, "location_ids": [ 0 ], "admin_regions": [ "string" ], "parent_city": "string", "include_postcodes": false, "exhaustive": false }, "max_results": 100, "workflow": { "spidersite": { "enabled": false, "mode": {}, "max_pages": 25, "crawl_strategy": "bestfirst", "target_pages": [ "contact", "about", "team", "news", "blog" ], "enable_spa": true, "spa_timeout": 30, "extract_team": false, "extract_company_info": false, "extract_pain_points": false, "product_description": {}, "icp_description": {}, "compendium": {}, "timeout": 30 }, "spiderverify": { "enabled": false, "check_gravatar": false, "check_dnsbl": false, "smtp_timeout_secs": 45, "max_emails_per_business": 10 }, "vayapin": { "enabled": false }, "social_media_enrichment": { "enabled": true }, "smartlead": { "enabled": false, "connection_id": {}, "remote_campaign_id": {}, "remote_campaign_name": {}, "limit": {}, "only_with_vayapin_seo": false, "only_with_vayapin_pin": false, "field_map": {} }, "filter_social_media": true, "filter_review_sites": true, "filter_directories": true, "filter_maps": true } }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/jobs/spiderMaps/campaigns/estimate", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"search_query": "string", "country_code": "string", "filter": {"mode": "all", "min_population": 0, "max_population": 0, "location_ids": [0], "admin_regions": ["string"], "parent_city": "string", "include_postcodes": false, "exhaustive": false}, "max_results": 100, "workflow": {"spidersite": {"enabled": false, "mode": {}, "max_pages": 25, "crawl_strategy": "bestfirst", "target_pages": ["contact", "about", "team", "news", "blog"], "enable_spa": true, "spa_timeout": 30, "extract_team": false, "extract_company_info": false, "extract_pain_points": false, "product_description": {}, "icp_description": {}, "compendium": {}, "timeout": 30}, "spiderverify": {"enabled": false, "check_gravatar": false, "check_dnsbl": false, "smtp_timeout_secs": 45, "max_emails_per_business": 10}, "vayapin": {"enabled": false}, "social_media_enrichment": {"enabled": true}, "smartlead": {"enabled": false, "connection_id": {}, "remote_campaign_id": {}, "remote_campaign_name": {}, "limit": {}, "only_with_vayapin_seo": false, "only_with_vayapin_pin": false, "field_map": {}}, "filter_social_media": true, "filter_review_sites": true, "filter_directories": true, "filter_maps": true}}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/jobs/spiderMaps/campaigns/estimate", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"search_query": "string", "country_code": "string", "filter": {"mode": "all", "min_population": 0, "max_population": 0, "location_ids": [0], "admin_regions": ["string"], "parent_city": "string", "include_postcodes": false, "exhaustive": false}, "max_results": 100, "workflow": {"spidersite": {"enabled": false, "mode": {}, "max_pages": 25, "crawl_strategy": "bestfirst", "target_pages": ["contact", "about", "team", "news", "blog"], "enable_spa": true, "spa_timeout": 30, "extract_team": false, "extract_company_info": false, "extract_pain_points": false, "product_description": {}, "icp_description": {}, "compendium": {}, "timeout": 30}, "spiderverify": {"enabled": false, "check_gravatar": false, "check_dnsbl": false, "smtp_timeout_secs": 45, "max_emails_per_business": 10}, "vayapin": {"enabled": false}, "social_media_enrichment": {"enabled": true}, "smartlead": {"enabled": false, "connection_id": {}, "remote_campaign_id": {}, "remote_campaign_name": {}, "limit": {}, "only_with_vayapin_seo": false, "only_with_vayapin_pin": false, "field_map": {}}, "filter_social_media": true, "filter_review_sites": true, "filter_directories": true, "filter_maps": true}}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"search_query": "string", "country_code": "string", "filter": {"mode": "all", "min_population": 0, "max_population": 0, "location_ids": [0], "admin_regions": ["string"], "parent_city": "string", "include_postcodes": false, "exhaustive": false}, "max_results": 100, "workflow": {"spidersite": {"enabled": false, "mode": {}, "max_pages": 25, "crawl_strategy": "bestfirst", "target_pages": ["contact", "about", "team", "news", "blog"], "enable_spa": true, "spa_timeout": 30, "extract_team": false, "extract_company_info": false, "extract_pain_points": false, "product_description": {}, "icp_description": {}, "compendium": {}, "timeout": 30}, "spiderverify": {"enabled": false, "check_gravatar": false, "check_dnsbl": false, "smtp_timeout_secs": 45, "max_emails_per_business": 10}, "vayapin": {"enabled": false}, "social_media_enrichment": {"enabled": true}, "smartlead": {"enabled": false, "connection_id": {}, "remote_campaign_id": {}, "remote_campaign_name": {}, "limit": {}, "only_with_vayapin_seo": false, "only_with_vayapin_pin": false, "field_map": {}}, "filter_social_media": true, "filter_review_sites": true, "filter_directories": true, "filter_maps": true}}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/jobs/spiderMaps/campaigns/estimate", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `PATCH` /api/v1/jobs/spiderMaps/campaigns/{campaign_id} **Update campaign configuration** Update an existing campaign's configuration. **What can be updated:** - `search_query` - Changes search term for future jobs - `name` - Campaign display name - SpiderMaps options (max_results, extract_reviews, extract_photos, lang, etc.) - `workflow` - Workflow configuration (merged with existing) **What cannot be updated:** - `country_code` - Would change location set - `filter` - Locations already computed at creation **Restrictions:** - Only active or stopped/paused campaigns can be updated - Completed campaigns cannot be modified - Changes only affect PENDING locations (already submitted jobs unaffected) **Example - Update search query and max_results:** ```json { "search_query": "coffee shops", "max_results": 200 } ``` **Example - Enable workflow after campaign creation:** ```json { "workflow": { "spidersite": { "enabled": true, "extract_team": true } } } ``` **Returns:** Full campaign status with updated values (same as GET /status endpoint). **Parameters** - `campaign_id` (path, string, required) **Request body** :::codegroup ```bash curl -X PATCH 'https://spideriq.ai/api/v1/jobs/spiderMaps/campaigns/{campaign_id}' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "search_query": "string", "query": "string", "name": "string", "max_results": 0, "extract_reviews": true, "extract_photos": true, "lang": "string", "store_images": true, "validate_phones": true, "fuzziq_enabled": true, "fuzziq_unique_only": true, "skip_proxy": true, "test": true, "workflow": { "spidersite": { "enabled": false, "mode": {}, "max_pages": 25, "crawl_strategy": "bestfirst", "target_pages": [ "contact", "about", "team", "news", "blog" ], "enable_spa": true, "spa_timeout": 30, "extract_team": false, "extract_company_info": false, "extract_pain_points": false, "product_description": {}, "icp_description": {}, "compendium": {}, "timeout": 30 }, "spiderverify": { "enabled": false, "check_gravatar": false, "check_dnsbl": false, "smtp_timeout_secs": 45, "max_emails_per_business": 10 }, "vayapin": { "enabled": false }, "social_media_enrichment": { "enabled": true }, "smartlead": { "enabled": false, "connection_id": {}, "remote_campaign_id": {}, "remote_campaign_name": {}, "limit": {}, "only_with_vayapin_seo": false, "only_with_vayapin_pin": false, "field_map": {} }, "filter_social_media": true, "filter_review_sites": true, "filter_directories": true, "filter_maps": true } }' ``` ```python import httpx resp = httpx.patch( "https://spideriq.ai/api/v1/jobs/spiderMaps/campaigns/{campaign_id}", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"search_query": "string", "query": "string", "name": "string", "max_results": 0, "extract_reviews": true, "extract_photos": true, "lang": "string", "store_images": true, "validate_phones": true, "fuzziq_enabled": true, "fuzziq_unique_only": true, "skip_proxy": true, "test": true, "workflow": {"spidersite": {"enabled": false, "mode": {}, "max_pages": 25, "crawl_strategy": "bestfirst", "target_pages": ["contact", "about", "team", "news", "blog"], "enable_spa": true, "spa_timeout": 30, "extract_team": false, "extract_company_info": false, "extract_pain_points": false, "product_description": {}, "icp_description": {}, "compendium": {}, "timeout": 30}, "spiderverify": {"enabled": false, "check_gravatar": false, "check_dnsbl": false, "smtp_timeout_secs": 45, "max_emails_per_business": 10}, "vayapin": {"enabled": false}, "social_media_enrichment": {"enabled": true}, "smartlead": {"enabled": false, "connection_id": {}, "remote_campaign_id": {}, "remote_campaign_name": {}, "limit": {}, "only_with_vayapin_seo": false, "only_with_vayapin_pin": false, "field_map": {}}, "filter_social_media": true, "filter_review_sites": true, "filter_directories": true, "filter_maps": true}}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/jobs/spiderMaps/campaigns/{campaign_id}", { method: "PATCH", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"search_query": "string", "query": "string", "name": "string", "max_results": 0, "extract_reviews": true, "extract_photos": true, "lang": "string", "store_images": true, "validate_phones": true, "fuzziq_enabled": true, "fuzziq_unique_only": true, "skip_proxy": true, "test": true, "workflow": {"spidersite": {"enabled": false, "mode": {}, "max_pages": 25, "crawl_strategy": "bestfirst", "target_pages": ["contact", "about", "team", "news", "blog"], "enable_spa": true, "spa_timeout": 30, "extract_team": false, "extract_company_info": false, "extract_pain_points": false, "product_description": {}, "icp_description": {}, "compendium": {}, "timeout": 30}, "spiderverify": {"enabled": false, "check_gravatar": false, "check_dnsbl": false, "smtp_timeout_secs": 45, "max_emails_per_business": 10}, "vayapin": {"enabled": false}, "social_media_enrichment": {"enabled": true}, "smartlead": {"enabled": false, "connection_id": {}, "remote_campaign_id": {}, "remote_campaign_name": {}, "limit": {}, "only_with_vayapin_seo": false, "only_with_vayapin_pin": false, "field_map": {}}, "filter_social_media": true, "filter_review_sites": true, "filter_directories": true, "filter_maps": true}}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"search_query": "string", "query": "string", "name": "string", "max_results": 0, "extract_reviews": true, "extract_photos": true, "lang": "string", "store_images": true, "validate_phones": true, "fuzziq_enabled": true, "fuzziq_unique_only": true, "skip_proxy": true, "test": true, "workflow": {"spidersite": {"enabled": false, "mode": {}, "max_pages": 25, "crawl_strategy": "bestfirst", "target_pages": ["contact", "about", "team", "news", "blog"], "enable_spa": true, "spa_timeout": 30, "extract_team": false, "extract_company_info": false, "extract_pain_points": false, "product_description": {}, "icp_description": {}, "compendium": {}, "timeout": 30}, "spiderverify": {"enabled": false, "check_gravatar": false, "check_dnsbl": false, "smtp_timeout_secs": 45, "max_emails_per_business": 10}, "vayapin": {"enabled": false}, "social_media_enrichment": {"enabled": true}, "smartlead": {"enabled": false, "connection_id": {}, "remote_campaign_id": {}, "remote_campaign_name": {}, "limit": {}, "only_with_vayapin_seo": false, "only_with_vayapin_pin": false, "field_map": {}}, "filter_social_media": true, "filter_review_sites": true, "filter_directories": true, "filter_maps": true}}`) req, _ := http.NewRequest("PATCH", "https://spideriq.ai/api/v1/jobs/spiderMaps/campaigns/{campaign_id}", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `DELETE` /api/v1/jobs/spiderMaps/campaigns/{campaign_id} **Delete a campaign** Delete a campaign and all of its related data. The campaign must be **stopped first** — this returns 409 if it still has active jobs (`submitted`/`processing`). Cascade removes campaign locations, workflow jobs, and run-tracking rows. Returns 404 if the campaign does not exist or is not owned by the caller. **Parameters** - `campaign_id` (path, string, required) :::codegroup ```bash curl -X DELETE 'https://spideriq.ai/api/v1/jobs/spiderMaps/campaigns/{campaign_id}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.delete( "https://spideriq.ai/api/v1/jobs/spiderMaps/campaigns/{campaign_id}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/jobs/spiderMaps/campaigns/{campaign_id}", { method: "DELETE", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("DELETE", "https://spideriq.ai/api/v1/jobs/spiderMaps/campaigns/{campaign_id}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/jobs/spiderMaps/campaigns/{campaign_id}/next **Get next location and submit job** Get the next location for a campaign and automatically submit a SpiderMaps job. **What happens:** 1. Finds the next pending location in the campaign 2. Submits a SpiderMaps job for that location 3. Updates campaign progress 4. Returns job details and progress **N8N/Xano Workflow:** ``` Loop while has_more == true: POST /campaigns/{id}/next # Wait for job to complete (optional) # Process results ``` **Response Fields:** - `has_more` - true if more locations remain - `current_task` - Details of the submitted job - `progress` - Campaign progress statistics **When **`**has_more**`** is false:** - Campaign is complete - No more locations to process **Parameters** - `campaign_id` (path, string, required) :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/jobs/spiderMaps/campaigns/{campaign_id}/next' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/jobs/spiderMaps/campaigns/{campaign_id}/next", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/jobs/spiderMaps/campaigns/{campaign_id}/next", { method: "POST", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/jobs/spiderMaps/campaigns/{campaign_id}/next", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/jobs/spiderMaps/campaigns/{campaign_id}/status **Get campaign status** Get detailed status and statistics for a campaign. **Returns:** - Campaign configuration (query, country, filters) - Progress statistics (completed, failed, pending) - Job statistics by status (queued, processing, completed, failed) - Total businesses found **Query Parameters:** - format: Response format — json (default), yaml, md or llm **Parameters** - `campaign_id` (path, string, required) - `format` (query, any, optional) — Response format. `json` (default) and `llm` return JSON; `yaml` returns text/yaml; `md` returns text/markdown. Any other value is a 422. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/jobs/spiderMaps/campaigns/{campaign_id}/status' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/jobs/spiderMaps/campaigns/{campaign_id}/status", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/jobs/spiderMaps/campaigns/{campaign_id}/status", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/jobs/spiderMaps/campaigns/{campaign_id}/status", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/jobs/spiderMaps/campaigns/{campaign_id}/stage-progress **Get per-stage campaign progress** Per-stage breakdown of campaign progress with a velocity-based ETA. Replaces the misleading "0/N 0%" rollup with location-level completed / active / queued / failed counters per pipeline stage (Maps / Site / Verify / Vayapin). The stage list is derived from the campaign's `workflow_config` — Site/Verify/Vayapin only appear when their config block has enabled=True. Cached in Redis for 30s. Bearer-authenticated mirror of `/api/v1/dashboard/client/campaigns/{id}/stage-progress`. **Parameters** - `campaign_id` (path, string, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/jobs/spiderMaps/campaigns/{campaign_id}/stage-progress' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/jobs/spiderMaps/campaigns/{campaign_id}/stage-progress", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/jobs/spiderMaps/campaigns/{campaign_id}/stage-progress", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/jobs/spiderMaps/campaigns/{campaign_id}/stage-progress", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/jobs/spiderMaps/campaigns/{campaign_id}/runs **Get campaign Inngest runs** Get all Inngest workflow runs for a campaign. **Returns:** - List of runs with status, timing, and Inngest correlation info - Useful for visualizing campaign workflow execution (waterfall diagram) **Note:** Run IDs are correlated asynchronously after event submission. New runs may take a few seconds to appear with full run_id. v2.54.0: Added for Inngest run visualization feature. **Parameters** - `campaign_id` (path, string, required) - `page` (query, integer, optional) — Page number - `page_size` (query, integer, optional) — Items per page :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/jobs/spiderMaps/campaigns/{campaign_id}/runs' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/jobs/spiderMaps/campaigns/{campaign_id}/runs", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/jobs/spiderMaps/campaigns/{campaign_id}/runs", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/jobs/spiderMaps/campaigns/{campaign_id}/runs", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/jobs/spiderMaps/campaigns/{campaign_id}/runs/{run_id} **Get campaign run detail with steps** Get detailed information about a specific Inngest run including all steps. **Returns:** - Full run details with timing and status - Steps array for waterfall visualization - Pre-computed waterfall_data for frontend rendering Use this endpoint to display the Inngest run waterfall trace. **Parameters** - `campaign_id` (path, string, required) - `run_id` (path, integer, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/jobs/spiderMaps/campaigns/{campaign_id}/runs/{run_id}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/jobs/spiderMaps/campaigns/{campaign_id}/runs/{run_id}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/jobs/spiderMaps/campaigns/{campaign_id}/runs/{run_id}", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/jobs/spiderMaps/campaigns/{campaign_id}/runs/{run_id}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/jobs/spiderMaps/campaigns/{campaign_id}/workflow-progress **Get workflow progress and health metrics** Get real-time workflow progress and health metrics for a campaign. **Returns:** - Active/completed/failed run counts - Details of currently running workflows with current step - Location progress statistics - Timing statistics (avg, min, max workflow duration) - Health metrics for stuck workflow detection **Use Cases:** - Dashboard progress monitoring - Detecting stuck workflows - Understanding workflow performance v2.60.0: Added for campaign workflow monitoring feature. **Parameters** - `campaign_id` (path, string, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/jobs/spiderMaps/campaigns/{campaign_id}/workflow-progress' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/jobs/spiderMaps/campaigns/{campaign_id}/workflow-progress", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/jobs/spiderMaps/campaigns/{campaign_id}/workflow-progress", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/jobs/spiderMaps/campaigns/{campaign_id}/workflow-progress", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/jobs/spiderMaps/campaigns/{campaign_id}/abort-workflow **Abort all running workflows for a campaign** Abort all running Inngest workflow runs for a campaign. **What happens:** 1. Campaign workflow_status is set to 'aborted' 2. All running/queued orchestrated_campaign_runs are marked as aborted 3. Submitted campaign_locations are marked as skipped 4. Running workflow steps will check for abort and stop early **Use Cases:** - Emergency stop when workflows are stuck - Cancel long-running campaigns - Clear stuck workflow runs before retry **Note:** This does NOT stop already-running worker jobs (SpiderMaps, SpiderSite, etc). It only prevents new workflow steps from starting. v2.60.0: Added for campaign workflow safeguards. **Parameters** - `campaign_id` (path, string, required) - `reason` (query, string, optional) — Reason for abort :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/jobs/spiderMaps/campaigns/{campaign_id}/abort-workflow' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/jobs/spiderMaps/campaigns/{campaign_id}/abort-workflow", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/jobs/spiderMaps/campaigns/{campaign_id}/abort-workflow", { method: "POST", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/jobs/spiderMaps/campaigns/{campaign_id}/abort-workflow", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/jobs/spiderMaps/campaigns/{campaign_id}/stop **Stop a campaign** Stop an active campaign. Stopped campaigns can be resumed later with `/continue`. Jobs already submitted will continue processing. **Parameters** - `campaign_id` (path, string, required) :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/jobs/spiderMaps/campaigns/{campaign_id}/stop' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/jobs/spiderMaps/campaigns/{campaign_id}/stop", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/jobs/spiderMaps/campaigns/{campaign_id}/stop", { method: "POST", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/jobs/spiderMaps/campaigns/{campaign_id}/stop", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/jobs/spiderMaps/campaigns/{campaign_id}/continue **Continue a stopped campaign** Resume a stopped or paused campaign. Campaign will continue from where it left off. **Parameters** - `campaign_id` (path, string, required) :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/jobs/spiderMaps/campaigns/{campaign_id}/continue' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/jobs/spiderMaps/campaigns/{campaign_id}/continue", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/jobs/spiderMaps/campaigns/{campaign_id}/continue", { method: "POST", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/jobs/spiderMaps/campaigns/{campaign_id}/continue", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/jobs/spiderMaps/campaigns/{campaign_id}/locations/{location_id}/retry **Retry a campaign location** Re-run a failed or completed location (dispatches worker jobs again). - `retry_mode="full"`: re-run the whole workflow from SpiderMaps - `retry_mode="site"`: keep Maps results, re-run SpiderSite + SpiderVerify - `retry_mode="verify"`: keep Site results, re-run SpiderVerify only `location_id` is the `id` returned by `GET /{campaign_id}/jobs` (campaign_locations.id). Max 3 retries per location — returns 400 once that limit is hit. **Parameters** - `campaign_id` (path, string, required) - `location_id` (path, integer, required) **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/jobs/spiderMaps/campaigns/{campaign_id}/locations/{location_id}/retry' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "retry_mode": "full" }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/jobs/spiderMaps/campaigns/{campaign_id}/locations/{location_id}/retry", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"retry_mode": "full"}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/jobs/spiderMaps/campaigns/{campaign_id}/locations/{location_id}/retry", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"retry_mode": "full"}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"retry_mode": "full"}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/jobs/spiderMaps/campaigns/{campaign_id}/locations/{location_id}/retry", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/jobs/spiderMaps/campaigns/{campaign_id}/retry-failed **Retry all failed locations** Re-run every failed location in the campaign (up to `max_locations`). Locations that already hit the 3-retry limit are skipped, not errored. **Parameters** - `campaign_id` (path, string, required) - `max_locations` (query, integer, optional) — Max locations to retry :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/jobs/spiderMaps/campaigns/{campaign_id}/retry-failed' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/jobs/spiderMaps/campaigns/{campaign_id}/retry-failed", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/jobs/spiderMaps/campaigns/{campaign_id}/retry-failed", { method: "POST", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/jobs/spiderMaps/campaigns/{campaign_id}/retry-failed", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/jobs/spiderMaps/campaigns **List campaigns** List all campaigns for the current client. **Filter Options:** - `status`: Filter by campaign status (active, paused, completed, stopped) - `country_code`: Filter by country **Query Parameters:** - format: Response format — json (default), yaml, md or llm **Parameters** - `status` (query, any, optional) — Campaign status - `country_code` (query, any, optional) — Country code - `page` (query, integer, optional) — Page number - `page_size` (query, integer, optional) — Items per page - `format` (query, any, optional) — Response format. `json` (default) and `llm` return JSON; `yaml` returns text/yaml; `md` returns text/markdown. Any other value is a 422. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/jobs/spiderMaps/campaigns' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/jobs/spiderMaps/campaigns", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/jobs/spiderMaps/campaigns", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/jobs/spiderMaps/campaigns", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/jobs/spiderMaps/campaigns/{campaign_id}/jobs **List jobs for a campaign** Get a paginated list of all jobs submitted for a campaign. **Filter Options:** - `status`: Filter by campaign_location status (pending, submitted, completed, failed, skipped) - `job_status`: Filter by actual job status (queued, processing, completed, failed) **Includes:** - Job IDs linked to each location - Job status from both campaign_locations and jobs tables - Results count and error messages - Submission and completion timestamps **Use Cases:** - Monitor job progress within a campaign - Identify failed jobs for retry - Get job IDs for fetching detailed results via `/{campaign_id}/jobs/{job_id}/results` **Parameters** - `campaign_id` (path, string, required) - `location_status` (query, any, optional) — Filter by campaign_location status - `job_status` (query, any, optional) — Filter by actual job status - `page` (query, integer, optional) — Page number - `page_size` (query, integer, optional) — Items per page - `include_summary` (query, boolean, optional) — Include status summary counts :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/jobs/spiderMaps/campaigns/{campaign_id}/jobs' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/jobs/spiderMaps/campaigns/{campaign_id}/jobs", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/jobs/spiderMaps/campaigns/{campaign_id}/jobs", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/jobs/spiderMaps/campaigns/{campaign_id}/jobs", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/jobs/spiderMaps/campaigns/{campaign_id}/workflow-results **Get aggregated workflow results** Get complete aggregated results from the workflow chain: SpiderMaps → SpiderSite → SpiderVerify **Returns data from all three services combined:** - SpiderMaps: Business listings (name, address, phone, rating) - SpiderSite: Website data (emails, social media, company info) - SpiderVerify: Email verification results (validity, score) **Note:** Only available for campaigns created with workflow configuration. **Query Parameters:** - format: `json` (default) / `llm` -> JSON; `yaml` -> text/yaml; `md` -> text/markdown. Any other value is a 422. Before SDS-26 this route declared no `format` at all, so FastAPI DROPPED it: `?format=md` answered `200 application/json` and so did `?format=bogus`. **Parameters** - `campaign_id` (path, string, required) - `include_filtered` (query, boolean, optional) — Include businesses that were filtered out - `format` (query, any, optional) — Response format. `json` (default) and `llm` return JSON; `yaml` returns text/yaml; `md` returns text/markdown. Any other value is a 422. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/jobs/spiderMaps/campaigns/{campaign_id}/workflow-results' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/jobs/spiderMaps/campaigns/{campaign_id}/workflow-results", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/jobs/spiderMaps/campaigns/{campaign_id}/workflow-results", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/jobs/spiderMaps/campaigns/{campaign_id}/workflow-results", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/jobs/spiderMaps/campaigns/{campaign_id}/jobs/{job_id}/results **Wait for workflow job completion (blocking)** Blocks until all workflow stages complete for a specific SpiderMaps job, then returns aggregated results from SpiderMaps + SpiderSite + SpiderVerify. **Timeouts:** - SpiderSite: 5 minutes per business - SpiderVerify: 2 minutes per business - Maximum total wait: 10 minutes **Partial Results:** If SpiderVerify times out but SpiderSite completed, returns partial results with SpiderSite data (compendium, emails_found, company_info, etc.) **Query Parameters:** - `wait=true` (default): Block until all businesses complete or timeout - `wait=false`: Return current status immediately (for polling) **Status Values:** - `queued` - Waiting for workers - `processing` - Currently being processed - `completed` - All stages finished successfully - `failed` - Processing failed - `partial` - SpiderVerify timed out but SpiderSite data available **Query Parameters:** - format: `json` (default) / `llm` -> JSON; `yaml` -> text/yaml; `md` -> text/markdown. Any other value is a 422. Before SDS-26 this route declared no `format` at all, so FastAPI DROPPED it and `?format=md` answered `200 application/json`. **Parameters** - `campaign_id` (path, string, required) - `job_id` (path, string, required) - `wait` (query, boolean, optional) — Wait for completion (blocking). If false, returns current state immediately. - `format` (query, any, optional) — Response format. `json` (default) and `llm` return JSON; `yaml` returns text/yaml; `md` returns text/markdown. Any other value is a 422. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/jobs/spiderMaps/campaigns/{campaign_id}/jobs/{job_id}/results' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/jobs/spiderMaps/campaigns/{campaign_id}/jobs/{job_id}/results", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/jobs/spiderMaps/campaigns/{campaign_id}/jobs/{job_id}/results", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/jobs/spiderMaps/campaigns/{campaign_id}/jobs/{job_id}/results", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## workflows Source: https://spideriq.ai/docs/api-reference/workflows ## `GET` /api/v1/workflows/types **List Workflow Types** List available workflow types. Returns all workflow configurations that can be used for campaigns. Each workflow type has different stages and capabilities. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/workflows/types' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/workflows/types", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/workflows/types", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/workflows/types", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response ## `GET` /api/v1/workflows/{job_id}/status **Get Workflow Status** Get the status of a workflow job. Args: job_id: WindMill job UUID Returns: Workflow job status including current step and progress. **Parameters** - `job_id` (path, string, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/workflows/{job_id}/status' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/workflows/{job_id}/status", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/workflows/{job_id}/status", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/workflows/{job_id}/status", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/workflows/{job_id}/result **Get Workflow Result** Get the result of a completed workflow job. Args: job_id: WindMill job UUID Returns: Workflow job result including aggregated data. **Parameters** - `job_id` (path, string, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/workflows/{job_id}/result' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/workflows/{job_id}/result", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/workflows/{job_id}/result", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/workflows/{job_id}/result", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/workflows/selection **Get Workflow Selection Info** Get which workflow would be selected based on configuration. This endpoint helps users understand which workflow will be used for their campaign configuration. Args: spidersite_enabled: Whether website crawling is enabled spiderverify_enabled: Whether email verification is enabled Returns: Selected workflow information and reasoning. **Parameters** - `spidersite_enabled` (query, boolean, optional) — Whether SpiderSite is enabled - `spiderverify_enabled` (query, boolean, optional) — Whether SpiderVerify is enabled :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/workflows/selection' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/workflows/selection", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/workflows/selection", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/workflows/selection", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## Lead Search Source: https://spideriq.ai/docs/api-reference/lead-search ## `POST` /api/v1/lead-search **Submit a single-location lead search** Run the full lead generation pipeline for a single search query. SpiderMaps → SpiderSite → SpiderVerify → VayaPin. Returns a `job_id` — poll `/api/v1/jobs/{job_id}/results` for the aggregated pipeline output. Unlike campaigns (which fan out across a country), this triggers one WindMill flow for one location. Disable pipeline stages via the `workflow` field in the request body. **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/lead-search' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "search_query": "string", "country_code": "US", "location": "string", "max_results": 20, "lang": "en", "workflow": { "spidersite": { "enabled": false, "mode": {}, "max_pages": 25, "crawl_strategy": "bestfirst", "target_pages": [ "contact", "about", "team", "news", "blog" ], "enable_spa": true, "spa_timeout": 30, "extract_team": false, "extract_company_info": false, "extract_pain_points": false, "product_description": {}, "icp_description": {}, "compendium": {}, "timeout": 30 }, "spiderverify": { "enabled": false, "check_gravatar": false, "check_dnsbl": false, "smtp_timeout_secs": 45, "max_emails_per_business": 10 }, "vayapin": { "enabled": false }, "social_media_enrichment": { "enabled": true }, "smartlead": { "enabled": false, "connection_id": {}, "remote_campaign_id": {}, "remote_campaign_name": {}, "limit": {}, "only_with_vayapin_seo": false, "only_with_vayapin_pin": false, "field_map": {} }, "filter_social_media": true, "filter_review_sites": true, "filter_directories": true, "filter_maps": true }, "test": false }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/lead-search", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"search_query": "string", "country_code": "US", "location": "string", "max_results": 20, "lang": "en", "workflow": {"spidersite": {"enabled": false, "mode": {}, "max_pages": 25, "crawl_strategy": "bestfirst", "target_pages": ["contact", "about", "team", "news", "blog"], "enable_spa": true, "spa_timeout": 30, "extract_team": false, "extract_company_info": false, "extract_pain_points": false, "product_description": {}, "icp_description": {}, "compendium": {}, "timeout": 30}, "spiderverify": {"enabled": false, "check_gravatar": false, "check_dnsbl": false, "smtp_timeout_secs": 45, "max_emails_per_business": 10}, "vayapin": {"enabled": false}, "social_media_enrichment": {"enabled": true}, "smartlead": {"enabled": false, "connection_id": {}, "remote_campaign_id": {}, "remote_campaign_name": {}, "limit": {}, "only_with_vayapin_seo": false, "only_with_vayapin_pin": false, "field_map": {}}, "filter_social_media": true, "filter_review_sites": true, "filter_directories": true, "filter_maps": true}, "test": false}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/lead-search", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"search_query": "string", "country_code": "US", "location": "string", "max_results": 20, "lang": "en", "workflow": {"spidersite": {"enabled": false, "mode": {}, "max_pages": 25, "crawl_strategy": "bestfirst", "target_pages": ["contact", "about", "team", "news", "blog"], "enable_spa": true, "spa_timeout": 30, "extract_team": false, "extract_company_info": false, "extract_pain_points": false, "product_description": {}, "icp_description": {}, "compendium": {}, "timeout": 30}, "spiderverify": {"enabled": false, "check_gravatar": false, "check_dnsbl": false, "smtp_timeout_secs": 45, "max_emails_per_business": 10}, "vayapin": {"enabled": false}, "social_media_enrichment": {"enabled": true}, "smartlead": {"enabled": false, "connection_id": {}, "remote_campaign_id": {}, "remote_campaign_name": {}, "limit": {}, "only_with_vayapin_seo": false, "only_with_vayapin_pin": false, "field_map": {}}, "filter_social_media": true, "filter_review_sites": true, "filter_directories": true, "filter_maps": true}, "test": false}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"search_query": "string", "country_code": "US", "location": "string", "max_results": 20, "lang": "en", "workflow": {"spidersite": {"enabled": false, "mode": {}, "max_pages": 25, "crawl_strategy": "bestfirst", "target_pages": ["contact", "about", "team", "news", "blog"], "enable_spa": true, "spa_timeout": 30, "extract_team": false, "extract_company_info": false, "extract_pain_points": false, "product_description": {}, "icp_description": {}, "compendium": {}, "timeout": 30}, "spiderverify": {"enabled": false, "check_gravatar": false, "check_dnsbl": false, "smtp_timeout_secs": 45, "max_emails_per_business": 10}, "vayapin": {"enabled": false}, "social_media_enrichment": {"enabled": true}, "smartlead": {"enabled": false, "connection_id": {}, "remote_campaign_id": {}, "remote_campaign_name": {}, "limit": {}, "only_with_vayapin_seo": false, "only_with_vayapin_pin": false, "field_map": {}}, "filter_social_media": true, "filter_review_sites": true, "filter_directories": true, "filter_maps": true}, "test": false}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/lead-search", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `201` — Job created and queued successfully - `401` — Authentication failed - `403` — Client account is inactive - `422` — Validation error - Invalid payload - `429` — Rate limit exceeded - `500` — Internal server error - `503` — Queue service unavailable ## Bulk Lead Sourcing Source: https://spideriq.ai/docs/api-reference/bulk-lead-sourcing ## `POST` /api/v1/bulk-lead-sourcing/submit **Submit a bulk lead-sourcing run** Source leads in bulk from a provider: ONE provider job covering many search terms and locations, returning one flat result set that is then deduplicated and fanned out one job per lead. The provider is **not** contacted during this request. A manifest row is created and returned immediately; the bulk worker drives it through submit -> poll -> fetch -> parse -> dedup -> fan-out. Poll `GET /api/v1/jobs/{job_id}/status` for progress. `settings.workflow` chooses which downstream stages each sourced lead runs through; `source.source_kind` bounds which of them are eligible (a LinkedIn company has no address, so it can never feed VayaPin). Runs are size-checked before anything is spent: a submission whose estimated record count exceeds your per-job ceiling — or whose estimated cost would cross your 24-hour spend cap — is refused with 429 and nothing is charged. **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/bulk-lead-sourcing/submit' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "source": { "provider": "string", "source_kind": "string", "queries": [ "string" ], "upload": { "upload_id": "string" }, "selection": { "selection_id": "string" }, "geo": [ { "label": "string", "latitude": 0.0, "longitude": 0.0, "country_code": "string", "region": "string" } ], "limits": { "max_records_per_query": 0, "max_total_records": 0 }, "filters": {}, "language": "en" }, "settings": { "workflow": { "spidersite": { "enabled": {}, "mode": {}, "max_pages": {}, "crawl_strategy": {}, "target_pages": {}, "enable_spa": {}, "spa_timeout": {}, "extract_team": {}, "extract_company_info": {}, "extract_pain_points": {}, "product_description": {}, "icp_description": {}, "compendium": {}, "timeout": {} }, "spiderverify": { "enabled": {}, "check_gravatar": {}, "check_dnsbl": {}, "smtp_timeout_secs": {}, "max_emails_per_business": {} }, "vayapin": { "enabled": {} }, "social_media_enrichment": { "enabled": {} }, "smartlead": { "enabled": {}, "connection_id": {}, "remote_campaign_id": {}, "remote_campaign_name": {}, "limit": {}, "only_with_vayapin_seo": {}, "only_with_vayapin_pin": {}, "field_map": {} }, "filter_social_media": true, "filter_review_sites": true, "filter_directories": true, "filter_maps": true } }, "priority": 5, "test": false }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/bulk-lead-sourcing/submit", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"source": {"provider": "string", "source_kind": "string", "queries": ["string"], "upload": {"upload_id": "string"}, "selection": {"selection_id": "string"}, "geo": [{"label": "string", "latitude": 0.0, "longitude": 0.0, "country_code": "string", "region": "string"}], "limits": {"max_records_per_query": 0, "max_total_records": 0}, "filters": {}, "language": "en"}, "settings": {"workflow": {"spidersite": {"enabled": {}, "mode": {}, "max_pages": {}, "crawl_strategy": {}, "target_pages": {}, "enable_spa": {}, "spa_timeout": {}, "extract_team": {}, "extract_company_info": {}, "extract_pain_points": {}, "product_description": {}, "icp_description": {}, "compendium": {}, "timeout": {}}, "spiderverify": {"enabled": {}, "check_gravatar": {}, "check_dnsbl": {}, "smtp_timeout_secs": {}, "max_emails_per_business": {}}, "vayapin": {"enabled": {}}, "social_media_enrichment": {"enabled": {}}, "smartlead": {"enabled": {}, "connection_id": {}, "remote_campaign_id": {}, "remote_campaign_name": {}, "limit": {}, "only_with_vayapin_seo": {}, "only_with_vayapin_pin": {}, "field_map": {}}, "filter_social_media": true, "filter_review_sites": true, "filter_directories": true, "filter_maps": true}}, "priority": 5, "test": false}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/bulk-lead-sourcing/submit", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"source": {"provider": "string", "source_kind": "string", "queries": ["string"], "upload": {"upload_id": "string"}, "selection": {"selection_id": "string"}, "geo": [{"label": "string", "latitude": 0.0, "longitude": 0.0, "country_code": "string", "region": "string"}], "limits": {"max_records_per_query": 0, "max_total_records": 0}, "filters": {}, "language": "en"}, "settings": {"workflow": {"spidersite": {"enabled": {}, "mode": {}, "max_pages": {}, "crawl_strategy": {}, "target_pages": {}, "enable_spa": {}, "spa_timeout": {}, "extract_team": {}, "extract_company_info": {}, "extract_pain_points": {}, "product_description": {}, "icp_description": {}, "compendium": {}, "timeout": {}}, "spiderverify": {"enabled": {}, "check_gravatar": {}, "check_dnsbl": {}, "smtp_timeout_secs": {}, "max_emails_per_business": {}}, "vayapin": {"enabled": {}}, "social_media_enrichment": {"enabled": {}}, "smartlead": {"enabled": {}, "connection_id": {}, "remote_campaign_id": {}, "remote_campaign_name": {}, "limit": {}, "only_with_vayapin_seo": {}, "only_with_vayapin_pin": {}, "field_map": {}}, "filter_social_media": true, "filter_review_sites": true, "filter_directories": true, "filter_maps": true}}, "priority": 5, "test": false}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"source": {"provider": "string", "source_kind": "string", "queries": ["string"], "upload": {"upload_id": "string"}, "selection": {"selection_id": "string"}, "geo": [{"label": "string", "latitude": 0.0, "longitude": 0.0, "country_code": "string", "region": "string"}], "limits": {"max_records_per_query": 0, "max_total_records": 0}, "filters": {}, "language": "en"}, "settings": {"workflow": {"spidersite": {"enabled": {}, "mode": {}, "max_pages": {}, "crawl_strategy": {}, "target_pages": {}, "enable_spa": {}, "spa_timeout": {}, "extract_team": {}, "extract_company_info": {}, "extract_pain_points": {}, "product_description": {}, "icp_description": {}, "compendium": {}, "timeout": {}}, "spiderverify": {"enabled": {}, "check_gravatar": {}, "check_dnsbl": {}, "smtp_timeout_secs": {}, "max_emails_per_business": {}}, "vayapin": {"enabled": {}}, "social_media_enrichment": {"enabled": {}}, "smartlead": {"enabled": {}, "connection_id": {}, "remote_campaign_id": {}, "remote_campaign_name": {}, "limit": {}, "only_with_vayapin_seo": {}, "only_with_vayapin_pin": {}, "field_map": {}}, "filter_social_media": true, "filter_review_sites": true, "filter_directories": true, "filter_maps": true}}, "priority": 5, "test": false}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/bulk-lead-sourcing/submit", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `202` — Successful Response - `201` — Job created and queued successfully - `401` — Authentication failed - `403` — Client account is inactive - `422` — Validation error - Invalid payload - `429` — Rate limit exceeded - `500` — Internal server error - `503` — Queue service unavailable ## Company Intel Source: https://spideriq.ai/docs/api-reference/company-intel ## `POST` /api/v1/company-intel **Submit single company intel request** Research a single company through the full intelligence pipeline. Perplexity → SpiderSite → CompanyData → Verify → People. Poll GET /api/v1/jobs/{job_id}/results for results. **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/company-intel' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "company_name": "string", "city": "string", "country_code": "string", "domain": "string", "linkedin_url": "string", "profile_mode": "short", "max_employees": 0, "config": { "discovery": { "enabled": true }, "spidersite": { "enabled": true, "mode": "leads", "max_pages": 25, "extract_team": true, "extract_company_info": true, "product_description": "string", "icp_description": "string" }, "spidercompanydata": { "enabled": true, "country": "auto", "include_financials": false }, "spiderverify": { "enabled": true, "max_emails": 50 }, "spiderpeople": { "enabled": true, "max_employees": 20, "profile_mode": "short" } }, "test": false }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/company-intel", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"company_name": "string", "city": "string", "country_code": "string", "domain": "string", "linkedin_url": "string", "profile_mode": "short", "max_employees": 0, "config": {"discovery": {"enabled": true}, "spidersite": {"enabled": true, "mode": "leads", "max_pages": 25, "extract_team": true, "extract_company_info": true, "product_description": "string", "icp_description": "string"}, "spidercompanydata": {"enabled": true, "country": "auto", "include_financials": false}, "spiderverify": {"enabled": true, "max_emails": 50}, "spiderpeople": {"enabled": true, "max_employees": 20, "profile_mode": "short"}}, "test": false}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/company-intel", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"company_name": "string", "city": "string", "country_code": "string", "domain": "string", "linkedin_url": "string", "profile_mode": "short", "max_employees": 0, "config": {"discovery": {"enabled": true}, "spidersite": {"enabled": true, "mode": "leads", "max_pages": 25, "extract_team": true, "extract_company_info": true, "product_description": "string", "icp_description": "string"}, "spidercompanydata": {"enabled": true, "country": "auto", "include_financials": false}, "spiderverify": {"enabled": true, "max_emails": 50}, "spiderpeople": {"enabled": true, "max_employees": 20, "profile_mode": "short"}}, "test": false}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"company_name": "string", "city": "string", "country_code": "string", "domain": "string", "linkedin_url": "string", "profile_mode": "short", "max_employees": 0, "config": {"discovery": {"enabled": true}, "spidersite": {"enabled": true, "mode": "leads", "max_pages": 25, "extract_team": true, "extract_company_info": true, "product_description": "string", "icp_description": "string"}, "spidercompanydata": {"enabled": true, "country": "auto", "include_financials": false}, "spiderverify": {"enabled": true, "max_emails": 50}, "spiderpeople": {"enabled": true, "max_employees": 20, "profile_mode": "short"}}, "test": false}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/company-intel", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `201` — Successful Response - `422` — Validation Error ## `POST` /api/v1/company-intel/batch **Submit batch company intel request** Research multiple companies in parallel (up to 50). Poll GET /api/v1/jobs/{job_id}/results for aggregated results. **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/company-intel/batch' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "companies": [ { "company_name": "string", "city": "string", "country_code": "string", "domain": "string", "linkedin_url": "string", "profile_mode": "short", "max_employees": 0, "config": { "discovery": { "enabled": {} }, "spidersite": { "enabled": {}, "mode": {}, "max_pages": {}, "extract_team": {}, "extract_company_info": {}, "product_description": {}, "icp_description": {} }, "spidercompanydata": { "enabled": {}, "country": {}, "include_financials": {} }, "spiderverify": { "enabled": {}, "max_emails": {} }, "spiderpeople": { "enabled": {}, "max_employees": {}, "profile_mode": {} } }, "test": false } ], "profile_mode": "short", "max_employees": 0, "config": { "discovery": { "enabled": true }, "spidersite": { "enabled": true, "mode": "leads", "max_pages": 25, "extract_team": true, "extract_company_info": true, "product_description": "string", "icp_description": "string" }, "spidercompanydata": { "enabled": true, "country": "auto", "include_financials": false }, "spiderverify": { "enabled": true, "max_emails": 50 }, "spiderpeople": { "enabled": true, "max_employees": 20, "profile_mode": "short" } }, "test": false }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/company-intel/batch", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"companies": [{"company_name": "string", "city": "string", "country_code": "string", "domain": "string", "linkedin_url": "string", "profile_mode": "short", "max_employees": 0, "config": {"discovery": {"enabled": {}}, "spidersite": {"enabled": {}, "mode": {}, "max_pages": {}, "extract_team": {}, "extract_company_info": {}, "product_description": {}, "icp_description": {}}, "spidercompanydata": {"enabled": {}, "country": {}, "include_financials": {}}, "spiderverify": {"enabled": {}, "max_emails": {}}, "spiderpeople": {"enabled": {}, "max_employees": {}, "profile_mode": {}}}, "test": false}], "profile_mode": "short", "max_employees": 0, "config": {"discovery": {"enabled": true}, "spidersite": {"enabled": true, "mode": "leads", "max_pages": 25, "extract_team": true, "extract_company_info": true, "product_description": "string", "icp_description": "string"}, "spidercompanydata": {"enabled": true, "country": "auto", "include_financials": false}, "spiderverify": {"enabled": true, "max_emails": 50}, "spiderpeople": {"enabled": true, "max_employees": 20, "profile_mode": "short"}}, "test": false}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/company-intel/batch", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"companies": [{"company_name": "string", "city": "string", "country_code": "string", "domain": "string", "linkedin_url": "string", "profile_mode": "short", "max_employees": 0, "config": {"discovery": {"enabled": {}}, "spidersite": {"enabled": {}, "mode": {}, "max_pages": {}, "extract_team": {}, "extract_company_info": {}, "product_description": {}, "icp_description": {}}, "spidercompanydata": {"enabled": {}, "country": {}, "include_financials": {}}, "spiderverify": {"enabled": {}, "max_emails": {}}, "spiderpeople": {"enabled": {}, "max_employees": {}, "profile_mode": {}}}, "test": false}], "profile_mode": "short", "max_employees": 0, "config": {"discovery": {"enabled": true}, "spidersite": {"enabled": true, "mode": "leads", "max_pages": 25, "extract_team": true, "extract_company_info": true, "product_description": "string", "icp_description": "string"}, "spidercompanydata": {"enabled": true, "country": "auto", "include_financials": false}, "spiderverify": {"enabled": true, "max_emails": 50}, "spiderpeople": {"enabled": true, "max_employees": 20, "profile_mode": "short"}}, "test": false}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"companies": [{"company_name": "string", "city": "string", "country_code": "string", "domain": "string", "linkedin_url": "string", "profile_mode": "short", "max_employees": 0, "config": {"discovery": {"enabled": {}}, "spidersite": {"enabled": {}, "mode": {}, "max_pages": {}, "extract_team": {}, "extract_company_info": {}, "product_description": {}, "icp_description": {}}, "spidercompanydata": {"enabled": {}, "country": {}, "include_financials": {}}, "spiderverify": {"enabled": {}, "max_emails": {}}, "spiderpeople": {"enabled": {}, "max_employees": {}, "profile_mode": {}}}, "test": false}], "profile_mode": "short", "max_employees": 0, "config": {"discovery": {"enabled": true}, "spidersite": {"enabled": true, "mode": "leads", "max_pages": 25, "extract_team": true, "extract_company_info": true, "product_description": "string", "icp_description": "string"}, "spidercompanydata": {"enabled": true, "country": "auto", "include_financials": false}, "spiderverify": {"enabled": true, "max_emails": 50}, "spiderpeople": {"enabled": true, "max_employees": 20, "profile_mode": "short"}}, "test": false}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/company-intel/batch", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `201` — Successful Response - `422` — Validation Error ## Flows Source: https://spideriq.ai/docs/api-reference/flows ## `GET` /api/v1/flows **List Flows** List flows in the marketplace. Filters: category, search (name+description). **Parameters** - `category` (query, any, optional) - `search` (query, any, optional) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/flows' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/flows", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/flows", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/flows", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/flows/{slug} **Get Flow** **Parameters** - `slug` (path, string, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/flows/{slug}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/flows/{slug}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/flows/{slug}", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/flows/{slug}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/flows/{slug}/run **Run Flow** Dispatch a flow run. Body shape determines mode (see FlowRunRequest). **Parameters** - `slug` (path, string, required) **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/flows/{slug}/run' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "input": {}, "inputs": [ {} ], "scope": {}, "priority": 0 }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/flows/{slug}/run", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"input": {}, "inputs": [{}], "scope": {}, "priority": 0}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/flows/{slug}/run", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"input": {}, "inputs": [{}], "scope": {}, "priority": 0}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"input": {}, "inputs": [{}], "scope": {}, "priority": 0}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/flows/{slug}/run", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `201` — Successful Response - `422` — Validation Error ## Flow Runs Source: https://spideriq.ai/docs/api-reference/flow-runs ## `GET` /api/v1/runs **List Runs** **Parameters** - `flow` (query, any, optional) — Filter by flow slug - `status` (query, any, optional) - `mode` (query, any, optional) — single|batch|campaign - `campaign_id` (query, any, optional) — Filter to runs whose payload._campaign_id matches - `batch_id` (query, any, optional) — Filter to runs whose payload._batch_id matches - `limit` (query, integer, optional) - `offset` (query, integer, optional) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/runs' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/runs", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/runs", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/runs", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/runs/{run_id} **Get Run** **Parameters** - `run_id` (path, string, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/runs/{run_id}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/runs/{run_id}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/runs/{run_id}", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/runs/{run_id}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/runs/{run_id}/cancel **Cancel Run** Cancel a queued or processing run owned by the caller. **Parameters** - `run_id` (path, string, required) :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/runs/{run_id}/cancel' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/runs/{run_id}/cancel", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/runs/{run_id}/cancel", { method: "POST", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/runs/{run_id}/cancel", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## Flow Run Groups Source: https://spideriq.ai/docs/api-reference/flow-run-groups ## `GET` /api/v1/run-groups/{group_id} **Get Run Group** Aggregate view for a batch (or, when wired, campaign) group. Phase 1 only resolves batch groups (jobs sharing payload._batch_id). Campaign groups will resolve from scraping_campaigns when campaign-mode dispatch is wired. **Parameters** - `group_id` (path, string, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/run-groups/{group_id}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/run-groups/{group_id}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/run-groups/{group_id}", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/run-groups/{group_id}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## Funnels (public render) Source: https://spideriq.ai/docs/api-reference/funnels-public-render ## `GET` /api/v1/funnels/{flow_id} **Fetch a funnel's unified public render payload** Returns the unified render payload for any kind (page/form/funnel/booking): the node graph, public-safe settings, inlined page snapshots, and the universal Liquid context namespaces (flow/form/funnel/booking/idap/page/question). Public + IP-rate-limited; tenancy resolved from flow_id. Append ?format=llm for an agent guidance envelope. **Parameters** - `flow_id` (path, string, required) - `current_node` (query, any, optional) — Node slug being rendered; defaults to the first page node. May be a composite inline-embed slug (~). - `include_pages` (query, boolean, optional) — Inline page-node snapshots (set false to skip). :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/funnels/{flow_id}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/funnels/{flow_id}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/funnels/{flow_id}", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/funnels/{flow_id}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `404` — Flow not found (RESOURCE_NOT_FOUND envelope) - `429` — Rate limited - `503` — Database unavailable - `422` — Validation Error ## `GET` /api/v1/funnels/{flow_id}/session **Read the caller's active flow session state** **Parameters** - `flow_id` (path, string, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/funnels/{flow_id}/session' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/funnels/{flow_id}/session", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/funnels/{flow_id}/session", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/funnels/{flow_id}/session", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `404` — No active session for this flow - `422` — Validation Error ## `PATCH` /api/v1/funnels/{flow_id}/session **Merge field/variable updates into the active flow session** **Parameters** - `flow_id` (path, string, required) **Request body** :::codegroup ```bash curl -X PATCH 'https://spideriq.ai/api/v1/funnels/{flow_id}/session' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "fields": {}, "questions": {}, "variables": {} }' ``` ```python import httpx resp = httpx.patch( "https://spideriq.ai/api/v1/funnels/{flow_id}/session", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"fields": {}, "questions": {}, "variables": {}}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/funnels/{flow_id}/session", { method: "PATCH", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"fields": {}, "questions": {}, "variables": {}}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"fields": {}, "questions": {}, "variables": {}}`) req, _ := http.NewRequest("PATCH", "https://spideriq.ai/api/v1/funnels/{flow_id}/session", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `404` — No active session for this flow - `422` — Validation Error ## `POST` /api/v1/funnels/{flow_id}/event **Fire a flow event; returns the next-node transition (200) or 204 if none** Records a flow event (submit/click/timer/scroll_milestone/entry/field_change) against the caller's session, evaluates the node's outgoing edges, and returns {next_node|end_flow, redirect_url} (200) when a branch fires — branching WINS over the renderer's sequential nav. Returns 204 when no edge matches (client navigates sequentially). An 'entry' event with no session cookie starts a fresh session and sets the signed funnel_session_id cookie. **Parameters** - `flow_id` (path, string, required) **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/funnels/{flow_id}/event' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "event": "click", "current_node": "string", "field": "string", "value": null, "fields": {}, "hidden": {} }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/funnels/{flow_id}/event", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"event": "click", "current_node": "string", "field": "string", "value": null, "fields": {}, "hidden": {}}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/funnels/{flow_id}/event", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"event": "click", "current_node": "string", "field": "string", "value": null, "fields": {}, "hidden": {}}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"event": "click", "current_node": "string", "field": "string", "value": null, "fields": {}, "hidden": {}}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/funnels/{flow_id}/event", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — A branch fired — next_node/end_flow returned - `204` — No branch fired — navigate sequentially - `404` — Flow not found - `422` — Validation Error ## Commerce Source: https://spideriq.ai/docs/api-reference/commerce ## `GET` /api/v1/commerce/orders **List Orders** List the calling tenant's orders, newest first, with optional filters. **Parameters** - `limit` (query, integer, optional) - `offset` (query, integer, optional) - `status` (query, any, optional) - `contact_email` (query, any, optional) - `since` (query, any, optional) — ISO-8601 lower bound on created_at (inclusive). :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/commerce/orders' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/commerce/orders", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/commerce/orders", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/commerce/orders", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/commerce/orders/stats **Order Stats** Aggregate order counts + revenue for the calling tenant over ``window``. **Parameters** - `window` (query, string, optional) — 7d | 30d | 90d | all :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/commerce/orders/stats' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/commerce/orders/stats", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/commerce/orders/stats", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/commerce/orders/stats", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/commerce/orders/{order_id} **Get Order** Fetch a single order by id, scoped to the calling tenant (404 otherwise). **Parameters** - `order_id` (path, string, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/commerce/orders/{order_id}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/commerce/orders/{order_id}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/commerce/orders/{order_id}", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/commerce/orders/{order_id}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## marketplace-hire Source: https://spideriq.ai/docs/api-reference/marketplace-hire ## `POST` /api/v1/marketplace/agents/{component_id}/hire **Hire Marketplace Agent** Hire an OPVS-employee marketplace tile (Feed B) → bound ``kind='agent'`` flow. The tile's ``agent_meta.opvs_catalog.profile_id`` is the hire key. See :func:`_hire_agent_flow` for the shared bind/persist path. **Parameters** - `component_id` (path, string, required) **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/marketplace/agents/{component_id}/hire' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "profile_id": "string", "project_id": "string", "flow_id": "string", "display_name": "string", "embed_origins": [ "string" ] }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/marketplace/agents/{component_id}/hire", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"profile_id": "string", "project_id": "string", "flow_id": "string", "display_name": "string", "embed_origins": ["string"]}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/marketplace/agents/{component_id}/hire", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"profile_id": "string", "project_id": "string", "flow_id": "string", "display_name": "string", "embed_origins": ["string"]}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"profile_id": "string", "project_id": "string", "flow_id": "string", "display_name": "string", "embed_origins": ["string"]}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/marketplace/agents/{component_id}/hire", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/marketplace/agents/by-profile/{catalog_profile_id}/hire **Hire Marketplace Agent By Profile** Hire by OPVS ``catalog_profile_id`` (Feed A — the tenant's own roster, AG.2). Binds directly by the OPVS profile id from ``GET /employees/hires``; for an already-owned hire this is a FREE reuse (no 402). Same 200-envelope / persist path as the tile hire — see :func:`_hire_agent_flow`. **Parameters** - `catalog_profile_id` (path, string, required) **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/marketplace/agents/by-profile/{catalog_profile_id}/hire' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "profile_id": "string", "project_id": "string", "flow_id": "string", "display_name": "string", "embed_origins": [ "string" ] }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/marketplace/agents/by-profile/{catalog_profile_id}/hire", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"profile_id": "string", "project_id": "string", "flow_id": "string", "display_name": "string", "embed_origins": ["string"]}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/marketplace/agents/by-profile/{catalog_profile_id}/hire", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"profile_id": "string", "project_id": "string", "flow_id": "string", "display_name": "string", "embed_origins": ["string"]}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"profile_id": "string", "project_id": "string", "flow_id": "string", "display_name": "string", "embed_origins": ["string"]}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/marketplace/agents/by-profile/{catalog_profile_id}/hire", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## Dashboard Flows Source: https://spideriq.ai/docs/api-reference/dashboard-flows ## `GET` /api/v1/dashboard/flows **List Flows** **Parameters** - `category` (query, any, optional) - `search` (query, any, optional) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/flows' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/flows", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/flows", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/flows", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/dashboard/flows/{slug} **Get Flow** **Parameters** - `slug` (path, string, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/flows/{slug}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/flows/{slug}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/flows/{slug}", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/flows/{slug}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/dashboard/flows/{slug}/analytics **Get Flow Analytics** Per-flow + per-client analytics: KPIs, time series, status breakdown, top scenarios, recent failures. Two paths: 1. **Single-service flows** (siteScraper, mapsSearch, emailVerify, …) — aggregate from `public.jobs` filtered by `type = flow.dispatch_type`. One job row per run. 1. **Pipeline flows** (localSeo / leadSearch / companyIntel / linkedinExtract) — aggregate from `public.scraping_campaigns`. The campaign row is the unit of work; sub-jobs (one per location × stage) link back via campaign_locations. Pre-`wave3-sweep` campaigns lack the `_flow_dispatch_type` tag so a workflow_config heuristic backfills the membership (see `_PIPELINE_LEGACY_HEURISTIC`). Cost (§C8) is not yet wired; `kpis.total_cost_cents` is always None today. **Parameters** - `slug` (path, string, required) - `range` (query, string, optional) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/flows/{slug}/analytics' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/flows/{slug}/analytics", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/flows/{slug}/analytics", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/flows/{slug}/analytics", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/dashboard/flows/{slug}/run **Run Flow** Dispatch a flow run from the dashboard. Same behavior as the public /api/v1/flows/{slug}/run endpoint — see api/v1/flows.py for the body contract and mode rules. **Parameters** - `slug` (path, string, required) **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/dashboard/flows/{slug}/run' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "input": {}, "inputs": [ {} ], "scope": {}, "priority": 0 }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/dashboard/flows/{slug}/run", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"input": {}, "inputs": [{}], "scope": {}, "priority": 0}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/flows/{slug}/run", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"input": {}, "inputs": [{}], "scope": {}, "priority": 0}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"input": {}, "inputs": [{}], "scope": {}, "priority": 0}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/dashboard/flows/{slug}/run", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `201` — Successful Response - `422` — Validation Error ## Dashboard Flow Runs Source: https://spideriq.ai/docs/api-reference/dashboard-flow-runs ## `GET` /api/v1/dashboard/runs **List Runs** **Parameters** - `flow` (query, any, optional) - `status` (query, any, optional) - `mode` (query, any, optional) - `campaign_id` (query, any, optional) - `batch_id` (query, any, optional) - `limit` (query, integer, optional) - `offset` (query, integer, optional) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/runs' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/runs", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/runs", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/runs", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/dashboard/runs/{run_id} **Get Run** **Parameters** - `run_id` (path, string, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/runs/{run_id}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/runs/{run_id}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/runs/{run_id}", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/runs/{run_id}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `DELETE` /api/v1/dashboard/runs/{run_id} **Delete Run** Soft-delete a run from the user's view. Sets ``jobs.hidden_at = NOW()`` so the row vanishes from list_runs and get_run returns 410 Gone. Hard delete is not used because the cascade through ``campaign_workflow_jobs`` (3 FK columns ON DELETE CASCADE) would mangle finished campaigns and orphan rows in ``public.results`` / ``crm_sync_log`` (no FK guards). Soft-hide keeps audit trails and analytics intact while honoring "remove from my list" UX. Constraints: - Only terminal-status runs (succeeded / failed / cancelled) can be hidden — in-flight runs (queued / processing) return 422 so the user is steered to ``/cancel`` first. - Tenant-scoped via ``client_id`` from the cookie auth context. - Idempotent at the row level: hiding an already-hidden row is a no-op (rowcount=0 → 404 like any other not-found run). **Parameters** - `run_id` (path, string, required) :::codegroup ```bash curl -X DELETE 'https://spideriq.ai/api/v1/dashboard/runs/{run_id}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.delete( "https://spideriq.ai/api/v1/dashboard/runs/{run_id}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/runs/{run_id}", { method: "DELETE", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("DELETE", "https://spideriq.ai/api/v1/dashboard/runs/{run_id}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/dashboard/runs/{run_id}/cancel **Cancel Run** **Parameters** - `run_id` (path, string, required) :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/dashboard/runs/{run_id}/cancel' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/dashboard/runs/{run_id}/cancel", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/runs/{run_id}/cancel", { method: "POST", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/dashboard/runs/{run_id}/cancel", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## Dashboard Flow Run Groups Source: https://spideriq.ai/docs/api-reference/dashboard-flow-run-groups ## `GET` /api/v1/dashboard/run-groups/{group_id} **Get Run Group** **Parameters** - `group_id` (path, string, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/run-groups/{group_id}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/run-groups/{group_id}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/run-groups/{group_id}", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/run-groups/{group_id}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## Dashboard Bulk Lead Sourcing Source: https://spideriq.ai/docs/api-reference/dashboard-bulk-lead-sourcing ## `GET` /api/v1/dashboard/bulk-lead-sourcing/sources **List Bulk Sources** Every source the dropdown should show, with availability resolved live. Availability comes from the adapter registry rather than a hardcoded list, so the UI cannot claim a provider the backend would refuse — and does not need a frontend change on the day a new adapter lands. ADMIN-ONLY sources are OMITTED, not disabled. `_ROADMAP_SOURCES` below renders a not-built-yet capability greyed out precisely so it is visible before it works; for a source whose client exposure is being withheld that is the opposite of what is wanted — a greyed-out entry advertises it to every tenant. The same filter runs on the submit path (`prepare_submission`), because both `/estimate` and `/submit` resolve the adapter by name from the body: a filtered dropdown is a menu, not a lock. ⚠️ No adapter is `admin_only` today — sortlist, the flag's first and so far only user, was opened to clients by the owner ruling of 2026-08-18. The filter is therefore live code with no production subject, which is exactly the state in which a "simplification" deletes it and nothing goes red. `test_sortlist_client_exposure` registers a throwaway admin-only adapter to keep the control real. The role comes from the authenticated session via the canonical dep — never from a header, query param or body field (Dashboard RBAC Rule 7). :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/sources' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/sources", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/sources", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/sources", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response ## `POST` /api/v1/dashboard/bulk-lead-sourcing/upload **Upload Bulk Source File** Store a records file. Nothing is enriched, nothing is queued, nothing is spent. THIS IS NOT A SUBMIT, AND THE SPLIT IS THE POINT. An upload is free and reversible: the file sits in storage, the client can abandon it, and the expiry sweep removes it. The run — and the downstream enrichment spend that comes with it — starts only at ``/submit``, behind the confirmation. Combining the two would mean picking a file *is* buying the enrichment, with no screen in between, which is precisely the accident the bulk confirmation exists to prevent. ``provider``/``source_kind`` are recorded at UPLOAD time rather than inferred at submit, so a file uploaded as CSV cannot be submitted as JSON. The parsers differ, and that mismatch would otherwise surface as an unreadable artifact several stages downstream instead of a 422 at the door. **The 64 MiB cap is enforced mid-stream, from the bytes actually received** — never from ``Content-Length`` or ``UploadFile.size``, both of which are supplied by the party being limited. Note also that host nginx caps the request body BEFORE this handler runs; the scoped `location` in `apps/web/nginx-app.spideriq.ai.conf` is set above the app's cap on purpose, so an over-size upload gets this clean JSON 413 rather than nginx's raw HTML page (fastapi/LEARNINGS.md, press 3.1). **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/upload' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "provider": "string", "source_kind": "string", "file": "string" }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/upload", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"provider": "string", "source_kind": "string", "file": "string"}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/upload", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"provider": "string", "source_kind": "string", "file": "string"}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"provider": "string", "source_kind": "string", "file": "string"}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/upload", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `201` — Successful Response - `422` — Validation Error ## `POST` /api/v1/dashboard/bulk-lead-sourcing/estimate **Estimate Bulk Lead Sourcing** Size the run WITHOUT committing to it. Writes nothing, consumes no quota and creates no ``dispatch_decisions`` row. Safe to call on every keystroke; the frontend debounces it anyway. ⚠️ **It may now READ from the provider** (card SDS-19, 2026-08-20) — it no longer "contacts no provider", and the weaker claim is the honest one. Sortlist's ``spec_probe`` issues one header-only GET against the public listing URL for the chosen pair, so a client is told that ``advertising x germany-de`` has no page BEFORE the confirm button rather than by a failed run afterwards. No credentials, no order, no spend, nothing written — and the verdict is cached, so a debounced form does not re-probe on every keystroke. ``would_exceed_cap`` is advisory only. The authoritative refusal is still the dispatcher's at submit time — this is the same comparison run early so the user finds out before the confirm button, not after it. **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/estimate' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "source": { "provider": "string", "source_kind": "string", "queries": [ "string" ], "upload": { "upload_id": "string" }, "selection": { "selection_id": "string" }, "geo": [ { "label": "string", "latitude": 0.0, "longitude": 0.0, "country_code": "string", "region": "string" } ], "limits": { "max_records_per_query": 0, "max_total_records": 0 }, "filters": {}, "language": "en" }, "settings": { "workflow": { "spidersite": { "enabled": {}, "mode": {}, "max_pages": {}, "crawl_strategy": {}, "target_pages": {}, "enable_spa": {}, "spa_timeout": {}, "extract_team": {}, "extract_company_info": {}, "extract_pain_points": {}, "product_description": {}, "icp_description": {}, "compendium": {}, "timeout": {} }, "spiderverify": { "enabled": {}, "check_gravatar": {}, "check_dnsbl": {}, "smtp_timeout_secs": {}, "max_emails_per_business": {} }, "vayapin": { "enabled": {} }, "social_media_enrichment": { "enabled": {} }, "smartlead": { "enabled": {}, "connection_id": {}, "remote_campaign_id": {}, "remote_campaign_name": {}, "limit": {}, "only_with_vayapin_seo": {}, "only_with_vayapin_pin": {}, "field_map": {} }, "filter_social_media": true, "filter_review_sites": true, "filter_directories": true, "filter_maps": true } }, "priority": 5, "test": false }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/estimate", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"source": {"provider": "string", "source_kind": "string", "queries": ["string"], "upload": {"upload_id": "string"}, "selection": {"selection_id": "string"}, "geo": [{"label": "string", "latitude": 0.0, "longitude": 0.0, "country_code": "string", "region": "string"}], "limits": {"max_records_per_query": 0, "max_total_records": 0}, "filters": {}, "language": "en"}, "settings": {"workflow": {"spidersite": {"enabled": {}, "mode": {}, "max_pages": {}, "crawl_strategy": {}, "target_pages": {}, "enable_spa": {}, "spa_timeout": {}, "extract_team": {}, "extract_company_info": {}, "extract_pain_points": {}, "product_description": {}, "icp_description": {}, "compendium": {}, "timeout": {}}, "spiderverify": {"enabled": {}, "check_gravatar": {}, "check_dnsbl": {}, "smtp_timeout_secs": {}, "max_emails_per_business": {}}, "vayapin": {"enabled": {}}, "social_media_enrichment": {"enabled": {}}, "smartlead": {"enabled": {}, "connection_id": {}, "remote_campaign_id": {}, "remote_campaign_name": {}, "limit": {}, "only_with_vayapin_seo": {}, "only_with_vayapin_pin": {}, "field_map": {}}, "filter_social_media": true, "filter_review_sites": true, "filter_directories": true, "filter_maps": true}}, "priority": 5, "test": false}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/estimate", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"source": {"provider": "string", "source_kind": "string", "queries": ["string"], "upload": {"upload_id": "string"}, "selection": {"selection_id": "string"}, "geo": [{"label": "string", "latitude": 0.0, "longitude": 0.0, "country_code": "string", "region": "string"}], "limits": {"max_records_per_query": 0, "max_total_records": 0}, "filters": {}, "language": "en"}, "settings": {"workflow": {"spidersite": {"enabled": {}, "mode": {}, "max_pages": {}, "crawl_strategy": {}, "target_pages": {}, "enable_spa": {}, "spa_timeout": {}, "extract_team": {}, "extract_company_info": {}, "extract_pain_points": {}, "product_description": {}, "icp_description": {}, "compendium": {}, "timeout": {}}, "spiderverify": {"enabled": {}, "check_gravatar": {}, "check_dnsbl": {}, "smtp_timeout_secs": {}, "max_emails_per_business": {}}, "vayapin": {"enabled": {}}, "social_media_enrichment": {"enabled": {}}, "smartlead": {"enabled": {}, "connection_id": {}, "remote_campaign_id": {}, "remote_campaign_name": {}, "limit": {}, "only_with_vayapin_seo": {}, "only_with_vayapin_pin": {}, "field_map": {}}, "filter_social_media": true, "filter_review_sites": true, "filter_directories": true, "filter_maps": true}}, "priority": 5, "test": false}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"source": {"provider": "string", "source_kind": "string", "queries": ["string"], "upload": {"upload_id": "string"}, "selection": {"selection_id": "string"}, "geo": [{"label": "string", "latitude": 0.0, "longitude": 0.0, "country_code": "string", "region": "string"}], "limits": {"max_records_per_query": 0, "max_total_records": 0}, "filters": {}, "language": "en"}, "settings": {"workflow": {"spidersite": {"enabled": {}, "mode": {}, "max_pages": {}, "crawl_strategy": {}, "target_pages": {}, "enable_spa": {}, "spa_timeout": {}, "extract_team": {}, "extract_company_info": {}, "extract_pain_points": {}, "product_description": {}, "icp_description": {}, "compendium": {}, "timeout": {}}, "spiderverify": {"enabled": {}, "check_gravatar": {}, "check_dnsbl": {}, "smtp_timeout_secs": {}, "max_emails_per_business": {}}, "vayapin": {"enabled": {}}, "social_media_enrichment": {"enabled": {}}, "smartlead": {"enabled": {}, "connection_id": {}, "remote_campaign_id": {}, "remote_campaign_name": {}, "limit": {}, "only_with_vayapin_seo": {}, "only_with_vayapin_pin": {}, "field_map": {}}, "filter_social_media": true, "filter_review_sites": true, "filter_directories": true, "filter_maps": true}}, "priority": 5, "test": false}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/estimate", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/dashboard/bulk-lead-sourcing/submit **Submit Bulk Lead Sourcing From Dashboard** Start the run. Same gate, same manifest, same response as the Bearer door. The only differences are how the caller proved who they are and which client_id that resolved to. ``token_id`` is left ``None``: per-token plan scoping is a PAT concept, and a cookie session has no token to scope by — the dispatcher then falls through to the client-level plan, which is correct. **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/submit' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "source": { "provider": "string", "source_kind": "string", "queries": [ "string" ], "upload": { "upload_id": "string" }, "selection": { "selection_id": "string" }, "geo": [ { "label": "string", "latitude": 0.0, "longitude": 0.0, "country_code": "string", "region": "string" } ], "limits": { "max_records_per_query": 0, "max_total_records": 0 }, "filters": {}, "language": "en" }, "settings": { "workflow": { "spidersite": { "enabled": {}, "mode": {}, "max_pages": {}, "crawl_strategy": {}, "target_pages": {}, "enable_spa": {}, "spa_timeout": {}, "extract_team": {}, "extract_company_info": {}, "extract_pain_points": {}, "product_description": {}, "icp_description": {}, "compendium": {}, "timeout": {} }, "spiderverify": { "enabled": {}, "check_gravatar": {}, "check_dnsbl": {}, "smtp_timeout_secs": {}, "max_emails_per_business": {} }, "vayapin": { "enabled": {} }, "social_media_enrichment": { "enabled": {} }, "smartlead": { "enabled": {}, "connection_id": {}, "remote_campaign_id": {}, "remote_campaign_name": {}, "limit": {}, "only_with_vayapin_seo": {}, "only_with_vayapin_pin": {}, "field_map": {} }, "filter_social_media": true, "filter_review_sites": true, "filter_directories": true, "filter_maps": true } }, "priority": 5, "test": false }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/submit", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"source": {"provider": "string", "source_kind": "string", "queries": ["string"], "upload": {"upload_id": "string"}, "selection": {"selection_id": "string"}, "geo": [{"label": "string", "latitude": 0.0, "longitude": 0.0, "country_code": "string", "region": "string"}], "limits": {"max_records_per_query": 0, "max_total_records": 0}, "filters": {}, "language": "en"}, "settings": {"workflow": {"spidersite": {"enabled": {}, "mode": {}, "max_pages": {}, "crawl_strategy": {}, "target_pages": {}, "enable_spa": {}, "spa_timeout": {}, "extract_team": {}, "extract_company_info": {}, "extract_pain_points": {}, "product_description": {}, "icp_description": {}, "compendium": {}, "timeout": {}}, "spiderverify": {"enabled": {}, "check_gravatar": {}, "check_dnsbl": {}, "smtp_timeout_secs": {}, "max_emails_per_business": {}}, "vayapin": {"enabled": {}}, "social_media_enrichment": {"enabled": {}}, "smartlead": {"enabled": {}, "connection_id": {}, "remote_campaign_id": {}, "remote_campaign_name": {}, "limit": {}, "only_with_vayapin_seo": {}, "only_with_vayapin_pin": {}, "field_map": {}}, "filter_social_media": true, "filter_review_sites": true, "filter_directories": true, "filter_maps": true}}, "priority": 5, "test": false}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/submit", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"source": {"provider": "string", "source_kind": "string", "queries": ["string"], "upload": {"upload_id": "string"}, "selection": {"selection_id": "string"}, "geo": [{"label": "string", "latitude": 0.0, "longitude": 0.0, "country_code": "string", "region": "string"}], "limits": {"max_records_per_query": 0, "max_total_records": 0}, "filters": {}, "language": "en"}, "settings": {"workflow": {"spidersite": {"enabled": {}, "mode": {}, "max_pages": {}, "crawl_strategy": {}, "target_pages": {}, "enable_spa": {}, "spa_timeout": {}, "extract_team": {}, "extract_company_info": {}, "extract_pain_points": {}, "product_description": {}, "icp_description": {}, "compendium": {}, "timeout": {}}, "spiderverify": {"enabled": {}, "check_gravatar": {}, "check_dnsbl": {}, "smtp_timeout_secs": {}, "max_emails_per_business": {}}, "vayapin": {"enabled": {}}, "social_media_enrichment": {"enabled": {}}, "smartlead": {"enabled": {}, "connection_id": {}, "remote_campaign_id": {}, "remote_campaign_name": {}, "limit": {}, "only_with_vayapin_seo": {}, "only_with_vayapin_pin": {}, "field_map": {}}, "filter_social_media": true, "filter_review_sites": true, "filter_directories": true, "filter_maps": true}}, "priority": 5, "test": false}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"source": {"provider": "string", "source_kind": "string", "queries": ["string"], "upload": {"upload_id": "string"}, "selection": {"selection_id": "string"}, "geo": [{"label": "string", "latitude": 0.0, "longitude": 0.0, "country_code": "string", "region": "string"}], "limits": {"max_records_per_query": 0, "max_total_records": 0}, "filters": {}, "language": "en"}, "settings": {"workflow": {"spidersite": {"enabled": {}, "mode": {}, "max_pages": {}, "crawl_strategy": {}, "target_pages": {}, "enable_spa": {}, "spa_timeout": {}, "extract_team": {}, "extract_company_info": {}, "extract_pain_points": {}, "product_description": {}, "icp_description": {}, "compendium": {}, "timeout": {}}, "spiderverify": {"enabled": {}, "check_gravatar": {}, "check_dnsbl": {}, "smtp_timeout_secs": {}, "max_emails_per_business": {}}, "vayapin": {"enabled": {}}, "social_media_enrichment": {"enabled": {}}, "smartlead": {"enabled": {}, "connection_id": {}, "remote_campaign_id": {}, "remote_campaign_name": {}, "limit": {}, "only_with_vayapin_seo": {}, "only_with_vayapin_pin": {}, "field_map": {}}, "filter_social_media": true, "filter_review_sites": true, "filter_directories": true, "filter_maps": true}}, "priority": 5, "test": false}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/submit", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `202` — Successful Response - `422` — Validation Error ## `GET` /api/v1/dashboard/bulk-lead-sourcing/past-runs **List Bulk Past Runs** The past runs this tenant could enrich, each with its label and its two counts. Reads only. Creates no selection, queues nothing, spends nothing — picking a run is free and only the submit is not. ``stages`` is a comma-separated list of the stages the NEW run would enable, and every ``eligible_leads`` is scoped to it (DECISION #2, design §3.3). It is REQUIRED rather than defaulted: a default stage set would silently answer a question the user did not ask, and the answer is a number they are about to size a purchase against. ``kind=job`` lists source jobs instead of campaigns. Inside a campaign (``campaign_id=…``) that is §7's escape hatch — a campaign whose eligible count is over the ceiling is split by picking one of its locations. Outside one it is how the standalone lead searches are reached, which for some tenants is most of the corpus. **Parameters** - `stages` (query, string, optional) - `kind` (query, string, optional) - `campaign_id` (query, any, optional) - `exclude_without_website` (query, boolean, optional) - `limit` (query, integer, optional) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/past-runs' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/past-runs", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/past-runs", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/past-runs", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/dashboard/bulk-lead-sourcing/corpus/fields **List Corpus Fields** The queryable field catalogue (§6.2). The UI builds every picker from this. Static — the same for every tenant, because it describes the SHAPE of the corpus, not its contents. Adding a filterable field is therefore an API deploy and never a dashboard rebuild, which is the whole point of serving it rather than hardcoding it client-side. Tenant-gated even though the payload carries no tenant data: it is a map of what can be asked, and an unauthenticated map of our schema is free reconnaissance. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/corpus/fields' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/corpus/fields", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/corpus/fields", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/corpus/fields", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response ## `GET` /api/v1/dashboard/bulk-lead-sourcing/corpus/values **List Corpus Field Values** Distinct values for ONE enumerable field, from THIS tenant's own corpus. `field` is a catalogue key, not a column: it is looked up, and an unknown or non-enumerable key is a 422. So although this endpoint reads real tenant data, there is no request field through which a caller can name a column, a table or a schema. Bounded hard (`LIMIT`), and only offered for fields the catalogue marks `enumerable` — a DISTINCT over free text on a six-figure table is a scan per keystroke. **Parameters** - `field` (query, string, required) - `q` (query, any, optional) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/corpus/values' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/corpus/values", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/corpus/values", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/corpus/values", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/dashboard/bulk-lead-sourcing/corpus/count **Count Corpus Leads** Both counts for a filter, WITHOUT saving anything (§6.5). This is the endpoint behind the live counter, so it is the one that fires most often. Two disciplines make that affordable and they belong on opposite sides of the wire: - the CLIENT debounces ~300 ms and **cancels** the in-flight request rather than queueing it — a user typing "pizzeria" must not leave 8 counts running (§11.4); - the SERVER bounds each one with `SET LOCAL statement_timeout`, so a legal-but- pathological AST that the structural limits let through cannot pin a core. Neither substitutes for the other: cancelling a request does not stop the query it already started, and a timeout does not stop eight of them being started. **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/corpus/count' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "source_kind": "corpus_query", "campaign_id": "string", "job_id": "string", "stages": [ "string" ], "exclude_without_website": true, "filter": {} }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/corpus/count", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"source_kind": "corpus_query", "campaign_id": "string", "job_id": "string", "stages": ["string"], "exclude_without_website": true, "filter": {}}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/corpus/count", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"source_kind": "corpus_query", "campaign_id": "string", "job_id": "string", "stages": ["string"], "exclude_without_website": true, "filter": {}}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"source_kind": "corpus_query", "campaign_id": "string", "job_id": "string", "stages": ["string"], "exclude_without_website": true, "filter": {}}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/corpus/count", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/dashboard/bulk-lead-sourcing/corpus/leads **Browse Corpus Leads** One page of matching leads, keyset-paginated (§11.5). POST rather than GET because the filter is a tree and a tree does not belong in a query string; `cursor`/`sort`/`limit` stay in the query string because they are the page controls, not the question. 🔴 No page NUMBER, and no OFFSET anywhere in the generated SQL. Measured, `LIMIT 50 OFFSET 100000` costs 177 ms and grows with depth; a keyset walk makes page 2,000 cost what page 1 costs. The cursor is opaque, and it is decoded and BOUND, never trusted. **Parameters** - `cursor` (query, any, optional) - `sort` (query, string, optional) - `limit` (query, integer, optional) **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/corpus/leads' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "source_kind": "corpus_query", "campaign_id": "string", "job_id": "string", "stages": [ "string" ], "exclude_without_website": true, "filter": {} }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/corpus/leads", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"source_kind": "corpus_query", "campaign_id": "string", "job_id": "string", "stages": ["string"], "exclude_without_website": true, "filter": {}}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/corpus/leads", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"source_kind": "corpus_query", "campaign_id": "string", "job_id": "string", "stages": ["string"], "exclude_without_website": true, "filter": {}}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"source_kind": "corpus_query", "campaign_id": "string", "job_id": "string", "stages": ["string"], "exclude_without_website": true, "filter": {}}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/corpus/leads", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/dashboard/bulk-lead-sourcing/selections **Create Bulk Corpus Selection** Create a selection over THIS tenant's own lead corpus, and size it. 201 with the selection id and the number of leads that would actually gain something. The submit then references the id alone — the filter is never sent on submit (design §2.2), which is what keeps a predicate that could name another tenant's data off the wire entirely. The count returned is **Gate 2's**, not the filter's: leads that will gain something from the requested stages. It is what the confirmation screen must show, because a run is sized in enrichment volume (`leads x stages`) and the filter's own count would over-state it — §6.5's "select 5,000, enrich 200". The tenant comes from the authenticated session via the canonical dep, and the corpus schema is derived from THAT client_id alone. There is no request field through which a caller can name a schema, a tenant, or a table. **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/selections' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "source_kind": "string", "campaign_id": "string", "job_id": "string", "stages": [ "string" ], "exclude_without_website": true, "filter": {} }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/selections", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"source_kind": "string", "campaign_id": "string", "job_id": "string", "stages": ["string"], "exclude_without_website": true, "filter": {}}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/selections", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"source_kind": "string", "campaign_id": "string", "job_id": "string", "stages": ["string"], "exclude_without_website": true, "filter": {}}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"source_kind": "string", "campaign_id": "string", "job_id": "string", "stages": ["string"], "exclude_without_website": true, "filter": {}}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/dashboard/bulk-lead-sourcing/selections", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `201` — Successful Response - `422` — Validation Error ## Send Deliverability Source: https://spideriq.ai/docs/api-reference/send-deliverability ## `GET` /api/v1/dashboard/send/deliverability/queue **Get Queue** Live queue depth — the sending-health page's "is the pipe draining" read. A tenant with no resolvable ``client_id`` gets the same shape a tenant with an empty queue gets, with ``available`` false: nothing was measured, and the response says so rather than reporting a calm zero. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/send/deliverability/queue' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/send/deliverability/queue", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/send/deliverability/queue", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/send/deliverability/queue", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response ## `GET` /api/v1/dashboard/send/deliverability/summary **Get Summary** Funnel totals, rates, thresholds and feedback-ingest health. A tenant with no resolvable ``client_id`` gets the same zeroed shape a tenant with no events gets — an empty dashboard, never a 403 or a 500. **Parameters** - `range` (query, string, optional) - `source_id` (query, any, optional) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/send/deliverability/summary' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/send/deliverability/summary", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/send/deliverability/summary", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/send/deliverability/summary", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/dashboard/send/deliverability/timeseries **Get Timeseries** Daily bounce & complaint rates, plus the threshold lines to draw them against. **Parameters** - `range` (query, string, optional) - `source_id` (query, any, optional) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/send/deliverability/timeseries' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/send/deliverability/timeseries", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/send/deliverability/timeseries", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/send/deliverability/timeseries", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/dashboard/send/deliverability/undeliverable **Get Undeliverable** Bounces / complaints / failures, newest first, keyset-paginated. **Parameters** - `range` (query, string, optional) - `kind` (query, string, optional) - `limit` (query, integer, optional) - `cursor` (query, any, optional) - `source_id` (query, any, optional) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/send/deliverability/undeliverable' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/send/deliverability/undeliverable", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/send/deliverability/undeliverable", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/send/deliverability/undeliverable", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/dashboard/send/deliverability/sources **Get Sources** Pacing state, sending identity and reputation for the caller's sources. ``range`` windows the reputation rates only — pacing state and caps are always current. It defaults to ``7d`` so every pre-2.U caller (U.2's deliverability rail, U.1's composer source picker) is unchanged. **Parameters** - `range` (query, string, optional) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/send/deliverability/sources' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/send/deliverability/sources", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/send/deliverability/sources", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/send/deliverability/sources", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## Send Broadcasts Source: https://spideriq.ai/docs/api-reference/send-broadcasts ## `GET` /api/v1/dashboard/send/broadcasts/paste-consent **This workspace's consent basis for pasted address lists** Full history, newest first, with the active declaration called out. Revoked declarations are kept and returned: "you have no basis" and "you revoked your basis last Tuesday" are different facts, and only the second one tells an operator what happened to their composer. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/send/broadcasts/paste-consent' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/send/broadcasts/paste-consent", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/send/broadcasts/paste-consent", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/send/broadcasts/paste-consent", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response ## `POST` /api/v1/dashboard/send/broadcasts/paste-consent **Declare (or supersede) the consent basis for pasted address lists** Record what lawful basis this workspace relies on for pasted addresses. Declaring again supersedes: the previous declaration is revoked in the same transaction, so a workspace has at most one active basis and its history stays readable. **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/dashboard/send/broadcasts/paste-consent' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "basis": "express_consent", "source_description": "string", "evidence_url": "string" }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/dashboard/send/broadcasts/paste-consent", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"basis": "express_consent", "source_description": "string", "evidence_url": "string"}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/send/broadcasts/paste-consent", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"basis": "express_consent", "source_description": "string", "evidence_url": "string"}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"basis": "express_consent", "source_description": "string", "evidence_url": "string"}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/dashboard/send/broadcasts/paste-consent", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `201` — Successful Response - `422` — Validation Error ## `DELETE` /api/v1/dashboard/send/broadcasts/paste-consent **Revoke the consent basis for pasted address lists** Withdraw the active declaration. Takes effect at both doors immediately — no new paste broadcast can be composed, and any already-composed paste draft is refused at queue time. Messages already in ``send_queue`` are not retracted; that is the same boundary ``DELETE /{broadcast_id}`` draws. :::codegroup ```bash curl -X DELETE 'https://spideriq.ai/api/v1/dashboard/send/broadcasts/paste-consent' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.delete( "https://spideriq.ai/api/v1/dashboard/send/broadcasts/paste-consent", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/send/broadcasts/paste-consent", { method: "DELETE", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("DELETE", "https://spideriq.ai/api/v1/dashboard/send/broadcasts/paste-consent", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `204` — Successful Response ## `POST` /api/v1/dashboard/send/broadcasts/audience/preview **Resolve an audience query to a deliverable count** Resolve WITHOUT persisting anything. Always reports the subtractions (suppressed / duplicates / invalid) so a shrinking audience reads as the deliverability guard working rather than as missing data. ``suppression_checked=false`` means the count is provisional — the queue path refuses outright in that state. **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/dashboard/send/broadcasts/audience/preview' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "mode": "crm", "board_id": "string", "email_status": "valid", "require_first_name": false, "topics": [ "string" ], "addresses": [ "string" ] }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/dashboard/send/broadcasts/audience/preview", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"mode": "crm", "board_id": "string", "email_status": "valid", "require_first_name": false, "topics": ["string"], "addresses": ["string"]}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/send/broadcasts/audience/preview", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"mode": "crm", "board_id": "string", "email_status": "valid", "require_first_name": false, "topics": ["string"], "addresses": ["string"]}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"mode": "crm", "board_id": "string", "email_status": "valid", "require_first_name": false, "topics": ["string"], "addresses": ["string"]}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/dashboard/send/broadcasts/audience/preview", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/dashboard/send/broadcasts/preview **Render a draft as a recipient will receive it** Server-side on purpose: this uses the SAME markdown renderer as fan-out, so the preview cannot drift from the message that actually goes out. Since CAN-SPAM.2 it also reads the caller's own brand — the tenant scope is load-bearing here, not merely a permission check, because the footer carries that brand's postal address and a preview rendered under someone else's would be showing an address this send will never carry. **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/dashboard/send/broadcasts/preview' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "subject_template": "", "body_md": "", "sample": { "email": "string", "first_name": "string", "full_name": "string", "current_company": "string" } }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/dashboard/send/broadcasts/preview", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"subject_template": "", "body_md": "", "sample": {"email": "string", "first_name": "string", "full_name": "string", "current_company": "string"}}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/send/broadcasts/preview", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"subject_template": "", "body_md": "", "sample": {"email": "string", "first_name": "string", "full_name": "string", "current_company": "string"}}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"subject_template": "", "body_md": "", "sample": {"email": "string", "first_name": "string", "full_name": "string", "current_company": "string"}}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/dashboard/send/broadcasts/preview", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/dashboard/send/broadcasts/capacity **Sending capacity and drain estimate for the selected sources** Combined effective (warmup-aware) daily cap, remaining capacity today, and how long the audience takes to drain at that rate. **Parameters** - `mailbox_ids` (query, any, optional) — Comma-separated mailbox ids. Omitted = every source the tenant owns. - `audience_size` (query, integer, optional) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/send/broadcasts/capacity' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/send/broadcasts/capacity", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/send/broadcasts/capacity", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/send/broadcasts/capacity", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/dashboard/send/broadcasts **List broadcasts** **Parameters** - `limit` (query, integer, optional) - `offset` (query, integer, optional) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/send/broadcasts' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/send/broadcasts", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/send/broadcasts", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/send/broadcasts", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/dashboard/send/broadcasts **Create a broadcast draft** Creates a ``draft``. Nothing is enqueued and no mail can leave until ``POST /{id}/queue`` — mirroring the admin composer's draft→publish split. **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/dashboard/send/broadcasts' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "name": "Untitled broadcast", "subject_template": "", "preheader": "string", "body_md": "", "from_name": "string", "reply_to": "string", "audience": { "mode": "crm", "board_id": "string", "email_status": "valid", "require_first_name": false, "topics": [ "string" ], "addresses": [ "string" ] }, "source_mailbox_ids": [ 0 ], "scheduled_at": "2026-01-01T00:00:00Z" }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/dashboard/send/broadcasts", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"name": "Untitled broadcast", "subject_template": "", "preheader": "string", "body_md": "", "from_name": "string", "reply_to": "string", "audience": {"mode": "crm", "board_id": "string", "email_status": "valid", "require_first_name": false, "topics": ["string"], "addresses": ["string"]}, "source_mailbox_ids": [0], "scheduled_at": "2026-01-01T00:00:00Z"}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/send/broadcasts", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"name": "Untitled broadcast", "subject_template": "", "preheader": "string", "body_md": "", "from_name": "string", "reply_to": "string", "audience": {"mode": "crm", "board_id": "string", "email_status": "valid", "require_first_name": false, "topics": ["string"], "addresses": ["string"]}, "source_mailbox_ids": [0], "scheduled_at": "2026-01-01T00:00:00Z"}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"name": "Untitled broadcast", "subject_template": "", "preheader": "string", "body_md": "", "from_name": "string", "reply_to": "string", "audience": {"mode": "crm", "board_id": "string", "email_status": "valid", "require_first_name": false, "topics": ["string"], "addresses": ["string"]}, "source_mailbox_ids": [0], "scheduled_at": "2026-01-01T00:00:00Z"}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/dashboard/send/broadcasts", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `201` — Successful Response - `422` — Validation Error ## `GET` /api/v1/dashboard/send/broadcasts/{broadcast_id} **Get a broadcast** Includes live delivery progress aggregated from ``send_queue``. **Parameters** - `broadcast_id` (path, string, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/send/broadcasts/{broadcast_id}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/send/broadcasts/{broadcast_id}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/send/broadcasts/{broadcast_id}", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/send/broadcasts/{broadcast_id}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `PATCH` /api/v1/dashboard/send/broadcasts/{broadcast_id} **Update a draft** True PATCH — only the fields present are written. Drafts only: editing a queued broadcast would change what the composer shows without changing a single already-queued message. **Parameters** - `broadcast_id` (path, string, required) **Request body** :::codegroup ```bash curl -X PATCH 'https://spideriq.ai/api/v1/dashboard/send/broadcasts/{broadcast_id}' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "name": "string", "subject_template": "string", "preheader": "string", "body_md": "string", "from_name": "string", "reply_to": "string", "audience": { "mode": "crm", "board_id": "string", "email_status": "valid", "require_first_name": false, "topics": [ "string" ], "addresses": [ "string" ] }, "source_mailbox_ids": [ 0 ], "scheduled_at": "2026-01-01T00:00:00Z" }' ``` ```python import httpx resp = httpx.patch( "https://spideriq.ai/api/v1/dashboard/send/broadcasts/{broadcast_id}", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"name": "string", "subject_template": "string", "preheader": "string", "body_md": "string", "from_name": "string", "reply_to": "string", "audience": {"mode": "crm", "board_id": "string", "email_status": "valid", "require_first_name": false, "topics": ["string"], "addresses": ["string"]}, "source_mailbox_ids": [0], "scheduled_at": "2026-01-01T00:00:00Z"}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/send/broadcasts/{broadcast_id}", { method: "PATCH", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"name": "string", "subject_template": "string", "preheader": "string", "body_md": "string", "from_name": "string", "reply_to": "string", "audience": {"mode": "crm", "board_id": "string", "email_status": "valid", "require_first_name": false, "topics": ["string"], "addresses": ["string"]}, "source_mailbox_ids": [0], "scheduled_at": "2026-01-01T00:00:00Z"}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"name": "string", "subject_template": "string", "preheader": "string", "body_md": "string", "from_name": "string", "reply_to": "string", "audience": {"mode": "crm", "board_id": "string", "email_status": "valid", "require_first_name": false, "topics": ["string"], "addresses": ["string"]}, "source_mailbox_ids": [0], "scheduled_at": "2026-01-01T00:00:00Z"}`) req, _ := http.NewRequest("PATCH", "https://spideriq.ai/api/v1/dashboard/send/broadcasts/{broadcast_id}", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `DELETE` /api/v1/dashboard/send/broadcasts/{broadcast_id} **Cancel a broadcast that has not been fanned out** Already-queued rows are deliberately NOT withdrawn — pulling messages out from under the send loop is a different, riskier operation than abandoning a draft. **Parameters** - `broadcast_id` (path, string, required) :::codegroup ```bash curl -X DELETE 'https://spideriq.ai/api/v1/dashboard/send/broadcasts/{broadcast_id}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.delete( "https://spideriq.ai/api/v1/dashboard/send/broadcasts/{broadcast_id}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/send/broadcasts/{broadcast_id}", { method: "DELETE", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("DELETE", "https://spideriq.ai/api/v1/dashboard/send/broadcasts/{broadcast_id}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `204` — Successful Response - `422` — Validation Error ## `POST` /api/v1/dashboard/send/broadcasts/{broadcast_id}/queue **Fan a broadcast out into the send queue** 202 — the rows are enqueued; the send loop delivers them on its own pace. ``already_enqueued > 0`` is a RESUMED fan-out, not a failure: the enqueue is idempotent per (broadcast, address), so a retry after an interruption fills only the gaps instead of double-sending. ``provider_configured=false`` means the messages are queued and will HOLD at ``queued`` until Mailgun credentials exist. That is a deliberate dormant state, reported honestly rather than surfaced as an error or faked as sent. **Parameters** - `broadcast_id` (path, string, required) :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/dashboard/send/broadcasts/{broadcast_id}/queue' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/dashboard/send/broadcasts/{broadcast_id}/queue", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/send/broadcasts/{broadcast_id}/queue", { method: "POST", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/dashboard/send/broadcasts/{broadcast_id}/queue", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `202` — Successful Response - `422` — Validation Error ## Send Campaigns Source: https://spideriq.ai/docs/api-reference/send-campaigns ## `GET` /api/v1/dashboard/send/campaigns **List this workspace's campaigns** **Parameters** - `limit` (query, integer, optional) - `offset` (query, integer, optional) - `status` (query, any, optional) — Filter to one lifecycle state. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/send/campaigns' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/send/campaigns", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/send/campaigns", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/send/campaigns", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/dashboard/send/campaigns **Create a campaign DRAFT** A draft sends nothing, and nothing about creating one arms it. A campaign only becomes eligible to mail anyone at ``POST /activate``, which a PAT cannot call. **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/dashboard/send/campaigns' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "name": "string", "folder": "string", "brand_id": 0, "timezone": "UTC", "active_days": [ 0 ], "starts_on": "2026-01-01", "new_leads_per_day": 0, "stop_condition": "reply", "variant_strategy": "round_robin", "company_pause": false, "track_opens": false, "track_clicks": true, "plain_text": true, "include_unsubscribe": true, "include_postal_address": true }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/dashboard/send/campaigns", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"name": "string", "folder": "string", "brand_id": 0, "timezone": "UTC", "active_days": [0], "starts_on": "2026-01-01", "new_leads_per_day": 0, "stop_condition": "reply", "variant_strategy": "round_robin", "company_pause": false, "track_opens": false, "track_clicks": true, "plain_text": true, "include_unsubscribe": true, "include_postal_address": true}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/send/campaigns", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"name": "string", "folder": "string", "brand_id": 0, "timezone": "UTC", "active_days": [0], "starts_on": "2026-01-01", "new_leads_per_day": 0, "stop_condition": "reply", "variant_strategy": "round_robin", "company_pause": false, "track_opens": false, "track_clicks": true, "plain_text": true, "include_unsubscribe": true, "include_postal_address": true}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"name": "string", "folder": "string", "brand_id": 0, "timezone": "UTC", "active_days": [0], "starts_on": "2026-01-01", "new_leads_per_day": 0, "stop_condition": "reply", "variant_strategy": "round_robin", "company_pause": false, "track_opens": false, "track_clicks": true, "plain_text": true, "include_unsubscribe": true, "include_postal_address": true}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/dashboard/send/campaigns", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `201` — Successful Response - `422` — Validation Error ## `GET` /api/v1/dashboard/send/campaigns/{campaign_id} **One campaign in full** **Parameters** - `campaign_id` (path, string, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/send/campaigns/{campaign_id}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/send/campaigns/{campaign_id}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/send/campaigns/{campaign_id}", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/send/campaigns/{campaign_id}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `PATCH` /api/v1/dashboard/send/campaigns/{campaign_id} **Patch a campaign's settings** True PATCH of the SHELL — schedule, stop condition, compliance toggles. Allowed while active (scheduling changes do not re-aim a cursor); the sequence itself is not. ``status`` is deliberately not a patchable field: a lifecycle change is an act with preconditions, and admitting it here would let a PATCH route around the cookie-only arming gate. **Parameters** - `campaign_id` (path, string, required) **Request body** :::codegroup ```bash curl -X PATCH 'https://spideriq.ai/api/v1/dashboard/send/campaigns/{campaign_id}' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "name": "string", "folder": "string", "timezone": "string", "active_days": [ 0 ], "starts_on": "2026-01-01", "new_leads_per_day": 0, "stop_condition": "reply", "variant_strategy": "round_robin", "company_pause": true, "track_opens": true, "track_clicks": true, "plain_text": true, "include_unsubscribe": true, "include_postal_address": true }' ``` ```python import httpx resp = httpx.patch( "https://spideriq.ai/api/v1/dashboard/send/campaigns/{campaign_id}", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"name": "string", "folder": "string", "timezone": "string", "active_days": [0], "starts_on": "2026-01-01", "new_leads_per_day": 0, "stop_condition": "reply", "variant_strategy": "round_robin", "company_pause": true, "track_opens": true, "track_clicks": true, "plain_text": true, "include_unsubscribe": true, "include_postal_address": true}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/send/campaigns/{campaign_id}", { method: "PATCH", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"name": "string", "folder": "string", "timezone": "string", "active_days": [0], "starts_on": "2026-01-01", "new_leads_per_day": 0, "stop_condition": "reply", "variant_strategy": "round_robin", "company_pause": true, "track_opens": true, "track_clicks": true, "plain_text": true, "include_unsubscribe": true, "include_postal_address": true}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"name": "string", "folder": "string", "timezone": "string", "active_days": [0], "starts_on": "2026-01-01", "new_leads_per_day": 0, "stop_condition": "reply", "variant_strategy": "round_robin", "company_pause": true, "track_opens": true, "track_clicks": true, "plain_text": true, "include_unsubscribe": true, "include_postal_address": true}`) req, _ := http.NewRequest("PATCH", "https://spideriq.ai/api/v1/dashboard/send/campaigns/{campaign_id}", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `DELETE` /api/v1/dashboard/send/campaigns/{campaign_id} **Delete a DRAFT campaign** **Parameters** - `campaign_id` (path, string, required) :::codegroup ```bash curl -X DELETE 'https://spideriq.ai/api/v1/dashboard/send/campaigns/{campaign_id}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.delete( "https://spideriq.ai/api/v1/dashboard/send/campaigns/{campaign_id}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/send/campaigns/{campaign_id}", { method: "DELETE", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("DELETE", "https://spideriq.ai/api/v1/dashboard/send/campaigns/{campaign_id}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `204` — Successful Response - `422` — Validation Error ## `GET` /api/v1/dashboard/send/campaigns/{campaign_id}/analytics **Everything the analytics screen renders, in one read** The read half of the campaign surface (card 4.B1). ONE endpoint rather than six because every number on the page is a slice of the same instant: a strip reading "5 sources sending" beside a chart built from a different second is a screen that contradicts itself while both halves are correct. 🔴 Two figures come back deliberately ABSENT rather than estimated, and a future change must not "fill them in": - ``live.capacity`` is INDICATIVE (``indicative`` is hard-``True``). ``send_claim_source()`` keeps one ``sent_today`` counter per mailbox and claims first-come-first-served — there is no per-campaign share to report. Planner ruling, BSV-131 2026-08-18: label it, do not grow a bound in the governor's hot path. - ``forecast.predicted`` / ``shortfall`` / ``attribution`` are ``None`` / empty. Reconstructing a past day's intended volume needs that day's effective cap and nothing records one; ``send_source_capacity`` is today-only and is overwritten at each day roll. Declared BEFORE ``PATCH /{campaign_id}`` in this file purely for reading order — the path is unambiguous either way, since ``analytics`` is a literal segment under a UUID param and cannot be read as a campaign id. **Parameters** - `campaign_id` (path, string, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/send/campaigns/{campaign_id}/analytics' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/send/campaigns/{campaign_id}/analytics", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/send/campaigns/{campaign_id}/analytics", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/send/campaigns/{campaign_id}/analytics", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/dashboard/send/campaigns/{campaign_id}/steps **Add a step to the sequence** ``wait_days`` is the gap BEFORE this step, so step 0 is normally 0. Returns the whole campaign, not just the step: the cumulative day offsets of every LATER step change when one is inserted, and returning only the new row would leave a caller holding a schedule that is quietly wrong. **Parameters** - `campaign_id` (path, string, required) **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/dashboard/send/campaigns/{campaign_id}/steps' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "step_index": 0, "kind": "email", "wait_days": 0, "variant": { "label": "A", "subject_template": "", "body_md": "", "weight": 100 } }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/dashboard/send/campaigns/{campaign_id}/steps", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"step_index": 0, "kind": "email", "wait_days": 0, "variant": {"label": "A", "subject_template": "", "body_md": "", "weight": 100}}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/send/campaigns/{campaign_id}/steps", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"step_index": 0, "kind": "email", "wait_days": 0, "variant": {"label": "A", "subject_template": "", "body_md": "", "weight": 100}}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"step_index": 0, "kind": "email", "wait_days": 0, "variant": {"label": "A", "subject_template": "", "body_md": "", "weight": 100}}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/dashboard/send/campaigns/{campaign_id}/steps", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `201` — Successful Response - `422` — Validation Error ## `PATCH` /api/v1/dashboard/send/campaigns/{campaign_id}/steps/{step_index} **Patch a step's wait or channel** **Parameters** - `campaign_id` (path, string, required) - `step_index` (path, integer, required) **Request body** :::codegroup ```bash curl -X PATCH 'https://spideriq.ai/api/v1/dashboard/send/campaigns/{campaign_id}/steps/{step_index}' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "kind": "email", "wait_days": 0 }' ``` ```python import httpx resp = httpx.patch( "https://spideriq.ai/api/v1/dashboard/send/campaigns/{campaign_id}/steps/{step_index}", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"kind": "email", "wait_days": 0}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/send/campaigns/{campaign_id}/steps/{step_index}", { method: "PATCH", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"kind": "email", "wait_days": 0}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"kind": "email", "wait_days": 0}`) req, _ := http.NewRequest("PATCH", "https://spideriq.ai/api/v1/dashboard/send/campaigns/{campaign_id}/steps/{step_index}", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `DELETE` /api/v1/dashboard/send/campaigns/{campaign_id}/steps/{step_index} **Delete a step and its variants** Later steps keep their indices — the gap is deliberate, see the service. **Parameters** - `campaign_id` (path, string, required) - `step_index` (path, integer, required) :::codegroup ```bash curl -X DELETE 'https://spideriq.ai/api/v1/dashboard/send/campaigns/{campaign_id}/steps/{step_index}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.delete( "https://spideriq.ai/api/v1/dashboard/send/campaigns/{campaign_id}/steps/{step_index}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/send/campaigns/{campaign_id}/steps/{step_index}", { method: "DELETE", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("DELETE", "https://spideriq.ai/api/v1/dashboard/send/campaigns/{campaign_id}/steps/{step_index}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/dashboard/send/campaigns/{campaign_id}/steps/{step_index}/variants **Add an A/B arm to a step** 🔴 ``subject_template=""`` MEANS CONTINUE THE THREAD — it is not blank. An empty subject omits the Subject header and replies into the lead's existing thread (design §2.4). Do not default one in on a follow-up step. **Parameters** - `campaign_id` (path, string, required) - `step_index` (path, integer, required) **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/dashboard/send/campaigns/{campaign_id}/steps/{step_index}/variants' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "label": "A", "subject_template": "", "body_md": "", "weight": 100 }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/dashboard/send/campaigns/{campaign_id}/steps/{step_index}/variants", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"label": "A", "subject_template": "", "body_md": "", "weight": 100}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/send/campaigns/{campaign_id}/steps/{step_index}/variants", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"label": "A", "subject_template": "", "body_md": "", "weight": 100}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"label": "A", "subject_template": "", "body_md": "", "weight": 100}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/dashboard/send/campaigns/{campaign_id}/steps/{step_index}/variants", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `201` — Successful Response - `422` — Validation Error ## `PATCH` /api/v1/dashboard/send/campaigns/{campaign_id}/steps/{step_index}/variants/{label} **Patch one arm's copy** **Parameters** - `campaign_id` (path, string, required) - `step_index` (path, integer, required) - `label` (path, string, required) **Request body** :::codegroup ```bash curl -X PATCH 'https://spideriq.ai/api/v1/dashboard/send/campaigns/{campaign_id}/steps/{step_index}/variants/{label}' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "subject_template": "string", "body_md": "string", "weight": 0 }' ``` ```python import httpx resp = httpx.patch( "https://spideriq.ai/api/v1/dashboard/send/campaigns/{campaign_id}/steps/{step_index}/variants/{label}", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"subject_template": "string", "body_md": "string", "weight": 0}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/send/campaigns/{campaign_id}/steps/{step_index}/variants/{label}", { method: "PATCH", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"subject_template": "string", "body_md": "string", "weight": 0}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"subject_template": "string", "body_md": "string", "weight": 0}`) req, _ := http.NewRequest("PATCH", "https://spideriq.ai/api/v1/dashboard/send/campaigns/{campaign_id}/steps/{step_index}/variants/{label}", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `DELETE` /api/v1/dashboard/send/campaigns/{campaign_id}/steps/{step_index}/variants/{label} **Delete an arm (never the last one)** **Parameters** - `campaign_id` (path, string, required) - `step_index` (path, integer, required) - `label` (path, string, required) :::codegroup ```bash curl -X DELETE 'https://spideriq.ai/api/v1/dashboard/send/campaigns/{campaign_id}/steps/{step_index}/variants/{label}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.delete( "https://spideriq.ai/api/v1/dashboard/send/campaigns/{campaign_id}/steps/{step_index}/variants/{label}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/send/campaigns/{campaign_id}/steps/{step_index}/variants/{label}", { method: "DELETE", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("DELETE", "https://spideriq.ai/api/v1/dashboard/send/campaigns/{campaign_id}/steps/{step_index}/variants/{label}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/dashboard/send/campaigns/{campaign_id}/sources **Attach a sending mailbox to this campaign** Attaching does not send. It says which mailboxes MAY carry this campaign. Allowed while active on purpose — adding capacity to a running sequence is the safe direction, and forcing a pause to add a mailbox is how an operator ends up over-sending on the one source they already had. **Parameters** - `campaign_id` (path, string, required) **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/dashboard/send/campaigns/{campaign_id}/sources' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "mailbox_id": 0 }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/dashboard/send/campaigns/{campaign_id}/sources", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"mailbox_id": 0}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/send/campaigns/{campaign_id}/sources", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"mailbox_id": 0}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"mailbox_id": 0}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/dashboard/send/campaigns/{campaign_id}/sources", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `201` — Successful Response - `422` — Validation Error ## `DELETE` /api/v1/dashboard/send/campaigns/{campaign_id}/sources/{mailbox_id} **Detach a sending mailbox** **Parameters** - `campaign_id` (path, string, required) - `mailbox_id` (path, integer, required) :::codegroup ```bash curl -X DELETE 'https://spideriq.ai/api/v1/dashboard/send/campaigns/{campaign_id}/sources/{mailbox_id}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.delete( "https://spideriq.ai/api/v1/dashboard/send/campaigns/{campaign_id}/sources/{mailbox_id}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/send/campaigns/{campaign_id}/sources/{mailbox_id}", { method: "DELETE", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("DELETE", "https://spideriq.ai/api/v1/dashboard/send/campaigns/{campaign_id}/sources/{mailbox_id}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/dashboard/send/campaigns/{campaign_id}/branches **Author a branch (SubSequence) on this campaign** A branch watches the sequence and re-aims or ends ONE lead's run of it. 🔴 ``match`` is a validated AST, never a predicate string — the grammar is ``services.send.branch_ast`` and a malformed one is refused at 422 rather than stored. ``{}`` is refused too: it is the column default and means *matches nothing*, so a branch authored that way would be permanently inert with nothing to see. Write ``{"op": "always"}`` to fire on every occurrence. Allowed while the campaign is ACTIVE. A branch re-aims no existing cursor — it states what happens next — so unlike a step edit it does not need the campaign paused. **Parameters** - `campaign_id` (path, string, required) **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/dashboard/send/campaigns/{campaign_id}/branches' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "trigger": "classification", "match": {}, "action": "stop", "action_arg": {}, "enabled": true }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/dashboard/send/campaigns/{campaign_id}/branches", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"trigger": "classification", "match": {}, "action": "stop", "action_arg": {}, "enabled": true}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/send/campaigns/{campaign_id}/branches", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"trigger": "classification", "match": {}, "action": "stop", "action_arg": {}, "enabled": true}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"trigger": "classification", "match": {}, "action": "stop", "action_arg": {}, "enabled": true}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/dashboard/send/campaigns/{campaign_id}/branches", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `201` — Successful Response - `422` — Validation Error ## `PATCH` /api/v1/dashboard/send/campaigns/{campaign_id}/branches/{branch_id} **Patch a branch, or disable it** ``trigger``+``match`` move together, and so do ``action``+``action_arg``. A match names fields belonging to ONE trigger's grammar, so patching either half alone would store a match its trigger cannot read — accepted, stored, and dead. The schema refuses the half-pair; repeat the unchanged one. ``enabled=false`` keeps the rule and its reasoning visible while stopping it firing, which is the state the design surfaces on purpose. It is not the same as deleting. **Parameters** - `campaign_id` (path, string, required) - `branch_id` (path, integer, required) **Request body** :::codegroup ```bash curl -X PATCH 'https://spideriq.ai/api/v1/dashboard/send/campaigns/{campaign_id}/branches/{branch_id}' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "trigger": "classification", "match": {}, "action": "stop", "action_arg": {}, "enabled": true }' ``` ```python import httpx resp = httpx.patch( "https://spideriq.ai/api/v1/dashboard/send/campaigns/{campaign_id}/branches/{branch_id}", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"trigger": "classification", "match": {}, "action": "stop", "action_arg": {}, "enabled": true}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/send/campaigns/{campaign_id}/branches/{branch_id}", { method: "PATCH", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"trigger": "classification", "match": {}, "action": "stop", "action_arg": {}, "enabled": true}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"trigger": "classification", "match": {}, "action": "stop", "action_arg": {}, "enabled": true}`) req, _ := http.NewRequest("PATCH", "https://spideriq.ai/api/v1/dashboard/send/campaigns/{campaign_id}/branches/{branch_id}", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `DELETE` /api/v1/dashboard/send/campaigns/{campaign_id}/branches/{branch_id} **Delete a branch** **Parameters** - `campaign_id` (path, string, required) - `branch_id` (path, integer, required) :::codegroup ```bash curl -X DELETE 'https://spideriq.ai/api/v1/dashboard/send/campaigns/{campaign_id}/branches/{branch_id}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.delete( "https://spideriq.ai/api/v1/dashboard/send/campaigns/{campaign_id}/branches/{branch_id}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/send/campaigns/{campaign_id}/branches/{branch_id}", { method: "DELETE", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("DELETE", "https://spideriq.ai/api/v1/dashboard/send/campaigns/{campaign_id}/branches/{branch_id}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/dashboard/send/campaigns/{campaign_id}/leads **Who is enrolled, where each one is, and why they are or are not moving** ``member``, like every other read on this router — it reports, it never acts. ``metrics_available: false`` on the response means the suppression and outcome reads did NOT run. Treat a lead's `blocked` state as unknown in that case rather than as "not suppressed": the two are indistinguishable in the payload and only one of them is safe to act on. **Parameters** - `campaign_id` (path, string, required) - `limit` (query, integer, optional) - `offset` (query, integer, optional) - `filter` (query, string, optional) — A VIEW, not a stored state: 'overdue' is the subset of in-sequence leads whose next step is past due, and 'replied' is the subset that left the sequence on an inbound. - `q` (query, any, optional) — Substring match on the address. The send cluster stores no name. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/send/campaigns/{campaign_id}/leads' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/send/campaigns/{campaign_id}/leads", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/send/campaigns/{campaign_id}/leads", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/send/campaigns/{campaign_id}/leads", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/dashboard/send/campaigns/{campaign_id}/steps/{step_index}/preview **Render one step as a recipient will receive it** Server-side, through the SAME renderer the send path uses. Two things an author can see nowhere else: the D-J compliance footer with this workspace's real postal address, and ``unresolved_merge_tags`` — the tags that will render EMPTY on a live send. That second one exists because the failure is silent: the placeholder is emitted, the provider accepts the message, and the only symptom is a greeting addressed to nobody. ``continues_thread: true`` with ``subject: null`` is the deliberate state of a follow-up step, not a missing subject. **Parameters** - `campaign_id` (path, string, required) - `step_index` (path, integer, required) **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/dashboard/send/campaigns/{campaign_id}/steps/{step_index}/preview' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "variant_label": "A", "sample": { "email": "string", "first_name": "string", "full_name": "string", "current_company": "string" } }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/dashboard/send/campaigns/{campaign_id}/steps/{step_index}/preview", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"variant_label": "A", "sample": {"email": "string", "first_name": "string", "full_name": "string", "current_company": "string"}}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/send/campaigns/{campaign_id}/steps/{step_index}/preview", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"variant_label": "A", "sample": {"email": "string", "first_name": "string", "full_name": "string", "current_company": "string"}}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"variant_label": "A", "sample": {"email": "string", "first_name": "string", "full_name": "string", "current_company": "string"}}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/dashboard/send/campaigns/{campaign_id}/steps/{step_index}/preview", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/dashboard/send/campaigns/{campaign_id}/activate **ARM this campaign — cookie-only, a human act** 🔴 THIS IS THE ONE ROUTE IN THE FILE THAT A PAT CANNOT CALL. Arming turns authored copy into mail sent to real people, unattended, for as long as leads keep enrolling. That is a decision a human takes in a session, not something a token does on their behalf — the same argument the paste-consent writes make, and the reason no ``send_activate_campaign`` MCP tool exists. An agent that has finished composing reports what is left to a human; the four preconditions below are refused by name so it can say exactly what. **Parameters** - `campaign_id` (path, string, required) :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/dashboard/send/campaigns/{campaign_id}/activate' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/dashboard/send/campaigns/{campaign_id}/activate", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/send/campaigns/{campaign_id}/activate", { method: "POST", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/dashboard/send/campaigns/{campaign_id}/activate", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/dashboard/send/campaigns/{campaign_id}/pause **Pause enrolment and advancement (reversible)** Deliberately PAT-reachable, unlike activate. An agent may always stop mail and may never start it. A safety verb an agent cannot reach is a safety verb nobody uses at 3am. **Parameters** - `campaign_id` (path, string, required) :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/dashboard/send/campaigns/{campaign_id}/pause' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/dashboard/send/campaigns/{campaign_id}/pause", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/send/campaigns/{campaign_id}/pause", { method: "POST", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/dashboard/send/campaigns/{campaign_id}/pause", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/dashboard/send/campaigns/{campaign_id}/stop **Stop this campaign permanently** Terminal, and PAT-reachable for the same reason pause is. **Parameters** - `campaign_id` (path, string, required) :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/dashboard/send/campaigns/{campaign_id}/stop' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/dashboard/send/campaigns/{campaign_id}/stop", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/send/campaigns/{campaign_id}/stop", { method: "POST", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/dashboard/send/campaigns/{campaign_id}/stop", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## Send BYO Domains Source: https://spideriq.ai/docs/api-reference/send-byo-domains ## `POST` /api/v1/dashboard/send/byo/check **Dry-run the DNS checks for a sending domain (stores nothing)** Verify SPF / DKIM / tracking-CNAME without storing or binding anything. Deliberately requires no API key: a tenant publishes records, checks, fixes and checks again, and none of that iteration should be gated on handing us a credential first. ⚠️ A ``null`` verdict on any check means UNDETERMINABLE, not "fine" — see ``RecordCheckOut.ok``. ``verified`` is true only when every check is true. **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/dashboard/send/byo/check' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "domain": "string", "provider": "mailgun", "dkim_selector": "mailo", "tracking_domain": "string" }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/dashboard/send/byo/check", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"domain": "string", "provider": "mailgun", "dkim_selector": "mailo", "tracking_domain": "string"}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/send/byo/check", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"domain": "string", "provider": "mailgun", "dkim_selector": "mailo", "tracking_domain": "string"}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"domain": "string", "provider": "mailgun", "dkim_selector": "mailo", "tracking_domain": "string"}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/dashboard/send/byo/check", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/dashboard/send/byo/enroll **Verify a domain, store the tenant's credential, and bind their source** The whole onboarding step, in the one order that is safe. Nothing is written unless the DNS verifies. Then the credential is stored BEFORE the source is bound — a source bound to a tenant with no stored credential resolves as "no binding", whose one documented fallback is the PLATFORM adapter, which would put this tenant's mail on the platform's key and domain. See the service module's order note. 201 rather than 200: this creates a tenant-scoped sending identity. **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/dashboard/send/byo/enroll' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "mailbox_id": 0, "domain": "string", "api_key": "string", "provider": "mailgun", "dkim_selector": "mailo", "tracking_domain": "string", "enable_tracking": true, "api_base": "", "brand_id": 0 }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/dashboard/send/byo/enroll", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"mailbox_id": 0, "domain": "string", "api_key": "string", "provider": "mailgun", "dkim_selector": "mailo", "tracking_domain": "string", "enable_tracking": true, "api_base": "", "brand_id": 0}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/send/byo/enroll", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"mailbox_id": 0, "domain": "string", "api_key": "string", "provider": "mailgun", "dkim_selector": "mailo", "tracking_domain": "string", "enable_tracking": true, "api_base": "", "brand_id": 0}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"mailbox_id": 0, "domain": "string", "api_key": "string", "provider": "mailgun", "dkim_selector": "mailo", "tracking_domain": "string", "enable_tracking": true, "api_base": "", "brand_id": 0}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/dashboard/send/byo/enroll", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `201` — Successful Response - `422` — Validation Error ## Send Sources Source: https://spideriq.ai/docs/api-reference/send-sources ## `POST` /api/v1/dashboard/send/sources/{mailbox_id}/promote **Graduate one of your warmed sending sources to 'active'** Clear a warmed source to send. Until a source reaches ``'active'`` it is never claimed and every broadcast against it returns ``no_sources`` — this is the transition that ends that. ⚠️ **It does not skip the warm-up.** A graduated source starts at DAY 1 of its ramp (8/day on the default curve), not at its steady ``daily_cap``, and it cannot send for ``min_gap_seconds`` after this call. Read ``effective_daily_cap`` in the response, not ``daily_cap``. ⚠️ **It refuses a source that has not soaked long enough**, with a 409 naming the days served and the days required. That refusal is the feature: a source promoted on day 0 is the cold-start reputation burn the warm-up tier exists to prevent. There is no tenant-side override — an administrator has one. **Parameters** - `mailbox_id` (path, integer, required) — The sending source to graduate. Must already be in 'warming'. :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/dashboard/send/sources/{mailbox_id}/promote' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/dashboard/send/sources/{mailbox_id}/promote", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/send/sources/{mailbox_id}/promote", { method: "POST", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/dashboard/send/sources/{mailbox_id}/promote", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## Send Sources (Admin) Source: https://spideriq.ai/docs/api-reference/send-sources-admin ## `POST` /api/v1/admin/send/sources/{mailbox_id}/promote **Promote any sending source, optionally waiving the reputation gate** The operator door — the whole pool, and the D7 override. Gate: ``require_super_admin_or_admin_key`` — an ``X-Admin-Key`` or a ``super_admin`` session. Deliberately NOT reachable with an ordinary tenant PAT: this is the only place the reputation precondition can be waived. ⚠️ **`**`**override_reputation_gate=true**`**` arms a source for real sending with no reputation brake behind it.** The ``ReputationBreaker`` is shipped and armed but has zero input until an owner provisions Google Postmaster / Microsoft SNDS credentials, so a source promoted under the override sends with nothing watching its complaint rate. That is a deliberate, attributed human decision — the Phase-1 tiny-smoke precedent — and it is logged as one. Pass ``reason``. Every other refusal still applies: the override waives D7 and D7 only. An immature source, an expired one and a non-``'warming'`` one are refused here exactly as they are on the tenant route. **Parameters** - `mailbox_id` (path, integer, required) — The sending source to graduate. Must already be in 'warming'. **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/admin/send/sources/{mailbox_id}/promote' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "override_reputation_gate": false, "reason": "string" }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/admin/send/sources/{mailbox_id}/promote", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"override_reputation_gate": false, "reason": "string"}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/admin/send/sources/{mailbox_id}/promote", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"override_reputation_gate": false, "reason": "string"}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"override_reputation_gate": false, "reason": "string"}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/admin/send/sources/{mailbox_id}/promote", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## Leads Source: https://spideriq.ai/docs/api-reference/leads ## `GET` /api/v1/leads **List Leads** List leads with pagination, search, and filters. **Parameters** - `page` (query, integer, optional) - `page_size` (query, integer, optional) - `search` (query, any, optional) - `source` (query, any, optional) - `has_email` (query, any, optional) - `has_verified_email` (query, any, optional) - `country_code` (query, any, optional) - `workflow_stage` (query, any, optional) - `sort_by` (query, string, optional) - `sort_dir` (query, string, optional) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/leads' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/leads", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/leads", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/leads", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/leads/stats **Lead Stats** KPI counts. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/leads/stats' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/leads/stats", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/leads/stats", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/leads/stats", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response ## `GET` /api/v1/leads/export **Export Leads** Export leads as CSV or JSON. **Parameters** - `format` (query, any, optional) — `csv` (default) returns a CSV attachment; `json` and `llm` return the JSON body. `llm` is a member because `middleware/format_llm.py` tells every Bearer agent to send it — before `SDS-33` it 422'd here. - `source` (query, any, optional) - `country_code` (query, any, optional) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/leads/export' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/leads/export", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/leads/export", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/leads/export", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/leads/{lead_id} **Get Lead** Full lead detail. **Parameters** - `lead_id` (path, integer, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/leads/{lead_id}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/leads/{lead_id}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/leads/{lead_id}", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/leads/{lead_id}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## FuzzIQ Deduplication Source: https://spideriq.ai/docs/api-reference/fuzziq-deduplication ## `POST` /api/v1/fuzziq/check **Check single record for duplicates** Check if a single record is a duplicate. Optionally add to canonical if unique. **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/fuzziq/check' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "record": { "city": "Chicago", "company_name": "McDonald's", "country": "United States", "google_place_id": "ChIJN1t_tDeuEmsRUsoyG83frY4", "phone": "+1-555-123-4567" }, "record_type": "business", "add_to_canonical": true, "campaign_id": "string", "threshold": 0.5 }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/fuzziq/check", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"record": {"city": "Chicago", "company_name": "McDonald's", "country": "United States", "google_place_id": "ChIJN1t_tDeuEmsRUsoyG83frY4", "phone": "+1-555-123-4567"}, "record_type": "business", "add_to_canonical": true, "campaign_id": "string", "threshold": 0.5}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/fuzziq/check", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"record": {"city": "Chicago", "company_name": "McDonald's", "country": "United States", "google_place_id": "ChIJN1t_tDeuEmsRUsoyG83frY4", "phone": "+1-555-123-4567"}, "record_type": "business", "add_to_canonical": true, "campaign_id": "string", "threshold": 0.5}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"record": {"city": "Chicago", "company_name": "McDonald's", "country": "United States", "google_place_id": "ChIJN1t_tDeuEmsRUsoyG83frY4", "phone": "+1-555-123-4567"}, "record_type": "business", "add_to_canonical": true, "campaign_id": "string", "threshold": 0.5}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/fuzziq/check", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/fuzziq/check-batch **Check batch of records for duplicates** Check multiple records (up to 100) for duplicates in a single request. **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/fuzziq/check-batch' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "add_to_canonical": true, "record_type": "business", "records": [ { "company_name": "McDonald's Paris", "google_place_id": "ChIJ123456789", "phone": "+33-1-23-45-67-89" }, { "company_name": "McDonald's Lyon", "google_place_id": "ChIJ987654321", "phone": "+33-4-56-78-90-12" } ], "source_worker": "spiderMaps" }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/fuzziq/check-batch", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"add_to_canonical": true, "record_type": "business", "records": [{"company_name": "McDonald's Paris", "google_place_id": "ChIJ123456789", "phone": "+33-1-23-45-67-89"}, {"company_name": "McDonald's Lyon", "google_place_id": "ChIJ987654321", "phone": "+33-4-56-78-90-12"}], "source_worker": "spiderMaps"}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/fuzziq/check-batch", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"add_to_canonical": true, "record_type": "business", "records": [{"company_name": "McDonald's Paris", "google_place_id": "ChIJ123456789", "phone": "+33-1-23-45-67-89"}, {"company_name": "McDonald's Lyon", "google_place_id": "ChIJ987654321", "phone": "+33-4-56-78-90-12"}], "source_worker": "spiderMaps"}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"add_to_canonical": true, "record_type": "business", "records": [{"company_name": "McDonald's Paris", "google_place_id": "ChIJ123456789", "phone": "+33-1-23-45-67-89"}, {"company_name": "McDonald's Lyon", "google_place_id": "ChIJ987654321", "phone": "+33-4-56-78-90-12"}], "source_worker": "spiderMaps"}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/fuzziq/check-batch", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/fuzziq/canonical **List canonical records** List canonical records for the authenticated client with optional filtering. **Parameters** - `record_type` (query, any, optional) - `campaign_id` (query, any, optional) - `limit` (query, integer, optional) - `offset` (query, integer, optional) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/fuzziq/canonical' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/fuzziq/canonical", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/fuzziq/canonical", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/fuzziq/canonical", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/fuzziq/canonical **Add record to canonical** Manually add a record to the canonical database. **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/fuzziq/canonical' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "record": { "city": "Chicago", "company_name": "McDonald's", "country": "United States", "google_place_id": "ChIJN1t_tDeuEmsRUsoyG83frY4", "phone": "+1-555-123-4567" }, "record_type": "business", "source": "manual" }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/fuzziq/canonical", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"record": {"city": "Chicago", "company_name": "McDonald's", "country": "United States", "google_place_id": "ChIJN1t_tDeuEmsRUsoyG83frY4", "phone": "+1-555-123-4567"}, "record_type": "business", "source": "manual"}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/fuzziq/canonical", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"record": {"city": "Chicago", "company_name": "McDonald's", "country": "United States", "google_place_id": "ChIJN1t_tDeuEmsRUsoyG83frY4", "phone": "+1-555-123-4567"}, "record_type": "business", "source": "manual"}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"record": {"city": "Chicago", "company_name": "McDonald's", "country": "United States", "google_place_id": "ChIJN1t_tDeuEmsRUsoyG83frY4", "phone": "+1-555-123-4567"}, "record_type": "business", "source": "manual"}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/fuzziq/canonical", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `201` — Successful Response - `422` — Validation Error ## `POST` /api/v1/fuzziq/canonical/import **Bulk import records** Import multiple records to the canonical database (up to 1000). **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/fuzziq/canonical/import' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "records": [ { "city": "Chicago", "company_name": "McDonald's", "country": "United States", "google_place_id": "ChIJN1t_tDeuEmsRUsoyG83frY4", "phone": "+1-555-123-4567" } ], "record_type": "business", "skip_duplicates": true }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/fuzziq/canonical/import", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"records": [{"city": "Chicago", "company_name": "McDonald's", "country": "United States", "google_place_id": "ChIJN1t_tDeuEmsRUsoyG83frY4", "phone": "+1-555-123-4567"}], "record_type": "business", "skip_duplicates": true}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/fuzziq/canonical/import", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"records": [{"city": "Chicago", "company_name": "McDonald's", "country": "United States", "google_place_id": "ChIJN1t_tDeuEmsRUsoyG83frY4", "phone": "+1-555-123-4567"}], "record_type": "business", "skip_duplicates": true}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"records": [{"city": "Chicago", "company_name": "McDonald's", "country": "United States", "google_place_id": "ChIJN1t_tDeuEmsRUsoyG83frY4", "phone": "+1-555-123-4567"}], "record_type": "business", "skip_duplicates": true}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/fuzziq/canonical/import", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/fuzziq/canonical/stats **Get FuzzIQ statistics** Get deduplication statistics for the authenticated client. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/fuzziq/canonical/stats' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/fuzziq/canonical/stats", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/fuzziq/canonical/stats", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/fuzziq/canonical/stats", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response ## Events Source: https://spideriq.ai/docs/api-reference/events ## `GET` /api/v1/events/stream **Stream Events** Server-Sent Events stream for real-time job monitoring. Connect to this endpoint to receive live job events for your client account. Events are streamed as they happen - no polling required. **Authentication:** Pass your credentials as a query parameter: ``` GET /api/v1/events/stream?token=client_id:api_key:api_secret ``` **Optional server-side filtering:** Append `?campaign_id=` or `?job_id=` to receive only the matching events (plus the `connected` handshake and `heartbeat` keep-alives). With no filter you get the full client-scoped firehose (unchanged, default). Filters are a union — supplying both shows events matching either id. ``` GET /api/v1/events/stream?token=...&campaign_id=camp_abc GET /api/v1/events/stream?token=...&job_id=550e8400-... ``` **Event Types:** - `connected`: Sent when connection is established - `job.queued`: Job submitted and queued for processing - `job.started`: Worker picked up the job - `job.completed`: Job finished successfully - `job.failed`: Job encountered an error - `campaign.terminal`: A campaign reached a terminal state (the definitive "done" signal — carries the chosen status + success_pct) - `heartbeat`: Keep-alive signal (every 30 seconds) **Event Format (SSE):** ``` event: job.completed data: {"job_id": "abc-123", "processing_time": 45.2, "results_count": 15} ``` **JavaScript Example:** ```javascript const token = 'cli_xxx:sk_xxx:secret_xxx'; const eventSource = new EventSource( `https://spideriq.ai/api/v1/events/stream?token=${token}` ); eventSource.addEventListener('job.completed', (e) => { const data = JSON.parse(e.data); console.log('Job completed:', data.job_id); }); ``` Returns: SSE stream of job events **Parameters** - `campaign_id` (query, any, optional) — Optional. Deliver only events for this campaign (plus connected/heartbeat). Note: per-job events carry no campaign id — use job_id to watch a single job. - `job_id` (query, any, optional) — Optional. Deliver only events for this job (plus connected/heartbeat). - `token` (query, any, optional) — Auth token (client_id:api_key:api_secret) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/events/stream' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/events/stream", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/events/stream", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/events/stream", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/events/status **Get Event Status** Get event service status. Returns: Event service health and active subscription count :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/events/status' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/events/status", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/events/status", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/events/status", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response ## Dashboard Events Source: https://spideriq.ai/docs/api-reference/dashboard-events ## `GET` /api/v1/dashboard/events/stream **Dashboard Events Stream** Server-Sent Events stream for the Live Activity Theater. Connect from the dashboard via ``new EventSource('/api/v1/dashboard/events/stream', { withCredentials: true })`` — Better Auth session cookie authenticates; X-Selected-Client-Id (set by api-client.ts on super_admin impersonation) determines the channel. Subscribed channel is ``events:{client_id}``; same Redis pubsub channel as the public Bearer endpoint at ``/api/v1/events/stream`` so any ``event_service.publish_event(client_id, …)`` call reaches both consumer types. Event types streamed (live as published — not exhaustive, evolves with backend hooks): - job.queued / job.started / job.completed / job.failed - campaign.created / campaign.location.completed / campaign.business.found / campaign.email.verified / campaign.vayapin.exported / campaign.completed - playbook.company.completed - spiderSite.scraped / spiderPeople.enriched / spiderCompanyData.fetched / spiderPublic.{instagram,linkedin,facebook}.crawled - resources.changed / resource.flagged - booking.created / booking.confirmed / booking.cancelled - content.page.published / content.post.published / content.site.deployed / content.template.applied - mail.outreach.reply / mail.security.flag - gate.key.created / gate.rate_limit.hit - heartbeat (every 30s — keep-alive) The frontend hook (apps/web/src/hooks/useLiveEventStream.ts) parses each event type against a Zod schema in apps/web/src/routes/client/live/cards/types.ts and dispatches it to the appropriate card component. Unknown event types fall back to a generic JsonCard renderer — the page never breaks when the backend adds new types. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/events/stream' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/events/stream", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/events/stream", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/events/stream", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response ## Proxy Source: https://spideriq.ai/docs/api-reference/proxy ## `POST` /api/v1/proxy/request **Get a proxy from the pool** Request a proxy from the pool for making requests. **Selection Logic:** 1. If `sticky_session_id` is provided, returns the same proxy if still valid 2. Filters by `country_code` or `location_id` if specified 3. Excludes any `exclude_modem_ids` 4. Selects from healthy modems using round-robin **Example:** ```python response = requests.post( "/api/v1/proxy/request", json={"country_code": "UA"}, headers={"Authorization": f"Bearer {token}"} ) proxy_url = response.json()["proxy_url"] ``` **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/proxy/request' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "country_code": "string", "location_id": "00000000-0000-0000-0000-000000000000", "location_code": "string", "device_type": "any", "sticky_session_id": "string", "exclude_modem_ids": [ "00000000-0000-0000-0000-000000000000" ], "exclude_iphone_ids": [ "00000000-0000-0000-0000-000000000000" ] }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/proxy/request", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"country_code": "string", "location_id": "00000000-0000-0000-0000-000000000000", "location_code": "string", "device_type": "any", "sticky_session_id": "string", "exclude_modem_ids": ["00000000-0000-0000-0000-000000000000"], "exclude_iphone_ids": ["00000000-0000-0000-0000-000000000000"]}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/proxy/request", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"country_code": "string", "location_id": "00000000-0000-0000-0000-000000000000", "location_code": "string", "device_type": "any", "sticky_session_id": "string", "exclude_modem_ids": ["00000000-0000-0000-0000-000000000000"], "exclude_iphone_ids": ["00000000-0000-0000-0000-000000000000"]}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"country_code": "string", "location_id": "00000000-0000-0000-0000-000000000000", "location_code": "string", "device_type": "any", "sticky_session_id": "string", "exclude_modem_ids": ["00000000-0000-0000-0000-000000000000"], "exclude_iphone_ids": ["00000000-0000-0000-0000-000000000000"]}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/proxy/request", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/proxy/pool/roundrobin **Get next proxy (round-robin)** Simple round-robin proxy selection. Each call returns the next available proxy in rotation. Optionally filter by country code. **Like Proxidize's Proxy Pooling feature.** **Parameters** - `country_code` (query, any, optional) — Filter by country :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/proxy/pool/roundrobin' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/proxy/pool/roundrobin", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/proxy/pool/roundrobin", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/proxy/pool/roundrobin", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/proxy/pool **List available proxies** List all available proxies in the pool. Similar to Proxidize's `/api/getinfo` endpoint. **Parameters** - `country_code` (query, any, optional) - `location_id` (query, any, optional) - `healthy_only` (query, boolean, optional) — Only return healthy modems :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/proxy/pool' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/proxy/pool", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/proxy/pool", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/proxy/pool", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/proxy/stats **Get pool statistics** Get aggregate statistics about the proxy pool. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/proxy/stats' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/proxy/stats", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/proxy/stats", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/proxy/stats", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response ## `POST` /api/v1/proxy/rotate **Request IP rotation** Request IP rotation for a specific modem. The rotation is queued and executed by the agent. Returns immediately with the command status. **Rotation Methods:** - `reconnect`: Disconnect and reconnect (fastest, ~5-15 seconds) - `airplane`: Toggle airplane mode (~10-20 seconds) - `reboot`: Full modem reboot (slowest, ~30-60 seconds) **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/proxy/rotate' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "modem_id": "00000000-0000-0000-0000-000000000000", "method": "reconnect", "reason": "string" }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/proxy/rotate", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"modem_id": "00000000-0000-0000-0000-000000000000", "method": "reconnect", "reason": "string"}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/proxy/rotate", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"modem_id": "00000000-0000-0000-0000-000000000000", "method": "reconnect", "reason": "string"}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"modem_id": "00000000-0000-0000-0000-000000000000", "method": "reconnect", "reason": "string"}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/proxy/rotate", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/proxy/rotate-all **Rotate all modems** Request IP rotation for all online modems. Optionally filter by location. Similar to Proxidize's `/api/rotate` endpoint. **Parameters** - `location_id` (query, any, optional) — Filter by location - `method` (query, any, optional) :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/proxy/rotate-all' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/proxy/rotate-all", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/proxy/rotate-all", { method: "POST", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/proxy/rotate-all", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/proxy/report **Report proxy failure** Report a proxy failure. After 3 failures within 5 minutes, the modem is marked as unhealthy. Call this when a proxy request fails (timeout, blocked, etc.). **Parameters** - `modem_id` (query, string, required) - `error` (query, string, required) — Error description :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/proxy/report' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/proxy/report", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/proxy/report", { method: "POST", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/proxy/report", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/proxy/report-success **Report proxy success** Report a successful proxy request. Resets failure counter. **Parameters** - `modem_id` (query, string, required) :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/proxy/report-success' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/proxy/report-success", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/proxy/report-success", { method: "POST", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/proxy/report-success", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## Proxy Agents Source: https://spideriq.ai/docs/api-reference/proxy-agents ## `POST` /api/v1/proxy/agents/heartbeat **Agent heartbeat** Submit agent heartbeat with modem statuses. **Should be called every 30 seconds.** The response includes: - Pending commands to execute - Config updates (if any) **Headers required:** - `X-Location-Code`: Location code (e.g., 'ua-odessa-1') - `X-Agent-Key`: Agent API key **Parameters** - `x-location-code` (header, string, required) — Location code - `x-agent-key` (header, string, required) — Agent API key **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/proxy/agents/heartbeat' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "location_code": "string", "agent_version": "string", "uptime_seconds": 0, "modems": [ { "imei": "string", "public_ip": "string", "signal_strength": 0, "status": "online", "is_healthy": true, "error_message": "string", "requests_since_last_heartbeat": 0, "bytes_since_last_heartbeat": 0, "speed_test": {} } ], "system_stats": {} }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/proxy/agents/heartbeat", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"location_code": "string", "agent_version": "string", "uptime_seconds": 0, "modems": [{"imei": "string", "public_ip": "string", "signal_strength": 0, "status": "online", "is_healthy": true, "error_message": "string", "requests_since_last_heartbeat": 0, "bytes_since_last_heartbeat": 0, "speed_test": {}}], "system_stats": {}}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/proxy/agents/heartbeat", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"location_code": "string", "agent_version": "string", "uptime_seconds": 0, "modems": [{"imei": "string", "public_ip": "string", "signal_strength": 0, "status": "online", "is_healthy": true, "error_message": "string", "requests_since_last_heartbeat": 0, "bytes_since_last_heartbeat": 0, "speed_test": {}}], "system_stats": {}}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"location_code": "string", "agent_version": "string", "uptime_seconds": 0, "modems": [{"imei": "string", "public_ip": "string", "signal_strength": 0, "status": "online", "is_healthy": true, "error_message": "string", "requests_since_last_heartbeat": 0, "bytes_since_last_heartbeat": 0, "speed_test": {}}], "system_stats": {}}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/proxy/agents/heartbeat", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/proxy/agents/commands/{command_id}/complete **Complete a command** Report command completion. Call this after executing a command received in the heartbeat response. **Headers required:** - `X-Location-Code`: Location code - `X-Agent-Key`: Agent API key **Parameters** - `command_id` (path, string, required) - `x-location-code` (header, string, required) — Location code - `x-agent-key` (header, string, required) — Agent API key **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/proxy/agents/commands/{command_id}/complete' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "success": true, "result": {}, "error_message": "string", "duration_seconds": 0.0 }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/proxy/agents/commands/{command_id}/complete", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"success": true, "result": {}, "error_message": "string", "duration_seconds": 0.0}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/proxy/agents/commands/{command_id}/complete", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"success": true, "result": {}, "error_message": "string", "duration_seconds": 0.0}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"success": true, "result": {}, "error_message": "string", "duration_seconds": 0.0}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/proxy/agents/commands/{command_id}/complete", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/proxy/agents/modems/register **Register a new modem** Register a new modem discovered by the agent. Called when a new USB modem is detected. **Headers required:** - `X-Location-Code`: Location code - `X-Agent-Key`: Agent API key **Parameters** - `imei` (query, string, required) - `proxy_port` (query, integer, required) - `iccid` (query, any, optional) - `phone_number` (query, any, optional) - `carrier` (query, any, optional) - `usb_port` (query, any, optional) - `x-location-code` (header, string, required) — Location code - `x-agent-key` (header, string, required) — Agent API key :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/proxy/agents/modems/register' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/proxy/agents/modems/register", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/proxy/agents/modems/register", { method: "POST", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/proxy/agents/modems/register", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/proxy/agents/sms/log **Log an SMS message** Log an incoming or outgoing SMS message. **Headers required:** - `X-Location-Code`: Location code - `X-Agent-Key`: Agent API key **Parameters** - `modem_id` (query, string, required) - `direction` (query, string, required) - `message` (query, string, required) - `phone_number` (query, any, optional) - `sms_id_on_device` (query, any, optional) - `x-location-code` (header, string, required) — Location code - `x-agent-key` (header, string, required) — Agent API key :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/proxy/agents/sms/log' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/proxy/agents/sms/log", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/proxy/agents/sms/log", { method: "POST", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/proxy/agents/sms/log", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## Media Library Source: https://spideriq.ai/docs/api-reference/media-library ## `GET` /api/v1/dashboard/media/catalog/assets **List Catalog Assets** List the tenant's media assets, newest first (soft-delete aware). Filters are AND-combined. The effective ``client_id`` is resolved by ``require_content_scoped_user_or_api_client`` (session user or API client; header/brand/super-admin selection). Use ``?format=yaml`` or ``?format=md`` for agent-friendly responses. **Parameters** - `kind` (query, any, optional) — Filter by asset class: image | video | doc - `folder` (query, any, optional) — Filter by exact DAM folder path - `tags` (query, any, optional) — Filter to assets carrying ALL given tags - `status` (query, any, optional) — Filter by status: pending | processing | ready | failed - `storage_tier` (query, any, optional) — Filter by storage tier: seaweedfs | r2 | peertube - `limit` (query, integer, optional) — Page size (1-500) - `offset` (query, integer, optional) — Pagination offset - `format` (query, any, optional) — Response format. `json` (default) and `llm` return JSON; `yaml` returns text/yaml; `md` returns text/markdown. Any other value is a 422. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/media/catalog/assets' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/media/catalog/assets", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/media/catalog/assets", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/media/catalog/assets", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/dashboard/media/catalog/counts **Count Catalog Assets** Aggregate counts over the tenant's WHOLE catalog (soft-delete aware). Returns ``{total, by_kind, by_tier}`` — the TRUE totals the Library's filter chips need (FX2a). The dashboard previously computed chip counts client-side over a newest-200 page, so anything past the first page was uncounted (the "Images 0" bug when the newest 200 rows were all videos). This server-side surface counts every matching row in one GROUPING SETS pass. Filters are AND-combined and mirror the list endpoint exactly, so the counts honour the same filter the grid is paging under. The effective ``client_id`` is resolved by ``require_content_scoped_user_or_api_client``. **Parameters** - `kind` (query, any, optional) — Filter by asset class: image | video | doc - `folder` (query, any, optional) — Filter by exact DAM folder path - `tags` (query, any, optional) — Filter to assets carrying ALL given tags - `status` (query, any, optional) — Filter by status: pending | processing | ready | failed - `storage_tier` (query, any, optional) — Filter by storage tier: seaweedfs | r2 | peertube - `format` (query, any, optional) — Response format. `json` (default) and `llm` return JSON; `yaml` returns text/yaml; `md` returns text/markdown. Any other value is a 422. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/media/catalog/counts' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/media/catalog/counts", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/media/catalog/counts", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/media/catalog/counts", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/dashboard/media/catalog/search **Search Catalog Assets** Search the tenant's media assets. ``q`` substring-matches key/folder; ``tags`` overlaps (ANY); ``kind`` narrows by type. All optional + AND-combined, newest first. Mirrors the 1.1 search surface (metadata containment stays service-only — it needs a typed JSON body; ``q`` + ``tags`` + ``kind`` cover the dashboard discovery path). **Parameters** - `q` (query, any, optional) — Case-insensitive substring match on key/folder - `kind` (query, any, optional) — Filter by asset class: image | video | doc - `tags` (query, any, optional) — Filter to assets sharing ANY given tag - `limit` (query, integer, optional) — Page size (1-500) - `format` (query, any, optional) — Response format. `json` (default) and `llm` return JSON; `yaml` returns text/yaml; `md` returns text/markdown. Any other value is a 422. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/media/catalog/search' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/media/catalog/search", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/media/catalog/search", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/media/catalog/search", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/dashboard/media/catalog/assets/{asset_id} **Get Catalog Asset** Fetch a single media asset by id. 404 if it does not exist for the tenant. **Parameters** - `asset_id` (path, string, required) - `format` (query, any, optional) — Response format. `json` (default) and `llm` return JSON; `yaml` returns text/yaml; `md` returns text/markdown. Any other value is a 422. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/media/catalog/assets/{asset_id}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/media/catalog/assets/{asset_id}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/media/catalog/assets/{asset_id}", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/media/catalog/assets/{asset_id}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `PATCH` /api/v1/dashboard/media/catalog/assets/{asset_id} **Update Catalog Asset** Patch a media asset's mutable fields (folder / tags / status / metadata). True PATCH — only the fields present in the body are written. ``alt_text`` and ``metadata`` both fold into a shallow JSONB merge (editing alt-text preserves ``content_hash`` etc.). 200 + the updated asset; 404 if no live asset with that id exists for the tenant. **Parameters** - `asset_id` (path, string, required) **Request body** :::codegroup ```bash curl -X PATCH 'https://spideriq.ai/api/v1/dashboard/media/catalog/assets/{asset_id}' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "folder": "string", "tags": [ "string" ], "status": "string", "metadata": {}, "alt_text": "string" }' ``` ```python import httpx resp = httpx.patch( "https://spideriq.ai/api/v1/dashboard/media/catalog/assets/{asset_id}", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"folder": "string", "tags": ["string"], "status": "string", "metadata": {}, "alt_text": "string"}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/media/catalog/assets/{asset_id}", { method: "PATCH", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"folder": "string", "tags": ["string"], "status": "string", "metadata": {}, "alt_text": "string"}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"folder": "string", "tags": ["string"], "status": "string", "metadata": {}, "alt_text": "string"}`) req, _ := http.NewRequest("PATCH", "https://spideriq.ai/api/v1/dashboard/media/catalog/assets/{asset_id}", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `DELETE` /api/v1/dashboard/media/catalog/assets/{asset_id} **Delete Catalog Asset** Soft-delete a media asset (``deleted_at = now()``). 204 on success, 404 if no live asset with that id exists. For an R2 image (``storage_tier='r2'``) the delete is COMPLETE, not just observational: an R2 image is dual-written (R2 object + legacy ``content_media`` row + this catalog row, all sharing one ``r2_key``), so deleting only the catalog row would orphan the other two — leaving the image visible in the legacy page-image picker and the object billing in R2. We mirror the create and clear the matching ``content_media`` row + R2 object (best-effort — a cleanup miss never fails the delete; the catalog row is already hidden). SeaweedFS/PeerTube tiers keep the observational-only behaviour (their bytes live on LLM11, which app/ never mutates — Rule 3). **Parameters** - `asset_id` (path, string, required) :::codegroup ```bash curl -X DELETE 'https://spideriq.ai/api/v1/dashboard/media/catalog/assets/{asset_id}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.delete( "https://spideriq.ai/api/v1/dashboard/media/catalog/assets/{asset_id}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/media/catalog/assets/{asset_id}", { method: "DELETE", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("DELETE", "https://spideriq.ai/api/v1/dashboard/media/catalog/assets/{asset_id}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `204` — Successful Response - `422` — Validation Error ## `GET` /api/v1/dashboard/media/catalog/assets/{asset_id}/derivative **Get Asset Derivative** Resolve (or lazily create + cache) an image derivative for this asset. On the first request for a given spec the image is derived with Pillow and stored in R2 under a deterministic ``derivatives/{tenant}/{hash}.{ext}`` key; subsequent identical requests return the cached URL with ``cache_hit=true`` and no work. **422** for a non-image asset or one not on the r2 tier (``unsupported_source``); **404** if the asset doesn't exist for the tenant. Derivatives are an evictable cache regenerable from the original (Rule 7). **Parameters** - `asset_id` (path, string, required) - `w` (query, any, optional) — Target width in px (1-4096) - `h` (query, any, optional) — Target height in px (1-4096) - `fit` (query, string, optional) — cover | contain | fill | crop16x9 | crop1x1 | crop4x3 - `fmt` (query, string, optional) — webp | avif | jpeg | png | auto - `q` (query, integer, optional) — Encode quality 1-100 (png ignores it) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/media/catalog/assets/{asset_id}/derivative' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/media/catalog/assets/{asset_id}/derivative", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/media/catalog/assets/{asset_id}/derivative", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/media/catalog/assets/{asset_id}/derivative", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## Media Analytics Source: https://spideriq.ai/docs/api-reference/media-analytics ## `GET` /api/v1/dashboard/media/analytics/overview **Media Analytics Overview** Library analytics rollup: video totals + most-viewed assets + the catalog kind/tier breakdown, joined to PeerTube views/viewers via one batched call. If PeerTube is unreachable the catalog data still returns with ``total_views`` null and ``source="unavailable"`` — never a 500. **Parameters** - `format` (query, any, optional) — Response format. `json` (default) and `llm` return JSON; `yaml` returns text/yaml; `md` returns text/markdown. Any other value is a 422. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/media/analytics/overview' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/media/analytics/overview", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/media/analytics/overview", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/media/analytics/overview", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/dashboard/media/analytics/assets/{asset_id} **Media Asset Analytics** Per-asset engagement. 404 if the asset doesn't exist for the tenant. Video assets carry PeerTube views/viewers (``source="peertube"``, or ``"unavailable"`` if the fetch failed — views null, never a 500). Non-video assets return ``engagement_available=false`` + ``source="not_applicable"`` (graceful — analytics is video-centric for now), NOT a 404/500. **Parameters** - `asset_id` (path, string, required) - `format` (query, any, optional) — Response format. `json` (default) and `llm` return JSON; `yaml` returns text/yaml; `md` returns text/markdown. Any other value is a 422. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/media/analytics/assets/{asset_id}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/media/analytics/assets/{asset_id}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/media/analytics/assets/{asset_id}", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/media/analytics/assets/{asset_id}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/dashboard/media/analytics/assets/{asset_id}/trend **Media Asset Trend** Per-asset view/viewer TREND over time, from the daily snapshot rollup (``public.media_stats_daily``). 404 if the asset doesn't exist for the tenant. ``points`` is the ``[{date, views, viewers}]`` series ascending by date. It's ``[]`` for a non-video asset, a video never snapshotted, or a brand-new video on day-0 — trends accrue FORWARD (PeerTube has no historical per-day series), so day-1 is a single point and fills in nightly. Unlike the live-PeerTube analytics routes, this reads only the local rollup, so it never depends on PeerTube being reachable at request time. **Parameters** - `asset_id` (path, string, required) - `range` (query, string, optional) — Window: 7d | 30d | 90d - `format` (query, any, optional) — Response format. `json` (default) and `llm` return JSON; `yaml` returns text/yaml; `md` returns text/markdown. Any other value is a 422. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/media/analytics/assets/{asset_id}/trend' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/media/analytics/assets/{asset_id}/trend", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/media/analytics/assets/{asset_id}/trend", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/media/analytics/assets/{asset_id}/trend", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/dashboard/media/analytics/assets/{asset_id}/watchtime **Media Asset Watchtime** Per-asset watch-time / retention (4.3b), proxied from PeerTube's owner-stats via the LLM11 import-api. 404 if the asset doesn't exist for the tenant. Video assets carry an ``overall`` block (watch-time in seconds, viewers, peak, countries) + a daily ``timeseries`` (viewers + watch-time-seconds). A non-video asset (or a video with no ``peertube_uuid``) returns ``engagement_available=false`` + ``overall=null``/``timeseries=null`` (graceful empty-state, NOT a 404/500). **RESILIENCE:** if LLM11/PeerTube is unreachable the response is ``source="unavailable"`` with null blocks — NEVER a 500. **Parameters** - `asset_id` (path, string, required) - `format` (query, any, optional) — Response format. `json` (default) and `llm` return JSON; `yaml` returns text/yaml; `md` returns text/markdown. Any other value is a 422. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/media/analytics/assets/{asset_id}/watchtime' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/media/analytics/assets/{asset_id}/watchtime", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/media/analytics/assets/{asset_id}/watchtime", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/media/analytics/assets/{asset_id}/watchtime", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## Media Upload Source: https://spideriq.ai/docs/api-reference/media-upload ## `POST` /api/v1/dashboard/media/upload **Upload Media Image** Upload a single image to Cloudflare R2 (``storage_tier='r2'``). Validates → transcodes to WebP (5 MB cap) → R2 → ``content_media`` row → catalogs into ``media_assets``. 201 + ``{success, media, asset}`` where ``media`` is the legacy record and ``asset`` is the catalog row (null only if the non-blocking catalog write missed — the upload still succeeded). **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/dashboard/media/upload' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "file": "string", "folder": "/", "alt_text": "string" }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/dashboard/media/upload", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"file": "string", "folder": "/", "alt_text": "string"}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/media/upload", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"file": "string", "folder": "/", "alt_text": "string"}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"file": "string", "folder": "/", "alt_text": "string"}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/dashboard/media/upload", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `201` — Successful Response - `422` — Validation Error ## `POST` /api/v1/dashboard/media/upload-from-url **Upload Media Image From Url** Host a remote image URL on Cloudflare R2 (``storage_tier='r2'``) — the **agent-callable, JSON-only** twin of ``POST /upload``. Agents reaching the content platform via ``skill_call`` can't send multipart, and a generated hero image lives at a provider URL (``kie.*`` etc.) that the ``cover_image_url`` allowlist rejects. This fetches the URL **SSRF-safely** and runs the same validate → WebP-transcode → R2 → ``content_media`` → catalog pipeline as ``/upload``; the returned ``media.r2_url`` is on our allowlisted host, so it then passes straight into ``createPost`` / ``content_update_page``. 201 + ``{success, media, asset}`` (same shape as ``/upload``). A bad/unreachable URL or non-image body is a **400**; an oversized body (>5 MB) is a **413**. **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/dashboard/media/upload-from-url' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "media_url": "string", "folder": "/", "alt_text": "string" }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/dashboard/media/upload-from-url", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"media_url": "string", "folder": "/", "alt_text": "string"}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/media/upload-from-url", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"media_url": "string", "folder": "/", "alt_text": "string"}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"media_url": "string", "folder": "/", "alt_text": "string"}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/dashboard/media/upload-from-url", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `201` — Successful Response - `422` — Validation Error ## `POST` /api/v1/dashboard/media/upload-file **Register Media File** Register a NON-IMAGE file on R2 (``storage_tier='r2'``) — press 3.1. The third media door, and the only one that does not transcode. `/upload` and the legacy `content/media` uploader both hard-gate on `validate_image()` and rewrite everything to WebP, so a media kit's ZIP bundle, SVG/EPS logo variants and PDF brand guide had no way to obtain a ``content_media`` row — and therefore no ``media_id`` that `POST /press/kits/{id}/assets` accepts. **The bytes decide the type**: the filename extension and the multipart `Content-Type` are both ignored (audit §12). Raster images are refused here with a 400 pointing at `/upload`, so nothing skips image validation. Archives are stored and served verbatim — never expanded or inspected server-side. 201 + ``{success, media, asset}``, the same shape as `/upload`. **400** for an unsupported or image type, **413** over the cap (100 MB for ZIP, 20 MB otherwise). **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/dashboard/media/upload-file' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "file": "string", "folder": "press-kits", "alt_text": "string" }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/dashboard/media/upload-file", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"file": "string", "folder": "press-kits", "alt_text": "string"}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/media/upload-file", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"file": "string", "folder": "press-kits", "alt_text": "string"}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"file": "string", "folder": "press-kits", "alt_text": "string"}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/dashboard/media/upload-file", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `201` — Successful Response - `422` — Validation Error ## `POST` /api/v1/dashboard/media/upload-batch **Upload Media Batch** Upload one or many files to SeaweedFS (``storage_tier='seaweedfs'``). Per-file (20 MB / 500 MB video) + batch (500 MB) weight caps and an extension allowlist run BEFORE any upload — a hard violation is a 413 and nothing uploads. Each file catalogs on success. 201 + ``{success, uploaded, failed, totals}`` (``success`` is true only when nothing failed). **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/dashboard/media/upload-batch' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "files": [ "string" ], "folder": "string", "preserve_filename": false }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/dashboard/media/upload-batch", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"files": ["string"], "folder": "string", "preserve_filename": false}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/media/upload-batch", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"files": ["string"], "folder": "string", "preserve_filename": false}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"files": ["string"], "folder": "string", "preserve_filename": false}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/dashboard/media/upload-batch", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `201` — Successful Response - `422` — Validation Error ## `POST` /api/v1/dashboard/media/import-video **Import Media Video** Import a remote video into the tenant's PeerTube channel (``storage_tier='peertube'``). LONG-RUNNING (encode takes minutes) — this does NOT block-wait the encode. The import is accepted (uuid minted, status ``processing``, catalog row written) and returns **202** immediately; the client polls the existing status path (``GET /api/v1/media/videos/{uuid}/status``) for completion. 202 + ``{success, peertube_uuid, video_id, status, watch_url, embed_url, player_url, message}``. 502 if the upstream import service rejects the kick-off. **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/dashboard/media/import-video' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "url": "string", "title": "string", "description": "string", "privacy": 2, "use_proxy": false }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/dashboard/media/import-video", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"url": "string", "title": "string", "description": "string", "privacy": 2, "use_proxy": false}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/media/import-video", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"url": "string", "title": "string", "description": "string", "privacy": 2, "use_proxy": false}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"url": "string", "title": "string", "description": "string", "privacy": 2, "use_proxy": false}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/dashboard/media/import-video", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `202` — Successful Response - `422` — Validation Error ## dashboard-members Source: https://spideriq.ai/docs/api-reference/dashboard-members ## `GET` /api/v1/dashboard/members **List Members** **Parameters** - `search` (query, any, optional) - `limit` (query, integer, optional) - `offset` (query, integer, optional) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/members' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/members", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/members", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/members", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/dashboard/members/quota **Get Member Quota** :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/members/quota' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/members/quota", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/members/quota", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/members/quota", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response ## `POST` /api/v1/dashboard/members/invite **Invite Member** **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/dashboard/members/invite' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "email": "string", "name": "string", "role": "string", "groups": [ "string" ], "site_name": "string", "login_url": "string" }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/dashboard/members/invite", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"email": "string", "name": "string", "role": "string", "groups": ["string"], "site_name": "string", "login_url": "string"}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/members/invite", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"email": "string", "name": "string", "role": "string", "groups": ["string"], "site_name": "string", "login_url": "string"}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"email": "string", "name": "string", "role": "string", "groups": ["string"], "site_name": "string", "login_url": "string"}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/dashboard/members/invite", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `201` — Successful Response - `422` — Validation Error ## `POST` /api/v1/dashboard/members/{user_id}/ban **Ban Member** **Parameters** - `user_id` (path, string, required) :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/dashboard/members/{user_id}/ban' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/dashboard/members/{user_id}/ban", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/members/{user_id}/ban", { method: "POST", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/dashboard/members/{user_id}/ban", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/dashboard/members/{user_id}/unban **Unban Member** **Parameters** - `user_id` (path, string, required) **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/dashboard/members/{user_id}/unban' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "role": "member" }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/dashboard/members/{user_id}/unban", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"role": "member"}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/members/{user_id}/unban", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"role": "member"}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"role": "member"}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/dashboard/members/{user_id}/unban", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/dashboard/members/{user_id}/role **Set Member Role** **Parameters** - `user_id` (path, string, required) **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/dashboard/members/{user_id}/role' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "role": "string" }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/dashboard/members/{user_id}/role", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"role": "string"}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/members/{user_id}/role", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"role": "string"}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"role": "string"}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/dashboard/members/{user_id}/role", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `PATCH` /api/v1/dashboard/members/{user_id} **Patch Site Member** **Parameters** - `user_id` (path, string, required) **Request body** :::codegroup ```bash curl -X PATCH 'https://spideriq.ai/api/v1/dashboard/members/{user_id}' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "status": "string", "groups": [ "string" ] }' ``` ```python import httpx resp = httpx.patch( "https://spideriq.ai/api/v1/dashboard/members/{user_id}", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"status": "string", "groups": ["string"]}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/members/{user_id}", { method: "PATCH", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"status": "string", "groups": ["string"]}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"status": "string", "groups": ["string"]}`) req, _ := http.NewRequest("PATCH", "https://spideriq.ai/api/v1/dashboard/members/{user_id}", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/dashboard/member-groups **List Groups** :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/member-groups' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/member-groups", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/member-groups", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/member-groups", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response ## `POST` /api/v1/dashboard/member-groups **Create Group** **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/dashboard/member-groups' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "slug": "string", "name": "string", "membership_type": "manual", "condition_predicate": {} }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/dashboard/member-groups", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"slug": "string", "name": "string", "membership_type": "manual", "condition_predicate": {}}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/member-groups", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"slug": "string", "name": "string", "membership_type": "manual", "condition_predicate": {}}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"slug": "string", "name": "string", "membership_type": "manual", "condition_predicate": {}}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/dashboard/member-groups", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `201` — Successful Response - `422` — Validation Error ## `PATCH` /api/v1/dashboard/member-groups/{group_id} **Update Group** **Parameters** - `group_id` (path, string, required) **Request body** :::codegroup ```bash curl -X PATCH 'https://spideriq.ai/api/v1/dashboard/member-groups/{group_id}' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "name": "string", "active": true, "condition_predicate": {}, "clear_predicate": false }' ``` ```python import httpx resp = httpx.patch( "https://spideriq.ai/api/v1/dashboard/member-groups/{group_id}", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"name": "string", "active": true, "condition_predicate": {}, "clear_predicate": false}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/member-groups/{group_id}", { method: "PATCH", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"name": "string", "active": true, "condition_predicate": {}, "clear_predicate": false}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"name": "string", "active": true, "condition_predicate": {}, "clear_predicate": false}`) req, _ := http.NewRequest("PATCH", "https://spideriq.ai/api/v1/dashboard/member-groups/{group_id}", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `DELETE` /api/v1/dashboard/member-groups/{group_id} **Delete Group** **Parameters** - `group_id` (path, string, required) :::codegroup ```bash curl -X DELETE 'https://spideriq.ai/api/v1/dashboard/member-groups/{group_id}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.delete( "https://spideriq.ai/api/v1/dashboard/member-groups/{group_id}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/member-groups/{group_id}", { method: "DELETE", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("DELETE", "https://spideriq.ai/api/v1/dashboard/member-groups/{group_id}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `204` — Successful Response - `422` — Validation Error ## `GET` /api/v1/dashboard/pages/{page_id}/access **Get Page Access** **Parameters** - `page_id` (path, string, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/pages/{page_id}/access' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/pages/{page_id}/access", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/pages/{page_id}/access", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/pages/{page_id}/access", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `PUT` /api/v1/dashboard/pages/{page_id}/access **Set Page Access** **Parameters** - `page_id` (path, string, required) **Request body** :::codegroup ```bash curl -X PUT 'https://spideriq.ai/api/v1/dashboard/pages/{page_id}/access' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "access": "string", "allowed_groups": [ "string" ] }' ``` ```python import httpx resp = httpx.put( "https://spideriq.ai/api/v1/dashboard/pages/{page_id}/access", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"access": "string", "allowed_groups": ["string"]}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/pages/{page_id}/access", { method: "PUT", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"access": "string", "allowed_groups": ["string"]}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"access": "string", "allowed_groups": ["string"]}`) req, _ := http.NewRequest("PUT", "https://spideriq.ai/api/v1/dashboard/pages/{page_id}/access", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/dashboard/data-restrictions **List Data Restrictions** **Parameters** - `source_id` (query, any, optional) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/data-restrictions' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/data-restrictions", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/data-restrictions", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/data-restrictions", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/dashboard/data-restrictions **Create Data Restriction** **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/dashboard/data-restrictions' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "source_id": "string", "group_slug": "string", "field": "string", "op": "in", "allowed_values": [ "string" ] }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/dashboard/data-restrictions", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"source_id": "string", "group_slug": "string", "field": "string", "op": "in", "allowed_values": ["string"]}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/data-restrictions", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"source_id": "string", "group_slug": "string", "field": "string", "op": "in", "allowed_values": ["string"]}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"source_id": "string", "group_slug": "string", "field": "string", "op": "in", "allowed_values": ["string"]}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/dashboard/data-restrictions", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `201` — Successful Response - `422` — Validation Error ## `PATCH` /api/v1/dashboard/data-restrictions/{restriction_id} **Update Data Restriction** **Parameters** - `restriction_id` (path, string, required) **Request body** :::codegroup ```bash curl -X PATCH 'https://spideriq.ai/api/v1/dashboard/data-restrictions/{restriction_id}' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "op": "string", "allowed_values": [ "string" ], "active": true }' ``` ```python import httpx resp = httpx.patch( "https://spideriq.ai/api/v1/dashboard/data-restrictions/{restriction_id}", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"op": "string", "allowed_values": ["string"], "active": true}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/data-restrictions/{restriction_id}", { method: "PATCH", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"op": "string", "allowed_values": ["string"], "active": true}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"op": "string", "allowed_values": ["string"], "active": true}`) req, _ := http.NewRequest("PATCH", "https://spideriq.ai/api/v1/dashboard/data-restrictions/{restriction_id}", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `DELETE` /api/v1/dashboard/data-restrictions/{restriction_id} **Delete Data Restriction** **Parameters** - `restriction_id` (path, string, required) :::codegroup ```bash curl -X DELETE 'https://spideriq.ai/api/v1/dashboard/data-restrictions/{restriction_id}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.delete( "https://spideriq.ai/api/v1/dashboard/data-restrictions/{restriction_id}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/data-restrictions/{restriction_id}", { method: "DELETE", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("DELETE", "https://spideriq.ai/api/v1/dashboard/data-restrictions/{restriction_id}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `204` — Successful Response - `422` — Validation Error ## `GET` /api/v1/dashboard/sso-config **Get Sso Config** :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/sso-config' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/sso-config", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/sso-config", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/sso-config", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response ## `PUT` /api/v1/dashboard/sso-config **Set Sso Config** **Request body** :::codegroup ```bash curl -X PUT 'https://spideriq.ai/api/v1/dashboard/sso-config' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "auto_provision": true, "default_role": "string", "default_groups": [ "string" ] }' ``` ```python import httpx resp = httpx.put( "https://spideriq.ai/api/v1/dashboard/sso-config", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"auto_provision": true, "default_role": "string", "default_groups": ["string"]}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/sso-config", { method: "PUT", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"auto_provision": true, "default_role": "string", "default_groups": ["string"]}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"auto_provision": true, "default_role": "string", "default_groups": ["string"]}`) req, _ := http.NewRequest("PUT", "https://spideriq.ai/api/v1/dashboard/sso-config", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/dashboard/sso-providers **List Sso Providers** :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/sso-providers' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/sso-providers", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/sso-providers", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/sso-providers", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response ## `GET` /api/v1/dashboard/sso-providers/{provider_id} **Get Sso Provider** **Parameters** - `provider_id` (path, string, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/sso-providers/{provider_id}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/sso-providers/{provider_id}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/sso-providers/{provider_id}", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/sso-providers/{provider_id}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `PUT` /api/v1/dashboard/sso-providers/{provider_id} **Upsert Sso Provider** **Parameters** - `provider_id` (path, string, required) **Request body** :::codegroup ```bash curl -X PUT 'https://spideriq.ai/api/v1/dashboard/sso-providers/{provider_id}' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "issuer": "string", "domain": "string", "client_id": "string", "client_secret": "string", "discovery_endpoint": "string", "scopes": [ "string" ], "pkce": true, "mapping": {} }' ``` ```python import httpx resp = httpx.put( "https://spideriq.ai/api/v1/dashboard/sso-providers/{provider_id}", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"issuer": "string", "domain": "string", "client_id": "string", "client_secret": "string", "discovery_endpoint": "string", "scopes": ["string"], "pkce": true, "mapping": {}}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/sso-providers/{provider_id}", { method: "PUT", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"issuer": "string", "domain": "string", "client_id": "string", "client_secret": "string", "discovery_endpoint": "string", "scopes": ["string"], "pkce": true, "mapping": {}}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"issuer": "string", "domain": "string", "client_id": "string", "client_secret": "string", "discovery_endpoint": "string", "scopes": ["string"], "pkce": true, "mapping": {}}`) req, _ := http.NewRequest("PUT", "https://spideriq.ai/api/v1/dashboard/sso-providers/{provider_id}", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `DELETE` /api/v1/dashboard/sso-providers/{provider_id} **Delete Sso Provider** **Parameters** - `provider_id` (path, string, required) :::codegroup ```bash curl -X DELETE 'https://spideriq.ai/api/v1/dashboard/sso-providers/{provider_id}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.delete( "https://spideriq.ai/api/v1/dashboard/sso-providers/{provider_id}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/sso-providers/{provider_id}", { method: "DELETE", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("DELETE", "https://spideriq.ai/api/v1/dashboard/sso-providers/{provider_id}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `204` — Successful Response - `422` — Validation Error ## SpiderMail Source: https://spideriq.ai/docs/api-reference/spidermail ## `GET` /api/v1/mail/providers **List Provider Presets** List the supported providers and their published connection defaults. Static configuration — no database access, no per-tenant content. It is still behind the normal mail read dependency rather than public: the list names our supported integrations, and there is no reason for it to be enumerable anonymously. Callers use this to render the provider picker and to pre-fill the connection form. Everything returned here is exactly what `POST /mailboxes` would have filled in for an omitted field, so a caller can equally omit the fields and skip this endpoint entirely — the presets are applied server-side either way. A preset may also carry `routed_providers`: named providers that work but are deliberately not presets. Only `generic_imap` carries any (Fastmail). The list is what a user searching for their own provider finds; leaving it unmapped here would restore exactly the silence card 3.1b exists to end, so it is asserted against this handler's real output, not against the registry. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/mail/providers' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/mail/providers", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/mail/providers", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/mail/providers", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response ## `GET` /api/v1/mail/mailboxes **List Mailboxes** List all mailboxes for the authenticated client (passwords excluded). Use ?format=yaml for agent-friendly output. **Parameters** - `format` (query, any, optional) — Output format: json, yaml, or llm :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/mail/mailboxes' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/mail/mailboxes", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/mail/mailboxes", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/mail/mailboxes", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/mail/mailboxes **Register Mailbox** Register a new email mailbox. Passwords are encrypted at rest. Resource-quota check (Plans Initiative F2, migration 248): a client may own at most `max_mailboxes` mailboxes. NULL = unlimited (the default for every existing client at F2 ship time). 403 returns a structured `error="resource_quota_exceeded"` body so dashboard / agent callers can surface the cap value directly. **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/mail/mailboxes' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "email_address": "alice@company.com", "display_name": "string", "provider": "zoho", "imap_host": "string", "imap_port": 0, "imap_username": "string", "imap_password": "string", "smtp_host": "string", "smtp_port": 0, "smtp_username": "string", "smtp_password": "string", "skip_verification": false, "sync_scope": "last_n", "sync_scope_n": 0, "sync_since_date": "2026-01-01", "poll_interval_seconds": 0 }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/mail/mailboxes", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"email_address": "alice@company.com", "display_name": "string", "provider": "zoho", "imap_host": "string", "imap_port": 0, "imap_username": "string", "imap_password": "string", "smtp_host": "string", "smtp_port": 0, "smtp_username": "string", "smtp_password": "string", "skip_verification": false, "sync_scope": "last_n", "sync_scope_n": 0, "sync_since_date": "2026-01-01", "poll_interval_seconds": 0}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/mail/mailboxes", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"email_address": "alice@company.com", "display_name": "string", "provider": "zoho", "imap_host": "string", "imap_port": 0, "imap_username": "string", "imap_password": "string", "smtp_host": "string", "smtp_port": 0, "smtp_username": "string", "smtp_password": "string", "skip_verification": false, "sync_scope": "last_n", "sync_scope_n": 0, "sync_since_date": "2026-01-01", "poll_interval_seconds": 0}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"email_address": "alice@company.com", "display_name": "string", "provider": "zoho", "imap_host": "string", "imap_port": 0, "imap_username": "string", "imap_password": "string", "smtp_host": "string", "smtp_port": 0, "smtp_username": "string", "smtp_password": "string", "skip_verification": false, "sync_scope": "last_n", "sync_scope_n": 0, "sync_since_date": "2026-01-01", "poll_interval_seconds": 0}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/mail/mailboxes", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `201` — Successful Response - `422` — Validation Error ## `GET` /api/v1/mail/mailboxes/stats **Get Mailbox Stats** Per-mailbox aggregates for the Master Inbox sidebar. ?format=yaml supported. At 200 mailboxes per client the naive pattern — one COUNT query per mailbox from the dashboard — costs 200 round-trips per page load. This endpoint returns the full stats row for every mailbox the client owns in a single aggregate query, plus a totals block so the sidebar header can render client-wide counts without a second call. Health is the SAME five-state contract `GET /mail/mailboxes` serves — `active` / `connecting` / `error` / `stalled` / `disabled`, plus `health_detail`, `health_since` and the classified `poll_error_cause` / `poll_error_action`. It is derived by the one shared `_apply_health()`, not computed here. 🔴 Why this docstring used to describe a DIFFERENT vocabulary --- Until 2026-08-10 this handler carried its own ladder — `ok` / `warning` / `stale` / `disabled` — with its own hardcoded one-hour staleness cutoff. Card UI.2 (#3046) derived health properly in `services/mail/mailbox_health.py` and wired `GET /mail/mailboxes`, the CLI and the MCP `list_mailboxes` tool, and **did not sweep this function.** So the two endpoints disagreed about the same row at the same moment, which is what a marketplace vendor measured and filed: GET /mail/mailboxes -> health="error" + health_detail GET /mail/mailboxes/stats -> health="warning" + no health_detail (healthy rows: "active" vs "ok") Two defects in one, and the second is the worse of them. `warning` was not in the documented state set at all, so an agent branching on the contract had no case for it — and the safe-looking fallback (treat unknown as healthy) is the dangerous one. On top of that the two thresholds differed (1h here vs `STALE_POLL_HOURS`, 6h, there), so a mailbox could read `stale` on one endpoint and `active` on the other purely from the mismatch. ⚠️ **The CI guard built to prevent exactly this could not see it.** `tests/unit/mail/test_mailbox_status_surfaces.py` located the region it inspects with `src.index("async def get_mailbox_stats")` — it used this function as the END BOUNDARY and stopped reading one line before the bug. LEARNINGS §89 counted the ternary "five separate times across four files"; this was the sixth site, in Python, just outside the window. A guard's coverage is defined by where it stops looking, and nothing announces that. Both surfaces now go through `_apply_health()`, so there is one definition and adding a state cannot repaint only half the product. **Parameters** - `format` (query, any, optional) — Output format: json, yaml, or llm :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/mail/mailboxes/stats' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/mail/mailboxes/stats", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/mail/mailboxes/stats", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/mail/mailboxes/stats", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `PATCH` /api/v1/mail/mailboxes/{email} **Update Mailbox** Update mailbox settings including template defaults. Supports updating: - display_name: Display name for From header - is_active: Enable/disable mailbox polling - default_template_id: Default template for outbound emails - template_variables: Default variable values (merged with job-level data) **Parameters** - `email` (path, string, required) **Request body** :::codegroup ```bash curl -X PATCH 'https://spideriq.ai/api/v1/mail/mailboxes/{email}' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "default_template_id": 1, "display_name": "Alice Smith", "template_variables": { "company_name": "Acme Corp", "phone": "+1 555-123-4567", "sender_name": "Alice Smith", "title": "Sales Director" } }' ``` ```python import httpx resp = httpx.patch( "https://spideriq.ai/api/v1/mail/mailboxes/{email}", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"default_template_id": 1, "display_name": "Alice Smith", "template_variables": {"company_name": "Acme Corp", "phone": "+1 555-123-4567", "sender_name": "Alice Smith", "title": "Sales Director"}}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/mail/mailboxes/{email}", { method: "PATCH", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"default_template_id": 1, "display_name": "Alice Smith", "template_variables": {"company_name": "Acme Corp", "phone": "+1 555-123-4567", "sender_name": "Alice Smith", "title": "Sales Director"}}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"default_template_id": 1, "display_name": "Alice Smith", "template_variables": {"company_name": "Acme Corp", "phone": "+1 555-123-4567", "sender_name": "Alice Smith", "title": "Sales Director"}}`) req, _ := http.NewRequest("PATCH", "https://spideriq.ai/api/v1/mail/mailboxes/{email}", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `DELETE` /api/v1/mail/mailboxes/{email} **Delete Mailbox** Delete a mailbox and all its messages. **Parameters** - `email` (path, string, required) :::codegroup ```bash curl -X DELETE 'https://spideriq.ai/api/v1/mail/mailboxes/{email}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.delete( "https://spideriq.ai/api/v1/mail/mailboxes/{email}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/mail/mailboxes/{email}", { method: "DELETE", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("DELETE", "https://spideriq.ai/api/v1/mail/mailboxes/{email}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/mail/mailboxes/{email}/test **Test Mailbox** Test IMAP and SMTP connectivity for a mailbox. Gated on `mail:test`, which `mail:write` and `email:admin` also satisfy — so every token that could reach this route before still can, and a read-only monitoring agent can now be given the narrow scope instead of the one that can delete mailboxes. See `_MAIL_TEST` for why this is a third scope rather than a move to `mail:read`. ⚠️ The probe is AUTH-only (`login()` on both protocols). Per LEARNINGS §27 a `MAIL FROM`/`RCPT` probe returns a false `250` on a send-blocked domain — Zoho rejects at DATA. A green result here is NOT evidence the mailbox can send. **Parameters** - `email` (path, string, required) :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/mail/mailboxes/{email}/test' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/mail/mailboxes/{email}/test", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/mail/mailboxes/{email}/test", { method: "POST", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/mail/mailboxes/{email}/test", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/mail/folders **Get Folders** Get folder list with message counts for a mailbox. **Parameters** - `email` (query, string, required) — Mailbox email address - `format` (query, any, optional) — Output format: json, yaml, or llm :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/mail/folders' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/mail/folders", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/mail/folders", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/mail/folders", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/mail/inbox **Get Inbox** Get inbox messages. Three scopes, resolved in priority order: 1. `?email=alice@...` → single mailbox (legacy; byte-identical response). 2. `?view_id=N` → apply saved view's filter_config from /mail/views. 3. neither → Master Inbox (client-wide), streams from the denormalized idx_mail_messages_client_*_date indexes introduced in migration 139. Direct query params (unread_only, starred_only, outreach_classification, has_attachments, folder, direction) override the saved view's equivalents. Use ?format=yaml for agent-friendly output. **Parameters** - `email` (query, any, optional) — Mailbox email address. Omit for Master Inbox (all client mailboxes). - `view_id` (query, any, optional) — Apply a saved view's filter_config (see /mail/views). Direct query params override the view's defaults. - `limit` (query, integer, optional) — Number of messages to return - `offset` (query, integer, optional) — Offset for pagination - `unread_only` (query, boolean, optional) — Only return unread messages - `starred_only` (query, boolean, optional) — Only return starred messages - `outreach_classification` (query, any, optional) — Filter by outreach classification (e.g. 'warmup'). Overrides a saved view's classification. - `has_attachments` (query, any, optional) — Filter to messages with (true) / without (false) attachments. - `folder` (query, any, optional) — Folder to filter by (INBOX, Sent, Drafts, Trash) - `direction` (query, any, optional) — Direction filter: inbound or outbound - `include_body` (query, boolean, optional) — Include full body_text/body_html on each message (avoids the N+1 fan-out to /mail/messages/{id}). Off by default for the lightweight preview-only response. - `format` (query, any, optional) — Output format: json or yaml :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/mail/inbox' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/mail/inbox", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/mail/inbox", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/mail/inbox", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/mail/messages/{message_id} **Get Message** Get full message by database ID. Use ?format=yaml for agent-friendly output. Also marks as read. **Parameters** - `message_id` (path, integer, required) - `format` (query, any, optional) — Output format: json or yaml - `include_attachments` (query, boolean, optional) — Include attachment summaries in response :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/mail/messages/{message_id}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/mail/messages/{message_id}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/mail/messages/{message_id}", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/mail/messages/{message_id}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `PATCH` /api/v1/mail/messages/{message_id} **Update Message Flags** Update message flags (is_read, is_starred, labels). **Parameters** - `message_id` (path, integer, required) **Request body** :::codegroup ```bash curl -X PATCH 'https://spideriq.ai/api/v1/mail/messages/{message_id}' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "is_read": true, "is_starred": true, "labels": [ "string" ], "notes": "string" }' ``` ```python import httpx resp = httpx.patch( "https://spideriq.ai/api/v1/mail/messages/{message_id}", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"is_read": true, "is_starred": true, "labels": ["string"], "notes": "string"}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/mail/messages/{message_id}", { method: "PATCH", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"is_read": true, "is_starred": true, "labels": ["string"], "notes": "string"}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"is_read": true, "is_starred": true, "labels": ["string"], "notes": "string"}`) req, _ := http.NewRequest("PATCH", "https://spideriq.ai/api/v1/mail/messages/{message_id}", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/mail/threads/{thread_id} **Get Thread** Get all messages in a thread. Use ?format=yaml for agent-friendly output. **Parameters** - `thread_id` (path, string, required) - `format` (query, any, optional) — Output format: json or yaml - `include_attachments` (query, boolean, optional) — Include attachment summaries in response :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/mail/threads/{thread_id}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/mail/threads/{thread_id}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/mail/threads/{thread_id}", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/mail/threads/{thread_id}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/mail/search **Search Messages** Search messages using full-text search and filters. **Parameters** - `email` (query, string, required) — Mailbox email address - `q` (query, any, optional) — Full-text search query - `from_addr` (query, any, optional) — Filter by sender address - `subject` (query, any, optional) — Filter by subject (substring match) - `since` (query, any, optional) — Messages since this date (ISO 8601) - `before` (query, any, optional) — Messages before this date (ISO 8601) - `format` (query, any, optional) — Output format: json, yaml, or llm :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/mail/search' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/mail/search", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/mail/search", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/mail/search", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/mail/messages/bulk **Bulk Update Messages** Bulk update messages: mark_read, mark_unread, archive, delete, add_label. **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/mail/messages/bulk' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "message_ids": [ 0 ], "action": "string", "label": "string" }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/mail/messages/bulk", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"message_ids": [0], "action": "string", "label": "string"}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/mail/messages/bulk", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"message_ids": [0], "action": "string", "label": "string"}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"message_ids": [0], "action": "string", "label": "string"}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/mail/messages/bulk", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/mail/messages/{message_id}/snooze **Snooze Message** Snooze a message until a specific time. **Parameters** - `message_id` (path, integer, required) **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/mail/messages/{message_id}/snooze' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "snoozed_until": "2026-01-01T00:00:00Z" }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/mail/messages/{message_id}/snooze", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"snoozed_until": "2026-01-01T00:00:00Z"}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/mail/messages/{message_id}/snooze", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"snoozed_until": "2026-01-01T00:00:00Z"}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"snoozed_until": "2026-01-01T00:00:00Z"}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/mail/messages/{message_id}/snooze", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `DELETE` /api/v1/mail/messages/{message_id}/snooze **Unsnooze Message** Unsnooze a message, returning it to its original folder. **Parameters** - `message_id` (path, integer, required) :::codegroup ```bash curl -X DELETE 'https://spideriq.ai/api/v1/mail/messages/{message_id}/snooze' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.delete( "https://spideriq.ai/api/v1/mail/messages/{message_id}/snooze", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/mail/messages/{message_id}/snooze", { method: "DELETE", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("DELETE", "https://spideriq.ai/api/v1/mail/messages/{message_id}/snooze", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/mail/snoozed **List Snoozed Messages** List all snoozed messages for the client. **Parameters** - `email` (query, any, optional) — Filter by mailbox email - `format` (query, any, optional) — Output format: json, yaml, or llm :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/mail/snoozed' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/mail/snoozed", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/mail/snoozed", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/mail/snoozed", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/mail/labels **List Labels** List all label definitions for the client. **Parameters** - `format` (query, any, optional) — Output format: json, yaml, or llm :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/mail/labels' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/mail/labels", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/mail/labels", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/mail/labels", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/mail/labels **Create Label** Create a new label definition. **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/mail/labels' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "name": "string", "color": "#6B7280" }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/mail/labels", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"name": "string", "color": "#6B7280"}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/mail/labels", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"name": "string", "color": "#6B7280"}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"name": "string", "color": "#6B7280"}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/mail/labels", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `201` — Successful Response - `422` — Validation Error ## `PATCH` /api/v1/mail/labels/{label_id} **Update Label** Update a label definition (name and/or color). **Parameters** - `label_id` (path, integer, required) **Request body** :::codegroup ```bash curl -X PATCH 'https://spideriq.ai/api/v1/mail/labels/{label_id}' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "name": "string", "color": "string" }' ``` ```python import httpx resp = httpx.patch( "https://spideriq.ai/api/v1/mail/labels/{label_id}", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"name": "string", "color": "string"}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/mail/labels/{label_id}", { method: "PATCH", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"name": "string", "color": "string"}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"name": "string", "color": "string"}`) req, _ := http.NewRequest("PATCH", "https://spideriq.ai/api/v1/mail/labels/{label_id}", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `DELETE` /api/v1/mail/labels/{label_id} **Delete Label** Delete a label definition. **Parameters** - `label_id` (path, integer, required) :::codegroup ```bash curl -X DELETE 'https://spideriq.ai/api/v1/mail/labels/{label_id}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.delete( "https://spideriq.ai/api/v1/mail/labels/{label_id}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/mail/labels/{label_id}", { method: "DELETE", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("DELETE", "https://spideriq.ai/api/v1/mail/labels/{label_id}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `204` — Successful Response - `422` — Validation Error ## `GET` /api/v1/mail/templates **List Templates** List all email templates for the client. **Parameters** - `template_type` (query, any, optional) — Filter by type: signature, header, layout, full - `active_only` (query, boolean, optional) — Only return active templates (false also returns soft-deleted ones) - `format` (query, any, optional) — Output format: json, yaml, or llm :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/mail/templates' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/mail/templates", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/mail/templates", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/mail/templates", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/mail/templates **Create Template** Create a new email template. Note on SQL: use `CAST(:variables AS jsonb)` for the JSONB column cast. The shorter Postgres `::jsonb` syntax does NOT work here because SQLAlchemy tokenizes `:variables::jsonb` as bind-parameter `:variables` followed by literal `::jsonb`, producing a PostgresSyntaxError at execute time. The mail_templates.py sibling endpoint uses the correct form (app/api/v1/mail_templates.py:92) and has always worked; this one shipped broken in v2.52.0 and was masked by an over-broad `except Exception → 409` that blamed every failure on name conflicts. Surfaced 2026-04-21 during the Stage 6 E2E test sweep (PR #X). **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/mail/templates' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "description": "Welcome email for new contacts", "html_source": "

Welcome {{ name }}!

{{ body }}

", "name": "welcome-email", "template_type": "full", "variables": [ "name", "body" ] }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/mail/templates", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"description": "Welcome email for new contacts", "html_source": "

Welcome {{ name }}!

{{ body }}

", "name": "welcome-email", "template_type": "full", "variables": ["name", "body"]}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/mail/templates", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"description": "Welcome email for new contacts", "html_source": "

Welcome {{ name }}!

{{ body }}

", "name": "welcome-email", "template_type": "full", "variables": ["name", "body"]}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"description": "Welcome email for new contacts", "html_source": "

Welcome {{ name }}!

{{ body }}

", "name": "welcome-email", "template_type": "full", "variables": ["name", "body"]}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/mail/templates", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `201` — Successful Response - `422` — Validation Error ## `GET` /api/v1/mail/templates/{template_id} **Get Template** Get a single template by ID. **Parameters** - `template_id` (path, integer, required) - `format` (query, any, optional) — Output format: json, yaml, or llm :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/mail/templates/{template_id}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/mail/templates/{template_id}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/mail/templates/{template_id}", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/mail/templates/{template_id}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `PATCH` /api/v1/mail/templates/{template_id} **Update Template** Update an existing template. **Parameters** - `template_id` (path, integer, required) **Request body** :::codegroup ```bash curl -X PATCH 'https://spideriq.ai/api/v1/mail/templates/{template_id}' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "name": "string", "description": "string", "template_type": "signature", "html_source": "string", "text_source": "string", "variables": [ "string" ], "is_active": true, "is_default": true }' ``` ```python import httpx resp = httpx.patch( "https://spideriq.ai/api/v1/mail/templates/{template_id}", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"name": "string", "description": "string", "template_type": "signature", "html_source": "string", "text_source": "string", "variables": ["string"], "is_active": true, "is_default": true}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/mail/templates/{template_id}", { method: "PATCH", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"name": "string", "description": "string", "template_type": "signature", "html_source": "string", "text_source": "string", "variables": ["string"], "is_active": true, "is_default": true}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"name": "string", "description": "string", "template_type": "signature", "html_source": "string", "text_source": "string", "variables": ["string"], "is_active": true, "is_default": true}`) req, _ := http.NewRequest("PATCH", "https://spideriq.ai/api/v1/mail/templates/{template_id}", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `DELETE` /api/v1/mail/templates/{template_id} **Delete Template** Delete a template (soft delete by setting is_active=false). **Parameters** - `template_id` (path, integer, required) :::codegroup ```bash curl -X DELETE 'https://spideriq.ai/api/v1/mail/templates/{template_id}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.delete( "https://spideriq.ai/api/v1/mail/templates/{template_id}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/mail/templates/{template_id}", { method: "DELETE", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("DELETE", "https://spideriq.ai/api/v1/mail/templates/{template_id}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `204` — Successful Response - `422` — Validation Error ## `GET` /api/v1/mail/security/events **List Security Events** List security events for this client's mailboxes. Security events are logged when: - Prompt injection patterns are detected in inbound emails - Credential leaks are blocked in outbound emails - Messages are quarantined or released **Parameters** - `email` (query, any, optional) — Filter by mailbox email address - `event_type` (query, any, optional) — Filter by event type (injection_detected, credential_blocked, etc.) - `limit` (query, integer, optional) — Number of events to return - `offset` (query, integer, optional) — Offset for pagination - `format` (query, any, optional) — Output format: json, yaml, or llm :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/mail/security/events' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/mail/security/events", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/mail/security/events", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/mail/security/events", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/mail/quarantine **List Quarantined Messages** List quarantined messages for this client's mailboxes. Quarantined messages are inbound emails flagged with potential prompt injection attacks. They are stored but marked for admin review. **Parameters** - `email` (query, any, optional) — Filter by mailbox email address - `limit` (query, integer, optional) — Number of messages to return - `offset` (query, integer, optional) — Offset for pagination - `format` (query, any, optional) — Output format: json, yaml, or llm :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/mail/quarantine' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/mail/quarantine", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/mail/quarantine", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/mail/quarantine", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/mail/messages/{message_id}/release **Release From Quarantine** Release a message from quarantine after admin review. This marks the message as safe and logs a security event. The message will then be visible to agents. **Parameters** - `message_id` (path, integer, required) :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/mail/messages/{message_id}/release' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/mail/messages/{message_id}/release", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/mail/messages/{message_id}/release", { method: "POST", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/mail/messages/{message_id}/release", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/mail/session **Get Session** Get session context for AI agent bootstrap. Returns mailbox info, unread count, and recent messages in a single call. This is optimized for agents that need to quickly understand mailbox state. Use ?format=yaml for token-efficient agent consumption (~60% token savings). **Parameters** - `email` (query, string, required) — Mailbox email address - `include_recent` (query, integer, optional) — Number of recent messages to include - `format` (query, any, optional) — Output format: json or yaml :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/mail/session' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/mail/session", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/mail/session", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/mail/session", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/mail/compose/assist **Compose Assist** AI-powered email composition assistant. Actions: - write: Generate new email from subject - rewrite: Improve existing content - expand: Make content longer - shorten: Condense to key points - formal: Make more professional - casual: Make more relaxed - fix_grammar: Correct errors Uses LiteLLM proxy for model routing. **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/mail/compose/assist' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "action": "write", "context": "", "subject": "string", "tone": "professional", "thread_context": "string" }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/mail/compose/assist", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"action": "write", "context": "", "subject": "string", "tone": "professional", "thread_context": "string"}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/mail/compose/assist", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"action": "write", "context": "", "subject": "string", "tone": "professional", "thread_context": "string"}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"action": "write", "context": "", "subject": "string", "tone": "professional", "thread_context": "string"}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/mail/compose/assist", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/mail/mailboxes/{email}/analytics **Get Mailbox Analytics** Per-mailbox message analytics for the inbox sidebar (DEMOCK-PLAN #13). Returns `totals` (sent / received / replies / unread) and a `by_day` inbound-vs-outbound time-series over the last `days`. Warmup traffic is excluded throughout (mirrors the inbox default) so the numbers reflect real mail, not Smartlead warmup noise. `replies` = inbound messages with an In-Reply-To header (a subset of `received`). Uses the existing `(mailbox_id, date)` index; both aggregates are single table scans. **Parameters** - `email` (path, string, required) - `days` (query, integer, optional) — Look-back window in days. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/mail/mailboxes/{email}/analytics' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/mail/mailboxes/{email}/analytics", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/mail/mailboxes/{email}/analytics", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/mail/mailboxes/{email}/analytics", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/mail/usage-summary **Get Usage Summary** AI-usage KPI card (DEMOCK-PLAN #5 / D1 — "AI usage this month"). `emails_handled` is a real `mail_messages` count for this client, calendar period-to-date, warmup excluded. `ai_tokens` / `est_cost_usd` are returned null with `ai_metering_available=false`: `/compose/assist` calls the raw litellm proxy with a shared master key, so its usage is not attributable to a client in `gate_request_logs`. Surfacing real AI tokens requires routing compose/assist through SpiderGate V2 — a deferred metering slice. **Parameters** - `period` (query, string, optional) — Calendar-to-date window: week | month | year. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/mail/usage-summary' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/mail/usage-summary", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/mail/usage-summary", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/mail/usage-summary", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/mail/agent-access **Get Agent Access** Client-wide agent-token list for the Agent Access table (DEMOCK-PLAN #19 / D2). Lists this client's active (non-revoked) `agent_tokens` with owner email resolved from `agent_users`. `agent_tokens.client_id` stores the `cli_xxx` slug, so this keys on `client.slug` (NOT the UUID). Per-mailbox columns are intentionally omitted — tokens are client-scoped with no mailbox link. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/mail/agent-access' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/mail/agent-access", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/mail/agent-access", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/mail/agent-access", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response ## `POST` /api/v1/mail/convert **Convert a mail body between Markdown and HTML** Convert `content` between Markdown and HTML. Synchronous and CPU-bound (~1 ms/KB measured), so the work runs in ``asyncio.to_thread`` and never blocks the event loop — CLAUDE.md Rule 1, the same hazard class as the SSE busy-loop incident in docs/services/fastapi/LEARNINGS.md. **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/mail/convert' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "content": "# Hi\n\nSee the **report**.", "from": "markdown", "mode": "fit", "to": "html" }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/mail/convert", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"content": "# Hi\n\nSee the **report**.", "from": "markdown", "mode": "fit", "to": "html"}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/mail/convert", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"content": "# Hi\n\nSee the **report**.", "from": "markdown", "mode": "fit", "to": "html"}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"content": "# Hi\n\nSee the **report**.", "from": "markdown", "mode": "fit", "to": "html"}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/mail/convert", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## SpiderMail Templates Source: https://spideriq.ai/docs/api-reference/spidermail-templates ## `PUT` /api/v1/mail/templates/{template_id} **Update Template** Update an existing template. Only provided fields are updated. Note: `PATCH /mail/templates/{id}` in mail.py is a second, independent update handler. Both are live and reached by different callers — the agent surfaces (`@spideriq/core`, the skill schema, the CLI) use PUT; the dashboard's `useUpdateTemplate` uses PATCH. This one additionally validates Jinja2 syntax, rejects a duplicate name with 409, and unsets sibling defaults when `is_default` is set. **Parameters** - `template_id` (path, integer, required) **Request body** :::codegroup ```bash curl -X PUT 'https://spideriq.ai/api/v1/mail/templates/{template_id}' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "name": "string", "description": "string", "template_type": "signature", "html_source": "string", "text_source": "string", "variables": [ "string" ], "is_active": true, "is_default": true }' ``` ```python import httpx resp = httpx.put( "https://spideriq.ai/api/v1/mail/templates/{template_id}", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"name": "string", "description": "string", "template_type": "signature", "html_source": "string", "text_source": "string", "variables": ["string"], "is_active": true, "is_default": true}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/mail/templates/{template_id}", { method: "PUT", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"name": "string", "description": "string", "template_type": "signature", "html_source": "string", "text_source": "string", "variables": ["string"], "is_active": true, "is_default": true}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"name": "string", "description": "string", "template_type": "signature", "html_source": "string", "text_source": "string", "variables": ["string"], "is_active": true, "is_default": true}`) req, _ := http.NewRequest("PUT", "https://spideriq.ai/api/v1/mail/templates/{template_id}", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/mail/templates/{template_id}/preview **Preview Template** Preview a template with sample data. Renders the template with provided variables and returns the result. Useful for testing templates before using them in emails. The request body accepts `variables` (current) or `template_data` (legacy) — `MailTemplatePreviewRequest` aliases both onto the same field. **Parameters** - `template_id` (path, integer, required) **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/mail/templates/{template_id}/preview' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "variables": { "body": "Thank you for your interest in our services.", "company_name": "Acme Corp", "name": "John Doe" } }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/mail/templates/{template_id}/preview", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"variables": {"body": "Thank you for your interest in our services.", "company_name": "Acme Corp", "name": "John Doe"}}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/mail/templates/{template_id}/preview", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"variables": {"body": "Thank you for your interest in our services.", "company_name": "Acme Corp", "name": "John Doe"}}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"variables": {"body": "Thank you for your interest in our services.", "company_name": "Acme Corp", "name": "John Doe"}}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/mail/templates/{template_id}/preview", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## SpiderMail Views Source: https://spideriq.ai/docs/api-reference/spidermail-views ## `GET` /api/v1/mail/views **List Views** List every view visible to the caller: - views the caller created (`created_by = current_user_id`), AND - NULL-owned views (client-wide, Bearer-created), AND - `is_shared = TRUE` views if `include_shared=true`. Session users see their own views plus team-shared ones by default. Bearer-token callers see all NULL-owned views (there is no per-user scope). **Parameters** - `include_shared` (query, boolean, optional) — Include team-shared views. - `format` (query, any, optional) — Output format: json, yaml, or llm :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/mail/views' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/mail/views", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/mail/views", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/mail/views", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/mail/views **Create View** Create a new saved view. `is_shared` defaults to false (default-private). `created_by` is filled from the session user; Bearer callers get NULL. **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/mail/views' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "name": "string", "color": "string", "filter_config": { "mailboxes": [ "string" ], "unread_only": false, "starred_only": false, "has_attachments": true, "from_contains": "string", "subject_contains": "string", "labels": [ "string" ], "received_after": "string", "received_before": "string", "outreach_classification": "string", "direction": "string" }, "column_config": [ { "key": "string", "width": 0, "visible": true } ], "sort_by": "date", "sort_direction": "DESC", "is_shared": false }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/mail/views", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"name": "string", "color": "string", "filter_config": {"mailboxes": ["string"], "unread_only": false, "starred_only": false, "has_attachments": true, "from_contains": "string", "subject_contains": "string", "labels": ["string"], "received_after": "string", "received_before": "string", "outreach_classification": "string", "direction": "string"}, "column_config": [{"key": "string", "width": 0, "visible": true}], "sort_by": "date", "sort_direction": "DESC", "is_shared": false}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/mail/views", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"name": "string", "color": "string", "filter_config": {"mailboxes": ["string"], "unread_only": false, "starred_only": false, "has_attachments": true, "from_contains": "string", "subject_contains": "string", "labels": ["string"], "received_after": "string", "received_before": "string", "outreach_classification": "string", "direction": "string"}, "column_config": [{"key": "string", "width": 0, "visible": true}], "sort_by": "date", "sort_direction": "DESC", "is_shared": false}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"name": "string", "color": "string", "filter_config": {"mailboxes": ["string"], "unread_only": false, "starred_only": false, "has_attachments": true, "from_contains": "string", "subject_contains": "string", "labels": ["string"], "received_after": "string", "received_before": "string", "outreach_classification": "string", "direction": "string"}, "column_config": [{"key": "string", "width": 0, "visible": true}], "sort_by": "date", "sort_direction": "DESC", "is_shared": false}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/mail/views", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `201` — Successful Response - `422` — Validation Error ## `GET` /api/v1/mail/views/{view_id} **Get View** Get one saved view. Use ?format=yaml for agent-friendly output. Card FMT.1 listed eleven lying routes, derived from the skill schema — which does not advertise `format` here. The MCP `get_view` tool does (`packages/mcp-tools/src/mail/mail.ts`), so this route had the same silent-200 defect on the surface the card did not read. It surfaced from the registry-wide parity guard, not from the list. **Parameters** - `view_id` (path, integer, required) - `format` (query, any, optional) — Output format: json, yaml, or llm :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/mail/views/{view_id}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/mail/views/{view_id}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/mail/views/{view_id}", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/mail/views/{view_id}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `PATCH` /api/v1/mail/views/{view_id} **Update View** **Parameters** - `view_id` (path, integer, required) **Request body** :::codegroup ```bash curl -X PATCH 'https://spideriq.ai/api/v1/mail/views/{view_id}' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "name": "string", "color": "string", "filter_config": { "mailboxes": [ "string" ], "unread_only": false, "starred_only": false, "has_attachments": true, "from_contains": "string", "subject_contains": "string", "labels": [ "string" ], "received_after": "string", "received_before": "string", "outreach_classification": "string", "direction": "string" }, "column_config": [ { "key": "string", "width": 0, "visible": true } ], "sort_by": "string", "sort_direction": "string", "is_shared": true }' ``` ```python import httpx resp = httpx.patch( "https://spideriq.ai/api/v1/mail/views/{view_id}", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"name": "string", "color": "string", "filter_config": {"mailboxes": ["string"], "unread_only": false, "starred_only": false, "has_attachments": true, "from_contains": "string", "subject_contains": "string", "labels": ["string"], "received_after": "string", "received_before": "string", "outreach_classification": "string", "direction": "string"}, "column_config": [{"key": "string", "width": 0, "visible": true}], "sort_by": "string", "sort_direction": "string", "is_shared": true}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/mail/views/{view_id}", { method: "PATCH", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"name": "string", "color": "string", "filter_config": {"mailboxes": ["string"], "unread_only": false, "starred_only": false, "has_attachments": true, "from_contains": "string", "subject_contains": "string", "labels": ["string"], "received_after": "string", "received_before": "string", "outreach_classification": "string", "direction": "string"}, "column_config": [{"key": "string", "width": 0, "visible": true}], "sort_by": "string", "sort_direction": "string", "is_shared": true}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"name": "string", "color": "string", "filter_config": {"mailboxes": ["string"], "unread_only": false, "starred_only": false, "has_attachments": true, "from_contains": "string", "subject_contains": "string", "labels": ["string"], "received_after": "string", "received_before": "string", "outreach_classification": "string", "direction": "string"}, "column_config": [{"key": "string", "width": 0, "visible": true}], "sort_by": "string", "sort_direction": "string", "is_shared": true}`) req, _ := http.NewRequest("PATCH", "https://spideriq.ai/api/v1/mail/views/{view_id}", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `DELETE` /api/v1/mail/views/{view_id} **Delete View** **Parameters** - `view_id` (path, integer, required) :::codegroup ```bash curl -X DELETE 'https://spideriq.ai/api/v1/mail/views/{view_id}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.delete( "https://spideriq.ai/api/v1/mail/views/{view_id}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/mail/views/{view_id}", { method: "DELETE", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("DELETE", "https://spideriq.ai/api/v1/mail/views/{view_id}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `204` — Successful Response - `422` — Validation Error ## SpiderMail Webhooks Source: https://spideriq.ai/docs/api-reference/spidermail-webhooks ## `GET` /api/v1/mail/webhooks **List Webhooks** List this client's registered inbound webhooks. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/mail/webhooks' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/mail/webhooks", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/mail/webhooks", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/mail/webhooks", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response ## `POST` /api/v1/mail/webhooks **Create Webhook** Register a new inbound webhook for this client. **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/mail/webhooks' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "target_url": "https://example.com", "secret": "string", "events": [ "string" ], "mailbox_filter": [ "string" ], "description": "string", "is_active": true }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/mail/webhooks", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"target_url": "https://example.com", "secret": "string", "events": ["string"], "mailbox_filter": ["string"], "description": "string", "is_active": true}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/mail/webhooks", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"target_url": "https://example.com", "secret": "string", "events": ["string"], "mailbox_filter": ["string"], "description": "string", "is_active": true}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"target_url": "https://example.com", "secret": "string", "events": ["string"], "mailbox_filter": ["string"], "description": "string", "is_active": true}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/mail/webhooks", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `201` — Successful Response - `422` — Validation Error ## `PATCH` /api/v1/mail/webhooks/{webhook_id} **Update Webhook** Update a webhook. Only the owning client may mutate it. **Parameters** - `webhook_id` (path, string, required) **Request body** :::codegroup ```bash curl -X PATCH 'https://spideriq.ai/api/v1/mail/webhooks/{webhook_id}' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "target_url": "https://example.com", "secret": "string", "events": [ "string" ], "mailbox_filter": [ "string" ], "description": "string", "is_active": true }' ``` ```python import httpx resp = httpx.patch( "https://spideriq.ai/api/v1/mail/webhooks/{webhook_id}", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"target_url": "https://example.com", "secret": "string", "events": ["string"], "mailbox_filter": ["string"], "description": "string", "is_active": true}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/mail/webhooks/{webhook_id}", { method: "PATCH", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"target_url": "https://example.com", "secret": "string", "events": ["string"], "mailbox_filter": ["string"], "description": "string", "is_active": true}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"target_url": "https://example.com", "secret": "string", "events": ["string"], "mailbox_filter": ["string"], "description": "string", "is_active": true}`) req, _ := http.NewRequest("PATCH", "https://spideriq.ai/api/v1/mail/webhooks/{webhook_id}", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `DELETE` /api/v1/mail/webhooks/{webhook_id} **Delete Webhook** Delete a webhook. Only the owning client may delete it. **Parameters** - `webhook_id` (path, string, required) :::codegroup ```bash curl -X DELETE 'https://spideriq.ai/api/v1/mail/webhooks/{webhook_id}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.delete( "https://spideriq.ai/api/v1/mail/webhooks/{webhook_id}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/mail/webhooks/{webhook_id}", { method: "DELETE", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("DELETE", "https://spideriq.ai/api/v1/mail/webhooks/{webhook_id}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `204` — Successful Response - `422` — Validation Error ## Email Admin Source: https://spideriq.ai/docs/api-reference/email-admin ## `GET` /api/v1/mail-admin/connections **List Connections** **Parameters** - `brand_id` (query, any, optional) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/mail-admin/connections' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/mail-admin/connections", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/mail-admin/connections", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/mail-admin/connections", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/mail-admin/connections **Connect** **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/mail-admin/connections' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "brand_id": 0, "provider": "zoho", "data_center": "eu", "org_external_id": "string", "vault_key_ref": "string", "label": "string", "end_client_name": "string", "refresh_token": "string", "client_id": "string", "client_secret": "string", "scope": "string" }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/mail-admin/connections", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"brand_id": 0, "provider": "zoho", "data_center": "eu", "org_external_id": "string", "vault_key_ref": "string", "label": "string", "end_client_name": "string", "refresh_token": "string", "client_id": "string", "client_secret": "string", "scope": "string"}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/mail-admin/connections", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"brand_id": 0, "provider": "zoho", "data_center": "eu", "org_external_id": "string", "vault_key_ref": "string", "label": "string", "end_client_name": "string", "refresh_token": "string", "client_id": "string", "client_secret": "string", "scope": "string"}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"brand_id": 0, "provider": "zoho", "data_center": "eu", "org_external_id": "string", "vault_key_ref": "string", "label": "string", "end_client_name": "string", "refresh_token": "string", "client_id": "string", "client_secret": "string", "scope": "string"}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/mail-admin/connections", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `201` — Successful Response - `422` — Validation Error ## `POST` /api/v1/mail-admin/connections/{connection_id}/revoke **Revoke Connection** **Parameters** - `connection_id` (path, string, required) :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/mail-admin/connections/{connection_id}/revoke' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/mail-admin/connections/{connection_id}/revoke", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/mail-admin/connections/{connection_id}/revoke", { method: "POST", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/mail-admin/connections/{connection_id}/revoke", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/mail-admin/connections/{connection_id}/mailboxes **List Mailboxes** **Parameters** - `connection_id` (path, string, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/mail-admin/connections/{connection_id}/mailboxes' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/mail-admin/connections/{connection_id}/mailboxes", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/mail-admin/connections/{connection_id}/mailboxes", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/mail-admin/connections/{connection_id}/mailboxes", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/mail-admin/connections/{connection_id}/mailboxes **Provision Mailbox** **Parameters** - `connection_id` (path, string, required) **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/mail-admin/connections/{connection_id}/mailboxes' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "local_part": "string", "domain": "string", "password": "string", "display_name": "string" }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/mail-admin/connections/{connection_id}/mailboxes", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"local_part": "string", "domain": "string", "password": "string", "display_name": "string"}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/mail-admin/connections/{connection_id}/mailboxes", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"local_part": "string", "domain": "string", "password": "string", "display_name": "string"}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"local_part": "string", "domain": "string", "password": "string", "display_name": "string"}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/mail-admin/connections/{connection_id}/mailboxes", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `201` — Successful Response - `422` — Validation Error ## `POST` /api/v1/mail-admin/connections/{connection_id}/enable-imap **Enable Imap** **Parameters** - `connection_id` (path, string, required) **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/mail-admin/connections/{connection_id}/enable-imap' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "targets": [ { "account_id": "string", "zuid": "string", "email": "string" } ] }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/mail-admin/connections/{connection_id}/enable-imap", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"targets": [{"account_id": "string", "zuid": "string", "email": "string"}]}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/mail-admin/connections/{connection_id}/enable-imap", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"targets": [{"account_id": "string", "zuid": "string", "email": "string"}]}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"targets": [{"account_id": "string", "zuid": "string", "email": "string"}]}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/mail-admin/connections/{connection_id}/enable-imap", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/mail-admin/connections/{connection_id}/set-password **Set Password** **Parameters** - `connection_id` (path, string, required) **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/mail-admin/connections/{connection_id}/set-password' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "account_id": "string", "zuid": "string", "password": "string", "email": "string" }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/mail-admin/connections/{connection_id}/set-password", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"account_id": "string", "zuid": "string", "password": "string", "email": "string"}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/mail-admin/connections/{connection_id}/set-password", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"account_id": "string", "zuid": "string", "password": "string", "email": "string"}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"account_id": "string", "zuid": "string", "password": "string", "email": "string"}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/mail-admin/connections/{connection_id}/set-password", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/mail-admin/connections/{connection_id}/register **Register Mailbox** **Parameters** - `connection_id` (path, string, required) **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/mail-admin/connections/{connection_id}/register' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "email": "string", "password": "string", "display_name": "string", "imap_host": "string", "imap_port": 0, "smtp_host": "string", "smtp_port": 0, "client_id": "00000000-0000-0000-0000-000000000000" }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/mail-admin/connections/{connection_id}/register", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"email": "string", "password": "string", "display_name": "string", "imap_host": "string", "imap_port": 0, "smtp_host": "string", "smtp_port": 0, "client_id": "00000000-0000-0000-0000-000000000000"}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/mail-admin/connections/{connection_id}/register", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"email": "string", "password": "string", "display_name": "string", "imap_host": "string", "imap_port": 0, "smtp_host": "string", "smtp_port": 0, "client_id": "00000000-0000-0000-0000-000000000000"}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"email": "string", "password": "string", "display_name": "string", "imap_host": "string", "imap_port": 0, "smtp_host": "string", "smtp_port": 0, "client_id": "00000000-0000-0000-0000-000000000000"}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/mail-admin/connections/{connection_id}/register", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `201` — Successful Response - `422` — Validation Error ## `POST` /api/v1/mail-admin/connections/{connection_id}/provision-and-link **Provision And Link** **Parameters** - `connection_id` (path, string, required) **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/mail-admin/connections/{connection_id}/provision-and-link' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "local_part": "string", "domain": "string", "password": "string", "display_name": "string", "account_id": "string", "zuid": "string", "client_id": "00000000-0000-0000-0000-000000000000" }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/mail-admin/connections/{connection_id}/provision-and-link", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"local_part": "string", "domain": "string", "password": "string", "display_name": "string", "account_id": "string", "zuid": "string", "client_id": "00000000-0000-0000-0000-000000000000"}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/mail-admin/connections/{connection_id}/provision-and-link", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"local_part": "string", "domain": "string", "password": "string", "display_name": "string", "account_id": "string", "zuid": "string", "client_id": "00000000-0000-0000-0000-000000000000"}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"local_part": "string", "domain": "string", "password": "string", "display_name": "string", "account_id": "string", "zuid": "string", "client_id": "00000000-0000-0000-0000-000000000000"}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/mail-admin/connections/{connection_id}/provision-and-link", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `201` — Successful Response - `422` — Validation Error ## `POST` /api/v1/mail-admin/connections/{connection_id}/health-check **Health Check** **Parameters** - `connection_id` (path, string, required) **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/mail-admin/connections/{connection_id}/health-check' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "email": "string", "password": "string" }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/mail-admin/connections/{connection_id}/health-check", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"email": "string", "password": "string"}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/mail-admin/connections/{connection_id}/health-check", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"email": "string", "password": "string"}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"email": "string", "password": "string"}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/mail-admin/connections/{connection_id}/health-check", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/mail-admin/connections/{connection_id}/add-to-smartlead **Add To Smartlead** **Parameters** - `connection_id` (path, string, required) **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/mail-admin/connections/{connection_id}/add-to-smartlead' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "email": "string", "smartlead_connection_id": 0, "max_email_per_day": 50, "warmup_enabled": false }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/mail-admin/connections/{connection_id}/add-to-smartlead", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"email": "string", "smartlead_connection_id": 0, "max_email_per_day": 50, "warmup_enabled": false}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/mail-admin/connections/{connection_id}/add-to-smartlead", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"email": "string", "smartlead_connection_id": 0, "max_email_per_day": 50, "warmup_enabled": false}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"email": "string", "smartlead_connection_id": 0, "max_email_per_day": 50, "warmup_enabled": false}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/mail-admin/connections/{connection_id}/add-to-smartlead", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `201` — Successful Response - `422` — Validation Error ## `GET` /api/v1/mail-admin/audit **List Audit** **Parameters** - `brand_id` (query, any, optional) - `connection_id` (query, any, optional) - `limit` (query, integer, optional) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/mail-admin/audit' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/mail-admin/audit", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/mail-admin/audit", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/mail-admin/audit", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## SpiderMail Attachments Source: https://spideriq.ai/docs/api-reference/spidermail-attachments ## `GET` /api/v1/mail/attachments/{attachment_id} **Get Attachment Content** Retrieve attachment content by attachment ID. This endpoint supports the three-layer progressive disclosure pattern: - **Layer 1 (summary)**: LLM-generated summary (~100 tokens) - **Layer 2 (preview)**: First 1500 characters of extracted text - **Layer 3 (full)**: Complete extracted text content (default) The attachment must belong to a mailbox owned by the authenticated client. Returns `text/plain` response for easy agent consumption. Use this endpoint when an agent needs deeper analysis of an attachment that was summarized in the email payload. **Parameters** - `attachment_id` (path, string, required) - `layer` (query, any, optional) — Content layer to retrieve: 'summary' (Layer 1), 'preview' (Layer 2), or 'full' (Layer 3, default) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/mail/attachments/{attachment_id}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/mail/attachments/{attachment_id}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/mail/attachments/{attachment_id}", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/mail/attachments/{attachment_id}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/mail/attachments/by-key/{storage_key} **Get Attachment By Storage Key** Retrieve attachment content by storage key. This is an alternative endpoint for agents that have the storage_key from the email payload's `retrieve_key` field. The storage key must correspond to an attachment owned by the client. **Parameters** - `storage_key` (path, string, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/mail/attachments/by-key/{storage_key}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/mail/attachments/by-key/{storage_key}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/mail/attachments/by-key/{storage_key}", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/mail/attachments/by-key/{storage_key}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/mail/attachments/message/{message_id} **List Message Attachments** List all attachments for a message. Returns Layer 1 (summary) and Layer 2 (preview) for each attachment, along with metadata and retrieve keys for fetching full content. **Parameters** - `message_id` (path, integer, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/mail/attachments/message/{message_id}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/mail/attachments/message/{message_id}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/mail/attachments/message/{message_id}", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/mail/attachments/message/{message_id}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## Dashboard Source: https://spideriq.ai/docs/api-reference/dashboard ## `GET` /api/v1/dashboard/me **Get Current User** Return the current authenticated dashboard user's info and role. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/me' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/me", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/me", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/me", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response ## `PATCH` /api/v1/dashboard/me/preferences **Update My Preferences** Update per-user UI preferences for the current dashboard user. Today the only field is `event_sounds_enabled`, the Live Theater audio toggle (Stage D). Returns the full updated `/me` payload so the client can refresh its cache from a single response. **Request body** :::codegroup ```bash curl -X PATCH 'https://spideriq.ai/api/v1/dashboard/me/preferences' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "event_sounds_enabled": true }' ``` ```python import httpx resp = httpx.patch( "https://spideriq.ai/api/v1/dashboard/me/preferences", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"event_sounds_enabled": true}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/me/preferences", { method: "PATCH", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"event_sounds_enabled": true}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"event_sounds_enabled": true}`) req, _ := http.NewRequest("PATCH", "https://spideriq.ai/api/v1/dashboard/me/preferences", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/dashboard/users **List Dashboard Users** List all dashboard users with their brand memberships. Super admin only. **Parameters** - `role` (query, any, optional) - `search` (query, any, optional) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/users' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/users", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/users", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/users", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/dashboard/users **Create Dashboard User** Provision a Better Auth user for dashboard access. Super admin only. **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/dashboard/users' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "user_id": "string", "role": "client_user", "client_id": "string", "permissions": [ "string" ] }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/dashboard/users", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"user_id": "string", "role": "client_user", "client_id": "string", "permissions": ["string"]}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/users", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"user_id": "string", "role": "client_user", "client_id": "string", "permissions": ["string"]}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"user_id": "string", "role": "client_user", "client_id": "string", "permissions": ["string"]}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/dashboard/users", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `201` — Successful Response - `422` — Validation Error ## `PATCH` /api/v1/dashboard/users/{user_id} **Update Dashboard User** Update a dashboard user's role, permissions, or active status. Super admin only. **Parameters** - `user_id` (path, string, required) **Request body** :::codegroup ```bash curl -X PATCH 'https://spideriq.ai/api/v1/dashboard/users/{user_id}' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "role": "string", "client_id": "string", "permissions": [ "string" ], "is_active": true }' ``` ```python import httpx resp = httpx.patch( "https://spideriq.ai/api/v1/dashboard/users/{user_id}", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"role": "string", "client_id": "string", "permissions": ["string"], "is_active": true}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/users/{user_id}", { method: "PATCH", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"role": "string", "client_id": "string", "permissions": ["string"], "is_active": true}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"role": "string", "client_id": "string", "permissions": ["string"], "is_active": true}`) req, _ := http.NewRequest("PATCH", "https://spideriq.ai/api/v1/dashboard/users/{user_id}", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `DELETE` /api/v1/dashboard/users/{user_id} **Delete Dashboard User** Fully remove a user from the platform. Super admin only. Removing a user in /admin/users used to delete ONLY the ``dashboard_users`` row, which hid the user from the admin list while leaving them an active brand member with live API tokens. This does the full cascade in one transaction: 1. Revoke every live agent token (PAT) issued to the user's email. 2. Mark the user's pending/active PAT requests ``revoked``. 3. Drop every brand membership (removes them from ALL brands). 4. Delete the user_profiles row. 5. Delete the dashboard_users row (dashboard access). 6. Delete the Better Auth ``user`` row (cascades account + session). Tokens are revoked (not deleted) so their billing/usage history survives. Identity/membership rows are hard-deleted, freeing the email for re-invite. **Parameters** - `user_id` (path, string, required) :::codegroup ```bash curl -X DELETE 'https://spideriq.ai/api/v1/dashboard/users/{user_id}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.delete( "https://spideriq.ai/api/v1/dashboard/users/{user_id}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/users/{user_id}", { method: "DELETE", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("DELETE", "https://spideriq.ai/api/v1/dashboard/users/{user_id}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/dashboard/users/{user_id}/brands **Add User To Brand** Directly add a user to a brand. Super admin only. Unlike the invitation flow, this immediately adds the user as a member. **Parameters** - `user_id` (path, string, required) **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/dashboard/users/{user_id}/brands' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "user_id": "string", "brand_id": 0, "role": "member" }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/dashboard/users/{user_id}/brands", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"user_id": "string", "brand_id": 0, "role": "member"}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/users/{user_id}/brands", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"user_id": "string", "brand_id": 0, "role": "member"}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"user_id": "string", "brand_id": 0, "role": "member"}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/dashboard/users/{user_id}/brands", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `201` — Successful Response - `422` — Validation Error ## `POST` /api/v1/dashboard/credentials/regenerate **Regenerate Client Credentials** Regenerate API credentials for the current user's client. - brand_admin and client_user: Regenerates their own client's credentials - super_admin: Must not use this endpoint (they don't have a client) Returns new credentials. Save them immediately - they cannot be retrieved again. :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/dashboard/credentials/regenerate' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/dashboard/credentials/regenerate", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/credentials/regenerate", { method: "POST", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/dashboard/credentials/regenerate", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response ## `GET` /api/v1/dashboard/users/{user_id}/generate-password **Generate Password For User** Generate a secure random password. Super admin only. Returns a 16-character password with mixed case, digits, and symbols. The password is NOT automatically set - use /set-password to apply it. **Parameters** - `user_id` (path, string, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/users/{user_id}/generate-password' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/users/{user_id}/generate-password", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/users/{user_id}/generate-password", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/users/{user_id}/generate-password", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/dashboard/users/{user_id}/set-password **Set User Password** Set a user's password directly. Super admin only. Updates the password in Better Auth's account table using scrypt hashing. Optionally sends the new password to the user via email. **Parameters** - `user_id` (path, string, required) **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/dashboard/users/{user_id}/set-password' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "new_password": "string", "send_email": false }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/dashboard/users/{user_id}/set-password", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"new_password": "string", "send_email": false}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/users/{user_id}/set-password", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"new_password": "string", "send_email": false}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"new_password": "string", "send_email": false}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/dashboard/users/{user_id}/set-password", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/dashboard/users/{user_id}/trigger-reset **Trigger Password Reset** Trigger a password reset email for a user. Super admin only. Creates a reset token and sends the password reset email to the user. **Parameters** - `user_id` (path, string, required) :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/dashboard/users/{user_id}/trigger-reset' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/dashboard/users/{user_id}/trigger-reset", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/users/{user_id}/trigger-reset", { method: "POST", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/dashboard/users/{user_id}/trigger-reset", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## Dashboard Client Source: https://spideriq.ai/docs/api-reference/dashboard-client ## `POST` /api/v1/dashboard/client/jobs/spiderMaps/submit **Submit Spider Maps Job** Submit a SpiderMaps job via dashboard session auth. This wraps the existing job submission logic but uses session cookies instead of Bearer token authentication. **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/dashboard/client/jobs/spiderMaps/submit' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{}' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/dashboard/client/jobs/spiderMaps/submit", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/client/jobs/spiderMaps/submit", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/dashboard/client/jobs/spiderMaps/submit", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/dashboard/client/jobs **List Client Jobs** List jobs for the authenticated client user. Multi-tenant scoping per ``resolve_client_scope``: - super_admin without scope header → all jobs (unfiltered) - super_admin + X-Selected-Client-Id → that client's jobs - super_admin + X-Brand-ID → all jobs in that brand - non-super_admin with client_id → that client's jobs - non-super_admin with brand_id → all jobs in their brand **Parameters** - `page` (query, integer, optional) - `page_size` (query, integer, optional) - `type_filter` (query, any, optional) - `status_filter` (query, any, optional) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/client/jobs' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/client/jobs", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/client/jobs", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/client/jobs", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/dashboard/client/jobs/{job_id}/status **Get Job Status** Get status for a specific job. **Parameters** - `job_id` (path, string, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/client/jobs/{job_id}/status' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/client/jobs/{job_id}/status", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/client/jobs/{job_id}/status", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/client/jobs/{job_id}/status", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/dashboard/client/jobs/{job_id}/results **Get Job Results** Get results for a completed job. Multi-tenant scoping per ``resolve_client_scope``: - super_admin (any scope) → any job - non-super_admin with client_id or brand_id → only their client/brand **Parameters** - `job_id` (path, string, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/client/jobs/{job_id}/results' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/client/jobs/{job_id}/results", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/client/jobs/{job_id}/results", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/client/jobs/{job_id}/results", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/dashboard/client/jobs/{job_id}/cancel **Cancel Job** Cancel a queued or processing job. Multi-tenant scoping per ``resolve_client_scope``: - super_admin (any scope) → any job - non-super_admin with client_id or brand_id → only their client/brand **Parameters** - `job_id` (path, string, required) :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/dashboard/client/jobs/{job_id}/cancel' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/dashboard/client/jobs/{job_id}/cancel", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/client/jobs/{job_id}/cancel", { method: "POST", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/dashboard/client/jobs/{job_id}/cancel", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/dashboard/client/research/submit **Submit Company Research** Submit a company research job via dashboard session auth. Supports both super_admin (with X-Selected-Client-Id header) and client users. **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/dashboard/client/research/submit' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{}' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/dashboard/client/research/submit", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/client/research/submit", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/dashboard/client/research/submit", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/dashboard/client/research **List Company Research** List company research jobs for the authenticated user. Multi-tenant scoping per ``resolve_client_scope``: - super_admin without scope → all research jobs - super_admin + X-Selected-Client-Id → that client's research - super_admin + X-Brand-ID → all research in brand - non-super_admin with client_id → that client's research - non-super_admin with brand_id → all research in their brand **Parameters** - `page` (query, integer, optional) - `page_size` (query, integer, optional) - `status` (query, any, optional) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/client/research' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/client/research", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/client/research", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/client/research", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/dashboard/client/research/{research_id}/status **Get Research Status** Get status for a specific research job. Multi-tenant scoping per ``resolve_client_scope``: - super_admin (any scope) → any research - non-super_admin → only their client's or brand's research **Parameters** - `research_id` (path, string, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/client/research/{research_id}/status' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/client/research/{research_id}/status", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/client/research/{research_id}/status", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/client/research/{research_id}/status", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/dashboard/client/research/{research_id}/results **Get Research Results** Get results for a completed research job. Multi-tenant scoping per ``resolve_client_scope``: - super_admin (any scope) → any research - non-super_admin → only their client's or brand's research **Parameters** - `research_id` (path, string, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/client/research/{research_id}/results' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/client/research/{research_id}/results", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/client/research/{research_id}/results", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/client/research/{research_id}/results", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/dashboard/client/research/{research_id}/pause **Pause Research** Pause a running research job. **Parameters** - `research_id` (path, string, required) :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/dashboard/client/research/{research_id}/pause' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/dashboard/client/research/{research_id}/pause", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/client/research/{research_id}/pause", { method: "POST", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/dashboard/client/research/{research_id}/pause", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/dashboard/client/research/{research_id}/resume **Resume Research** Resume a paused research job. **Parameters** - `research_id` (path, string, required) :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/dashboard/client/research/{research_id}/resume' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/dashboard/client/research/{research_id}/resume", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/client/research/{research_id}/resume", { method: "POST", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/dashboard/client/research/{research_id}/resume", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/dashboard/client/research/{research_id}/cancel **Cancel Research** Cancel a research job. **Parameters** - `research_id` (path, string, required) :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/dashboard/client/research/{research_id}/cancel' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/dashboard/client/research/{research_id}/cancel", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/client/research/{research_id}/cancel", { method: "POST", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/dashboard/client/research/{research_id}/cancel", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/dashboard/client/locations/countries **List Countries Dashboard** List all available countries with location counts. Used for campaign creation country dropdown. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/client/locations/countries' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/client/locations/countries", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/client/locations/countries", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/client/locations/countries", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response ## `GET` /api/v1/dashboard/client/locations/selectable-units **List Selectable Units Dashboard** Flat, alphabetically merged list of selectable geo units for searchable pickers (the "smart box" typeahead). Each unit is either a whole country (``kind="country"``) or a US state (``kind="state"``). When ``states_as_units`` is true the US country row is omitted and its states are injected as top-level units, side by side with countries — type "flor" → Florida, "germ" → Germany. This makes selecting "the whole USA" impossible by construction (the volume guard is the backstop). When false, every country (US included) is returned as-is. **Parameters** - `states_as_units` (query, boolean, optional) — When true, countries in the states-as-units set (US) are dropped and replaced by their admin regions (states) as top-level selectable units — so 'all of USA' cannot be picked for a ZIP campaign. When false, returns plain countries (US included as a whole) — for address/attribute pickers where the country is the correct unit. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/client/locations/selectable-units' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/client/locations/selectable-units", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/client/locations/selectable-units", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/client/locations/selectable-units", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/dashboard/client/locations/regions **List Regions Dashboard** List all admin regions (states/provinces) for a country. Used for campaign creation region filter. **Parameters** - `country_code` (query, string, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/client/locations/regions' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/client/locations/regions", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/client/locations/regions", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/client/locations/regions", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/dashboard/client/locations/stats **Get Country Stats Dashboard** Get location statistics for a country. Used to show how many locations match different filter modes. **Parameters** - `country_code` (query, string, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/client/locations/stats' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/client/locations/stats", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/client/locations/stats", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/client/locations/stats", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/dashboard/client/locations **List Locations Dashboard** List locations with filtering and pagination. Used for campaign creation location selection (custom mode). **Parameters** - `country_code` (query, any, optional) - `search` (query, any, optional) - `location_type` (query, any, optional) - `page` (query, integer, optional) - `page_size` (query, integer, optional) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/client/locations' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/client/locations", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/client/locations", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/client/locations", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## Dashboard Campaigns Source: https://spideriq.ai/docs/api-reference/dashboard-campaigns ## `GET` /api/v1/dashboard/client/campaigns/quota **Get Quota Status** Get the current campaign quota status for the authenticated client. Returns remaining quotas for daily, monthly, and concurrent campaigns. For super_admin without a selected client, returns unlimited quotas. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/client/campaigns/quota' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/client/campaigns/quota", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/client/campaigns/quota", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/client/campaigns/quota", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response ## `GET` /api/v1/dashboard/client/campaigns/active-locations **List Active Locations** Return lat/lng dots for the Live Theater Tactical Map. V1.0 (default flags off, back-compat): just queued/in-progress location dots (gray). Powers the queued layer when a campaign first starts. V1.1 (flags on): full geographic state of the user's active campaigns. Adds dim-gold completed-location dots (persist while campaign is active) and bright-gold per-business dots (one per business found at each location). Solves the "map empty after dots age out of SSE buffer" UX bug. Multi-tenant scoped: - super_admin + X-Selected-Client-Id → that client - super_admin + X-Brand-ID only → all clients in that brand - super_admin without scope → empty list - client_user / brand_admin w/ client_id → that client - brand-scoped user (client_id NULL, brand_id set) → all clients in that brand - non-super_admin without scope → empty list Total response capped at 5000 rows when include_businesses=true, 500 otherwise. **Parameters** - `include_completed` (query, boolean, optional) — If true, also return dim-gold dots for completed locations of active campaigns. - `include_businesses` (query, boolean, optional) — If true, also return bright-gold per-business dots from public.results.data->'businesses'. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/client/campaigns/active-locations' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/client/campaigns/active-locations", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/client/campaigns/active-locations", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/client/campaigns/active-locations", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/dashboard/client/campaigns **List Campaigns** List campaigns for the authenticated client. For super_admin: Can filter by X-Selected-Client-Id or X-Brand-ID header. For client_user/brand_admin: Filter by X-Brand-ID header or their client_id. **Parameters** - `page` (query, integer, optional) - `page_size` (query, integer, optional) - `status` (query, any, optional) - `country_code` (query, any, optional) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/client/campaigns' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/client/campaigns", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/client/campaigns", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/client/campaigns", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/dashboard/client/campaigns **Create Campaign** Create a new SpiderMaps campaign. Enforces quota limits before creation. **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/dashboard/client/campaigns' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "search_query": "string", "query": "string", "country_code": "string", "name": "string", "filter": { "mode": "all", "min_population": 0, "max_population": 0, "location_ids": [ 0 ], "admin_regions": [ "string" ], "parent_city": "string", "include_postcodes": false, "exhaustive": false }, "max_results": 100, "extract_reviews": false, "extract_photos": false, "lang": "en", "store_images": true, "validate_phones": true, "fuzziq_enabled": true, "fuzziq_unique_only": true, "skip_proxy": false, "test": false, "workflow": { "spidersite": { "enabled": false, "mode": {}, "max_pages": 25, "crawl_strategy": "bestfirst", "target_pages": [ "contact", "about", "team", "news", "blog" ], "enable_spa": true, "spa_timeout": 30, "extract_team": false, "extract_company_info": false, "extract_pain_points": false, "product_description": {}, "icp_description": {}, "compendium": {}, "timeout": 30 }, "spiderverify": { "enabled": false, "check_gravatar": false, "check_dnsbl": false, "smtp_timeout_secs": 45, "max_emails_per_business": 10 }, "vayapin": { "enabled": false }, "social_media_enrichment": { "enabled": true }, "smartlead": { "enabled": false, "connection_id": {}, "remote_campaign_id": {}, "remote_campaign_name": {}, "limit": {}, "only_with_vayapin_seo": false, "only_with_vayapin_pin": false, "field_map": {} }, "filter_social_media": true, "filter_review_sites": true, "filter_directories": true, "filter_maps": true } }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/dashboard/client/campaigns", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"search_query": "string", "query": "string", "country_code": "string", "name": "string", "filter": {"mode": "all", "min_population": 0, "max_population": 0, "location_ids": [0], "admin_regions": ["string"], "parent_city": "string", "include_postcodes": false, "exhaustive": false}, "max_results": 100, "extract_reviews": false, "extract_photos": false, "lang": "en", "store_images": true, "validate_phones": true, "fuzziq_enabled": true, "fuzziq_unique_only": true, "skip_proxy": false, "test": false, "workflow": {"spidersite": {"enabled": false, "mode": {}, "max_pages": 25, "crawl_strategy": "bestfirst", "target_pages": ["contact", "about", "team", "news", "blog"], "enable_spa": true, "spa_timeout": 30, "extract_team": false, "extract_company_info": false, "extract_pain_points": false, "product_description": {}, "icp_description": {}, "compendium": {}, "timeout": 30}, "spiderverify": {"enabled": false, "check_gravatar": false, "check_dnsbl": false, "smtp_timeout_secs": 45, "max_emails_per_business": 10}, "vayapin": {"enabled": false}, "social_media_enrichment": {"enabled": true}, "smartlead": {"enabled": false, "connection_id": {}, "remote_campaign_id": {}, "remote_campaign_name": {}, "limit": {}, "only_with_vayapin_seo": false, "only_with_vayapin_pin": false, "field_map": {}}, "filter_social_media": true, "filter_review_sites": true, "filter_directories": true, "filter_maps": true}}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/client/campaigns", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"search_query": "string", "query": "string", "country_code": "string", "name": "string", "filter": {"mode": "all", "min_population": 0, "max_population": 0, "location_ids": [0], "admin_regions": ["string"], "parent_city": "string", "include_postcodes": false, "exhaustive": false}, "max_results": 100, "extract_reviews": false, "extract_photos": false, "lang": "en", "store_images": true, "validate_phones": true, "fuzziq_enabled": true, "fuzziq_unique_only": true, "skip_proxy": false, "test": false, "workflow": {"spidersite": {"enabled": false, "mode": {}, "max_pages": 25, "crawl_strategy": "bestfirst", "target_pages": ["contact", "about", "team", "news", "blog"], "enable_spa": true, "spa_timeout": 30, "extract_team": false, "extract_company_info": false, "extract_pain_points": false, "product_description": {}, "icp_description": {}, "compendium": {}, "timeout": 30}, "spiderverify": {"enabled": false, "check_gravatar": false, "check_dnsbl": false, "smtp_timeout_secs": 45, "max_emails_per_business": 10}, "vayapin": {"enabled": false}, "social_media_enrichment": {"enabled": true}, "smartlead": {"enabled": false, "connection_id": {}, "remote_campaign_id": {}, "remote_campaign_name": {}, "limit": {}, "only_with_vayapin_seo": false, "only_with_vayapin_pin": false, "field_map": {}}, "filter_social_media": true, "filter_review_sites": true, "filter_directories": true, "filter_maps": true}}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"search_query": "string", "query": "string", "country_code": "string", "name": "string", "filter": {"mode": "all", "min_population": 0, "max_population": 0, "location_ids": [0], "admin_regions": ["string"], "parent_city": "string", "include_postcodes": false, "exhaustive": false}, "max_results": 100, "extract_reviews": false, "extract_photos": false, "lang": "en", "store_images": true, "validate_phones": true, "fuzziq_enabled": true, "fuzziq_unique_only": true, "skip_proxy": false, "test": false, "workflow": {"spidersite": {"enabled": false, "mode": {}, "max_pages": 25, "crawl_strategy": "bestfirst", "target_pages": ["contact", "about", "team", "news", "blog"], "enable_spa": true, "spa_timeout": 30, "extract_team": false, "extract_company_info": false, "extract_pain_points": false, "product_description": {}, "icp_description": {}, "compendium": {}, "timeout": 30}, "spiderverify": {"enabled": false, "check_gravatar": false, "check_dnsbl": false, "smtp_timeout_secs": 45, "max_emails_per_business": 10}, "vayapin": {"enabled": false}, "social_media_enrichment": {"enabled": true}, "smartlead": {"enabled": false, "connection_id": {}, "remote_campaign_id": {}, "remote_campaign_name": {}, "limit": {}, "only_with_vayapin_seo": false, "only_with_vayapin_pin": false, "field_map": {}}, "filter_social_media": true, "filter_review_sites": true, "filter_directories": true, "filter_maps": true}}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/dashboard/client/campaigns", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `201` — Successful Response - `422` — Validation Error ## `POST` /api/v1/dashboard/client/campaigns/estimate **Estimate Campaign** Pre-flight campaign estimate for the dashboard create flow. Writes NO rows and is NOT a dispatcher submission. Returns the location count plus per-stage billable-job COUNTS (not USD). ``requires_upgrade`` is layered with the client's real per-plan ``max_locations`` so the create UI can show Proceed vs Upgrade. **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/dashboard/client/campaigns/estimate' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "search_query": "string", "country_code": "string", "filter": { "mode": "all", "min_population": 0, "max_population": 0, "location_ids": [ 0 ], "admin_regions": [ "string" ], "parent_city": "string", "include_postcodes": false, "exhaustive": false }, "max_results": 100, "workflow": { "spidersite": { "enabled": false, "mode": {}, "max_pages": 25, "crawl_strategy": "bestfirst", "target_pages": [ "contact", "about", "team", "news", "blog" ], "enable_spa": true, "spa_timeout": 30, "extract_team": false, "extract_company_info": false, "extract_pain_points": false, "product_description": {}, "icp_description": {}, "compendium": {}, "timeout": 30 }, "spiderverify": { "enabled": false, "check_gravatar": false, "check_dnsbl": false, "smtp_timeout_secs": 45, "max_emails_per_business": 10 }, "vayapin": { "enabled": false }, "social_media_enrichment": { "enabled": true }, "smartlead": { "enabled": false, "connection_id": {}, "remote_campaign_id": {}, "remote_campaign_name": {}, "limit": {}, "only_with_vayapin_seo": false, "only_with_vayapin_pin": false, "field_map": {} }, "filter_social_media": true, "filter_review_sites": true, "filter_directories": true, "filter_maps": true } }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/dashboard/client/campaigns/estimate", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"search_query": "string", "country_code": "string", "filter": {"mode": "all", "min_population": 0, "max_population": 0, "location_ids": [0], "admin_regions": ["string"], "parent_city": "string", "include_postcodes": false, "exhaustive": false}, "max_results": 100, "workflow": {"spidersite": {"enabled": false, "mode": {}, "max_pages": 25, "crawl_strategy": "bestfirst", "target_pages": ["contact", "about", "team", "news", "blog"], "enable_spa": true, "spa_timeout": 30, "extract_team": false, "extract_company_info": false, "extract_pain_points": false, "product_description": {}, "icp_description": {}, "compendium": {}, "timeout": 30}, "spiderverify": {"enabled": false, "check_gravatar": false, "check_dnsbl": false, "smtp_timeout_secs": 45, "max_emails_per_business": 10}, "vayapin": {"enabled": false}, "social_media_enrichment": {"enabled": true}, "smartlead": {"enabled": false, "connection_id": {}, "remote_campaign_id": {}, "remote_campaign_name": {}, "limit": {}, "only_with_vayapin_seo": false, "only_with_vayapin_pin": false, "field_map": {}}, "filter_social_media": true, "filter_review_sites": true, "filter_directories": true, "filter_maps": true}}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/client/campaigns/estimate", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"search_query": "string", "country_code": "string", "filter": {"mode": "all", "min_population": 0, "max_population": 0, "location_ids": [0], "admin_regions": ["string"], "parent_city": "string", "include_postcodes": false, "exhaustive": false}, "max_results": 100, "workflow": {"spidersite": {"enabled": false, "mode": {}, "max_pages": 25, "crawl_strategy": "bestfirst", "target_pages": ["contact", "about", "team", "news", "blog"], "enable_spa": true, "spa_timeout": 30, "extract_team": false, "extract_company_info": false, "extract_pain_points": false, "product_description": {}, "icp_description": {}, "compendium": {}, "timeout": 30}, "spiderverify": {"enabled": false, "check_gravatar": false, "check_dnsbl": false, "smtp_timeout_secs": 45, "max_emails_per_business": 10}, "vayapin": {"enabled": false}, "social_media_enrichment": {"enabled": true}, "smartlead": {"enabled": false, "connection_id": {}, "remote_campaign_id": {}, "remote_campaign_name": {}, "limit": {}, "only_with_vayapin_seo": false, "only_with_vayapin_pin": false, "field_map": {}}, "filter_social_media": true, "filter_review_sites": true, "filter_directories": true, "filter_maps": true}}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"search_query": "string", "country_code": "string", "filter": {"mode": "all", "min_population": 0, "max_population": 0, "location_ids": [0], "admin_regions": ["string"], "parent_city": "string", "include_postcodes": false, "exhaustive": false}, "max_results": 100, "workflow": {"spidersite": {"enabled": false, "mode": {}, "max_pages": 25, "crawl_strategy": "bestfirst", "target_pages": ["contact", "about", "team", "news", "blog"], "enable_spa": true, "spa_timeout": 30, "extract_team": false, "extract_company_info": false, "extract_pain_points": false, "product_description": {}, "icp_description": {}, "compendium": {}, "timeout": 30}, "spiderverify": {"enabled": false, "check_gravatar": false, "check_dnsbl": false, "smtp_timeout_secs": 45, "max_emails_per_business": 10}, "vayapin": {"enabled": false}, "social_media_enrichment": {"enabled": true}, "smartlead": {"enabled": false, "connection_id": {}, "remote_campaign_id": {}, "remote_campaign_name": {}, "limit": {}, "only_with_vayapin_seo": false, "only_with_vayapin_pin": false, "field_map": {}}, "filter_social_media": true, "filter_review_sites": true, "filter_directories": true, "filter_maps": true}}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/dashboard/client/campaigns/estimate", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/dashboard/client/campaigns/{campaign_id} **Get Campaign Status** Get detailed status for a specific campaign. **Parameters** - `campaign_id` (path, string, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/client/campaigns/{campaign_id}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/client/campaigns/{campaign_id}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/client/campaigns/{campaign_id}", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/client/campaigns/{campaign_id}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `PATCH` /api/v1/dashboard/client/campaigns/{campaign_id} **Update Campaign** Update an existing campaign's configuration. **What can be updated:** - `search_query` - Changes search term for future jobs - `name` - Campaign display name - SpiderMaps options (max_results, extract_reviews, etc.) - `workflow` - Workflow configuration (merged with existing) **Restrictions:** - Only active or stopped/paused campaigns can be updated - Completed campaigns cannot be modified - Changes only affect PENDING locations **Parameters** - `campaign_id` (path, string, required) **Request body** :::codegroup ```bash curl -X PATCH 'https://spideriq.ai/api/v1/dashboard/client/campaigns/{campaign_id}' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "search_query": "string", "query": "string", "name": "string", "max_results": 0, "extract_reviews": true, "extract_photos": true, "lang": "string", "store_images": true, "validate_phones": true, "fuzziq_enabled": true, "fuzziq_unique_only": true, "skip_proxy": true, "test": true, "workflow": { "spidersite": { "enabled": false, "mode": {}, "max_pages": 25, "crawl_strategy": "bestfirst", "target_pages": [ "contact", "about", "team", "news", "blog" ], "enable_spa": true, "spa_timeout": 30, "extract_team": false, "extract_company_info": false, "extract_pain_points": false, "product_description": {}, "icp_description": {}, "compendium": {}, "timeout": 30 }, "spiderverify": { "enabled": false, "check_gravatar": false, "check_dnsbl": false, "smtp_timeout_secs": 45, "max_emails_per_business": 10 }, "vayapin": { "enabled": false }, "social_media_enrichment": { "enabled": true }, "smartlead": { "enabled": false, "connection_id": {}, "remote_campaign_id": {}, "remote_campaign_name": {}, "limit": {}, "only_with_vayapin_seo": false, "only_with_vayapin_pin": false, "field_map": {} }, "filter_social_media": true, "filter_review_sites": true, "filter_directories": true, "filter_maps": true } }' ``` ```python import httpx resp = httpx.patch( "https://spideriq.ai/api/v1/dashboard/client/campaigns/{campaign_id}", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"search_query": "string", "query": "string", "name": "string", "max_results": 0, "extract_reviews": true, "extract_photos": true, "lang": "string", "store_images": true, "validate_phones": true, "fuzziq_enabled": true, "fuzziq_unique_only": true, "skip_proxy": true, "test": true, "workflow": {"spidersite": {"enabled": false, "mode": {}, "max_pages": 25, "crawl_strategy": "bestfirst", "target_pages": ["contact", "about", "team", "news", "blog"], "enable_spa": true, "spa_timeout": 30, "extract_team": false, "extract_company_info": false, "extract_pain_points": false, "product_description": {}, "icp_description": {}, "compendium": {}, "timeout": 30}, "spiderverify": {"enabled": false, "check_gravatar": false, "check_dnsbl": false, "smtp_timeout_secs": 45, "max_emails_per_business": 10}, "vayapin": {"enabled": false}, "social_media_enrichment": {"enabled": true}, "smartlead": {"enabled": false, "connection_id": {}, "remote_campaign_id": {}, "remote_campaign_name": {}, "limit": {}, "only_with_vayapin_seo": false, "only_with_vayapin_pin": false, "field_map": {}}, "filter_social_media": true, "filter_review_sites": true, "filter_directories": true, "filter_maps": true}}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/client/campaigns/{campaign_id}", { method: "PATCH", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"search_query": "string", "query": "string", "name": "string", "max_results": 0, "extract_reviews": true, "extract_photos": true, "lang": "string", "store_images": true, "validate_phones": true, "fuzziq_enabled": true, "fuzziq_unique_only": true, "skip_proxy": true, "test": true, "workflow": {"spidersite": {"enabled": false, "mode": {}, "max_pages": 25, "crawl_strategy": "bestfirst", "target_pages": ["contact", "about", "team", "news", "blog"], "enable_spa": true, "spa_timeout": 30, "extract_team": false, "extract_company_info": false, "extract_pain_points": false, "product_description": {}, "icp_description": {}, "compendium": {}, "timeout": 30}, "spiderverify": {"enabled": false, "check_gravatar": false, "check_dnsbl": false, "smtp_timeout_secs": 45, "max_emails_per_business": 10}, "vayapin": {"enabled": false}, "social_media_enrichment": {"enabled": true}, "smartlead": {"enabled": false, "connection_id": {}, "remote_campaign_id": {}, "remote_campaign_name": {}, "limit": {}, "only_with_vayapin_seo": false, "only_with_vayapin_pin": false, "field_map": {}}, "filter_social_media": true, "filter_review_sites": true, "filter_directories": true, "filter_maps": true}}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"search_query": "string", "query": "string", "name": "string", "max_results": 0, "extract_reviews": true, "extract_photos": true, "lang": "string", "store_images": true, "validate_phones": true, "fuzziq_enabled": true, "fuzziq_unique_only": true, "skip_proxy": true, "test": true, "workflow": {"spidersite": {"enabled": false, "mode": {}, "max_pages": 25, "crawl_strategy": "bestfirst", "target_pages": ["contact", "about", "team", "news", "blog"], "enable_spa": true, "spa_timeout": 30, "extract_team": false, "extract_company_info": false, "extract_pain_points": false, "product_description": {}, "icp_description": {}, "compendium": {}, "timeout": 30}, "spiderverify": {"enabled": false, "check_gravatar": false, "check_dnsbl": false, "smtp_timeout_secs": 45, "max_emails_per_business": 10}, "vayapin": {"enabled": false}, "social_media_enrichment": {"enabled": true}, "smartlead": {"enabled": false, "connection_id": {}, "remote_campaign_id": {}, "remote_campaign_name": {}, "limit": {}, "only_with_vayapin_seo": false, "only_with_vayapin_pin": false, "field_map": {}}, "filter_social_media": true, "filter_review_sites": true, "filter_directories": true, "filter_maps": true}}`) req, _ := http.NewRequest("PATCH", "https://spideriq.ai/api/v1/dashboard/client/campaigns/{campaign_id}", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `DELETE` /api/v1/dashboard/client/campaigns/{campaign_id} **Delete Campaign** Delete a campaign and all related data. Requirements: - Campaign must be stopped (no active jobs) - User must own the campaign Cascade deletes: - campaign_locations (auto via FK) - campaign_workflow_jobs (auto via FK) - orchestrated_campaign_runs (manual) - location_worker_runs (manual) **Parameters** - `campaign_id` (path, string, required) :::codegroup ```bash curl -X DELETE 'https://spideriq.ai/api/v1/dashboard/client/campaigns/{campaign_id}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.delete( "https://spideriq.ai/api/v1/dashboard/client/campaigns/{campaign_id}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/client/campaigns/{campaign_id}", { method: "DELETE", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("DELETE", "https://spideriq.ai/api/v1/dashboard/client/campaigns/{campaign_id}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/dashboard/client/campaigns/{campaign_id}/stage-progress **Get Campaign Stage Progress** Return per-stage campaign progress (Maps / Site / Verify / Vayapin). Replaces the misleading single "0/N 0%" bar with location-level completed/active/queued/failed counts per pipeline stage, plus a velocity-based ETA. Stage list is derived from the campaign's `workflow_config` — Site/Verify/Vayapin only appear when their config block has enabled=True. Cached in Redis for 30s (chrome polls every 15s while active). **Parameters** - `campaign_id` (path, string, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/client/campaigns/{campaign_id}/stage-progress' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/client/campaigns/{campaign_id}/stage-progress", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/client/campaigns/{campaign_id}/stage-progress", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/client/campaigns/{campaign_id}/stage-progress", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/dashboard/client/campaigns/{campaign_id}/jobs **List Campaign Jobs** List jobs for a specific campaign with pagination. **Parameters** - `campaign_id` (path, string, required) - `page` (query, integer, optional) - `page_size` (query, integer, optional) - `status` (query, any, optional) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/client/campaigns/{campaign_id}/jobs' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/client/campaigns/{campaign_id}/jobs", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/client/campaigns/{campaign_id}/jobs", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/client/campaigns/{campaign_id}/jobs", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/dashboard/client/campaigns/{campaign_id}/next **Submit Next Job** Submit the next pending job in the campaign. Two safety properties this endpoint enforces: 1. **Cross-tenant authorization (Bug #2-bis fix).** Without this check, any authenticated ``client_user`` could call /next on any campaign by guessing its ID — the job would be billed to the caller's own ``client_id`` while writing into another tenant's ``scraping_campaigns.workflow_*`` counters. ``can_access_campaign`` resolves the rightful owner. 1. **Tenant Bearer forging (Bug #2 fix).** The campaign WindMill flow's sub-modules (SpiderMaps, SpiderSite, SpiderVerify, VayaPin) are strict-required since the §D1 audit-2 redeploy: an empty ``api_token`` raises ``ValueError('api_token is required')`` rather than falling back to ``f/spideriq/api_token`` (which holds a stale workspace Bearer). We forge the Bearer from the **campaign owner's** stored credentials, not the caller's — super_admin impersonating VAYAPIN must dispatch as VAYAPIN, not as themselves. Shared helper at ``app/services/tenant_bearer.py``. Mirror of ``_resolve_tenant_bearer`` in ``app/api/v1/jobs/pipeline_flows.py``. **Parameters** - `campaign_id` (path, string, required) :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/dashboard/client/campaigns/{campaign_id}/next' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/dashboard/client/campaigns/{campaign_id}/next", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/client/campaigns/{campaign_id}/next", { method: "POST", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/dashboard/client/campaigns/{campaign_id}/next", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/dashboard/client/campaigns/{campaign_id}/stop **Stop Campaign** Stop/pause a campaign. **Parameters** - `campaign_id` (path, string, required) :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/dashboard/client/campaigns/{campaign_id}/stop' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/dashboard/client/campaigns/{campaign_id}/stop", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/client/campaigns/{campaign_id}/stop", { method: "POST", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/dashboard/client/campaigns/{campaign_id}/stop", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/dashboard/client/campaigns/{campaign_id}/continue **Continue Campaign** Resume a stopped campaign. **Parameters** - `campaign_id` (path, string, required) :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/dashboard/client/campaigns/{campaign_id}/continue' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/dashboard/client/campaigns/{campaign_id}/continue", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/client/campaigns/{campaign_id}/continue", { method: "POST", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/dashboard/client/campaigns/{campaign_id}/continue", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/dashboard/client/campaigns/{campaign_id}/jobs/{job_id}/results **Get Job Results** Get results for a specific job in the campaign. If wait=True, blocks until the job completes (up to 10 minutes). **Parameters** - `campaign_id` (path, string, required) - `job_id` (path, string, required) - `wait` (query, boolean, optional) — Wait for job completion :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/client/campaigns/{campaign_id}/jobs/{job_id}/results' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/client/campaigns/{campaign_id}/jobs/{job_id}/results", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/client/campaigns/{campaign_id}/jobs/{job_id}/results", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/client/campaigns/{campaign_id}/jobs/{job_id}/results", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/dashboard/client/campaigns/{campaign_id}/export **Export Campaign Results** Export all campaign results. Exports all completed job results for a campaign in JSON or CSV format. **Parameters** - `campaign_id` (path, string, required) - `format` (query, string, optional) — Export format: json or csv :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/client/campaigns/{campaign_id}/export' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/client/campaigns/{campaign_id}/export", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/client/campaigns/{campaign_id}/export", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/client/campaigns/{campaign_id}/export", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/dashboard/client/campaigns/{campaign_id}/runs **List Campaign Runs** Get Inngest workflow runs for a campaign. Returns tracked runs from the orchestrated_campaign_runs table, showing run status, timing, and step counts. **Parameters** - `campaign_id` (path, string, required) - `page` (query, integer, optional) - `page_size` (query, integer, optional) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/client/campaigns/{campaign_id}/runs' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/client/campaigns/{campaign_id}/runs", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/client/campaigns/{campaign_id}/runs", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/client/campaigns/{campaign_id}/runs", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/dashboard/client/campaigns/{campaign_id}/runs/{run_id} **Get Campaign Run Detail** Get detailed information about a specific campaign run. Returns run details including step-level information for waterfall visualization. **Parameters** - `campaign_id` (path, string, required) - `run_id` (path, integer, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/client/campaigns/{campaign_id}/runs/{run_id}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/client/campaigns/{campaign_id}/runs/{run_id}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/client/campaigns/{campaign_id}/runs/{run_id}", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/client/campaigns/{campaign_id}/runs/{run_id}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/dashboard/client/campaigns/{campaign_id}/locations/{location_id}/runs **Get Location Worker Runs** Get worker runs for a specific campaign location. Returns SpiderMaps, SpiderSite, and SpiderVerify runs with timing and results. Enables per-job visibility in campaign detail UI. **Parameters** - `campaign_id` (path, string, required) - `location_id` (path, integer, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/client/campaigns/{campaign_id}/locations/{location_id}/runs' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/client/campaigns/{campaign_id}/locations/{location_id}/runs", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/client/campaigns/{campaign_id}/locations/{location_id}/runs", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/client/campaigns/{campaign_id}/locations/{location_id}/runs", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/dashboard/client/campaigns/{campaign_id}/locations/{location_id}/retry **Retry Location** Retry a failed or completed location. Always auto-submits. - retry_mode="full": Re-run entire workflow from SpiderMaps - retry_mode="site": Keep Maps results, re-run SpiderSite + SpiderVerify - retry_mode="verify": Keep Site results, re-run SpiderVerify only Max 3 retries per location. Returns 400 if limit exceeded. **Parameters** - `campaign_id` (path, string, required) - `location_id` (path, integer, required) **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/dashboard/client/campaigns/{campaign_id}/locations/{location_id}/retry' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "retry_mode": "full" }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/dashboard/client/campaigns/{campaign_id}/locations/{location_id}/retry", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"retry_mode": "full"}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/client/campaigns/{campaign_id}/locations/{location_id}/retry", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"retry_mode": "full"}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"retry_mode": "full"}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/dashboard/client/campaigns/{campaign_id}/locations/{location_id}/retry", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/dashboard/client/campaigns/{campaign_id}/locations/{location_id}/reset **Reset Stuck Location** Reset a location stuck in 'submitted' (processing) state. - mark_as="failed": Mark as failed (default) - mark_as="pending": Reset to pending for re-submission **Parameters** - `campaign_id` (path, string, required) - `location_id` (path, integer, required) - `mark_as` (query, string, optional) :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/dashboard/client/campaigns/{campaign_id}/locations/{location_id}/reset' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/dashboard/client/campaigns/{campaign_id}/locations/{location_id}/reset", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/client/campaigns/{campaign_id}/locations/{location_id}/reset", { method: "POST", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/dashboard/client/campaigns/{campaign_id}/locations/{location_id}/reset", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/dashboard/client/campaigns/{campaign_id}/retry-failed **Bulk Retry Failed** Retry all failed locations in the campaign (up to max_locations). Only retries locations that haven't exceeded the max retry limit (3). Returns list of location IDs that were reset and re-submitted. **Parameters** - `campaign_id` (path, string, required) - `max_locations` (query, integer, optional) :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/dashboard/client/campaigns/{campaign_id}/retry-failed' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/dashboard/client/campaigns/{campaign_id}/retry-failed", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/client/campaigns/{campaign_id}/retry-failed", { method: "POST", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/dashboard/client/campaigns/{campaign_id}/retry-failed", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## Dashboard Live Theater Source: https://spideriq.ai/docs/api-reference/dashboard-live-theater ## `GET` /api/v1/dashboard/client/events/recent **List Recent Events** Synthesize the last N campaign-lifecycle events from existing tables and return them in SSE-envelope shape. Skips events fired in the last 60 seconds — the SSE stream handles those, prevents double-render when prefill + live race. V1.2: enriched payloads + card variety: - ``campaign.location.completed`` now JOINs ``campaign_workflow_jobs`` for real per-location ``sitesCrawled``/``emailsVerified`` counts (previously hardcoded 0 → all KPIs except BUSINESSES showed 0). - Synthesized ``campaign.vayapin.exported`` events from ``public.results`` worker_type='vayapin' so PINS KPI ticks and the Intel Feed has VayapinExportedCard variety. - Synthesized ``campaign.business.found`` events from ``campaign_workflow_jobs`` (capped per campaign so business cards don't dominate the rolling buffer). - Per-event-type response quota — instead of chronological top-N biased toward whichever type has highest cadence, return up to ``per_type`` of each type. Keeps the Intel Feed visually varied. Default window 7 days, max 30 days. Default 50 events, max 200. **Parameters** - `limit` (query, integer, optional) - `hours` (query, integer, optional) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/client/events/recent' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/client/events/recent", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/client/events/recent", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/client/events/recent", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## Dashboard Workspace Source: https://spideriq.ai/docs/api-reference/dashboard-workspace ## `GET` /api/v1/dashboard/overview/stats **Overview Stats** Real Overview KPIs (running jobs, leads, verified emails, gate spend, sparkline, campaign health) for the resolved tenant scope. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/overview/stats' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/overview/stats", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/overview/stats", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/overview/stats", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response ## `GET` /api/v1/dashboard/activity **Activity** Campaign-lifecycle activity feed, newest first, cursor-paginated. **Parameters** - `limit` (query, integer, optional) - `cursor` (query, any, optional) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/activity' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/activity", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/activity", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/activity", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/dashboard/usage **Usage** Daily jobs/leads/cost series + totals + billing-cycle lead quota. **Parameters** - `period` (query, string, optional) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/usage' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/usage", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/usage", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/usage", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## Dashboard PAT Source: https://spideriq.ai/docs/api-reference/dashboard-pat ## `GET` /api/v1/dashboard/pat/requests **List PAT requests (admin only)** List pending and historical PAT requests visible to the current admin. Visibility: - ``super_admin``: all requests across all clients. - ``brand_admin``: requests targeting clients in their active brand memberships. - ``client_user``: 403 — admin role required to review requests. Default sort: newest first. Default page size: 50. **Parameters** - `status` (query, any, optional) — Filter by status (default: all) - `limit` (query, integer, optional) - `offset` (query, integer, optional) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/pat/requests' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/pat/requests", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/pat/requests", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/pat/requests", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/dashboard/pat/requests/{request_id} **PAT request detail (admin only)** Detail view for a single PAT request. Returns 404 either when the request doesn't exist OR when the caller's role doesn't permit viewing it (don't leak existence to brand admins outside their scope). **Parameters** - `request_id` (path, string, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/pat/requests/{request_id}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/pat/requests/{request_id}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/pat/requests/{request_id}", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/pat/requests/{request_id}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/dashboard/pat/requests/{request_id}/approve **Approve PAT request via dashboard session auth** Approve a pending PAT request. Replaces the email-link approve URL for admins who already have a dashboard session. The PAT token issued by approval is cached for the polling agent in Redis (same channel as the email flow) — the dashboard caller does not see it. **Parameters** - `request_id` (path, string, required) :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/dashboard/pat/requests/{request_id}/approve' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/dashboard/pat/requests/{request_id}/approve", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/pat/requests/{request_id}/approve", { method: "POST", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/dashboard/pat/requests/{request_id}/approve", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/dashboard/pat/requests/{request_id}/deny **Deny PAT request via dashboard session auth** Deny a pending PAT request via dashboard session auth. **Parameters** - `request_id` (path, string, required) :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/dashboard/pat/requests/{request_id}/deny' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/dashboard/pat/requests/{request_id}/deny", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/pat/requests/{request_id}/deny", { method: "POST", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/dashboard/pat/requests/{request_id}/deny", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## Dashboard - Token Plans Source: https://spideriq.ai/docs/api-reference/dashboard-token-plans ## `GET` /api/v1/dashboard/token-plans **List my per-token plans** The (token × service_type) grid scoped to the authenticated client. Same shape as the admin list, but only ever this client's own tokens. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/token-plans' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/token-plans", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/token-plans", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/token-plans", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response ## `PUT` /api/v1/dashboard/token-plans/{token_id}/{service_type} **Upsert one of my per-token plans (clamped to my client plan)** Partial upsert of a token's plan. The token must belong to the caller (403 otherwise). Each supplied budget cap is clamped to the client's CSP cap; any field exceeding it returns 422 listing every violation. 404 when the client has no CSP for this service_type. Fires NOTIFY on write. **Parameters** - `token_id` (path, string, required) - `service_type` (path, string, required) **Request body** :::codegroup ```bash curl -X PUT 'https://spideriq.ai/api/v1/dashboard/token-plans/{token_id}/{service_type}' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "enabled": true, "jobs_per_hour": 0, "jobs_per_24h": 0, "monthly_cap_calendar": 0, "monthly_cap_period": "string", "cost_ceiling_usd_per_24h": 0.0, "monthly_spend_cap_usd": 0.0, "max_inflight_jobs": 0, "time_window_start": "string", "time_window_end": "string", "time_window_tz": "string", "priority_tier": 0 }' ``` ```python import httpx resp = httpx.put( "https://spideriq.ai/api/v1/dashboard/token-plans/{token_id}/{service_type}", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"enabled": true, "jobs_per_hour": 0, "jobs_per_24h": 0, "monthly_cap_calendar": 0, "monthly_cap_period": "string", "cost_ceiling_usd_per_24h": 0.0, "monthly_spend_cap_usd": 0.0, "max_inflight_jobs": 0, "time_window_start": "string", "time_window_end": "string", "time_window_tz": "string", "priority_tier": 0}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/token-plans/{token_id}/{service_type}", { method: "PUT", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"enabled": true, "jobs_per_hour": 0, "jobs_per_24h": 0, "monthly_cap_calendar": 0, "monthly_cap_period": "string", "cost_ceiling_usd_per_24h": 0.0, "monthly_spend_cap_usd": 0.0, "max_inflight_jobs": 0, "time_window_start": "string", "time_window_end": "string", "time_window_tz": "string", "priority_tier": 0}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"enabled": true, "jobs_per_hour": 0, "jobs_per_24h": 0, "monthly_cap_calendar": 0, "monthly_cap_period": "string", "cost_ceiling_usd_per_24h": 0.0, "monthly_spend_cap_usd": 0.0, "max_inflight_jobs": 0, "time_window_start": "string", "time_window_end": "string", "time_window_tz": "string", "priority_tier": 0}`) req, _ := http.NewRequest("PUT", "https://spideriq.ai/api/v1/dashboard/token-plans/{token_id}/{service_type}", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `DELETE` /api/v1/dashboard/token-plans/{token_id}/{service_type} **Delete one of my per-token plans (revert to my client default)** Revert the token to the client default for this service_type. 403 if the token isn't the caller's; 404 if no override exists. Fires NOTIFY. **Parameters** - `token_id` (path, string, required) - `service_type` (path, string, required) :::codegroup ```bash curl -X DELETE 'https://spideriq.ai/api/v1/dashboard/token-plans/{token_id}/{service_type}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.delete( "https://spideriq.ai/api/v1/dashboard/token-plans/{token_id}/{service_type}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/token-plans/{token_id}/{service_type}", { method: "DELETE", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("DELETE", "https://spideriq.ai/api/v1/dashboard/token-plans/{token_id}/{service_type}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `204` — Successful Response - `422` — Validation Error ## Dashboard - Token Plans (UI) Source: https://spideriq.ai/docs/api-reference/dashboard-token-plans-ui ## `GET` /api/v1/dashboard/client/token-plans **List my per-token plans (dashboard session)** The (token × service_type) grid scoped to the session's effective client. Same shape as the admin + Bearer lists, but only ever this client's own tokens. super_admin sees the impersonated client (X-Selected-Client-Id / X-Brand-ID). :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/client/token-plans' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/client/token-plans", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/client/token-plans", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/client/token-plans", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response ## `PUT` /api/v1/dashboard/client/token-plans/{token_id}/{service_type} **Upsert one of my per-token plans (clamped to my client plan)** Partial upsert of a token's plan. The token must belong to the session's effective client (403 otherwise). Each supplied budget cap is clamped to the client's CSP cap; any field exceeding it returns 422 listing every violation. 404 when the client has no CSP for this service_type. Fires NOTIFY on write. **Parameters** - `token_id` (path, string, required) - `service_type` (path, string, required) **Request body** :::codegroup ```bash curl -X PUT 'https://spideriq.ai/api/v1/dashboard/client/token-plans/{token_id}/{service_type}' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "enabled": true, "jobs_per_hour": 0, "jobs_per_24h": 0, "monthly_cap_calendar": 0, "monthly_cap_period": "string", "cost_ceiling_usd_per_24h": 0.0, "monthly_spend_cap_usd": 0.0, "max_inflight_jobs": 0, "time_window_start": "string", "time_window_end": "string", "time_window_tz": "string", "priority_tier": 0 }' ``` ```python import httpx resp = httpx.put( "https://spideriq.ai/api/v1/dashboard/client/token-plans/{token_id}/{service_type}", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"enabled": true, "jobs_per_hour": 0, "jobs_per_24h": 0, "monthly_cap_calendar": 0, "monthly_cap_period": "string", "cost_ceiling_usd_per_24h": 0.0, "monthly_spend_cap_usd": 0.0, "max_inflight_jobs": 0, "time_window_start": "string", "time_window_end": "string", "time_window_tz": "string", "priority_tier": 0}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/client/token-plans/{token_id}/{service_type}", { method: "PUT", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"enabled": true, "jobs_per_hour": 0, "jobs_per_24h": 0, "monthly_cap_calendar": 0, "monthly_cap_period": "string", "cost_ceiling_usd_per_24h": 0.0, "monthly_spend_cap_usd": 0.0, "max_inflight_jobs": 0, "time_window_start": "string", "time_window_end": "string", "time_window_tz": "string", "priority_tier": 0}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"enabled": true, "jobs_per_hour": 0, "jobs_per_24h": 0, "monthly_cap_calendar": 0, "monthly_cap_period": "string", "cost_ceiling_usd_per_24h": 0.0, "monthly_spend_cap_usd": 0.0, "max_inflight_jobs": 0, "time_window_start": "string", "time_window_end": "string", "time_window_tz": "string", "priority_tier": 0}`) req, _ := http.NewRequest("PUT", "https://spideriq.ai/api/v1/dashboard/client/token-plans/{token_id}/{service_type}", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `DELETE` /api/v1/dashboard/client/token-plans/{token_id}/{service_type} **Delete one of my per-token plans (revert to my client default)** Revert the token to the client default for this service_type. 403 if the token isn't the session client's; 404 if no override exists. Fires NOTIFY. **Parameters** - `token_id` (path, string, required) - `service_type` (path, string, required) :::codegroup ```bash curl -X DELETE 'https://spideriq.ai/api/v1/dashboard/client/token-plans/{token_id}/{service_type}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.delete( "https://spideriq.ai/api/v1/dashboard/client/token-plans/{token_id}/{service_type}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/client/token-plans/{token_id}/{service_type}", { method: "DELETE", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("DELETE", "https://spideriq.ai/api/v1/dashboard/client/token-plans/{token_id}/{service_type}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `204` — Successful Response - `422` — Validation Error ## Dashboard Plans Source: https://spideriq.ai/docs/api-reference/dashboard-plans ## `GET` /api/v1/dashboard/plans/me **Get My Subscriptions** The caller's active/trialing/past_due subscriptions + per-service monthly usage bars. Empty list when the caller has no client scope. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/plans/me' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/plans/me", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/plans/me", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/plans/me", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response ## `GET` /api/v1/dashboard/plans/entitlements **Get My Entitlements** The caller's owner-granted per-client feature entitlements (``client_service_plans`` rows with ``enabled=true``), translated to client-facing feature flags. Drives the flow-config UI's active-vs-upsell rendering (e.g. the Social Media Enrichment block). Defaults every feature to ``false`` when the caller has no client scope. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/plans/entitlements' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/plans/entitlements", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/plans/entitlements", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/plans/entitlements", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response ## `GET` /api/v1/dashboard/plans/browse **Browse Tariffs** All active tariffs (latest version each) the caller can subscribe to. Includes the Stripe Price ids + cents so the UI computes the annual gate + savings client-side. ``is_current`` marks the caller's existing tariffs. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/plans/browse' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/plans/browse", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/plans/browse", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/plans/browse", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response ## `GET` /api/v1/dashboard/plans/browse/bundles **Browse Bundles** Active bundle offerings with pre/post-discount pricing. Empty when no bundles are defined. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/plans/browse/bundles' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/plans/browse/bundles", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/plans/browse/bundles", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/plans/browse/bundles", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response ## `GET` /api/v1/dashboard/plans/invoices **List Invoices** The caller's recent Stripe invoices (last 20). Empty when the client has no Stripe Customer yet (never subscribed). :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/plans/invoices' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/plans/invoices", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/plans/invoices", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/plans/invoices", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response ## `POST` /api/v1/dashboard/plans/billing-portal **Create Billing Portal** Create a Stripe Billing Portal session and return its URL. The client manages payment methods, invoices, and cancellation there. :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/dashboard/plans/billing-portal' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/dashboard/plans/billing-portal", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/plans/billing-portal", { method: "POST", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/dashboard/plans/billing-portal", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response ## `POST` /api/v1/dashboard/plans/upgrade-preview **Upgrade Preview** Proration preview for switching the caller's current subscription to a different tariff version. Resolves the target Stripe Price (raises 409 if annual is requested but unminted, 404 if the version is unknown), then asks Stripe for the upcoming prorated invoice. Falls back to a no-proration estimate when the caller has no active subscription to migrate. **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/dashboard/plans/upgrade-preview' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "from_tariff_version_id": 0, "to_tariff_version_id": 0, "billing_interval": "month" }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/dashboard/plans/upgrade-preview", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"from_tariff_version_id": 0, "to_tariff_version_id": 0, "billing_interval": "month"}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/plans/upgrade-preview", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"from_tariff_version_id": 0, "to_tariff_version_id": 0, "billing_interval": "month"}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"from_tariff_version_id": 0, "to_tariff_version_id": 0, "billing_interval": "month"}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/dashboard/plans/upgrade-preview", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/dashboard/plans/recent-denies **Recent Denies** The caller's recently DENIED dispatch decisions (last 30 days, 20 max) — what the customer can act on (rate / cost / quota). Partition-pruned by the decided_at predicate. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/plans/recent-denies' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/plans/recent-denies", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/plans/recent-denies", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/plans/recent-denies", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response ## `POST` /api/v1/dashboard/plans/subscribe **Subscribe** Create a Stripe-hosted Checkout Session for the caller to subscribe to a tariff version. Returns the hosted checkout URL the frontend redirects to. 404 unknown tariff · 409 annual-not-available (both via the typed exceptions' handlers in app/main.py). **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/dashboard/plans/subscribe' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "tariff_version_id": 0, "billing_interval": "month", "success_url": "string", "cancel_url": "string" }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/dashboard/plans/subscribe", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"tariff_version_id": 0, "billing_interval": "month", "success_url": "string", "cancel_url": "string"}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/plans/subscribe", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"tariff_version_id": 0, "billing_interval": "month", "success_url": "string", "cancel_url": "string"}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"tariff_version_id": 0, "billing_interval": "month", "success_url": "string", "cancel_url": "string"}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/dashboard/plans/subscribe", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/dashboard/plans/change **Change Plan** Switch the caller's existing subscription item to a new tariff_version and/or billing interval — IN PLACE on the same Stripe subscription (no new sub, no double-billing). Prorations are invoiced immediately (``always_invoice``) so the customer is charged the difference today, per the upgrade-preview copy. 404 if the caller holds no active item for ``from_tariff_version_id``; 409 if the target requires annual billing but has no annual Price (both via the typed exceptions' handlers in app/main.py). **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/dashboard/plans/change' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "from_tariff_version_id": 0, "to_tariff_version_id": 0, "billing_interval": "month" }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/dashboard/plans/change", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"from_tariff_version_id": 0, "to_tariff_version_id": 0, "billing_interval": "month"}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/plans/change", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"from_tariff_version_id": 0, "to_tariff_version_id": 0, "billing_interval": "month"}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"from_tariff_version_id": 0, "to_tariff_version_id": 0, "billing_interval": "month"}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/dashboard/plans/change", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/dashboard/plans/cancel **Cancel Subscription** Schedule the caller's active subscription to cancel at the end of the current billing period. The customer keeps access until ``current_period_end``; no item is deleted, so ``/resume`` can un-cancel. No body — a client has exactly one active parent subscription. 404 if the caller has no active subscription (via the typed exception's handler in app/main.py). :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/dashboard/plans/cancel' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/dashboard/plans/cancel", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/plans/cancel", { method: "POST", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/dashboard/plans/cancel", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response ## `POST` /api/v1/dashboard/plans/resume **Resume Subscription** Un-cancel the caller's active subscription (clear ``cancel_at_period_end``) — the correct "changed my mind before the period rolls" path, instead of minting a new subscription. No body. 404 if no active subscription; 409 if the subscription is not currently scheduled to cancel (both via the typed exceptions' handlers in app/main.py). :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/dashboard/plans/resume' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/dashboard/plans/resume", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/plans/resume", { method: "POST", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/dashboard/plans/resume", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response ## Dashboard Leads Source: https://spideriq.ai/docs/api-reference/dashboard-leads ## `GET` /api/v1/dashboard/client/leads **List Leads** List leads with pagination, search, and filters. **Parameters** - `page` (query, integer, optional) - `page_size` (query, integer, optional) - `search` (query, any, optional) - `source` (query, any, optional) - `has_email` (query, any, optional) - `has_verified_email` (query, any, optional) - `country_code` (query, any, optional) - `workflow_stage` (query, any, optional) - `sort_by` (query, string, optional) - `sort_dir` (query, string, optional) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/client/leads' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/client/leads", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/client/leads", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/client/leads", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/dashboard/client/leads/stats **Lead Stats** KPI counts for the leads dashboard header. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/client/leads/stats' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/client/leads/stats", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/client/leads/stats", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/client/leads/stats", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response ## `GET` /api/v1/dashboard/client/leads/export **Export Leads** Export all leads as CSV or JSON. **Parameters** - `format` (query, any, optional) — `csv` (default) returns a CSV attachment; `json` and `llm` return the JSON body. `llm` is a member because `middleware/format_llm.py` tells every Bearer agent to send it — before `SDS-33` it 422'd here. - `source` (query, any, optional) - `country_code` (query, any, optional) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/client/leads/export' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/client/leads/export", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/client/leads/export", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/client/leads/export", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/dashboard/client/leads/{lead_id} **Get Lead** Full lead detail with all enrichment data. **Parameters** - `lead_id` (path, integer, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/client/leads/{lead_id}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/client/leads/{lead_id}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/client/leads/{lead_id}", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/client/leads/{lead_id}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## Dashboard VayaPin Source: https://spideriq.ai/docs/api-reference/dashboard-vayapin ## `GET` /api/v1/dashboard/vayapin/stats **Get Vayapin Stats** Get VayaPin dashboard KPI statistics. Honors only the SCOPE filters (search / country / campaign / date range) — `status` + `has_seo` are deliberately ignored here because these KPIs ARE the status/SEO breakdown (Total / With-SEO / Success-rate). **Parameters** - `search` (query, any, optional) - `country` (query, any, optional) - `campaign_id` (query, any, optional) - `date_from` (query, any, optional) - `date_to` (query, any, optional) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/vayapin/stats' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/vayapin/stats", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/vayapin/stats", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/vayapin/stats", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/dashboard/vayapin/funnel **Get Vayapin Funnel** Conversion funnel: businesses → with-email → pin-created → SEO-enriched. Honors only the SCOPE filters (search / country / campaign / date range) — like `/stats`, it deliberately ignores `status` / `has_seo` / `reason` because the funnel IS that breakdown. Computed in a single pass via COUNT(*) FILTER over all rows in scope (NOT gated on `vayapin_status IS NOT NULL`), so businesses that never reached the pin stage are counted in `businesses_total`. Monotonicity (F.5, Option A). Every step is a genuine subset of the one above, so the chart shrinks left→right and never inverts: - `with_email` = the SAME gate pin-creation uses (module f: `skip = not best_email`, where best_email = a verified-domain-matched email OR any found site email). So a row counts if `emails_found` OR `emails_verified` is a non-empty array — OR the row already produced a pin (`vayapin_status='exported'`), because a created pin is proof-positive that the gate passed even on the ~755 older rows whose email arrays were never persisted back to CWJ. This makes `pin_created` a true subset of `with_email`. - `seo_enriched` is gated on `vayapin_status='exported'` too, so a pin whose PATCH failed but whose SEO markdown was generated cannot inflate the SEO step above `pin_created`. Pin-creation behavior is UNCHANGED — this is a display/definition change only. A secondary `with_verified_email` count (verified array non-empty) is returned for insight but is NOT part of the monotonic chain. VC.3 — the response additionally carries `metrics` (the eleven VayaCore contract fields plus one reconciliation aid, each RAW/unclamped and either valued or unavailable-with-a-reason) and `window` (the half-open UTC instant range actually queried). Everything above is untouched: the dashboard keeps its clamped chain, and no clamped field is ever substituted for a raw one. **Parameters** - `search` (query, any, optional) - `country` (query, any, optional) - `campaign_id` (query, any, optional) - `date_from` (query, any, optional) - `date_to` (query, any, optional) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/vayapin/funnel' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/vayapin/funnel", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/vayapin/funnel", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/vayapin/funnel", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/dashboard/vayapin/reasons **Get Vayapin Reasons** Reason breakdowns behind the funnel drops. `fail_reasons` = GROUP BY `vayapin_fail_reason` (why no pin — populated forward by the 2.1 callback). `seo_reasons` = GROUP BY `vayapin_seo_reason` (why no SEO content). Only non-null reasons returned, most-frequent first; served by the mig-379 partial indexes. Honors the SCOPE filters only (like `/stats`). **Parameters** - `search` (query, any, optional) - `country` (query, any, optional) - `campaign_id` (query, any, optional) - `date_from` (query, any, optional) - `date_to` (query, any, optional) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/vayapin/reasons' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/vayapin/reasons", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/vayapin/reasons", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/vayapin/reasons", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/dashboard/vayapin/pins **Get Vayapin Pins** Get paginated list of VayaPin rows with search and filters. Shows ALL businesses that reached the VayaPin stage — including failed and never-reached (`vayapin_status` NULL) rows — so the dashboard can surface WHY a lead got no pin (previously these were hidden by a `vayapin_status IS NOT NULL` gate). Use `status=exported` to narrow to created pins. **Parameters** - `page` (query, integer, optional) - `page_size` (query, integer, optional) - `search` (query, any, optional) - `country` (query, any, optional) - `status` (query, any, optional) - `has_seo` (query, any, optional) - `reason` (query, any, optional) — Filter to rows whose fail_reason OR seo_reason matches - `campaign_id` (query, any, optional) - `date_from` (query, any, optional) — Filter PINs exported on/after this date (YYYY-MM-DD) - `date_to` (query, any, optional) — Filter PINs exported on/before this date (YYYY-MM-DD) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/vayapin/pins' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/vayapin/pins", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/vayapin/pins", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/vayapin/pins", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/dashboard/vayapin/pins/{cwj_id} **Get Vayapin Pin Detail** Get full PIN detail including SEO content and API audit log. **Parameters** - `cwj_id` (path, integer, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/vayapin/pins/{cwj_id}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/vayapin/pins/{cwj_id}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/vayapin/pins/{cwj_id}", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/vayapin/pins/{cwj_id}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/dashboard/vayapin/countries **Get Vayapin Countries** Get country distribution for chart (honors the full filter bar). **Parameters** - `search` (query, any, optional) - `country` (query, any, optional) - `status` (query, any, optional) - `has_seo` (query, any, optional) - `campaign_id` (query, any, optional) - `date_from` (query, any, optional) - `date_to` (query, any, optional) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/vayapin/countries' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/vayapin/countries", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/vayapin/countries", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/vayapin/countries", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/dashboard/vayapin/campaigns **Get Vayapin Campaigns** List campaigns (flows) that produced VayaPin PINs, for the filter dropdown. Intentionally NOT filtered — it populates the campaign dropdown itself, so it must always show the full set of options regardless of the active filters. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/vayapin/campaigns' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/vayapin/campaigns", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/vayapin/campaigns", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/vayapin/campaigns", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response ## `GET` /api/v1/dashboard/vayapin/timeline **Get Vayapin Timeline** Get daily export counts for timeline chart (honors the full filter bar). When an explicit date range is set, it bounds the x-axis; otherwise the `days` lookback window applies. **Parameters** - `days` (query, integer, optional) - `search` (query, any, optional) - `country` (query, any, optional) - `status` (query, any, optional) - `has_seo` (query, any, optional) - `campaign_id` (query, any, optional) - `date_from` (query, any, optional) - `date_to` (query, any, optional) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/vayapin/timeline' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/vayapin/timeline", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/vayapin/timeline", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/vayapin/timeline", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/dashboard/vayapin/export-csv **Export Vayapin Csv** Export VayaPin rows as CSV (honors the active filter bar). Mirrors `/pins`: shows ALL rows in scope (including failed / never-reached), with the mig-379 reason columns so a client can diagnose drops in a spreadsheet. **Parameters** - `search` (query, any, optional) - `country` (query, any, optional) - `status` (query, any, optional) - `has_seo` (query, any, optional) - `reason` (query, any, optional) — Filter to rows whose fail_reason OR seo_reason matches - `campaign_id` (query, any, optional) - `date_from` (query, any, optional) - `date_to` (query, any, optional) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/vayapin/export-csv' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/vayapin/export-csv", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/vayapin/export-csv", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/vayapin/export-csv", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## Company Research Source: https://spideriq.ai/docs/api-reference/company-research ## `POST` /api/v1/company-research/submit **Submit company research job** Submit a new company research job with Celery workflow orchestration. ## Input Types - **locations**: List of locations to search (triggers SpiderMaps discovery) - **domains**: List of domains to research directly (skips SpiderMaps) ## Pipeline Stages 1. **SpiderMaps** - Business discovery from Google Maps 2. **SpiderSite** - Website crawling for contacts 3. **SpiderCompanyData** - Company registry enrichment (US/UK/EU) 4. **Social Enrichment** - Instagram, Facebook, LinkedIn 5. **SpiderVerify** - Email verification ## SpiderCompanyData Configuration ```json { "config": { "spidercompanydata": { "enabled": true, "countries_filter": ["US", "GB"], "include_officers": true, "include_financials": false } } } ``` **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/company-research/submit' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "name": "string", "locations": [ { "city": "string", "state": "string", "country": "string", "postal_code": "string", "address": "string" } ], "domains": [ "string" ], "search_query": "string", "config": { "spidermaps": { "max_results": 100, "search_radius_km": 10.0, "include_place_details": true }, "spidersite": { "enabled": true, "max_pages": 10, "crawl_strategy": "bestfirst", "target_pages": [ "contact", "about", "team" ], "enable_spa": true, "spa_timeout": 30, "extract_team": false, "extract_company_info": true, "timeout": 30 }, "spidercompanydata": { "enabled": false, "countries_filter": {}, "include_officers": false, "include_financials": false, "include_vat_validation": false, "match_threshold": 0.7, "timeout": 30 }, "social_enrichment": { "enabled": true, "instagram": true, "facebook": true, "linkedin": true, "timeout": 30 }, "spiderverify": { "enabled": true, "check_gravatar": false, "check_dnsbl": false, "smtp_timeout_secs": 45, "max_emails_per_company": 10 }, "domain_filter": { "filter_social_media": true, "filter_review_sites": true, "filter_directories": true, "custom_blacklist": [ {} ] }, "deduplication": true }, "webhook_url": "string", "webhook_secret": "string", "priority": 5 }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/company-research/submit", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"name": "string", "locations": [{"city": "string", "state": "string", "country": "string", "postal_code": "string", "address": "string"}], "domains": ["string"], "search_query": "string", "config": {"spidermaps": {"max_results": 100, "search_radius_km": 10.0, "include_place_details": true}, "spidersite": {"enabled": true, "max_pages": 10, "crawl_strategy": "bestfirst", "target_pages": ["contact", "about", "team"], "enable_spa": true, "spa_timeout": 30, "extract_team": false, "extract_company_info": true, "timeout": 30}, "spidercompanydata": {"enabled": false, "countries_filter": {}, "include_officers": false, "include_financials": false, "include_vat_validation": false, "match_threshold": 0.7, "timeout": 30}, "social_enrichment": {"enabled": true, "instagram": true, "facebook": true, "linkedin": true, "timeout": 30}, "spiderverify": {"enabled": true, "check_gravatar": false, "check_dnsbl": false, "smtp_timeout_secs": 45, "max_emails_per_company": 10}, "domain_filter": {"filter_social_media": true, "filter_review_sites": true, "filter_directories": true, "custom_blacklist": [{}]}, "deduplication": true}, "webhook_url": "string", "webhook_secret": "string", "priority": 5}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/company-research/submit", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"name": "string", "locations": [{"city": "string", "state": "string", "country": "string", "postal_code": "string", "address": "string"}], "domains": ["string"], "search_query": "string", "config": {"spidermaps": {"max_results": 100, "search_radius_km": 10.0, "include_place_details": true}, "spidersite": {"enabled": true, "max_pages": 10, "crawl_strategy": "bestfirst", "target_pages": ["contact", "about", "team"], "enable_spa": true, "spa_timeout": 30, "extract_team": false, "extract_company_info": true, "timeout": 30}, "spidercompanydata": {"enabled": false, "countries_filter": {}, "include_officers": false, "include_financials": false, "include_vat_validation": false, "match_threshold": 0.7, "timeout": 30}, "social_enrichment": {"enabled": true, "instagram": true, "facebook": true, "linkedin": true, "timeout": 30}, "spiderverify": {"enabled": true, "check_gravatar": false, "check_dnsbl": false, "smtp_timeout_secs": 45, "max_emails_per_company": 10}, "domain_filter": {"filter_social_media": true, "filter_review_sites": true, "filter_directories": true, "custom_blacklist": [{}]}, "deduplication": true}, "webhook_url": "string", "webhook_secret": "string", "priority": 5}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"name": "string", "locations": [{"city": "string", "state": "string", "country": "string", "postal_code": "string", "address": "string"}], "domains": ["string"], "search_query": "string", "config": {"spidermaps": {"max_results": 100, "search_radius_km": 10.0, "include_place_details": true}, "spidersite": {"enabled": true, "max_pages": 10, "crawl_strategy": "bestfirst", "target_pages": ["contact", "about", "team"], "enable_spa": true, "spa_timeout": 30, "extract_team": false, "extract_company_info": true, "timeout": 30}, "spidercompanydata": {"enabled": false, "countries_filter": {}, "include_officers": false, "include_financials": false, "include_vat_validation": false, "match_threshold": 0.7, "timeout": 30}, "social_enrichment": {"enabled": true, "instagram": true, "facebook": true, "linkedin": true, "timeout": 30}, "spiderverify": {"enabled": true, "check_gravatar": false, "check_dnsbl": false, "smtp_timeout_secs": 45, "max_emails_per_company": 10}, "domain_filter": {"filter_social_media": true, "filter_review_sites": true, "filter_directories": true, "custom_blacklist": [{}]}, "deduplication": true}, "webhook_url": "string", "webhook_secret": "string", "priority": 5}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/company-research/submit", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `201` — Successful Response - `422` — Validation Error ## `GET` /api/v1/company-research/{research_id}/status **Get research job status** Get the current status and progress of a research job. **Parameters** - `research_id` (path, string, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/company-research/{research_id}/status' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/company-research/{research_id}/status", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/company-research/{research_id}/status", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/company-research/{research_id}/status", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/company-research/{research_id}/results **Get research job results** Get the results of a completed research job. Includes all discovered companies with: - Contact information (emails, phones) - Registry data (registration number, officers, financials) - Social media profiles (Instagram, Facebook, LinkedIn) - Company info extracted from website **Parameters** - `research_id` (path, string, required) - `include_registry_data` (query, boolean, optional) — Include SpiderCompanyData registry results :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/company-research/{research_id}/results' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/company-research/{research_id}/results", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/company-research/{research_id}/results", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/company-research/{research_id}/results", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/company-research/{research_id}/cancel **Cancel research job** Cancel a running or queued research job. **Parameters** - `research_id` (path, string, required) :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/company-research/{research_id}/cancel' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/company-research/{research_id}/cancel", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/company-research/{research_id}/cancel", { method: "POST", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/company-research/{research_id}/cancel", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/company-research **List research jobs** List all research jobs for the current client. **Parameters** - `status` (query, any, optional) — Filter by status: queued, processing, completed, failed, cancelled - `limit` (query, integer, optional) - `offset` (query, integer, optional) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/company-research' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/company-research", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/company-research", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/company-research", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/company-research/{research_id}/pause **Pause research job** Pause a running research job. The job will stop processing new companies but will complete any currently running tasks. Use `/resume` to continue. **Parameters** - `research_id` (path, string, required) :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/company-research/{research_id}/pause' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/company-research/{research_id}/pause", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/company-research/{research_id}/pause", { method: "POST", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/company-research/{research_id}/pause", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/company-research/{research_id}/resume **Resume paused research job** Resume a paused research job and continue processing. **Parameters** - `research_id` (path, string, required) :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/company-research/{research_id}/resume' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/company-research/{research_id}/resume", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/company-research/{research_id}/resume", { method: "POST", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/company-research/{research_id}/resume", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `PATCH` /api/v1/company-research/{research_id} **Update research job configuration** Update configuration for a research job. Allowed updates: - name: Job display name - config: Stage configurations (spidercompanydata, social_enrichment, etc.) - priority: Job priority (0-10) - auto_submit: Auto-submit settings **Parameters** - `research_id` (path, string, required) **Request body** :::codegroup ```bash curl -X PATCH 'https://spideriq.ai/api/v1/company-research/{research_id}' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "name": "string", "config": { "spidermaps": { "max_results": 100, "search_radius_km": 10.0, "include_place_details": true }, "spidersite": { "enabled": true, "max_pages": 10, "crawl_strategy": "bestfirst", "target_pages": [ "contact", "about", "team" ], "enable_spa": true, "spa_timeout": 30, "extract_team": false, "extract_company_info": true, "timeout": 30 }, "spidercompanydata": { "enabled": false, "countries_filter": {}, "include_officers": false, "include_financials": false, "include_vat_validation": false, "match_threshold": 0.7, "timeout": 30 }, "social_enrichment": { "enabled": true, "instagram": true, "facebook": true, "linkedin": true, "timeout": 30 }, "spiderverify": { "enabled": true, "check_gravatar": false, "check_dnsbl": false, "smtp_timeout_secs": 45, "max_emails_per_company": 10 }, "domain_filter": { "filter_social_media": true, "filter_review_sites": true, "filter_directories": true, "custom_blacklist": [ {} ] }, "deduplication": true }, "priority": 0, "webhook_url": "string", "webhook_secret": "string" }' ``` ```python import httpx resp = httpx.patch( "https://spideriq.ai/api/v1/company-research/{research_id}", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"name": "string", "config": {"spidermaps": {"max_results": 100, "search_radius_km": 10.0, "include_place_details": true}, "spidersite": {"enabled": true, "max_pages": 10, "crawl_strategy": "bestfirst", "target_pages": ["contact", "about", "team"], "enable_spa": true, "spa_timeout": 30, "extract_team": false, "extract_company_info": true, "timeout": 30}, "spidercompanydata": {"enabled": false, "countries_filter": {}, "include_officers": false, "include_financials": false, "include_vat_validation": false, "match_threshold": 0.7, "timeout": 30}, "social_enrichment": {"enabled": true, "instagram": true, "facebook": true, "linkedin": true, "timeout": 30}, "spiderverify": {"enabled": true, "check_gravatar": false, "check_dnsbl": false, "smtp_timeout_secs": 45, "max_emails_per_company": 10}, "domain_filter": {"filter_social_media": true, "filter_review_sites": true, "filter_directories": true, "custom_blacklist": [{}]}, "deduplication": true}, "priority": 0, "webhook_url": "string", "webhook_secret": "string"}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/company-research/{research_id}", { method: "PATCH", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"name": "string", "config": {"spidermaps": {"max_results": 100, "search_radius_km": 10.0, "include_place_details": true}, "spidersite": {"enabled": true, "max_pages": 10, "crawl_strategy": "bestfirst", "target_pages": ["contact", "about", "team"], "enable_spa": true, "spa_timeout": 30, "extract_team": false, "extract_company_info": true, "timeout": 30}, "spidercompanydata": {"enabled": false, "countries_filter": {}, "include_officers": false, "include_financials": false, "include_vat_validation": false, "match_threshold": 0.7, "timeout": 30}, "social_enrichment": {"enabled": true, "instagram": true, "facebook": true, "linkedin": true, "timeout": 30}, "spiderverify": {"enabled": true, "check_gravatar": false, "check_dnsbl": false, "smtp_timeout_secs": 45, "max_emails_per_company": 10}, "domain_filter": {"filter_social_media": true, "filter_review_sites": true, "filter_directories": true, "custom_blacklist": [{}]}, "deduplication": true}, "priority": 0, "webhook_url": "string", "webhook_secret": "string"}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"name": "string", "config": {"spidermaps": {"max_results": 100, "search_radius_km": 10.0, "include_place_details": true}, "spidersite": {"enabled": true, "max_pages": 10, "crawl_strategy": "bestfirst", "target_pages": ["contact", "about", "team"], "enable_spa": true, "spa_timeout": 30, "extract_team": false, "extract_company_info": true, "timeout": 30}, "spidercompanydata": {"enabled": false, "countries_filter": {}, "include_officers": false, "include_financials": false, "include_vat_validation": false, "match_threshold": 0.7, "timeout": 30}, "social_enrichment": {"enabled": true, "instagram": true, "facebook": true, "linkedin": true, "timeout": 30}, "spiderverify": {"enabled": true, "check_gravatar": false, "check_dnsbl": false, "smtp_timeout_secs": 45, "max_emails_per_company": 10}, "domain_filter": {"filter_social_media": true, "filter_review_sites": true, "filter_directories": true, "custom_blacklist": [{}]}, "deduplication": true}, "priority": 0, "webhook_url": "string", "webhook_secret": "string"}`) req, _ := http.NewRequest("PATCH", "https://spideriq.ai/api/v1/company-research/{research_id}", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/company-research/{research_id}/wait **Wait for research job completion** Wait for research job to complete and return results. This is a blocking endpoint that polls until: - Job completes (returns results) - Job fails (returns error) - Timeout reached (returns current status) **Parameters** - `research_id` (path, string, required) - `timeout` (query, integer, optional) — Timeout in seconds - `poll_interval` (query, integer, optional) — Poll interval in seconds :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/company-research/{research_id}/wait' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/company-research/{research_id}/wait", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/company-research/{research_id}/wait", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/company-research/{research_id}/wait", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## Dashboard Playbooks Source: https://spideriq.ai/docs/api-reference/dashboard-playbooks ## `GET` /api/v1/playbooks **List Playbooks** List playbooks for the authenticated user. Supports filtering by: - status: queued, processing, completed, failed, paused - playbook_type: basic, plus - active_only: Only show playbooks with locations or companies (default: true) Supports sorting by: - created_at, name, status, current_stage, companies_found, companies_processed, emails_verified **Parameters** - `page` (query, integer, optional) - `page_size` (query, integer, optional) - `status` (query, any, optional) - `playbook_type` (query, any, optional) - `sort_by` (query, any, optional) — Field to sort by - `sort_order` (query, any, optional) — Sort order: asc or desc - `active_only` (query, boolean, optional) — Only show playbooks with actual workflow data :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/playbooks' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/playbooks", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/playbooks", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/playbooks", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/playbooks **Create Playbook** Create a new playbook. Supports both 'basic' (Maps → Site → Verify) and 'plus' (full pipeline) types. **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/playbooks' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{}' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/playbooks", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/playbooks", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/playbooks", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/playbooks/{playbook_id}/status **Get Playbook Status** Get detailed status for a specific playbook. **Parameters** - `playbook_id` (path, string, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/playbooks/{playbook_id}/status' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/playbooks/{playbook_id}/status", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/playbooks/{playbook_id}/status", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/playbooks/{playbook_id}/status", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `PATCH` /api/v1/playbooks/{playbook_id} **Update Playbook** Update a playbook's configuration. Allows updating: - name: Playbook display name - config: Worker configuration (spidermaps, spidersite, etc.) - priority: Job priority - webhook_url: Webhook for notifications - webhook_secret: Webhook signing secret **Parameters** - `playbook_id` (path, string, required) **Request body** :::codegroup ```bash curl -X PATCH 'https://spideriq.ai/api/v1/playbooks/{playbook_id}' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{}' ``` ```python import httpx resp = httpx.patch( "https://spideriq.ai/api/v1/playbooks/{playbook_id}", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/playbooks/{playbook_id}", { method: "PATCH", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{}`) req, _ := http.NewRequest("PATCH", "https://spideriq.ai/api/v1/playbooks/{playbook_id}", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/playbooks/{playbook_id}/results **Get Playbook Results** Get results (companies) for a specific playbook. **Parameters** - `playbook_id` (path, string, required) - `page` (query, integer, optional) - `page_size` (query, integer, optional) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/playbooks/{playbook_id}/results' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/playbooks/{playbook_id}/results", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/playbooks/{playbook_id}/results", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/playbooks/{playbook_id}/results", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/playbooks/{playbook_id}/pause **Pause Playbook** Pause a running playbook. **Parameters** - `playbook_id` (path, string, required) :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/playbooks/{playbook_id}/pause' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/playbooks/{playbook_id}/pause", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/playbooks/{playbook_id}/pause", { method: "POST", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/playbooks/{playbook_id}/pause", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/playbooks/{playbook_id}/resume **Resume Playbook** Resume a paused playbook. **Parameters** - `playbook_id` (path, string, required) :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/playbooks/{playbook_id}/resume' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/playbooks/{playbook_id}/resume", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/playbooks/{playbook_id}/resume", { method: "POST", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/playbooks/{playbook_id}/resume", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/playbooks/{playbook_id}/cancel **Cancel Playbook** Cancel a playbook. **Parameters** - `playbook_id` (path, string, required) :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/playbooks/{playbook_id}/cancel' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/playbooks/{playbook_id}/cancel", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/playbooks/{playbook_id}/cancel", { method: "POST", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/playbooks/{playbook_id}/cancel", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/playbooks/{playbook_id}/retry **Retry Playbook** Retry a playbook by re-submitting it with the same configuration. Creates a new playbook with the same settings as the original. Works for completed, failed, or cancelled playbooks. **Parameters** - `playbook_id` (path, string, required) :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/playbooks/{playbook_id}/retry' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/playbooks/{playbook_id}/retry", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/playbooks/{playbook_id}/retry", { method: "POST", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/playbooks/{playbook_id}/retry", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/playbooks/{playbook_id}/locations/{location_id}/stop **Stop Stuck Location** Stop a stuck location job. Marks the location as failed and cancels any pending/processing jobs. Use this when a location is stuck for too long. **Parameters** - `playbook_id` (path, string, required) - `location_id` (path, integer, required) :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/playbooks/{playbook_id}/locations/{location_id}/stop' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/playbooks/{playbook_id}/locations/{location_id}/stop", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/playbooks/{playbook_id}/locations/{location_id}/stop", { method: "POST", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/playbooks/{playbook_id}/locations/{location_id}/stop", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/playbooks/{playbook_id}/runs **Get Playbook Runs** Get Inngest workflow runs for a playbook. Returns the list of workflow executions with their status and timing. **Parameters** - `playbook_id` (path, string, required) - `page` (query, integer, optional) - `page_size` (query, integer, optional) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/playbooks/{playbook_id}/runs' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/playbooks/{playbook_id}/runs", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/playbooks/{playbook_id}/runs", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/playbooks/{playbook_id}/runs", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/playbooks/{playbook_id}/runs/{run_id} **Get Playbook Run Detail** Get detailed information about a specific workflow run. Includes waterfall trace data if available. **Parameters** - `playbook_id` (path, string, required) - `run_id` (path, integer, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/playbooks/{playbook_id}/runs/{run_id}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/playbooks/{playbook_id}/runs/{run_id}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/playbooks/{playbook_id}/runs/{run_id}", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/playbooks/{playbook_id}/runs/{run_id}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## Brands Source: https://spideriq.ai/docs/api-reference/brands ## `GET` /api/v1/brands **List My Brands** List all brands the current user is a member of. Returns brands with the user's role in each brand. For super_admin, returns ALL brands with admin role. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/brands' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/brands", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/brands", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/brands", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response ## `POST` /api/v1/brands **Create Brand** Create a new brand. The current user becomes the owner of the brand. **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/brands' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "brand_name": "string", "slug": "string" }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/brands", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"brand_name": "string", "slug": "string"}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/brands", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"brand_name": "string", "slug": "string"}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"brand_name": "string", "slug": "string"}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/brands", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `201` — Successful Response - `422` — Validation Error ## `GET` /api/v1/brands/{brand_id} **Get Brand** Get brand details. User must be a member of the brand. **Parameters** - `brand_id` (path, integer, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/brands/{brand_id}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/brands/{brand_id}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/brands/{brand_id}", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/brands/{brand_id}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `PATCH` /api/v1/brands/{brand_id} **Update Brand** Update brand details. Requires admin or owner role in the brand. **Parameters** - `brand_id` (path, integer, required) **Request body** :::codegroup ```bash curl -X PATCH 'https://spideriq.ai/api/v1/brands/{brand_id}' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "brand_name": "string", "subscription_status": "inactive", "is_active": true, "billing_email": "user@example.com" }' ``` ```python import httpx resp = httpx.patch( "https://spideriq.ai/api/v1/brands/{brand_id}", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"brand_name": "string", "subscription_status": "inactive", "is_active": true, "billing_email": "user@example.com"}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/brands/{brand_id}", { method: "PATCH", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"brand_name": "string", "subscription_status": "inactive", "is_active": true, "billing_email": "user@example.com"}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"brand_name": "string", "subscription_status": "inactive", "is_active": true, "billing_email": "user@example.com"}`) req, _ := http.NewRequest("PATCH", "https://spideriq.ai/api/v1/brands/{brand_id}", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/brands/{brand_id}/settings **Get Brand Settings** Get brand settings. Requires admin access. **Parameters** - `brand_id` (path, integer, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/brands/{brand_id}/settings' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/brands/{brand_id}/settings", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/brands/{brand_id}/settings", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/brands/{brand_id}/settings", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `PATCH` /api/v1/brands/{brand_id}/settings **Update Brand Settings** Update brand settings. Requires admin access. **Parameters** - `brand_id` (path, integer, required) **Request body** :::codegroup ```bash curl -X PATCH 'https://spideriq.ai/api/v1/brands/{brand_id}/settings' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "default_rate_limit": 0, "default_campaign_quota_daily": 0, "default_campaign_quota_monthly": 0, "default_campaign_max_concurrent": 0, "default_campaign_max_locations": 0, "default_auto_campaign_enabled": true, "default_auto_campaign_rate_per_hour": 0, "default_auto_campaign_capacity_threshold": 0, "fuzziq_enabled": true, "opensearch_enabled": true, "brand_voice": "string", "brand_tone": "string", "brand_name_meaning": "string", "brand_promise": "string", "logo_url": "string" }' ``` ```python import httpx resp = httpx.patch( "https://spideriq.ai/api/v1/brands/{brand_id}/settings", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"default_rate_limit": 0, "default_campaign_quota_daily": 0, "default_campaign_quota_monthly": 0, "default_campaign_max_concurrent": 0, "default_campaign_max_locations": 0, "default_auto_campaign_enabled": true, "default_auto_campaign_rate_per_hour": 0, "default_auto_campaign_capacity_threshold": 0, "fuzziq_enabled": true, "opensearch_enabled": true, "brand_voice": "string", "brand_tone": "string", "brand_name_meaning": "string", "brand_promise": "string", "logo_url": "string"}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/brands/{brand_id}/settings", { method: "PATCH", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"default_rate_limit": 0, "default_campaign_quota_daily": 0, "default_campaign_quota_monthly": 0, "default_campaign_max_concurrent": 0, "default_campaign_max_locations": 0, "default_auto_campaign_enabled": true, "default_auto_campaign_rate_per_hour": 0, "default_auto_campaign_capacity_threshold": 0, "fuzziq_enabled": true, "opensearch_enabled": true, "brand_voice": "string", "brand_tone": "string", "brand_name_meaning": "string", "brand_promise": "string", "logo_url": "string"}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"default_rate_limit": 0, "default_campaign_quota_daily": 0, "default_campaign_quota_monthly": 0, "default_campaign_max_concurrent": 0, "default_campaign_max_locations": 0, "default_auto_campaign_enabled": true, "default_auto_campaign_rate_per_hour": 0, "default_auto_campaign_capacity_threshold": 0, "fuzziq_enabled": true, "opensearch_enabled": true, "brand_voice": "string", "brand_tone": "string", "brand_name_meaning": "string", "brand_promise": "string", "logo_url": "string"}`) req, _ := http.NewRequest("PATCH", "https://spideriq.ai/api/v1/brands/{brand_id}/settings", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/brands/{brand_id}/logo **Upload Brand Logo** Upload brand logo. Requires admin access. The image will be: - Center-cropped to square - Resized to 256x256 max - Converted to WebP format **Parameters** - `brand_id` (path, integer, required) **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/brands/{brand_id}/logo' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "file": "string" }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/brands/{brand_id}/logo", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"file": "string"}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/brands/{brand_id}/logo", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"file": "string"}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"file": "string"}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/brands/{brand_id}/logo", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `DELETE` /api/v1/brands/{brand_id}/logo **Delete Brand Logo** Delete brand logo. Requires admin access. **Parameters** - `brand_id` (path, integer, required) :::codegroup ```bash curl -X DELETE 'https://spideriq.ai/api/v1/brands/{brand_id}/logo' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.delete( "https://spideriq.ai/api/v1/brands/{brand_id}/logo", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/brands/{brand_id}/logo", { method: "DELETE", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("DELETE", "https://spideriq.ai/api/v1/brands/{brand_id}/logo", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/brands/{brand_id}/information **Get Brand Information** Get brand business information. Requires admin access. **Parameters** - `brand_id` (path, integer, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/brands/{brand_id}/information' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/brands/{brand_id}/information", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/brands/{brand_id}/information", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/brands/{brand_id}/information", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `PATCH` /api/v1/brands/{brand_id}/information **Update Brand Information** Update brand business information. Requires admin access. **Parameters** - `brand_id` (path, integer, required) **Request body** :::codegroup ```bash curl -X PATCH 'https://spideriq.ai/api/v1/brands/{brand_id}/information' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "company_name": "string", "website": "string", "description": "string", "industry": "string", "phone": "string", "mobile": "string", "street1": "string", "street2": "string", "city": "string", "state": "string", "postcode": "string", "country": "string", "tax_id": "string", "vat_id": "string", "support_email": "user@example.com", "linkedin": "string", "youtube": "string", "instagram": "string", "x": "string", "facebook": "string" }' ``` ```python import httpx resp = httpx.patch( "https://spideriq.ai/api/v1/brands/{brand_id}/information", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"company_name": "string", "website": "string", "description": "string", "industry": "string", "phone": "string", "mobile": "string", "street1": "string", "street2": "string", "city": "string", "state": "string", "postcode": "string", "country": "string", "tax_id": "string", "vat_id": "string", "support_email": "user@example.com", "linkedin": "string", "youtube": "string", "instagram": "string", "x": "string", "facebook": "string"}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/brands/{brand_id}/information", { method: "PATCH", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"company_name": "string", "website": "string", "description": "string", "industry": "string", "phone": "string", "mobile": "string", "street1": "string", "street2": "string", "city": "string", "state": "string", "postcode": "string", "country": "string", "tax_id": "string", "vat_id": "string", "support_email": "user@example.com", "linkedin": "string", "youtube": "string", "instagram": "string", "x": "string", "facebook": "string"}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"company_name": "string", "website": "string", "description": "string", "industry": "string", "phone": "string", "mobile": "string", "street1": "string", "street2": "string", "city": "string", "state": "string", "postcode": "string", "country": "string", "tax_id": "string", "vat_id": "string", "support_email": "user@example.com", "linkedin": "string", "youtube": "string", "instagram": "string", "x": "string", "facebook": "string"}`) req, _ := http.NewRequest("PATCH", "https://spideriq.ai/api/v1/brands/{brand_id}/information", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/brands/{brand_id}/members **List Brand Members** List all members of a brand. User must be a member of the brand to view. **Parameters** - `brand_id` (path, integer, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/brands/{brand_id}/members' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/brands/{brand_id}/members", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/brands/{brand_id}/members", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/brands/{brand_id}/members", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `PATCH` /api/v1/brands/{brand_id}/members/{member_user_id} **Update Member** Update a member's role or position. Requires admin access. Cannot modify owners. **Parameters** - `brand_id` (path, integer, required) - `member_user_id` (path, string, required) **Request body** :::codegroup ```bash curl -X PATCH 'https://spideriq.ai/api/v1/brands/{brand_id}/members/{member_user_id}' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "role": "owner", "position": "string", "status": "active" }' ``` ```python import httpx resp = httpx.patch( "https://spideriq.ai/api/v1/brands/{brand_id}/members/{member_user_id}", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"role": "owner", "position": "string", "status": "active"}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/brands/{brand_id}/members/{member_user_id}", { method: "PATCH", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"role": "owner", "position": "string", "status": "active"}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"role": "owner", "position": "string", "status": "active"}`) req, _ := http.NewRequest("PATCH", "https://spideriq.ai/api/v1/brands/{brand_id}/members/{member_user_id}", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `DELETE` /api/v1/brands/{brand_id}/members/{member_user_id} **Remove Member** Remove a member from the brand. Requires admin access. Cannot remove owners. **Parameters** - `brand_id` (path, integer, required) - `member_user_id` (path, string, required) :::codegroup ```bash curl -X DELETE 'https://spideriq.ai/api/v1/brands/{brand_id}/members/{member_user_id}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.delete( "https://spideriq.ai/api/v1/brands/{brand_id}/members/{member_user_id}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/brands/{brand_id}/members/{member_user_id}", { method: "DELETE", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("DELETE", "https://spideriq.ai/api/v1/brands/{brand_id}/members/{member_user_id}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/brands/{brand_id}/invitations **List Brand Invitations** List invitations for a brand. Requires admin access to the brand. **Parameters** - `brand_id` (path, integer, required) - `status` (query, any, optional) — Filter by status: pending, accepted, expired, canceled :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/brands/{brand_id}/invitations' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/brands/{brand_id}/invitations", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/brands/{brand_id}/invitations", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/brands/{brand_id}/invitations", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/brands/{brand_id}/invitations **Create Invitation** Invite a user to join the brand. Requires admin access. Sends an email invitation. **Parameters** - `brand_id` (path, integer, required) **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/brands/{brand_id}/invitations' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "email": "user@example.com", "role": "member" }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/brands/{brand_id}/invitations", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"email": "user@example.com", "role": "member"}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/brands/{brand_id}/invitations", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"email": "user@example.com", "role": "member"}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"email": "user@example.com", "role": "member"}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/brands/{brand_id}/invitations", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `201` — Successful Response - `422` — Validation Error ## `DELETE` /api/v1/brands/{brand_id}/invitations/{invitation_id} **Revoke Invitation** Revoke a pending invitation. Requires admin access. **Parameters** - `brand_id` (path, integer, required) - `invitation_id` (path, integer, required) :::codegroup ```bash curl -X DELETE 'https://spideriq.ai/api/v1/brands/{brand_id}/invitations/{invitation_id}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.delete( "https://spideriq.ai/api/v1/brands/{brand_id}/invitations/{invitation_id}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/brands/{brand_id}/invitations/{invitation_id}", { method: "DELETE", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("DELETE", "https://spideriq.ai/api/v1/brands/{brand_id}/invitations/{invitation_id}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/brands/{brand_id}/invitations/{invitation_id}/resend **Resend Invitation** Resend an invitation (generates new token, resets expiry). Requires admin access. **Parameters** - `brand_id` (path, integer, required) - `invitation_id` (path, integer, required) :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/brands/{brand_id}/invitations/{invitation_id}/resend' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/brands/{brand_id}/invitations/{invitation_id}/resend", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/brands/{brand_id}/invitations/{invitation_id}/resend", { method: "POST", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/brands/{brand_id}/invitations/{invitation_id}/resend", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/brands/invitations/view/{token} **View Invitation By Token** View invitation details by token. This is a public endpoint (no auth required) so users can see the invitation details before signing up/logging in. **Parameters** - `token` (path, string, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/brands/invitations/view/{token}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/brands/invitations/view/{token}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/brands/invitations/view/{token}", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/brands/invitations/view/{token}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/brands/invitations/accept **Accept Invitation** Accept an invitation to join a brand. The invitation token is obtained from the invitation email link. User must be logged in to accept. This endpoint uses session-only auth because the user may not be provisioned in dashboard_users yet. **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/brands/invitations/accept' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "token": "string" }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/brands/invitations/accept", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"token": "string"}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/brands/invitations/accept", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"token": "string"}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"token": "string"}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/brands/invitations/accept", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/brands/invitations/pending **Get My Pending Invitations** Get pending invitations for the current user's email. Useful to show invitations on the dashboard. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/brands/invitations/pending' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/brands/invitations/pending", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/brands/invitations/pending", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/brands/invitations/pending", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response ## `GET` /api/v1/brands/admin/all **List All Brands** List all brands in the system. Super admin only. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/brands/admin/all' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/brands/admin/all", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/brands/admin/all", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/brands/admin/all", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response ## Agent Users Source: https://spideriq.ai/docs/api-reference/agent-users ## `GET` /api/v1/brands/{brand_id}/agent-users **List Agent Users** **Parameters** - `brand_id` (path, integer, required) - `include_inactive` (query, boolean, optional) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/brands/{brand_id}/agent-users' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/brands/{brand_id}/agent-users", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/brands/{brand_id}/agent-users", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/brands/{brand_id}/agent-users", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `DELETE` /api/v1/brands/{brand_id}/agent-users/{agent_user_id} **Revoke Agent User** Revoke the agent's token *for this brand only*. Cross-brand isolation: the agent keeps working in every other brand where it still has a live token. The underlying ``agent_users`` row persists — if the agent re-auths later, we'll refresh its tokens again with the same OPVS identity. **Parameters** - `brand_id` (path, integer, required) - `agent_user_id` (path, string, required) :::codegroup ```bash curl -X DELETE 'https://spideriq.ai/api/v1/brands/{brand_id}/agent-users/{agent_user_id}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.delete( "https://spideriq.ai/api/v1/brands/{brand_id}/agent-users/{agent_user_id}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/brands/{brand_id}/agent-users/{agent_user_id}", { method: "DELETE", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("DELETE", "https://spideriq.ai/api/v1/brands/{brand_id}/agent-users/{agent_user_id}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/brands/{brand_id}/agent-users/{agent_user_id}/regenerate **Regenerate Agent Token** Rotate the PAT for this agent in this brand. The plaintext is returned in the response — this is a dashboard-initiated rotation and the caller is an authenticated admin, so there's no Redis hand-off dance (that exists for the email-approval flow where the agent and the approver are different entities). Agent must update its stored token manually, same as any API-key rotation. **Parameters** - `brand_id` (path, integer, required) - `agent_user_id` (path, string, required) :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/brands/{brand_id}/agent-users/{agent_user_id}/regenerate' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/brands/{brand_id}/agent-users/{agent_user_id}/regenerate", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/brands/{brand_id}/agent-users/{agent_user_id}/regenerate", { method: "POST", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/brands/{brand_id}/agent-users/{agent_user_id}/regenerate", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `PATCH` /api/v1/brands/{brand_id}/agent-users/{agent_user_id}/rename **Rename Agent User** Rename an agent — its readable **display name** and/or its public **OPVS handle**. Two independent operations, either or both in one call: - **Display name** — edits the local ``agent_users.display_name`` with owner-scoped uniqueness (an owner can't have two agents with the same name), exactly as before. - **OPVS handle** — when ``new_handle`` is supplied, calls the OPVS rename API (``POST /opvs/agents/rename``) with the agent's own decrypted ``api_key``, then updates ``agent_users.opvs_address`` to the canonical address OPVS returns. The old handle is disposed per ``old_address_disposition`` (default ``alias`` for ``alias_ttl_days``). ``api_key`` is unchanged by the rename; a future rotated key is re-encrypted defensively. Contract: ``docs/external/opvs-agent-rename-api-reply-2026-07-13.md``. Brand isolation: the caller must be an admin of a brand where this agent holds a live token. **Parameters** - `brand_id` (path, integer, required) - `agent_user_id` (path, string, required) **Request body** :::codegroup ```bash curl -X PATCH 'https://spideriq.ai/api/v1/brands/{brand_id}/agent-users/{agent_user_id}/rename' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "display_name": "string", "new_handle": "string", "old_address_disposition": "alias", "alias_ttl_days": 30 }' ``` ```python import httpx resp = httpx.patch( "https://spideriq.ai/api/v1/brands/{brand_id}/agent-users/{agent_user_id}/rename", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"display_name": "string", "new_handle": "string", "old_address_disposition": "alias", "alias_ttl_days": 30}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/brands/{brand_id}/agent-users/{agent_user_id}/rename", { method: "PATCH", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"display_name": "string", "new_handle": "string", "old_address_disposition": "alias", "alias_ttl_days": 30}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"display_name": "string", "new_handle": "string", "old_address_disposition": "alias", "alias_ttl_days": 30}`) req, _ := http.NewRequest("PATCH", "https://spideriq.ai/api/v1/brands/{brand_id}/agent-users/{agent_user_id}/rename", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/brands/{brand_id}/agent-users/{agent_user_id}/handle-available **Check Agent Handle Available** Owner-gated proxy for OPVS Op1 (``GET /opvs/agents/handle-available``). The dashboard calls this (debounced) before submitting a handle rename so it can show name-free/taken live. Scoped to an agent the caller administers in this brand — the agent's own ``opvs_domain`` is used. An illegal handle short-circuits to ``available:false, reason:"invalid_format"`` without a round-trip (matching the upstream contract). **Parameters** - `brand_id` (path, integer, required) - `agent_user_id` (path, string, required) - `handle` (query, string, required) — Candidate OPVS handle :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/brands/{brand_id}/agent-users/{agent_user_id}/handle-available' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/brands/{brand_id}/agent-users/{agent_user_id}/handle-available", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/brands/{brand_id}/agent-users/{agent_user_id}/handle-available", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/brands/{brand_id}/agent-users/{agent_user_id}/handle-available", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/brands/{brand_id}/agent-users/{agent_user_id}/inbox **Get Agent Inbox** List OPVS messages addressed to this agent (newest first). **Parameters** - `brand_id` (path, integer, required) - `agent_user_id` (path, string, required) - `status` (query, any, optional) - `limit` (query, integer, optional) - `since` (query, any, optional) - `thread_id` (query, any, optional) - `subaddress` (query, any, optional) — +session sub-address filter :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/brands/{brand_id}/agent-users/{agent_user_id}/inbox' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/brands/{brand_id}/agent-users/{agent_user_id}/inbox", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/brands/{brand_id}/agent-users/{agent_user_id}/inbox", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/brands/{brand_id}/agent-users/{agent_user_id}/inbox", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/brands/{brand_id}/agent-users/{agent_user_id}/messages/{message_id} **Get Agent Message** Read one OPVS message in full (agent must be sender or recipient). **Parameters** - `brand_id` (path, integer, required) - `agent_user_id` (path, string, required) - `message_id` (path, string, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/brands/{brand_id}/agent-users/{agent_user_id}/messages/{message_id}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/brands/{brand_id}/agent-users/{agent_user_id}/messages/{message_id}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/brands/{brand_id}/agent-users/{agent_user_id}/messages/{message_id}", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/brands/{brand_id}/agent-users/{agent_user_id}/messages/{message_id}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/brands/{brand_id}/agent-users/{agent_user_id}/threads/{thread_id} **Get Agent Thread** Read a full OPVS conversation thread for this agent. **Parameters** - `brand_id` (path, integer, required) - `agent_user_id` (path, string, required) - `thread_id` (path, string, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/brands/{brand_id}/agent-users/{agent_user_id}/threads/{thread_id}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/brands/{brand_id}/agent-users/{agent_user_id}/threads/{thread_id}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/brands/{brand_id}/agent-users/{agent_user_id}/threads/{thread_id}", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/brands/{brand_id}/agent-users/{agent_user_id}/threads/{thread_id}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/brands/{brand_id}/agent-users/{agent_user_id}/send **Send Agent Message** Send a new OPVS message from this agent to another OPVS address. **Parameters** - `brand_id` (path, integer, required) - `agent_user_id` (path, string, required) **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/brands/{brand_id}/agent-users/{agent_user_id}/send' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "to_address": "string", "body": "string", "message_type": "inform", "priority": "normal", "thread_id": "string", "tags": [ "string" ] }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/brands/{brand_id}/agent-users/{agent_user_id}/send", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"to_address": "string", "body": "string", "message_type": "inform", "priority": "normal", "thread_id": "string", "tags": ["string"]}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/brands/{brand_id}/agent-users/{agent_user_id}/send", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"to_address": "string", "body": "string", "message_type": "inform", "priority": "normal", "thread_id": "string", "tags": ["string"]}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"to_address": "string", "body": "string", "message_type": "inform", "priority": "normal", "thread_id": "string", "tags": ["string"]}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/brands/{brand_id}/agent-users/{agent_user_id}/send", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `201` — Successful Response - `422` — Validation Error ## `POST` /api/v1/brands/{brand_id}/agent-users/{agent_user_id}/messages/{message_id}/reply **Reply Agent Message** Reply on the thread of an existing OPVS message. **Parameters** - `brand_id` (path, integer, required) - `agent_user_id` (path, string, required) - `message_id` (path, string, required) **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/brands/{brand_id}/agent-users/{agent_user_id}/messages/{message_id}/reply' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "body": "string", "tags": [ "string" ] }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/brands/{brand_id}/agent-users/{agent_user_id}/messages/{message_id}/reply", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"body": "string", "tags": ["string"]}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/brands/{brand_id}/agent-users/{agent_user_id}/messages/{message_id}/reply", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"body": "string", "tags": ["string"]}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"body": "string", "tags": ["string"]}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/brands/{brand_id}/agent-users/{agent_user_id}/messages/{message_id}/reply", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `201` — Successful Response - `422` — Validation Error ## `PATCH` /api/v1/brands/{brand_id}/agent-users/{agent_user_id}/messages/{message_id}/status **Set Agent Message Status** Transition an inbound message's lifecycle (seen/working/resolved/...). **Parameters** - `brand_id` (path, integer, required) - `agent_user_id` (path, string, required) - `message_id` (path, string, required) **Request body** :::codegroup ```bash curl -X PATCH 'https://spideriq.ai/api/v1/brands/{brand_id}/agent-users/{agent_user_id}/messages/{message_id}/status' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "status": "string" }' ``` ```python import httpx resp = httpx.patch( "https://spideriq.ai/api/v1/brands/{brand_id}/agent-users/{agent_user_id}/messages/{message_id}/status", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"status": "string"}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/brands/{brand_id}/agent-users/{agent_user_id}/messages/{message_id}/status", { method: "PATCH", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"status": "string"}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"status": "string"}`) req, _ := http.NewRequest("PATCH", "https://spideriq.ai/api/v1/brands/{brand_id}/agent-users/{agent_user_id}/messages/{message_id}/status", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## Auth Profile Source: https://spideriq.ai/docs/api-reference/auth-profile ## `GET` /api/v1/auth/me **Get User Me** Get current user's full profile including brand memberships. This endpoint is used by the settings page to display user info and the list of brands the user belongs to. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/auth/me' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/auth/me", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/auth/me", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/auth/me", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response ## `PATCH` /api/v1/auth/profile **Update Profile** Update the current user's profile fields. Updatable fields: firstname, lastname, mobile **Request body** :::codegroup ```bash curl -X PATCH 'https://spideriq.ai/api/v1/auth/profile' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "firstname": "string", "lastname": "string", "mobile": "string" }' ``` ```python import httpx resp = httpx.patch( "https://spideriq.ai/api/v1/auth/profile", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"firstname": "string", "lastname": "string", "mobile": "string"}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/auth/profile", { method: "PATCH", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"firstname": "string", "lastname": "string", "mobile": "string"}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"firstname": "string", "lastname": "string", "mobile": "string"}`) req, _ := http.NewRequest("PATCH", "https://spideriq.ai/api/v1/auth/profile", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/auth/profile/photo **Upload Profile Photo** Upload a profile photo. Accepts: image/png, image/jpeg, image/gif, image/svg+xml Max size: 2MB Stored as base64 data URL in database. **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/auth/profile/photo' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "file": "string" }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/auth/profile/photo", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"file": "string"}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/auth/profile/photo", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"file": "string"}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"file": "string"}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/auth/profile/photo", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `DELETE` /api/v1/auth/profile/photo **Remove Profile Photo** Remove the current user's profile photo. :::codegroup ```bash curl -X DELETE 'https://spideriq.ai/api/v1/auth/profile/photo' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.delete( "https://spideriq.ai/api/v1/auth/profile/photo", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/auth/profile/photo", { method: "DELETE", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("DELETE", "https://spideriq.ai/api/v1/auth/profile/photo", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response ## `GET` /api/v1/auth/connectors **Get Connectors** Get list of OAuth providers connected to this account. Returns providers like 'google', 'github' with connection status. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/auth/connectors' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/auth/connectors", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/auth/connectors", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/auth/connectors", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response ## `GET` /api/v1/auth/workspaces **List projects the caller can operate on** Returns every workspace (project) the authenticated caller has access to. Used by `spideriq use ` to resolve a project name and write ./spideriq.json for per-session binding (Phase 11+12 Lock 3). - **api_client / PAT**: returns the single client the token is scoped to. - **client_user / brand_admin**: returns clients they are provisioned for. - **super_admin**: returns all active clients. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/auth/workspaces' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/auth/workspaces", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/auth/workspaces", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/auth/workspaces", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response ## `GET` /api/v1/auth/whoami **Who am I + which project is my token bound to?** Unified identity endpoint for agents and dashboard users. Returns the current client_id plus human-readable project_name so an agent can verify its binding before a destructive deploy. Expired PATs receive a distinguishable `{error: 'token_expired'}` 401 instead of a generic 'invalid token' message. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/auth/whoami' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/auth/whoami", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/auth/whoami", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/auth/whoami", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response ## `PATCH` /api/v1/auth/me/notifications-dnd **Patch Notifications Dnd** Update the caller's DND/timezone fields on `dashboard_users`. Used by the Notifications settings tab Card 1. Quiet hours are interpreted in the user's timezone by the sidecar (slice 3.2), so all three (tz, start, end) move together — clients re-send the timezone whenever they change it, but absent fields preserve the existing value. Setting `clear_window=true` clears both dnd_start and dnd_end together (the only way to disable quiet hours). **Request body** :::codegroup ```bash curl -X PATCH 'https://spideriq.ai/api/v1/auth/me/notifications-dnd' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "timezone": "string", "dnd_start": "string", "dnd_end": "string", "dnd_days_off": 0, "clear_window": false }' ``` ```python import httpx resp = httpx.patch( "https://spideriq.ai/api/v1/auth/me/notifications-dnd", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"timezone": "string", "dnd_start": "string", "dnd_end": "string", "dnd_days_off": 0, "clear_window": false}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/auth/me/notifications-dnd", { method: "PATCH", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"timezone": "string", "dnd_start": "string", "dnd_end": "string", "dnd_days_off": 0, "clear_window": false}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"timezone": "string", "dnd_start": "string", "dnd_end": "string", "dnd_days_off": 0, "clear_window": false}`) req, _ := http.NewRequest("PATCH", "https://spideriq.ai/api/v1/auth/me/notifications-dnd", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## crm-provision Source: https://spideriq.ai/docs/api-reference/crm-provision ## `POST` /api/v1/crm/provision **Provision the CRM workspace (idempotent)** Create (or return the existing) CRM workspace for the caller's client. Thin wrapper around :func:`services.crm.provision_workspace`, which handles the two-session dance (RLS-scoped group/board creation, then plain-role IDAP backfill). Safe to call repeatedly. **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/crm/provision' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "name": "string", "backfill_limit": 0 }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/crm/provision", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"name": "string", "backfill_limit": 0}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/crm/provision", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"name": "string", "backfill_limit": 0}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"name": "string", "backfill_limit": 0}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/crm/provision", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/crm/health **CRM schema health check** Return 200 if the CRM DDL is in place, the RLS policies are enabled, and both CRM roles exist. No tenant scope — safe to expose. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/crm/health' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/crm/health", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/crm/health", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/crm/health", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response ## crm-boards Source: https://spideriq.ai/docs/api-reference/crm-boards ## `GET` /api/v1/crm/boards **List CRM boards** **Parameters** - `include_archived` (query, boolean, optional) - `page` (query, integer, optional) - `per_page` (query, integer, optional) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/crm/boards' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/crm/boards", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/crm/boards", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/crm/boards", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/crm/boards **Create a CRM board** **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/crm/boards' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "name": "string", "description": "string", "icon": "string", "color": "string", "board_type": "string", "seed_default_columns": true, "column_preset": "string" }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/crm/boards", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"name": "string", "description": "string", "icon": "string", "color": "string", "board_type": "string", "seed_default_columns": true, "column_preset": "string"}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/crm/boards", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"name": "string", "description": "string", "icon": "string", "color": "string", "board_type": "string", "seed_default_columns": true, "column_preset": "string"}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"name": "string", "description": "string", "icon": "string", "color": "string", "board_type": "string", "seed_default_columns": true, "column_preset": "string"}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/crm/boards", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `201` — Successful Response - `422` — Validation Error ## `GET` /api/v1/crm/boards/{board_id} **Get a CRM board** **Parameters** - `board_id` (path, string, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/crm/boards/{board_id}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/crm/boards/{board_id}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/crm/boards/{board_id}", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/crm/boards/{board_id}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `PATCH` /api/v1/crm/boards/{board_id} **Update a CRM board** **Parameters** - `board_id` (path, string, required) **Request body** :::codegroup ```bash curl -X PATCH 'https://spideriq.ai/api/v1/crm/boards/{board_id}' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "name": "string", "description": "string", "icon": "string", "color": "string", "is_archived": true, "settings": {}, "field_config": [ { "key": "string", "label": "string", "type": "string", "visible": true, "position": 0, "group": "string", "options": [ {} ] } ], "idap_source": { "enabled": true, "resource_type": "string", "filters": { "flags": "!rejected", "campaign_id": "string" }, "column_flag_map": {}, "field_map": {}, "last_sync_cursor": "string", "last_sync_at": "string" } }' ``` ```python import httpx resp = httpx.patch( "https://spideriq.ai/api/v1/crm/boards/{board_id}", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"name": "string", "description": "string", "icon": "string", "color": "string", "is_archived": true, "settings": {}, "field_config": [{"key": "string", "label": "string", "type": "string", "visible": true, "position": 0, "group": "string", "options": [{}]}], "idap_source": {"enabled": true, "resource_type": "string", "filters": {"flags": "!rejected", "campaign_id": "string"}, "column_flag_map": {}, "field_map": {}, "last_sync_cursor": "string", "last_sync_at": "string"}}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/crm/boards/{board_id}", { method: "PATCH", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"name": "string", "description": "string", "icon": "string", "color": "string", "is_archived": true, "settings": {}, "field_config": [{"key": "string", "label": "string", "type": "string", "visible": true, "position": 0, "group": "string", "options": [{}]}], "idap_source": {"enabled": true, "resource_type": "string", "filters": {"flags": "!rejected", "campaign_id": "string"}, "column_flag_map": {}, "field_map": {}, "last_sync_cursor": "string", "last_sync_at": "string"}}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"name": "string", "description": "string", "icon": "string", "color": "string", "is_archived": true, "settings": {}, "field_config": [{"key": "string", "label": "string", "type": "string", "visible": true, "position": 0, "group": "string", "options": [{}]}], "idap_source": {"enabled": true, "resource_type": "string", "filters": {"flags": "!rejected", "campaign_id": "string"}, "column_flag_map": {}, "field_map": {}, "last_sync_cursor": "string", "last_sync_at": "string"}}`) req, _ := http.NewRequest("PATCH", "https://spideriq.ai/api/v1/crm/boards/{board_id}", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `DELETE` /api/v1/crm/boards/{board_id} **Delete a CRM board** **Parameters** - `board_id` (path, string, required) :::codegroup ```bash curl -X DELETE 'https://spideriq.ai/api/v1/crm/boards/{board_id}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.delete( "https://spideriq.ai/api/v1/crm/boards/{board_id}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/crm/boards/{board_id}", { method: "DELETE", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("DELETE", "https://spideriq.ai/api/v1/crm/boards/{board_id}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `204` — Successful Response - `422` — Validation Error ## crm-board-groups Source: https://spideriq.ai/docs/api-reference/crm-board-groups ## `GET` /api/v1/crm/board-groups **List board groups** :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/crm/board-groups' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/crm/board-groups", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/crm/board-groups", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/crm/board-groups", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response ## `GET` /api/v1/crm/board-groups/{group_id} **Get a board group** **Parameters** - `group_id` (path, string, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/crm/board-groups/{group_id}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/crm/board-groups/{group_id}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/crm/board-groups/{group_id}", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/crm/board-groups/{group_id}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `PATCH` /api/v1/crm/board-groups/{group_id} **Update a board group** **Parameters** - `group_id` (path, string, required) **Request body** :::codegroup ```bash curl -X PATCH 'https://spideriq.ai/api/v1/crm/board-groups/{group_id}' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "name": "string", "settings": {} }' ``` ```python import httpx resp = httpx.patch( "https://spideriq.ai/api/v1/crm/board-groups/{group_id}", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"name": "string", "settings": {}}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/crm/board-groups/{group_id}", { method: "PATCH", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"name": "string", "settings": {}}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"name": "string", "settings": {}}`) req, _ := http.NewRequest("PATCH", "https://spideriq.ai/api/v1/crm/board-groups/{group_id}", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `DELETE` /api/v1/crm/board-groups/{group_id} **Delete a board group** **Parameters** - `group_id` (path, string, required) :::codegroup ```bash curl -X DELETE 'https://spideriq.ai/api/v1/crm/board-groups/{group_id}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.delete( "https://spideriq.ai/api/v1/crm/board-groups/{group_id}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/crm/board-groups/{group_id}", { method: "DELETE", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("DELETE", "https://spideriq.ai/api/v1/crm/board-groups/{group_id}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `204` — Successful Response - `422` — Validation Error ## crm-columns Source: https://spideriq.ai/docs/api-reference/crm-columns ## `GET` /api/v1/crm/boards/{board_id}/columns **List columns for a board** **Parameters** - `board_id` (path, string, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/crm/boards/{board_id}/columns' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/crm/boards/{board_id}/columns", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/crm/boards/{board_id}/columns", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/crm/boards/{board_id}/columns", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/crm/boards/{board_id}/columns **Create a column** **Parameters** - `board_id` (path, string, required) **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/crm/boards/{board_id}/columns' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "name": "string", "slug": "string", "position": 0, "color": "string", "is_done_column": false, "mapped_status": "string" }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/crm/boards/{board_id}/columns", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"name": "string", "slug": "string", "position": 0, "color": "string", "is_done_column": false, "mapped_status": "string"}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/crm/boards/{board_id}/columns", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"name": "string", "slug": "string", "position": 0, "color": "string", "is_done_column": false, "mapped_status": "string"}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"name": "string", "slug": "string", "position": 0, "color": "string", "is_done_column": false, "mapped_status": "string"}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/crm/boards/{board_id}/columns", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `201` — Successful Response - `422` — Validation Error ## `GET` /api/v1/crm/columns/{column_id} **Get a column** **Parameters** - `column_id` (path, string, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/crm/columns/{column_id}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/crm/columns/{column_id}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/crm/columns/{column_id}", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/crm/columns/{column_id}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `PATCH` /api/v1/crm/columns/{column_id} **Update a column** **Parameters** - `column_id` (path, string, required) **Request body** :::codegroup ```bash curl -X PATCH 'https://spideriq.ai/api/v1/crm/columns/{column_id}' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "name": "string", "color": "string", "wip_limit": 0, "is_done_column": true, "mapped_status": "string" }' ``` ```python import httpx resp = httpx.patch( "https://spideriq.ai/api/v1/crm/columns/{column_id}", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"name": "string", "color": "string", "wip_limit": 0, "is_done_column": true, "mapped_status": "string"}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/crm/columns/{column_id}", { method: "PATCH", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"name": "string", "color": "string", "wip_limit": 0, "is_done_column": true, "mapped_status": "string"}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"name": "string", "color": "string", "wip_limit": 0, "is_done_column": true, "mapped_status": "string"}`) req, _ := http.NewRequest("PATCH", "https://spideriq.ai/api/v1/crm/columns/{column_id}", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `DELETE` /api/v1/crm/columns/{column_id} **Delete a column** **Parameters** - `column_id` (path, string, required) :::codegroup ```bash curl -X DELETE 'https://spideriq.ai/api/v1/crm/columns/{column_id}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.delete( "https://spideriq.ai/api/v1/crm/columns/{column_id}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/crm/columns/{column_id}", { method: "DELETE", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("DELETE", "https://spideriq.ai/api/v1/crm/columns/{column_id}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `204` — Successful Response - `422` — Validation Error ## `POST` /api/v1/crm/boards/{board_id}/columns/reorder **Reorder columns** **Parameters** - `board_id` (path, string, required) **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/crm/boards/{board_id}/columns/reorder' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "columns": [ { "id": "00000000-0000-0000-0000-000000000000", "position": 0 } ] }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/crm/boards/{board_id}/columns/reorder", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"columns": [{"id": "00000000-0000-0000-0000-000000000000", "position": 0}]}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/crm/boards/{board_id}/columns/reorder", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"columns": [{"id": "00000000-0000-0000-0000-000000000000", "position": 0}]}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"columns": [{"id": "00000000-0000-0000-0000-000000000000", "position": 0}]}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/crm/boards/{board_id}/columns/reorder", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## crm-tasks Source: https://spideriq.ai/docs/api-reference/crm-tasks ## `GET` /api/v1/crm/boards/{board_id}/tasks **List tasks on a board** **Parameters** - `board_id` (path, string, required) - `column_id` (query, any, optional) - `limit` (query, integer, optional) - `offset` (query, integer, optional) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/crm/boards/{board_id}/tasks' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/crm/boards/{board_id}/tasks", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/crm/boards/{board_id}/tasks", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/crm/boards/{board_id}/tasks", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/crm/boards/{board_id}/tasks **Create a task** **Parameters** - `board_id` (path, string, required) **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/crm/boards/{board_id}/tasks' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "column_id": "00000000-0000-0000-0000-000000000000", "title": "string", "description": "string", "priority": "string", "labels": [ "string" ], "custom_fields": {}, "source": "string", "external_ref": "string" }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/crm/boards/{board_id}/tasks", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"column_id": "00000000-0000-0000-0000-000000000000", "title": "string", "description": "string", "priority": "string", "labels": ["string"], "custom_fields": {}, "source": "string", "external_ref": "string"}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/crm/boards/{board_id}/tasks", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"column_id": "00000000-0000-0000-0000-000000000000", "title": "string", "description": "string", "priority": "string", "labels": ["string"], "custom_fields": {}, "source": "string", "external_ref": "string"}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"column_id": "00000000-0000-0000-0000-000000000000", "title": "string", "description": "string", "priority": "string", "labels": ["string"], "custom_fields": {}, "source": "string", "external_ref": "string"}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/crm/boards/{board_id}/tasks", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `201` — Successful Response - `422` — Validation Error ## `GET` /api/v1/crm/tasks/{task_id} **Get a task** **Parameters** - `task_id` (path, string, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/crm/tasks/{task_id}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/crm/tasks/{task_id}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/crm/tasks/{task_id}", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/crm/tasks/{task_id}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `PATCH` /api/v1/crm/tasks/{task_id} **Update a task** **Parameters** - `task_id` (path, string, required) **Request body** :::codegroup ```bash curl -X PATCH 'https://spideriq.ai/api/v1/crm/tasks/{task_id}' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "column_id": "00000000-0000-0000-0000-000000000000", "title": "string", "description": "string", "status": "string", "priority": "string", "position": 0, "due_date": "2026-01-01T00:00:00Z", "labels": [ "string" ], "custom_fields": {} }' ``` ```python import httpx resp = httpx.patch( "https://spideriq.ai/api/v1/crm/tasks/{task_id}", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"column_id": "00000000-0000-0000-0000-000000000000", "title": "string", "description": "string", "status": "string", "priority": "string", "position": 0, "due_date": "2026-01-01T00:00:00Z", "labels": ["string"], "custom_fields": {}}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/crm/tasks/{task_id}", { method: "PATCH", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"column_id": "00000000-0000-0000-0000-000000000000", "title": "string", "description": "string", "status": "string", "priority": "string", "position": 0, "due_date": "2026-01-01T00:00:00Z", "labels": ["string"], "custom_fields": {}}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"column_id": "00000000-0000-0000-0000-000000000000", "title": "string", "description": "string", "status": "string", "priority": "string", "position": 0, "due_date": "2026-01-01T00:00:00Z", "labels": ["string"], "custom_fields": {}}`) req, _ := http.NewRequest("PATCH", "https://spideriq.ai/api/v1/crm/tasks/{task_id}", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `DELETE` /api/v1/crm/tasks/{task_id} **Soft-delete a task** **Parameters** - `task_id` (path, string, required) :::codegroup ```bash curl -X DELETE 'https://spideriq.ai/api/v1/crm/tasks/{task_id}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.delete( "https://spideriq.ai/api/v1/crm/tasks/{task_id}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/crm/tasks/{task_id}", { method: "DELETE", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("DELETE", "https://spideriq.ai/api/v1/crm/tasks/{task_id}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `204` — Successful Response - `422` — Validation Error ## `POST` /api/v1/crm/tasks/{task_id}/move **Move a task to a column/position** **Parameters** - `task_id` (path, string, required) **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/crm/tasks/{task_id}/move' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "column_id": "00000000-0000-0000-0000-000000000000", "position": 0 }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/crm/tasks/{task_id}/move", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"column_id": "00000000-0000-0000-0000-000000000000", "position": 0}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/crm/tasks/{task_id}/move", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"column_id": "00000000-0000-0000-0000-000000000000", "position": 0}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"column_id": "00000000-0000-0000-0000-000000000000", "position": 0}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/crm/tasks/{task_id}/move", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## crm-relationships Source: https://spideriq.ai/docs/api-reference/crm-relationships ## `GET` /api/v1/crm/tasks/{task_id}/relationships **List card relationships** **Parameters** - `task_id` (path, string, required) - `type` (query, any, optional) — Filter by type :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/crm/tasks/{task_id}/relationships' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/crm/tasks/{task_id}/relationships", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/crm/tasks/{task_id}/relationships", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/crm/tasks/{task_id}/relationships", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/crm/tasks/{task_id}/relationships **Create a card relationship** **Parameters** - `task_id` (path, string, required) **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/crm/tasks/{task_id}/relationships' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "parent_id": "00000000-0000-0000-0000-000000000000", "relationship_type": "string", "role": "string", "extra_data": {} }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/crm/tasks/{task_id}/relationships", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"parent_id": "00000000-0000-0000-0000-000000000000", "relationship_type": "string", "role": "string", "extra_data": {}}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/crm/tasks/{task_id}/relationships", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"parent_id": "00000000-0000-0000-0000-000000000000", "relationship_type": "string", "role": "string", "extra_data": {}}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"parent_id": "00000000-0000-0000-0000-000000000000", "relationship_type": "string", "role": "string", "extra_data": {}}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/crm/tasks/{task_id}/relationships", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `201` — Successful Response - `422` — Validation Error ## `GET` /api/v1/crm/tasks/{task_id}/relationships/{rel_type}/cards **Get related cards** **Parameters** - `task_id` (path, string, required) - `rel_type` (path, string, required) - `direction` (query, string, optional) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/crm/tasks/{task_id}/relationships/{rel_type}/cards' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/crm/tasks/{task_id}/relationships/{rel_type}/cards", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/crm/tasks/{task_id}/relationships/{rel_type}/cards", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/crm/tasks/{task_id}/relationships/{rel_type}/cards", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `PATCH` /api/v1/crm/card-relationships/{rel_id} **Update a card relationship** **Parameters** - `rel_id` (path, string, required) **Request body** :::codegroup ```bash curl -X PATCH 'https://spideriq.ai/api/v1/crm/card-relationships/{rel_id}' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "role": "string", "extra_data": {} }' ``` ```python import httpx resp = httpx.patch( "https://spideriq.ai/api/v1/crm/card-relationships/{rel_id}", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"role": "string", "extra_data": {}}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/crm/card-relationships/{rel_id}", { method: "PATCH", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"role": "string", "extra_data": {}}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"role": "string", "extra_data": {}}`) req, _ := http.NewRequest("PATCH", "https://spideriq.ai/api/v1/crm/card-relationships/{rel_id}", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `DELETE` /api/v1/crm/card-relationships/{rel_id} **Delete a card relationship** **Parameters** - `rel_id` (path, string, required) :::codegroup ```bash curl -X DELETE 'https://spideriq.ai/api/v1/crm/card-relationships/{rel_id}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.delete( "https://spideriq.ai/api/v1/crm/card-relationships/{rel_id}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/crm/card-relationships/{rel_id}", { method: "DELETE", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("DELETE", "https://spideriq.ai/api/v1/crm/card-relationships/{rel_id}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `204` — Successful Response - `422` — Validation Error ## `GET` /api/v1/crm/cards/search **Cross-board card search** **Parameters** - `q` (query, string, required) - `board_type` (query, any, optional) - `limit` (query, integer, optional) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/crm/cards/search' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/crm/cards/search", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/crm/cards/search", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/crm/cards/search", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## crm-master-views Source: https://spideriq.ai/docs/api-reference/crm-master-views ## `GET` /api/v1/crm/master/tasks **Cross-board CRM task aggregation** **Parameters** - `board_type` (query, string, required) - `status` (query, any, optional) — Comma-separated status filter - `priority` (query, any, optional) - `board_ids` (query, any, optional) - `search` (query, any, optional) - `sort` (query, any, optional) - `group_by` (query, any, optional) - `include_done` (query, boolean, optional) - `limit` (query, integer, optional) - `offset` (query, integer, optional) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/crm/master/tasks' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/crm/master/tasks", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/crm/master/tasks", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/crm/master/tasks", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/crm/master/session **Executive master session with stuck/overdue alerts** **Parameters** - `board_type` (query, string, required) - `stuck_threshold_hours` (query, integer, optional) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/crm/master/session' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/crm/master/session", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/crm/master/session", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/crm/master/session", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/crm/master/groups/{group_id}/tasks **Task aggregation scoped to one CRM workspace group** **Parameters** - `group_id` (path, string, required) - `group_by` (query, any, optional) - `sort` (query, any, optional) - `include_done` (query, boolean, optional) - `limit` (query, integer, optional) - `offset` (query, integer, optional) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/crm/master/groups/{group_id}/tasks' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/crm/master/groups/{group_id}/tasks", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/crm/master/groups/{group_id}/tasks", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/crm/master/groups/{group_id}/tasks", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## crm-schema Source: https://spideriq.ai/docs/api-reference/crm-schema ## `GET` /api/v1/crm/schema **Introspect CRM schema for form-field mapping** Return the CRM resource list, or one resource type's introspected schema. **Parameters** - `resource_type` (query, any, optional) — CRM resource type to introspect (TABLE_MAP key or board type). Omit to list all available resource types. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/crm/schema' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/crm/schema", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/crm/schema", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/crm/schema", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## Integrations Source: https://spideriq.ai/docs/api-reference/integrations ## `GET` /api/v1/brands/{brand_id}/integrations **List Integration Overview** Overview of all configured providers with aggregated stats for a brand. Provider list, key counts, healthy counts, and `daily_limit` capacity come from `api_integrations` (the config). Daily usage, last-used timestamp, today's spend, and this month's spend come from `gate_request_logs` (the v3.2.0 litellm.Router flow's source of truth). The legacy `daily_count` + `cached_usage_*` columns stayed zero because the v1 mark_usage callback that wrote them is no longer in the request path. See PR #664. Groups strictly by `provider_name` and resolves the display label from `provider_registry.PROVIDER_REGISTRY` — `provider_label` rows are cosmetic-only and ignored (PR #657 fix for the Mistral duplicate). **Parameters** - `brand_id` (path, integer, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/brands/{brand_id}/integrations' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/brands/{brand_id}/integrations", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/brands/{brand_id}/integrations", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/brands/{brand_id}/integrations", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/brands/{brand_id}/integrations **Create Integration** Create a new API integration (vault key) for this brand. Vault self-service: a human brand_admin/super_admin (session) OR the brand's own agent (PAT with the opt-in ``gate:vault:write`` scope) may add a key. The new row is stamped ``brands_id = brand_id``; for a PAT, ``require_vault_writer`` has already verified the token owns ``brand_id``, so it can only ever add a key to its own brand. **Parameters** - `brand_id` (path, integer, required) **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/brands/{brand_id}/integrations' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "provider_name": "string", "key_label": "string", "credentials": {}, "daily_limit": 0, "minute_limit": 0, "is_primary": false, "country_code": "string", "spend_limit_amount": 0.0, "spend_limit_period": "string", "spend_limit_action": "string", "share_with_pool": false, "free_models_only": false, "priority": 0, "hourly_limit": 0, "token_daily_limit": 0, "token_monthly_limit": 0, "supports_embeddings": true, "usage_policy": "string", "allowed_activities": [ "string" ], "allowed_consumers": [ "string" ] }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/brands/{brand_id}/integrations", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"provider_name": "string", "key_label": "string", "credentials": {}, "daily_limit": 0, "minute_limit": 0, "is_primary": false, "country_code": "string", "spend_limit_amount": 0.0, "spend_limit_period": "string", "spend_limit_action": "string", "share_with_pool": false, "free_models_only": false, "priority": 0, "hourly_limit": 0, "token_daily_limit": 0, "token_monthly_limit": 0, "supports_embeddings": true, "usage_policy": "string", "allowed_activities": ["string"], "allowed_consumers": ["string"]}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/brands/{brand_id}/integrations", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"provider_name": "string", "key_label": "string", "credentials": {}, "daily_limit": 0, "minute_limit": 0, "is_primary": false, "country_code": "string", "spend_limit_amount": 0.0, "spend_limit_period": "string", "spend_limit_action": "string", "share_with_pool": false, "free_models_only": false, "priority": 0, "hourly_limit": 0, "token_daily_limit": 0, "token_monthly_limit": 0, "supports_embeddings": true, "usage_policy": "string", "allowed_activities": ["string"], "allowed_consumers": ["string"]}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"provider_name": "string", "key_label": "string", "credentials": {}, "daily_limit": 0, "minute_limit": 0, "is_primary": false, "country_code": "string", "spend_limit_amount": 0.0, "spend_limit_period": "string", "spend_limit_action": "string", "share_with_pool": false, "free_models_only": false, "priority": 0, "hourly_limit": 0, "token_daily_limit": 0, "token_monthly_limit": 0, "supports_embeddings": true, "usage_policy": "string", "allowed_activities": ["string"], "allowed_consumers": ["string"]}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/brands/{brand_id}/integrations", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/brands/{brand_id}/integrations/providers **List Provider Templates** List available provider templates with field schemas. Each template carries its provider→ToS classification (VAULT.1 2c.1, dark) so the add-key UI can pre-forbid ToS-locked activity toggles before a key even exists. Fail-closed: a provider with no confirmed classification reports all 6 activities forbidden with classification_status='unclassified'. **Parameters** - `brand_id` (path, integer, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/brands/{brand_id}/integrations/providers' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/brands/{brand_id}/integrations/providers", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/brands/{brand_id}/integrations/providers", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/brands/{brand_id}/integrations/providers", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/brands/{brand_id}/gate/subscription-tiers **List Subscription Tiers** List curated subscription packages (provider_subscription_tiers) for the vault key editor's package dropdown (VAULT subscription-billing, D2). Brand-scoped read (any brand member) — the catalog is global/curated, not brand data, but the route is brand-owned for a consistent auth surface with the rest of the vault. Fail-soft: an absent table (pre-mig-447) returns []. **Parameters** - `brand_id` (path, integer, required) - `provider` (query, any, optional) — Filter to one provider's packages (e.g. minimax) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/brands/{brand_id}/gate/subscription-tiers' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/brands/{brand_id}/gate/subscription-tiers", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/brands/{brand_id}/gate/subscription-tiers", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/brands/{brand_id}/gate/subscription-tiers", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/brands/{brand_id}/integrations/all **List Integrations** List all integration keys for a brand, optionally filtered by provider. Each row carries log-derived stats (requests_24h, spend_today, spend_month, last_used_at) aggregated from `gate_request_logs` joined on `integration_id`. Legacy `daily_count` / `cached_usage_*` / `last_used_at` columns stay populated for backward compatibility but are stale — the v3.2.0 litellm.Router flow doesn't update them. **Parameters** - `brand_id` (path, integer, required) - `provider_name` (query, any, optional) — Filter by provider name :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/brands/{brand_id}/integrations/all' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/brands/{brand_id}/integrations/all", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/brands/{brand_id}/integrations/all", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/brands/{brand_id}/integrations/all", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/brands/{brand_id}/integrations/{integration_id} **Get Integration** Get a single integration key detail. **Parameters** - `brand_id` (path, integer, required) - `integration_id` (path, integer, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/brands/{brand_id}/integrations/{integration_id}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/brands/{brand_id}/integrations/{integration_id}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/brands/{brand_id}/integrations/{integration_id}", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/brands/{brand_id}/integrations/{integration_id}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `PATCH` /api/v1/brands/{brand_id}/integrations/{integration_id} **Update Integration** Update an integration key. Gated by ``require_vault_writer`` (VAULT.1 3.1): a brand_admin/super_admin session OR a ``gate:vault:write`` PAT that owns this brand. Brand ownership is doubly enforced — the dep's ``_pat_client_owns_brand`` check for PATs and the ``WHERE … AND brands_id = $2`` fetch below (404 for a foreign key). The ``allowed_consumers`` ToS consumer-lock is additionally super-admin-only. **Parameters** - `brand_id` (path, integer, required) - `integration_id` (path, integer, required) **Request body** :::codegroup ```bash curl -X PATCH 'https://spideriq.ai/api/v1/brands/{brand_id}/integrations/{integration_id}' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "key_label": "string", "credentials": {}, "daily_limit": 0, "minute_limit": 0, "is_primary": true, "is_active": true, "country_code": "string", "spend_limit_amount": 0.0, "spend_limit_period": "string", "spend_limit_action": "string", "share_with_pool": true, "free_models_only": true, "supports_embeddings": true, "usage_policy": "string", "allowed_activities": [ "string" ], "allowed_consumers": [ "string" ], "billing_mode": "string", "subscription_tier": "string", "base_url": "string" }' ``` ```python import httpx resp = httpx.patch( "https://spideriq.ai/api/v1/brands/{brand_id}/integrations/{integration_id}", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"key_label": "string", "credentials": {}, "daily_limit": 0, "minute_limit": 0, "is_primary": true, "is_active": true, "country_code": "string", "spend_limit_amount": 0.0, "spend_limit_period": "string", "spend_limit_action": "string", "share_with_pool": true, "free_models_only": true, "supports_embeddings": true, "usage_policy": "string", "allowed_activities": ["string"], "allowed_consumers": ["string"], "billing_mode": "string", "subscription_tier": "string", "base_url": "string"}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/brands/{brand_id}/integrations/{integration_id}", { method: "PATCH", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"key_label": "string", "credentials": {}, "daily_limit": 0, "minute_limit": 0, "is_primary": true, "is_active": true, "country_code": "string", "spend_limit_amount": 0.0, "spend_limit_period": "string", "spend_limit_action": "string", "share_with_pool": true, "free_models_only": true, "supports_embeddings": true, "usage_policy": "string", "allowed_activities": ["string"], "allowed_consumers": ["string"], "billing_mode": "string", "subscription_tier": "string", "base_url": "string"}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"key_label": "string", "credentials": {}, "daily_limit": 0, "minute_limit": 0, "is_primary": true, "is_active": true, "country_code": "string", "spend_limit_amount": 0.0, "spend_limit_period": "string", "spend_limit_action": "string", "share_with_pool": true, "free_models_only": true, "supports_embeddings": true, "usage_policy": "string", "allowed_activities": ["string"], "allowed_consumers": ["string"], "billing_mode": "string", "subscription_tier": "string", "base_url": "string"}`) req, _ := http.NewRequest("PATCH", "https://spideriq.ai/api/v1/brands/{brand_id}/integrations/{integration_id}", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `DELETE` /api/v1/brands/{brand_id}/integrations/{integration_id} **Delete Integration** Delete an integration key. Brand admin only. **Parameters** - `brand_id` (path, integer, required) - `integration_id` (path, integer, required) :::codegroup ```bash curl -X DELETE 'https://spideriq.ai/api/v1/brands/{brand_id}/integrations/{integration_id}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.delete( "https://spideriq.ai/api/v1/brands/{brand_id}/integrations/{integration_id}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/brands/{brand_id}/integrations/{integration_id}", { method: "DELETE", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("DELETE", "https://spideriq.ai/api/v1/brands/{brand_id}/integrations/{integration_id}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/brands/{brand_id}/integrations/oauth/self/start **Self Oauth Start** Begin a self-serve OAuth connect for the caller's own new subscription key. Brand is the validated path segment (`require_brand_owner`); no invite needed. Only `oauth_pkce` subscription providers (Claude Code / Codex / Gemini CLI) — those use the provider's OWN registered redirect (localhost:1455 / claude.com), so there is no app.spideriq.ai callback to register. Mirrors `contribute_oauth_start` (contribute.py) minus the invite binding. **Parameters** - `brand_id` (path, integer, required) **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/brands/{brand_id}/integrations/oauth/self/start' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "provider": "string" }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/brands/{brand_id}/integrations/oauth/self/start", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"provider": "string"}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/brands/{brand_id}/integrations/oauth/self/start", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"provider": "string"}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"provider": "string"}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/brands/{brand_id}/integrations/oauth/self/start", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/brands/{brand_id}/integrations/oauth/self/callback **Self Oauth Callback** Complete a self-serve OAuth connect: exchange the pasted code for tokens and vault them for THIS brand. Clones `contribute_oauth_callback`'s exchange + INSERT + INJECT-only de-pool; brand comes from the path, not an invite. **Parameters** - `brand_id` (path, integer, required) **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/brands/{brand_id}/integrations/oauth/self/callback' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "provider": "string", "code": "string", "state": "string" }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/brands/{brand_id}/integrations/oauth/self/callback", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"provider": "string", "code": "string", "state": "string"}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/brands/{brand_id}/integrations/oauth/self/callback", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"provider": "string", "code": "string", "state": "string"}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"provider": "string", "code": "string", "state": "string"}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/brands/{brand_id}/integrations/oauth/self/callback", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/brands/{brand_id}/integrations/{integration_id}/reset-usage **Reset Integration Usage** Reset daily and minute usage counters. Brand admin only. **Parameters** - `brand_id` (path, integer, required) - `integration_id` (path, integer, required) :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/brands/{brand_id}/integrations/{integration_id}/reset-usage' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/brands/{brand_id}/integrations/{integration_id}/reset-usage", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/brands/{brand_id}/integrations/{integration_id}/reset-usage", { method: "POST", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/brands/{brand_id}/integrations/{integration_id}/reset-usage", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/brands/{brand_id}/integrations/{integration_id}/reauth **Request Integration Reauth** Issue a single-use re-auth link for one of this brand's vault credentials and email it to the key's contributor. The link lets them re-login / paste a fresh key and UPDATE the same row in place (see app/api/v1/gate/reauth.py). Guards: - non-super_admin callers must be a member of ``brand_id`` (403 otherwise), - the credential must belong to ``brand_id`` (404 otherwise) — a brand can never re-auth a pool / other-tenant key it doesn't own. **Parameters** - `brand_id` (path, integer, required) - `integration_id` (path, integer, required) **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/brands/{brand_id}/integrations/{integration_id}/reauth' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "notify_contributor": false }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/brands/{brand_id}/integrations/{integration_id}/reauth", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"notify_contributor": false}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/brands/{brand_id}/integrations/{integration_id}/reauth", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"notify_contributor": false}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"notify_contributor": false}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/brands/{brand_id}/integrations/{integration_id}/reauth", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/brands/{brand_id}/integrations/spend/provider/{provider_name} **Get Provider Spend Overview** Get aggregated spend overview for a provider. **Parameters** - `brand_id` (path, integer, required) - `provider_name` (path, string, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/brands/{brand_id}/integrations/spend/provider/{provider_name}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/brands/{brand_id}/integrations/spend/provider/{provider_name}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/brands/{brand_id}/integrations/spend/provider/{provider_name}", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/brands/{brand_id}/integrations/spend/provider/{provider_name}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/brands/{brand_id}/integrations/{integration_id}/spend **Get Integration Spend** Get spend data for a specific integration with history. **Parameters** - `brand_id` (path, integer, required) - `integration_id` (path, integer, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/brands/{brand_id}/integrations/{integration_id}/spend' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/brands/{brand_id}/integrations/{integration_id}/spend", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/brands/{brand_id}/integrations/{integration_id}/spend", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/brands/{brand_id}/integrations/{integration_id}/spend", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/brands/{brand_id}/integrations/{integration_id}/test **Test Integration Connection** Test if an API key is working by making a lightweight API call. **Parameters** - `brand_id` (path, integer, required) - `integration_id` (path, integer, required) :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/brands/{brand_id}/integrations/{integration_id}/test' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/brands/{brand_id}/integrations/{integration_id}/test", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/brands/{brand_id}/integrations/{integration_id}/test", { method: "POST", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/brands/{brand_id}/integrations/{integration_id}/test", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/brands/{brand_id}/integrations/{integration_id}/sync-billing **Sync Integration Billing** Trigger billing sync for an integration using its billing adapter. **Parameters** - `brand_id` (path, integer, required) - `integration_id` (path, integer, required) :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/brands/{brand_id}/integrations/{integration_id}/sync-billing' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/brands/{brand_id}/integrations/{integration_id}/sync-billing", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/brands/{brand_id}/integrations/{integration_id}/sync-billing", { method: "POST", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/brands/{brand_id}/integrations/{integration_id}/sync-billing", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/admin/integrations/pool/stats **Get Pool Stats** Get statistics for the shared key pool. 🔴 GUR-21 — reports the pool TWO ways, and the difference between them. ``pool_integrations`` counts ``share_with_pool = TRUE``, which is ONE clause of the seven-clause question the Router actually asks. On the live primary, 2026-08-25, this console showed **56** while the Router admitted **46** — nine deactivated mistral keys and integration 107 (``opvs``, ``is_pool_eligible = FALSE``). An operator sizing the fleet off 56 was sizing it off invisible capacity. ``admitted_by_router`` is the Router's own predicate, rendered from ``services.gate.pool_admission`` — the same tuple ``FETCH_HEALTHY_KEYS_SQL`` builds its WHERE block from, so the two cannot drift apart again silently. ``exclusion_reasons`` breaks the gap down by cause, one reason per key, and is zero-filled so an absent reason reads as zero rather than as a missing field. Both numbers are published deliberately. A corrected single number would hide the flagged-but-not-admitted condition, which is the thing worth seeing. ⚠️ ``total_daily_used`` / ``capacity_used_percent`` are ``daily_count``-derived and measure the LEGACY Path-A selection path only. `get_next_available_key` increments the counter; the litellm Router does not. Measured 2026-08-25: ``gate_request_logs`` carries 9,326 gateway requests for the day against ``SUM(daily_count) = 2,276``, and **13 of the 14 keys that actually served today read ``daily_count = 0``**. Do not read this percentage as gateway utilisation — `/admin/gate/pool-utilization` derives rotation from the LOGS and is the number that answers that question. 🔴 The column has been called "dead" twice in planning notes and "live" once, and each claim was true about a different thing. It is a LIVE admission filter and ORDER BY tie-break in both Path-A queries (so it must not be dropped), and it is near-blind as a measurement of gateway traffic (so it must not be reported as one). :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/admin/integrations/pool/stats' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/admin/integrations/pool/stats", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/admin/integrations/pool/stats", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/admin/integrations/pool/stats", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response ## `GET` /api/v1/admin/integrations/pool **List Pool Integrations** List all pool integrations (share_with_pool=true). Admin only. **Parameters** - `provider_name` (query, any, optional) — Filter by provider name - `only_available` (query, boolean, optional) — Only show available keys :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/admin/integrations/pool' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/admin/integrations/pool", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/admin/integrations/pool", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/admin/integrations/pool", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/admin/integrations/{integration_id}/reset-failures **Reset Integration Failures** Reset consecutive failures and set health to healthy. Admin only. **Parameters** - `integration_id` (path, integer, required) :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/admin/integrations/{integration_id}/reset-failures' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/admin/integrations/{integration_id}/reset-failures", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/admin/integrations/{integration_id}/reset-failures", { method: "POST", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/admin/integrations/{integration_id}/reset-failures", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/admin/integrations/reset-daily **Reset Daily Counters** Reset daily counters for all integrations. Admin only. :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/admin/integrations/reset-daily' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/admin/integrations/reset-daily", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/admin/integrations/reset-daily", { method: "POST", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/admin/integrations/reset-daily", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response ## Mail Outreach Source: https://spideriq.ai/docs/api-reference/mail-outreach ## `GET` /api/v1/brands/{brand_id}/mail/outreach/connections **List Outreach Connections** List every outreach sidecar for this brand, newest first. **Parameters** - `brand_id` (path, integer, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/brands/{brand_id}/mail/outreach/connections' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/brands/{brand_id}/mail/outreach/connections", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/brands/{brand_id}/mail/outreach/connections", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/brands/{brand_id}/mail/outreach/connections", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/brands/{brand_id}/mail/outreach/connections/{connection_id} **Get Outreach Connection** **Parameters** - `brand_id` (path, integer, required) - `connection_id` (path, integer, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/brands/{brand_id}/mail/outreach/connections/{connection_id}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/brands/{brand_id}/mail/outreach/connections/{connection_id}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/brands/{brand_id}/mail/outreach/connections/{connection_id}", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/brands/{brand_id}/mail/outreach/connections/{connection_id}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `PATCH` /api/v1/brands/{brand_id}/mail/outreach/connections/{connection_id} **Update Outreach Connection** Edit workspace_id / warmup_tag / lemwarm_domains / is_active. **Parameters** - `brand_id` (path, integer, required) - `connection_id` (path, integer, required) **Request body** :::codegroup ```bash curl -X PATCH 'https://spideriq.ai/api/v1/brands/{brand_id}/mail/outreach/connections/{connection_id}' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "workspace_id": "string", "warmup_tag": "string", "lemwarm_domains": [ "string" ], "is_active": true, "smartlead_client_id": "string", "max_active_leads": 0 }' ``` ```python import httpx resp = httpx.patch( "https://spideriq.ai/api/v1/brands/{brand_id}/mail/outreach/connections/{connection_id}", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"workspace_id": "string", "warmup_tag": "string", "lemwarm_domains": ["string"], "is_active": true, "smartlead_client_id": "string", "max_active_leads": 0}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/brands/{brand_id}/mail/outreach/connections/{connection_id}", { method: "PATCH", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"workspace_id": "string", "warmup_tag": "string", "lemwarm_domains": ["string"], "is_active": true, "smartlead_client_id": "string", "max_active_leads": 0}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"workspace_id": "string", "warmup_tag": "string", "lemwarm_domains": ["string"], "is_active": true, "smartlead_client_id": "string", "max_active_leads": 0}`) req, _ := http.NewRequest("PATCH", "https://spideriq.ai/api/v1/brands/{brand_id}/mail/outreach/connections/{connection_id}", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `DELETE` /api/v1/brands/{brand_id}/mail/outreach/connections/{connection_id} **Delete Outreach Connection** Remove the outreach sidecar. Leaves api_integrations intact (user must delete the integration itself via IntegrationsTab to remove credentials). FK cascades senders + campaigns + health_snapshots on this sidecar. **Parameters** - `brand_id` (path, integer, required) - `connection_id` (path, integer, required) :::codegroup ```bash curl -X DELETE 'https://spideriq.ai/api/v1/brands/{brand_id}/mail/outreach/connections/{connection_id}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.delete( "https://spideriq.ai/api/v1/brands/{brand_id}/mail/outreach/connections/{connection_id}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/brands/{brand_id}/mail/outreach/connections/{connection_id}", { method: "DELETE", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("DELETE", "https://spideriq.ai/api/v1/brands/{brand_id}/mail/outreach/connections/{connection_id}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/brands/{brand_id}/mail/outreach/connections/{connection_id}/sync **Sync Outreach Connection** Call adapter.list_senders(), upsert into mail_outreach_senders, and try to resolve each sender's email against mail_mailboxes for this brand's clients (so the classifier can key off mailbox_id). **Parameters** - `brand_id` (path, integer, required) - `connection_id` (path, integer, required) :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/brands/{brand_id}/mail/outreach/connections/{connection_id}/sync' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/brands/{brand_id}/mail/outreach/connections/{connection_id}/sync", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/brands/{brand_id}/mail/outreach/connections/{connection_id}/sync", { method: "POST", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/brands/{brand_id}/mail/outreach/connections/{connection_id}/sync", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/brands/{brand_id}/mail/outreach/senders **List Outreach Senders** All senders across every outreach connection on this brand. **Parameters** - `brand_id` (path, integer, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/brands/{brand_id}/mail/outreach/senders' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/brands/{brand_id}/mail/outreach/senders", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/brands/{brand_id}/mail/outreach/senders", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/brands/{brand_id}/mail/outreach/senders", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/brands/{brand_id}/mail/outreach/senders/{sender_id}/health **Get Sender Health** Time-series for a single sender's health card. `days` bounds the history returned (default 30 — matches the Slice F detail chart). The most-recent snapshot is duplicated into `latest` so the client doesn't have to index into history[0]. **Parameters** - `brand_id` (path, integer, required) - `sender_id` (path, integer, required) - `days` (query, integer, optional) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/brands/{brand_id}/mail/outreach/senders/{sender_id}/health' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/brands/{brand_id}/mail/outreach/senders/{sender_id}/health", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/brands/{brand_id}/mail/outreach/senders/{sender_id}/health", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/brands/{brand_id}/mail/outreach/senders/{sender_id}/health", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/brands/{brand_id}/mail/outreach/warmup-trend **Get Warmup Trend** Per-brand daily warmup/deliverability time-series (#26/#32). Buckets the brand's snapshots by UTC day, summing the 24h send/inbox counters and averaging the health score. Feeds the outreach Warmup chart AND the security 30-day trend. Hits idx_health_snapshots_brand_polled via the (brands_id, polled_at) range; date_trunc is in SELECT/GROUP BY only, so the index range scan on polled_at is preserved. **Parameters** - `brand_id` (path, integer, required) - `days` (query, integer, optional) — Trailing window in days. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/brands/{brand_id}/mail/outreach/warmup-trend' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/brands/{brand_id}/mail/outreach/warmup-trend", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/brands/{brand_id}/mail/outreach/warmup-trend", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/brands/{brand_id}/mail/outreach/warmup-trend", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/brands/{brand_id}/mail/outreach/health/overview **Get Health Overview** Brand outreach overview envelope: the per-sender grid plus brand-level deltas (#23) and quota (#24). `senders` is the latest snapshot per sender (one row per sender — powers the HealthCards grid, hits idx_health_snapshots_sender_polled via DISTINCT ON). `deltas` is this-7d vs prior-7d; `quota` is 30d send volume vs the plan cap. **Parameters** - `brand_id` (path, integer, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/brands/{brand_id}/mail/outreach/health/overview' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/brands/{brand_id}/mail/outreach/health/overview", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/brands/{brand_id}/mail/outreach/health/overview", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/brands/{brand_id}/mail/outreach/health/overview", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/brands/{brand_id}/mail/outreach/connections/{connection_id}/campaigns **List Remote Campaigns** Campaigns visible to this connection — powers the 'pick a campaign' picker. **Parameters** - `brand_id` (path, integer, required) - `connection_id` (path, integer, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/brands/{brand_id}/mail/outreach/connections/{connection_id}/campaigns' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/brands/{brand_id}/mail/outreach/connections/{connection_id}/campaigns", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/brands/{brand_id}/mail/outreach/connections/{connection_id}/campaigns", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/brands/{brand_id}/mail/outreach/connections/{connection_id}/campaigns", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/brands/{brand_id}/mail/outreach/connections/{connection_id}/push-status **Get Push Status** Quota usage + per-campaign push counts for this connection. **Parameters** - `brand_id` (path, integer, required) - `connection_id` (path, integer, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/brands/{brand_id}/mail/outreach/connections/{connection_id}/push-status' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/brands/{brand_id}/mail/outreach/connections/{connection_id}/push-status", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/brands/{brand_id}/mail/outreach/connections/{connection_id}/push-status", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/brands/{brand_id}/mail/outreach/connections/{connection_id}/push-status", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/brands/{brand_id}/mail/outreach/connections/{connection_id}/push **Push Campaign Leads** Push a SpiderIQ campaign's verified leads into the chosen remote campaign. **Parameters** - `brand_id` (path, integer, required) - `connection_id` (path, integer, required) **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/brands/{brand_id}/mail/outreach/connections/{connection_id}/push' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "spideriq_campaign_id": "string", "remote_campaign_id": "string", "smartlead_campaign_name": "string", "limit": 0, "only_with_vayapin_seo": false, "only_with_vayapin_pin": false }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/brands/{brand_id}/mail/outreach/connections/{connection_id}/push", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"spideriq_campaign_id": "string", "remote_campaign_id": "string", "smartlead_campaign_name": "string", "limit": 0, "only_with_vayapin_seo": false, "only_with_vayapin_pin": false}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/brands/{brand_id}/mail/outreach/connections/{connection_id}/push", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"spideriq_campaign_id": "string", "remote_campaign_id": "string", "smartlead_campaign_name": "string", "limit": 0, "only_with_vayapin_seo": false, "only_with_vayapin_pin": false}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"spideriq_campaign_id": "string", "remote_campaign_id": "string", "smartlead_campaign_name": "string", "limit": 0, "only_with_vayapin_seo": false, "only_with_vayapin_pin": false}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/brands/{brand_id}/mail/outreach/connections/{connection_id}/push", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/brands/{brand_id}/mail/outreach/connections/{connection_id}/remove **Remove Campaign Leads** Remove tracked leads from a remote campaign (frees account lead credits). **Parameters** - `brand_id` (path, integer, required) - `connection_id` (path, integer, required) **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/brands/{brand_id}/mail/outreach/connections/{connection_id}/remove' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "remote_campaign_id": "string", "spideriq_campaign_id": "string", "emails": [ "string" ] }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/brands/{brand_id}/mail/outreach/connections/{connection_id}/remove", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"remote_campaign_id": "string", "spideriq_campaign_id": "string", "emails": ["string"]}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/brands/{brand_id}/mail/outreach/connections/{connection_id}/remove", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"remote_campaign_id": "string", "spideriq_campaign_id": "string", "emails": ["string"]}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"remote_campaign_id": "string", "spideriq_campaign_id": "string", "emails": ["string"]}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/brands/{brand_id}/mail/outreach/connections/{connection_id}/remove", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## Catalog Evals (OPVS) Source: https://spideriq.ai/docs/api-reference/catalog-evals-opvs ## `POST` /api/v1/catalog/models/{model_ref}/evals **Contribute Eval** Contribute a machine-eval aggregate (kind:auto) or human rating (kind:human). Upserts into the kind-scoped partial-unique key space so a re-report of the same (model_ref, task_type, source[, rated_by]) updates in place and the two kinds never collide (G4). ``kind:human`` requires the master/super_admin actor; a broker key is 403'd. **Parameters** - `model_ref` (path, string, required) **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/catalog/models/{model_ref}/evals' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "kind": "string", "source": "string", "task_type": "string", "runs": 0, "successes": 0, "failures": 0, "no_gate_runs": 0, "success_rate": 0.0, "avg_cost_usd": 0.0, "avg_turns": 0.0, "avg_gate_duration_ms": 0, "method": "string", "window_first_run_at": "2026-01-01T00:00:00Z", "window_last_run_at": "2026-01-01T00:00:00Z", "confidence": "string", "stars": 0.0, "rated_by": "string", "review_text": "string", "provenance": {} }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/catalog/models/{model_ref}/evals", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"kind": "string", "source": "string", "task_type": "string", "runs": 0, "successes": 0, "failures": 0, "no_gate_runs": 0, "success_rate": 0.0, "avg_cost_usd": 0.0, "avg_turns": 0.0, "avg_gate_duration_ms": 0, "method": "string", "window_first_run_at": "2026-01-01T00:00:00Z", "window_last_run_at": "2026-01-01T00:00:00Z", "confidence": "string", "stars": 0.0, "rated_by": "string", "review_text": "string", "provenance": {}}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/catalog/models/{model_ref}/evals", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"kind": "string", "source": "string", "task_type": "string", "runs": 0, "successes": 0, "failures": 0, "no_gate_runs": 0, "success_rate": 0.0, "avg_cost_usd": 0.0, "avg_turns": 0.0, "avg_gate_duration_ms": 0, "method": "string", "window_first_run_at": "2026-01-01T00:00:00Z", "window_last_run_at": "2026-01-01T00:00:00Z", "confidence": "string", "stars": 0.0, "rated_by": "string", "review_text": "string", "provenance": {}}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"kind": "string", "source": "string", "task_type": "string", "runs": 0, "successes": 0, "failures": 0, "no_gate_runs": 0, "success_rate": 0.0, "avg_cost_usd": 0.0, "avg_turns": 0.0, "avg_gate_duration_ms": 0, "method": "string", "window_first_run_at": "2026-01-01T00:00:00Z", "window_last_run_at": "2026-01-01T00:00:00Z", "confidence": "string", "stars": 0.0, "rated_by": "string", "review_text": "string", "provenance": {}}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/catalog/models/{model_ref}/evals", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `201` — Successful Response - `422` — Validation Error ## `PATCH` /api/v1/catalog/models/{model_ref}/evals/{eval_id} **Patch Eval** Update one eval row by id (COALESCE-preserve — only supplied fields change). The periodic auto rollup PATCHes its own auto rows; it can NEVER reach a human row — a ``"broker"`` actor is 403'd from touching ``kind='human'`` (G4 at the API layer). Identity columns (model_ref / kind / source / task_type) are immutable. **Parameters** - `model_ref` (path, string, required) - `eval_id` (path, integer, required) **Request body** :::codegroup ```bash curl -X PATCH 'https://spideriq.ai/api/v1/catalog/models/{model_ref}/evals/{eval_id}' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "runs": 0, "successes": 0, "failures": 0, "no_gate_runs": 0, "success_rate": 0.0, "avg_cost_usd": 0.0, "avg_turns": 0.0, "avg_gate_duration_ms": 0, "method": "string", "window_first_run_at": "2026-01-01T00:00:00Z", "window_last_run_at": "2026-01-01T00:00:00Z", "confidence": "string", "stars": 0.0, "review_text": "string", "provenance": {} }' ``` ```python import httpx resp = httpx.patch( "https://spideriq.ai/api/v1/catalog/models/{model_ref}/evals/{eval_id}", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"runs": 0, "successes": 0, "failures": 0, "no_gate_runs": 0, "success_rate": 0.0, "avg_cost_usd": 0.0, "avg_turns": 0.0, "avg_gate_duration_ms": 0, "method": "string", "window_first_run_at": "2026-01-01T00:00:00Z", "window_last_run_at": "2026-01-01T00:00:00Z", "confidence": "string", "stars": 0.0, "review_text": "string", "provenance": {}}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/catalog/models/{model_ref}/evals/{eval_id}", { method: "PATCH", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"runs": 0, "successes": 0, "failures": 0, "no_gate_runs": 0, "success_rate": 0.0, "avg_cost_usd": 0.0, "avg_turns": 0.0, "avg_gate_duration_ms": 0, "method": "string", "window_first_run_at": "2026-01-01T00:00:00Z", "window_last_run_at": "2026-01-01T00:00:00Z", "confidence": "string", "stars": 0.0, "review_text": "string", "provenance": {}}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"runs": 0, "successes": 0, "failures": 0, "no_gate_runs": 0, "success_rate": 0.0, "avg_cost_usd": 0.0, "avg_turns": 0.0, "avg_gate_duration_ms": 0, "method": "string", "window_first_run_at": "2026-01-01T00:00:00Z", "window_last_run_at": "2026-01-01T00:00:00Z", "confidence": "string", "stars": 0.0, "review_text": "string", "provenance": {}}`) req, _ := http.NewRequest("PATCH", "https://spideriq.ai/api/v1/catalog/models/{model_ref}/evals/{eval_id}", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## Dashboard Gate Source: https://spideriq.ai/docs/api-reference/dashboard-gate ## `GET` /api/v1/brands/{brand_id}/gate/stats **Get Brand Gate Stats** Brand's Gate stats: agent count, requests this month, spend. **Parameters** - `brand_id` (path, integer, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/brands/{brand_id}/gate/stats' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/brands/{brand_id}/gate/stats", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/brands/{brand_id}/gate/stats", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/brands/{brand_id}/gate/stats", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/brands/{brand_id}/gate/agents **List Brand Agents** List agent tokens belonging to this brand. Status comes from `agent_token_status` (the canonical view — revoked > expired > !is_active > active) so this dashboard agrees with Settings → Members → Agents on what's alive. Reading raw `agent_tokens.is_active` was the root of the "10 Active / 0 active" divergence in gate-keys-coherence-plan.md §1. `created_by` is JOINed against Better Auth `"user"` (`user.id` is TEXT; SpiderGate LEARNINGS #18). NULL `created_by_user_id` surfaces as `{"name": "System"}` — legacy or agent-self-registered tokens have no dashboard operator on the hook. **Parameters** - `brand_id` (path, integer, required) - `include_inactive` (query, boolean, optional) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/brands/{brand_id}/gate/agents' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/brands/{brand_id}/gate/agents", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/brands/{brand_id}/gate/agents", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/brands/{brand_id}/gate/agents", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `PATCH` /api/v1/brands/{brand_id}/gate/agents/{token_id} **Update Brand Agent** Update agent's budget/models/rate limits. Brand admin required. **Parameters** - `brand_id` (path, integer, required) - `token_id` (path, string, required) **Request body** :::codegroup ```bash curl -X PATCH 'https://spideriq.ai/api/v1/brands/{brand_id}/gate/agents/{token_id}' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{}' ``` ```python import httpx resp = httpx.patch( "https://spideriq.ai/api/v1/brands/{brand_id}/gate/agents/{token_id}", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/brands/{brand_id}/gate/agents/{token_id}", { method: "PATCH", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{}`) req, _ := http.NewRequest("PATCH", "https://spideriq.ai/api/v1/brands/{brand_id}/gate/agents/{token_id}", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/brands/{brand_id}/gate/agents/{token_id}/detail **Get Brand Agent Detail** Detailed view of a single agent token: config + usage + recent activity. Powers the per-key detail page modeled after OpenRouter's API key page — config card, usage cards (today/week/month/total), per-model spend breakdown, and recent activity rows. **Parameters** - `brand_id` (path, integer, required) - `token_id` (path, string, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/brands/{brand_id}/gate/agents/{token_id}/detail' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/brands/{brand_id}/gate/agents/{token_id}/detail", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/brands/{brand_id}/gate/agents/{token_id}/detail", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/brands/{brand_id}/gate/agents/{token_id}/detail", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/brands/{brand_id}/gate/usage **Get Brand Usage** Brand's SpiderGate usage: turns vs attempts, outcome split, cost avoided. Read-only aggregate over `gate_request_logs`. Emits no billable event and adds no metered surface, so it is deliberately NOT wired to `dispatcher_submission()`; the parent `spiderGateLlm` service is already metered where the spend actually happens. ⚠️ Scoped `WHERE brand_id = :brand_id` — this is "my spend", NOT capacity. A pooled key serves the whole pool, so a brand-filtered per-key rollup under-counts by 72% (SpiderGate LEARNINGS P2.1). Never reuse these numbers as a quota or headroom input. **Parameters** - `brand_id` (path, integer, required) - `from` (query, any, optional) — Window start (inclusive), ISO-8601. Naive values are read as UTC. Pair with `to` for a real calendar range — e.g. from=2026-08-01T00:00:00Z&to=2026-09-01T00:00:00Z is THIS MONTH, which is not the same window as a rolling 30 days. - `to` (query, any, optional) — Window end (exclusive), ISO-8601. Defaults to now. - `days` (query, any, optional) — DEPRECATED — rolling lookback in days, kept for one release for the shipped dashboard and the `gate_usage` MCP tool. Use `from`/`to`: a lookback cannot express a calendar month. Ignored when `from`/`to` are supplied. - `bucket` (query, string, optional) — Grain of the time series in `by_bucket`. - `compare` (query, boolean, optional) — Also compute the immediately preceding window of identical length and return it under `comparison`. Doubles the query cost. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/brands/{brand_id}/gate/usage' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/brands/{brand_id}/gate/usage", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/brands/{brand_id}/gate/usage", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/brands/{brand_id}/gate/usage", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/brands/{brand_id}/gate/providers **Get Gate Providers** List available LLM providers with logos and descriptions. Read-only for clients — no edit capabilities. **Parameters** - `brand_id` (path, integer, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/brands/{brand_id}/gate/providers' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/brands/{brand_id}/gate/providers", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/brands/{brand_id}/gate/providers", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/brands/{brand_id}/gate/providers", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/brands/{brand_id}/gate/playground/completions **Playground Completions** Proxy chat completions for the Playground UI (dashboard session auth). Records every refusal, then re-raises it unchanged (STUDIO.1, ``SQAR2-105``), mirroring the public route's wrapper (LOG.1, ``SQAR2-101``). 🔴 **Why a wrapper and not a recorder per `**`**raise**`**`.** LOG.1 established that the count of refusal sites is not stable — a recorder bolted onto each site is correct the day it ships and silently incomplete at the next ``raise``. Every exit of this route passes through here by construction. The three load-bearing properties are LOG.1's, unchanged: - **It changes no decision.** ``raise`` re-raises the identical exception. - **It cannot fail the request.** A recorder that turns a clean 4xx into an opaque 500 is strictly worse than no recorder — hence the inner ``try``/``except`` around the recording itself, not a trusted promise made in another module. - **It does not depend on the engine** — one refusal is *"engine unavailable"*. ⚠️ **What it does NOT cover, said out loud — three classes:** 1. **A refusal raised by a DEPENDENCY.** FastAPI resolves dependencies before calling this function, so ``require_tenant_member_or_agent``'s ``400 "Please select a client or brand first"`` never enters this ``try``. That 400 is exactly what made the owner's 2026-09-02 Fork undiagnosable — it is fixed at source in ``api-client.postStream`` (the Studio now sends the tenant headers), not recorded here. Recording it would mean an app-level handler over a dependency shared by ~20 non-gate routes, which is a different layer. Same boundary LOG.1 drew. 1. **A streaming response that fails mid-body**, after the ``StreamingResponse`` has been returned. The fork/merge lanes are covered separately inside ``services.gate.fusion`` — see ``_record_lane_refusal``. 1. **A fork lane that fails while the whole request succeeds** — a 200 with three dead lanes raises nothing here. Also ``fusion``'s job. **Parameters** - `brand_id` (path, integer, required) **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/brands/{brand_id}/gate/playground/completions' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{}' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/brands/{brand_id}/gate/playground/completions", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/brands/{brand_id}/gate/playground/completions", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/brands/{brand_id}/gate/playground/completions", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/brands/{brand_id}/gate/playground/media/models **Playground Media Models** List media-generation models for the ⌘J picker (rich per-model/provider metadata). ``modality`` filters to one fine tab; ``modality_group`` filters to one coarse tab (image|video|audio); ``tags`` requires ALL given facet chips; ``include_inactive`` also returns coming_soon/disabled rows (greyed out). **Parameters** - `brand_id` (path, integer, required) - `modality` (query, any, optional) - `modality_group` (query, any, optional) - `tags` (query, any, optional) - `include_inactive` (query, boolean, optional) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/brands/{brand_id}/gate/playground/media/models' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/brands/{brand_id}/gate/playground/media/models", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/brands/{brand_id}/gate/playground/media/models", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/brands/{brand_id}/gate/playground/media/models", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/brands/{brand_id}/gate/playground/media/generations **Playground Media Generations** Generate media (image/video/audio) for the Studio and return its stored URL. Session-authed + brand-scoped; the vault key is resolved server-side and never reaches the browser. ``body`` = {model: '/', prompt, …params}. **Parameters** - `brand_id` (path, integer, required) **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/brands/{brand_id}/gate/playground/media/generations' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{}' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/brands/{brand_id}/gate/playground/media/generations", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/brands/{brand_id}/gate/playground/media/generations", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/brands/{brand_id}/gate/playground/media/generations", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/brands/{brand_id}/gate/catalog/models **Brand Catalog Models** Enriched LLM/chat catalog for the model browser (Chat/fusion modalities). Session-authed proxy → catalog.get_enriched_catalog (identical shape to the brand-PAT GET /api/gate/v1/catalog/models). Read-only, not metered. **Parameters** - `brand_id` (path, integer, required) - `provider` (query, any, optional) - `search` (query, any, optional) - `tag` (query, any, optional) - `fields` (query, any, optional) - `free_only` (query, boolean, optional) - `servable_only` (query, boolean, optional) - `limit` (query, integer, optional) - `offset` (query, integer, optional) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/brands/{brand_id}/gate/catalog/models' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/brands/{brand_id}/gate/catalog/models", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/brands/{brand_id}/gate/catalog/models", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/brands/{brand_id}/gate/catalog/models", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/brands/{brand_id}/gate/catalog/models/{model_id} **Brand Catalog Model** Full enriched record for one model — the browser's detail pane (lazy). Session-authed proxy → catalog.get_model_record. 404 when unknown. **Parameters** - `brand_id` (path, integer, required) - `model_id` (path, string, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/brands/{brand_id}/gate/catalog/models/{model_id}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/brands/{brand_id}/gate/catalog/models/{model_id}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/brands/{brand_id}/gate/catalog/models/{model_id}", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/brands/{brand_id}/gate/catalog/models/{model_id}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/brands/{brand_id}/gate/catalog/categories **Brand Catalog Categories** Category taxonomy for the DB-driven chips + score bars (CT.4). Session- authed proxy → catalog.get_categories. Read-only, not metered. **Parameters** - `brand_id` (path, integer, required) - `include_inactive` (query, boolean, optional) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/brands/{brand_id}/gate/catalog/categories' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/brands/{brand_id}/gate/catalog/categories", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/brands/{brand_id}/gate/catalog/categories", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/brands/{brand_id}/gate/catalog/categories", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/brands/{brand_id}/gate/catalog/leaderboard **Brand Catalog Leaderboard** Model leaderboard (global, per-task, or per-category via `category=`). Session-authed proxy → catalog.get_leaderboard. Category mode ranks by the materialised within-category percentile (§13-C). Read-only, not metered. **Parameters** - `brand_id` (path, integer, required) - `task_type` (query, any, optional) - `category` (query, any, optional) - `sort` (query, string, optional) - `limit` (query, integer, optional) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/brands/{brand_id}/gate/catalog/leaderboard' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/brands/{brand_id}/gate/catalog/leaderboard", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/brands/{brand_id}/gate/catalog/leaderboard", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/brands/{brand_id}/gate/catalog/leaderboard", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/brands/{brand_id}/gate/favorites **List Model Favorites** This user's ★ favorite models (newest-first). ``modality`` optionally scopes to one tab. Read-only, fails open to an empty list. **Parameters** - `brand_id` (path, integer, required) - `modality` (query, any, optional) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/brands/{brand_id}/gate/favorites' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/brands/{brand_id}/gate/favorites", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/brands/{brand_id}/gate/favorites", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/brands/{brand_id}/gate/favorites", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/brands/{brand_id}/gate/favorites **Add Model Favorite** Star a model for this user (idempotent). Returns the stored favorite. **Parameters** - `brand_id` (path, integer, required) **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/brands/{brand_id}/gate/favorites' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "spidergate_id": "string", "modality": "chat" }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/brands/{brand_id}/gate/favorites", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"spidergate_id": "string", "modality": "chat"}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/brands/{brand_id}/gate/favorites", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"spidergate_id": "string", "modality": "chat"}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"spidergate_id": "string", "modality": "chat"}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/brands/{brand_id}/gate/favorites", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `201` — Successful Response - `422` — Validation Error ## `DELETE` /api/v1/brands/{brand_id}/gate/favorites/{spidergate_id} **Remove Model Favorite** Un-star a model for this user (idempotent — 204 whether or not it existed). **Parameters** - `brand_id` (path, integer, required) - `spidergate_id` (path, string, required) :::codegroup ```bash curl -X DELETE 'https://spideriq.ai/api/v1/brands/{brand_id}/gate/favorites/{spidergate_id}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.delete( "https://spideriq.ai/api/v1/brands/{brand_id}/gate/favorites/{spidergate_id}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/brands/{brand_id}/gate/favorites/{spidergate_id}", { method: "DELETE", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("DELETE", "https://spideriq.ai/api/v1/brands/{brand_id}/gate/favorites/{spidergate_id}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `204` — Successful Response - `422` — Validation Error ## `GET` /api/v1/brands/{brand_id}/gate/capacity **Get Brand Gate Capacity** Key pressure + subscription windows for the usage page's right rail. Two blocks with **different scopes**, labelled as such in the payload: ``key_pressure`` ``traffic_scope="pool_wide"``. Counted with NO ``brand_id`` predicate, because a provider counts every request on a key regardless of which tenant caused it and a ``share_with_pool`` key serves the whole pool. Never render this as "your usage". ``subscription_windows`` ``scope="brand_keys"``. This brand's own keys. Each row is ``state="measured"`` with a ``window_percent`` float where the tier's request cap and the request counter can be honestly divided, and ``state="unmeasurable"`` with ``window_percent=None`` where they cannot (GUR-11 gave the division matching units; GUR-16 turned it on). ``gauge_available`` is DERIVED from those rows and is a different question from "here is a number": it says a gauge is drawable somewhere on this panel. It was a hardcoded ``False`` until GUR-16, citing a blocker that had already closed. **Parameters** - `brand_id` (path, integer, required) - `days` (query, integer, optional) — Lookback window in days - `key_limit` (query, integer, optional) — Max subscription-window rows :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/brands/{brand_id}/gate/capacity' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/brands/{brand_id}/gate/capacity", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/brands/{brand_id}/gate/capacity", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/brands/{brand_id}/gate/capacity", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/brands/{brand_id}/gate/flow **Get Brand Gate Flow** ``requested alias -> served model -> provider`` cross-tab for the Sankey. Brand-scoped: these are this tenant's own requests. The provider is resolved through ``integration_id -> api_integrations``. It is **not** ``gate_request_logs.provider``, which carries the litellm wire prefix and stamps MiniMax as ``openai``. **Parameters** - `brand_id` (path, integer, required) - `days` (query, integer, optional) — Lookback window in days - `limit` (query, integer, optional) — Max cross-tab rows :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/brands/{brand_id}/gate/flow' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/brands/{brand_id}/gate/flow", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/brands/{brand_id}/gate/flow", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/brands/{brand_id}/gate/flow", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/brands/{brand_id}/gate/agent-keys **Create an agent key directly (dashboard-driven mint)** Mint a new agent token under the given brand. S2 unification: the identity half now goes through the SAME resolver the email-approval path uses (`services.agent_identity.resolve_or_create_agent_user`) — so a dashboard mint and a CLI enrollment for the same operator resolve to the SAME agent account, keyed on the opaque `agent_ref`, and BOTH mint a real OPVS address (block-until-minted). The PAT minted here is therefore the self-identifying form `spideriq_pat__`, not the legacy shape it used to emit. The full secret is returned exactly once in the response body. Subsequent reads of the token (list / detail endpoints) only ever see `token_prefix`. **Parameters** - `brand_id` (path, integer, required) **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/brands/{brand_id}/gate/agent-keys' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "name": "string", "description": "string", "photo": "string", "scopes": [ "string" ], "allowed_models": [ "string" ], "free_models_only": false, "ttl_hours": 720, "monthly_budget_usd": 0.0, "soft_budget_usd": 0.0, "rate_limit_rpm": 0, "rate_limit_rpd": 0, "owner_email": "string" }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/brands/{brand_id}/gate/agent-keys", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"name": "string", "description": "string", "photo": "string", "scopes": ["string"], "allowed_models": ["string"], "free_models_only": false, "ttl_hours": 720, "monthly_budget_usd": 0.0, "soft_budget_usd": 0.0, "rate_limit_rpm": 0, "rate_limit_rpd": 0, "owner_email": "string"}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/brands/{brand_id}/gate/agent-keys", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"name": "string", "description": "string", "photo": "string", "scopes": ["string"], "allowed_models": ["string"], "free_models_only": false, "ttl_hours": 720, "monthly_budget_usd": 0.0, "soft_budget_usd": 0.0, "rate_limit_rpm": 0, "rate_limit_rpd": 0, "owner_email": "string"}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"name": "string", "description": "string", "photo": "string", "scopes": ["string"], "allowed_models": ["string"], "free_models_only": false, "ttl_hours": 720, "monthly_budget_usd": 0.0, "soft_budget_usd": 0.0, "rate_limit_rpm": 0, "rate_limit_rpd": 0, "owner_email": "string"}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/brands/{brand_id}/gate/agent-keys", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `201` — Successful Response - `422` — Validation Error ## `POST` /api/v1/brands/{brand_id}/gate/agent-keys/avatar **Upload an avatar image for an agent key (≤2 MB)** Upload an avatar image to R2 under `agent-avatars/`. MED-01: enforce ~2 MB body cap before reading the full payload. We also validate Content-Type + extension to make sure the operator hasn't pointed a profile picture URL at /etc/shadow or similar. **Parameters** - `brand_id` (path, integer, required) **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/brands/{brand_id}/gate/agent-keys/avatar' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "file": "string" }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/brands/{brand_id}/gate/agent-keys/avatar", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"file": "string"}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/brands/{brand_id}/gate/agent-keys/avatar", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"file": "string"}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"file": "string"}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/brands/{brand_id}/gate/agent-keys/avatar", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `413` — Upload exceeds 2 MB limit - `415` — Unsupported image type - `422` — Validation Error ## Dashboard Gate Traces Source: https://spideriq.ai/docs/api-reference/dashboard-gate-traces ## `GET` /api/v1/brands/{brand_id}/gate/traces **List Traces** List traces for a brand. Proxies LangFuse with DB fallback. **Parameters** - `brand_id` (path, integer, required) - `page` (query, integer, optional) - `limit` (query, integer, optional) - `agent_id` (query, any, optional) - `model` (query, any, optional) - `status` (query, any, optional) - `from_date` (query, any, optional) - `to_date` (query, any, optional) - `min_latency_ms` (query, any, optional) - `min_cost_usd` (query, any, optional) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/brands/{brand_id}/gate/traces' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/brands/{brand_id}/gate/traces", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/brands/{brand_id}/gate/traces", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/brands/{brand_id}/gate/traces", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/brands/{brand_id}/gate/traces/{trace_id} **Get Trace Detail** Get full trace detail with spans and content. **Parameters** - `brand_id` (path, integer, required) - `trace_id` (path, string, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/brands/{brand_id}/gate/traces/{trace_id}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/brands/{brand_id}/gate/traces/{trace_id}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/brands/{brand_id}/gate/traces/{trace_id}", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/brands/{brand_id}/gate/traces/{trace_id}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/brands/{brand_id}/gate/traces/stats/summary **Get Trace Stats** Aggregated trace statistics from gate_request_logs. **Parameters** - `brand_id` (path, integer, required) - `days` (query, integer, optional) - `agent_id` (query, any, optional) - `model` (query, any, optional) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/brands/{brand_id}/gate/traces/stats/summary' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/brands/{brand_id}/gate/traces/stats/summary", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/brands/{brand_id}/gate/traces/stats/summary", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/brands/{brand_id}/gate/traces/stats/summary", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## Dashboard Gate Studio Source: https://spideriq.ai/docs/api-reference/dashboard-gate-studio ## `GET` /api/v1/gate/studio/projects **List Projects** List the brand's projects, newest-touched first. **Parameters** - `limit` (query, integer, optional) - `offset` (query, integer, optional) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/gate/studio/projects' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/gate/studio/projects", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/gate/studio/projects", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/gate/studio/projects", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/gate/studio/projects **Create Project** Create a Studio project for the caller's brand. **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/gate/studio/projects' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "name": "string" }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/gate/studio/projects", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"name": "string"}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/gate/studio/projects", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"name": "string"}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"name": "string"}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/gate/studio/projects", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `201` — Successful Response - `422` — Validation Error ## `GET` /api/v1/gate/studio/projects/{project_id} **Get Project** A single project + its sessions + asset gallery. **Parameters** - `project_id` (path, string, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/gate/studio/projects/{project_id}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/gate/studio/projects/{project_id}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/gate/studio/projects/{project_id}", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/gate/studio/projects/{project_id}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `PATCH` /api/v1/gate/studio/projects/{project_id} **Rename Project** Rename a project. **Parameters** - `project_id` (path, string, required) **Request body** :::codegroup ```bash curl -X PATCH 'https://spideriq.ai/api/v1/gate/studio/projects/{project_id}' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "name": "string" }' ``` ```python import httpx resp = httpx.patch( "https://spideriq.ai/api/v1/gate/studio/projects/{project_id}", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"name": "string"}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/gate/studio/projects/{project_id}", { method: "PATCH", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"name": "string"}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"name": "string"}`) req, _ := http.NewRequest("PATCH", "https://spideriq.ai/api/v1/gate/studio/projects/{project_id}", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `DELETE` /api/v1/gate/studio/projects/{project_id} **Delete Project** Delete a project (cascades to its sessions + assets). **Parameters** - `project_id` (path, string, required) :::codegroup ```bash curl -X DELETE 'https://spideriq.ai/api/v1/gate/studio/projects/{project_id}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.delete( "https://spideriq.ai/api/v1/gate/studio/projects/{project_id}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/gate/studio/projects/{project_id}", { method: "DELETE", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("DELETE", "https://spideriq.ai/api/v1/gate/studio/projects/{project_id}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `204` — Successful Response - `422` — Validation Error ## `POST` /api/v1/gate/studio/projects/{project_id}/sessions **Create Session** Open a conversation inside a project. **Parameters** - `project_id` (path, string, required) **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/gate/studio/projects/{project_id}/sessions' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "title": "string", "project_id": "00000000-0000-0000-0000-000000000000", "mode": "single" }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/gate/studio/projects/{project_id}/sessions", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"title": "string", "project_id": "00000000-0000-0000-0000-000000000000", "mode": "single"}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/gate/studio/projects/{project_id}/sessions", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"title": "string", "project_id": "00000000-0000-0000-0000-000000000000", "mode": "single"}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"title": "string", "project_id": "00000000-0000-0000-0000-000000000000", "mode": "single"}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/gate/studio/projects/{project_id}/sessions", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `201` — Successful Response - `422` — Validation Error ## `GET` /api/v1/gate/studio/sessions **List Sessions** Cross-project session list for the History picker (incl. Scratch). **Parameters** - `limit` (query, integer, optional) - `offset` (query, integer, optional) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/gate/studio/sessions' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/gate/studio/sessions", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/gate/studio/sessions", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/gate/studio/sessions", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/gate/studio/sessions **Create Session Standalone** Open a conversation. ``project_id`` optional — omit for a project-less Scratch conversation. 404 if a given project is not this brand's. **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/gate/studio/sessions' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "title": "string", "project_id": "00000000-0000-0000-0000-000000000000", "mode": "single" }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/gate/studio/sessions", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"title": "string", "project_id": "00000000-0000-0000-0000-000000000000", "mode": "single"}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/gate/studio/sessions", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"title": "string", "project_id": "00000000-0000-0000-0000-000000000000", "mode": "single"}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"title": "string", "project_id": "00000000-0000-0000-0000-000000000000", "mode": "single"}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/gate/studio/sessions", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `201` — Successful Response - `422` — Validation Error ## `GET` /api/v1/gate/studio/sessions/{session_id} **Get Session** Reopen a conversation — the session + its ordered messages. 404 if not this brand's. **Parameters** - `session_id` (path, string, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/gate/studio/sessions/{session_id}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/gate/studio/sessions/{session_id}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/gate/studio/sessions/{session_id}", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/gate/studio/sessions/{session_id}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `PATCH` /api/v1/gate/studio/sessions/{session_id} **Rename Session** Rename a session (editable header title). **Parameters** - `session_id` (path, string, required) **Request body** :::codegroup ```bash curl -X PATCH 'https://spideriq.ai/api/v1/gate/studio/sessions/{session_id}' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "title": "string" }' ``` ```python import httpx resp = httpx.patch( "https://spideriq.ai/api/v1/gate/studio/sessions/{session_id}", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"title": "string"}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/gate/studio/sessions/{session_id}", { method: "PATCH", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"title": "string"}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"title": "string"}`) req, _ := http.NewRequest("PATCH", "https://spideriq.ai/api/v1/gate/studio/sessions/{session_id}", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `DELETE` /api/v1/gate/studio/sessions/{session_id} **Delete Session** Delete a session (cascades to its chat messages). **Parameters** - `session_id` (path, string, required) :::codegroup ```bash curl -X DELETE 'https://spideriq.ai/api/v1/gate/studio/sessions/{session_id}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.delete( "https://spideriq.ai/api/v1/gate/studio/sessions/{session_id}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/gate/studio/sessions/{session_id}", { method: "DELETE", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("DELETE", "https://spideriq.ai/api/v1/gate/studio/sessions/{session_id}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `204` — Successful Response - `422` — Validation Error ## `POST` /api/v1/gate/studio/sessions/{session_id}/messages **Save Session Messages** Append a batch of chat messages to a session (persist-on-send). 404 if the session is not this brand's. **Parameters** - `session_id` (path, string, required) **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/gate/studio/sessions/{session_id}/messages' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "messages": [ { "role": "string", "content": "", "model": "string", "meta": {} } ] }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/gate/studio/sessions/{session_id}/messages", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"messages": [{"role": "string", "content": "", "model": "string", "meta": {}}]}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/gate/studio/sessions/{session_id}/messages", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"messages": [{"role": "string", "content": "", "model": "string", "meta": {}}]}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"messages": [{"role": "string", "content": "", "model": "string", "meta": {}}]}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/gate/studio/sessions/{session_id}/messages", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `201` — Successful Response - `422` — Validation Error ## `POST` /api/v1/gate/studio/projects/{project_id}/assets **Link Asset** Attach a generated media asset (SpiderMedia URL) to the project gallery. **Parameters** - `project_id` (path, string, required) **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/gate/studio/projects/{project_id}/assets' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "media_url": "string", "kind": "image", "title": "string", "session_id": "00000000-0000-0000-0000-000000000000", "meta": {} }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/gate/studio/projects/{project_id}/assets", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"media_url": "string", "kind": "image", "title": "string", "session_id": "00000000-0000-0000-0000-000000000000", "meta": {}}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/gate/studio/projects/{project_id}/assets", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"media_url": "string", "kind": "image", "title": "string", "session_id": "00000000-0000-0000-0000-000000000000", "meta": {}}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"media_url": "string", "kind": "image", "title": "string", "session_id": "00000000-0000-0000-0000-000000000000", "meta": {}}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/gate/studio/projects/{project_id}/assets", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `201` — Successful Response - `422` — Validation Error ## `DELETE` /api/v1/gate/studio/projects/{project_id}/assets/{asset_id} **Unlink Asset** Remove an asset from the project gallery. **Parameters** - `project_id` (path, string, required) - `asset_id` (path, string, required) :::codegroup ```bash curl -X DELETE 'https://spideriq.ai/api/v1/gate/studio/projects/{project_id}/assets/{asset_id}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.delete( "https://spideriq.ai/api/v1/gate/studio/projects/{project_id}/assets/{asset_id}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/gate/studio/projects/{project_id}/assets/{asset_id}", { method: "DELETE", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("DELETE", "https://spideriq.ai/api/v1/gate/studio/projects/{project_id}/assets/{asset_id}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `204` — Successful Response - `422` — Validation Error ## Dashboard Gate Prompts Source: https://spideriq.ai/docs/api-reference/dashboard-gate-prompts ## `GET` /api/v1/gate/studio/projects/{project_id}/prompts **List Project Prompts** List a project's saved prompts, newest-touched first. **Parameters** - `project_id` (path, string, required) - `limit` (query, integer, optional) - `offset` (query, integer, optional) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/gate/studio/projects/{project_id}/prompts' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/gate/studio/projects/{project_id}/prompts", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/gate/studio/projects/{project_id}/prompts", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/gate/studio/projects/{project_id}/prompts", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/gate/studio/projects/{project_id}/prompts **Create Prompt** Create a saved prompt inside a project. 404 if the project is not this brand's; 409 if the name already exists in the project. **Parameters** - `project_id` (path, string, required) **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/gate/studio/projects/{project_id}/prompts' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "name": "string", "description": "string", "system_prompt": "string", "model": "string", "settings": {}, "reference_media_ids": [ null ] }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/gate/studio/projects/{project_id}/prompts", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"name": "string", "description": "string", "system_prompt": "string", "model": "string", "settings": {}, "reference_media_ids": [null]}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/gate/studio/projects/{project_id}/prompts", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"name": "string", "description": "string", "system_prompt": "string", "model": "string", "settings": {}, "reference_media_ids": [null]}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"name": "string", "description": "string", "system_prompt": "string", "model": "string", "settings": {}, "reference_media_ids": [null]}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/gate/studio/projects/{project_id}/prompts", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `201` — Successful Response - `422` — Validation Error ## `GET` /api/v1/gate/studio/prompts **Search Prompts** Search the brand's saved prompts (name/description ILIKE), optionally narrowed to one project. Omit ``query`` to list all. **Parameters** - `query` (query, any, optional) - `project_id` (query, any, optional) - `limit` (query, integer, optional) - `offset` (query, integer, optional) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/gate/studio/prompts' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/gate/studio/prompts", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/gate/studio/prompts", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/gate/studio/prompts", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/gate/studio/prompts/{prompt_id} **Get Prompt** Fetch one prompt by internal id (uuid) or ``prompt_…`` public_id. **Parameters** - `prompt_id` (path, string, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/gate/studio/prompts/{prompt_id}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/gate/studio/prompts/{prompt_id}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/gate/studio/prompts/{prompt_id}", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/gate/studio/prompts/{prompt_id}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `PATCH` /api/v1/gate/studio/prompts/{prompt_id} **Update Prompt** Partial update. Only supplied fields change; ``settings`` / ``reference_media_ids`` REPLACE (not deep-merge). 409 on a name collision. **Parameters** - `prompt_id` (path, string, required) **Request body** :::codegroup ```bash curl -X PATCH 'https://spideriq.ai/api/v1/gate/studio/prompts/{prompt_id}' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "name": "string", "description": "string", "system_prompt": "string", "model": "string", "settings": {}, "reference_media_ids": [ null ] }' ``` ```python import httpx resp = httpx.patch( "https://spideriq.ai/api/v1/gate/studio/prompts/{prompt_id}", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"name": "string", "description": "string", "system_prompt": "string", "model": "string", "settings": {}, "reference_media_ids": [null]}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/gate/studio/prompts/{prompt_id}", { method: "PATCH", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"name": "string", "description": "string", "system_prompt": "string", "model": "string", "settings": {}, "reference_media_ids": [null]}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"name": "string", "description": "string", "system_prompt": "string", "model": "string", "settings": {}, "reference_media_ids": [null]}`) req, _ := http.NewRequest("PATCH", "https://spideriq.ai/api/v1/gate/studio/prompts/{prompt_id}", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `DELETE` /api/v1/gate/studio/prompts/{prompt_id} **Delete Prompt** Delete a prompt by id or public_id. **Parameters** - `prompt_id` (path, string, required) :::codegroup ```bash curl -X DELETE 'https://spideriq.ai/api/v1/gate/studio/prompts/{prompt_id}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.delete( "https://spideriq.ai/api/v1/gate/studio/prompts/{prompt_id}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/gate/studio/prompts/{prompt_id}", { method: "DELETE", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("DELETE", "https://spideriq.ai/api/v1/gate/studio/prompts/{prompt_id}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `204` — Successful Response - `422` — Validation Error ## Dashboard Gate Client Aliases Source: https://spideriq.ai/docs/api-reference/dashboard-gate-client-aliases ## `GET` /api/v1/dashboard/gate/client-aliases **List your client aliases** **Parameters** - `limit` (query, integer, optional) - `offset` (query, integer, optional) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/gate/client-aliases' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/gate/client-aliases", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/gate/client-aliases", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/gate/client-aliases", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/dashboard/gate/client-aliases **Create a client alias** 1. 🔴 The alias is LIVE for your tenant on return — creation is publication. **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/dashboard/gate/client-aliases' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "name": "string", "description": "string", "enabled": true, "slots": [ { "integration_id": 0, "provider": "string", "model": "string", "enabled": true } ] }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/dashboard/gate/client-aliases", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"name": "string", "description": "string", "enabled": true, "slots": [{"integration_id": 0, "provider": "string", "model": "string", "enabled": true}]}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/gate/client-aliases", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"name": "string", "description": "string", "enabled": true, "slots": [{"integration_id": 0, "provider": "string", "model": "string", "enabled": true}]}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"name": "string", "description": "string", "enabled": true, "slots": [{"integration_id": 0, "provider": "string", "model": "string", "enabled": true}]}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/dashboard/gate/client-aliases", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `201` — Successful Response - `422` — Validation Error ## `GET` /api/v1/dashboard/gate/client-aliases/{name} **Read one client alias** **Parameters** - `name` (path, string, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/dashboard/gate/client-aliases/{name}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/dashboard/gate/client-aliases/{name}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/gate/client-aliases/{name}", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/dashboard/gate/client-aliases/{name}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `PATCH` /api/v1/dashboard/gate/client-aliases/{name} **Update a client alias** **Parameters** - `name` (path, string, required) **Request body** :::codegroup ```bash curl -X PATCH 'https://spideriq.ai/api/v1/dashboard/gate/client-aliases/{name}' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "description": "string", "enabled": true, "slots": [ { "integration_id": 0, "provider": "string", "model": "string", "enabled": true } ] }' ``` ```python import httpx resp = httpx.patch( "https://spideriq.ai/api/v1/dashboard/gate/client-aliases/{name}", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"description": "string", "enabled": true, "slots": [{"integration_id": 0, "provider": "string", "model": "string", "enabled": true}]}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/gate/client-aliases/{name}", { method: "PATCH", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"description": "string", "enabled": true, "slots": [{"integration_id": 0, "provider": "string", "model": "string", "enabled": true}]}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"description": "string", "enabled": true, "slots": [{"integration_id": 0, "provider": "string", "model": "string", "enabled": true}]}`) req, _ := http.NewRequest("PATCH", "https://spideriq.ai/api/v1/dashboard/gate/client-aliases/{name}", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `DELETE` /api/v1/dashboard/gate/client-aliases/{name} **Delete a client alias** **Parameters** - `name` (path, string, required) :::codegroup ```bash curl -X DELETE 'https://spideriq.ai/api/v1/dashboard/gate/client-aliases/{name}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.delete( "https://spideriq.ai/api/v1/dashboard/gate/client-aliases/{name}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/dashboard/gate/client-aliases/{name}", { method: "DELETE", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("DELETE", "https://spideriq.ai/api/v1/dashboard/gate/client-aliases/{name}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `204` — Successful Response - `422` — Validation Error ## IDAP Source: https://spideriq.ai/docs/api-reference/idap ## `GET` /api/v1/idap/health **IDAP health check** Liveness probe for the IDAP router. Unauthenticated. Returns the list of supported `resource_types` (businesses, people, leads, ...) backed by `norm_cli_*` per-client tables. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/idap/health' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/idap/health", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/idap/health", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/idap/health", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response ## `POST` /api/v1/idap/batch **Batch-fetch resources by IDAP ref** Resolve many `IdapRef` entries (type + id) in a single call. Missing or unauthorized refs are returned in the `errors` map; successful lookups populate `results`. Scoped to the authenticated client's `norm_cli_*` tables. **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/idap/batch' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "refs": [ "string" ], "fields": "string", "include": "string" }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/idap/batch", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"refs": ["string"], "fields": "string", "include": "string"}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/idap/batch", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"refs": ["string"], "fields": "string", "include": "string"}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"refs": ["string"], "fields": "string", "include": "string"}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/idap/batch", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Request succeeded - `401` — Authentication failed - `404` — Resource not found in the authenticated client's tenant-scoped tables - `422` — Validation error (invalid resource_type, cursor, filter, or flag payload) - `429` — Rate limit exceeded ## `GET` /api/v1/idap/media/{media_id} **Proxy media binary from SeaweedFS** Stream a media file (image/video/document) from SeaweedFS through the API, enforcing tenant scoping. Supports conditional requests (`If-None-Match`, `If-Modified-Since` → 304), `?thumb=1` for a 400px thumbnail, and `?download=1` to force `Content-Disposition: attachment`. **Parameters** - `media_id` (path, string, required) - `thumb` (query, boolean, optional) — Return 400px thumbnail - `download` (query, boolean, optional) — Set Content-Disposition: attachment :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/idap/media/{media_id}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/idap/media/{media_id}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/idap/media/{media_id}", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/idap/media/{media_id}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Request succeeded - `401` — Authentication failed - `404` — Resource not found in the authenticated client's tenant-scoped tables - `422` — Validation error (invalid resource_type, cursor, filter, or flag payload) - `429` — Rate limit exceeded - `304` — Not modified (conditional-request hit) ## `GET` /api/v1/idap/{resource_type}/search **Full-text search within a resource type** Full-text search across the authenticated client's `norm_cli_*` table for the given `resource_type`. Use `q` for the query, `fields` for column projection, `flags` to filter by flag keys, and `?format=json|yaml|md|llm` for AI-agent-friendly output. **Parameters** - `resource_type` (path, string, required) - `q` (query, string, required) — Search query - `limit` (query, integer, optional) - `fields` (query, any, optional) - `format` (query, any, optional) — Response format. `json` (default) and `llm` return JSON; `yaml` returns text/yaml; `md` returns text/markdown. Any other value is a 422. - `flags` (query, any, optional) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/idap/{resource_type}/search' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/idap/{resource_type}/search", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/idap/{resource_type}/search", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/idap/{resource_type}/search", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Request succeeded - `401` — Authentication failed - `404` — Resource not found in the authenticated client's tenant-scoped tables - `422` — Validation error (invalid resource_type, cursor, filter, or flag payload) - `429` — Rate limit exceeded ## `GET` /api/v1/idap/{resource_type}/stats **Aggregate stats for a resource type** Dashboard-oriented aggregates for the authenticated client's `norm_cli_{resource_type}` table: total count, per-flag-key counts, recency buckets. Cheap — pre-aggregated on read. Supports `?format=json|yaml|md|llm` for AI-agent-friendly output. **Parameters** - `resource_type` (path, string, required) - `format` (query, any, optional) — Response format. `json` (default) and `llm` return JSON; `yaml` returns text/yaml; `md` returns text/markdown. Any other value is a 422. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/idap/{resource_type}/stats' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/idap/{resource_type}/stats", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/idap/{resource_type}/stats", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/idap/{resource_type}/stats", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Request succeeded - `401` — Authentication failed - `404` — Resource not found in the authenticated client's tenant-scoped tables - `422` — Validation error (invalid resource_type, cursor, filter, or flag payload) - `429` — Rate limit exceeded ## `GET` /api/v1/idap/{resource_type}/resolve **Resolve a resource by external identifier** Look up a single resource by an external id (Google `place_id`, email `domain`, etc.) within the authenticated client's tenant scope. Returns the canonical IDAP record without requiring the internal UUID. Exactly one external identifier query param must be supplied. **Parameters** - `resource_type` (path, string, required) - `place_id` (query, any, optional) — Google Place ID (e.g. 0x47e66fdad6f1cc73:0x341211b3fccd79e1) - `domain` (query, any, optional) — Domain name (e.g. mariospizzeria.com) - `email` (query, any, optional) — Email address (e.g. info@example.com) - `url` (query, any, optional) — LinkedIn URL or other URL identifier (linkedin_profiles only) - `linkedin` (query, any, optional) — LinkedIn profile URL on a contact (e.g. https://linkedin.com/in/jane) - `twitter` (query, any, optional) — Twitter profile URL on a contact - `vat` (query, any, optional) — VAT number (e.g. DE123456789, GB123456789) - `registration_number` (query, any, optional) — National registration number (e.g. UK Companies House 00445790) - `lei` (query, any, optional) — Legal Entity Identifier (20-char ISO 17442 code) - `tax_id` (query, any, optional) — National tax id (e.g. US EIN, DE Steuernummer) - `source_id` (query, any, optional) — Registry-provided source id (admin/dev path — composite UNIQUE with `source`, seq-scans) - `pin_name` (query, any, optional) — VayaPin PIN name (e.g. 'BB:TAPAS') — pins only - `pin_data_set_id` (query, any, optional) — VayaPin data set UUID — pins only (natural key) - `account_id` (query, any, optional) — VayaPin account UUID — pins only - `pin_subscription_id` (query, any, optional) — VayaPin subscription UUID — pins only - `include` (query, any, optional) — Comma-separated related types to include (e.g. 'emails,phones') - `fields` (query, any, optional) — Comma-separated field projection - `format` (query, any, optional) — Response format. `json` (default) and `llm` return JSON; `yaml` returns text/yaml; `md` returns text/markdown. Any other value is a 422. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/idap/{resource_type}/resolve' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/idap/{resource_type}/resolve", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/idap/{resource_type}/resolve", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/idap/{resource_type}/resolve", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Request succeeded - `401` — Authentication failed - `404` — Resource not found in the authenticated client's tenant-scoped tables - `422` — Validation error (invalid resource_type, cursor, filter, or flag payload) - `429` — Rate limit exceeded ## `GET` /api/v1/idap/{resource_type}/duplicates **Find duplicate resources sharing a common external key** Return clusters of resources in the authenticated client's tenant that share the same value for a whitelisted external key (e.g. two or more `businesses` rows with the same `google_place_id`). Each cluster has count >= 2 — single-occurrence values are filtered out by HAVING COUNT(*) > 1. Used in dedupe workflows: surface the candidates here, then call `DELETE /idap//{id}` (Wave D.2) to remove the duplicate. Whitelisted `key` values for `businesses` (Wave D.1): `google_place_id`, `domain`, `phone_e164` (direct columns), `vat`, `registration_number`, `lei`, `tax_id` (joined via `company_registry.business_id`). **Parameters** - `resource_type` (path, string, required) - `key` (query, string, required) — Whitelisted clustering key. Allowed values depend on `resource_type` — see endpoint description. Free strings outside the whitelist return 400. - `limit` (query, integer, optional) — Max clusters to return (1..500). Default 100. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/idap/{resource_type}/duplicates' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/idap/{resource_type}/duplicates", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/idap/{resource_type}/duplicates", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/idap/{resource_type}/duplicates", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Request succeeded - `401` — Authentication failed - `404` — Resource not found in the authenticated client's tenant-scoped tables - `422` — Validation error (invalid resource_type, cursor, filter, or flag payload) - `429` — Rate limit exceeded ## `POST` /api/v1/idap/{resource_type}/flags/bulk **Bulk add/remove flags on many resources** Apply flag writes (add/remove) to many resources of the same type in one call. Per-row failures are returned in `errors`; successful writes go in `results`. Scoped to the authenticated client's tenant tables. **Parameters** - `resource_type` (path, string, required) **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/idap/{resource_type}/flags/bulk' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "updates": [ { "resource_id": "string", "add": [ "string" ], "remove": [ "string" ] } ], "flagged_by": "string", "reason": "string" }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/idap/{resource_type}/flags/bulk", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"updates": [{"resource_id": "string", "add": ["string"], "remove": ["string"]}], "flagged_by": "string", "reason": "string"}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/idap/{resource_type}/flags/bulk", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"updates": [{"resource_id": "string", "add": ["string"], "remove": ["string"]}], "flagged_by": "string", "reason": "string"}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"updates": [{"resource_id": "string", "add": ["string"], "remove": ["string"]}], "flagged_by": "string", "reason": "string"}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/idap/{resource_type}/flags/bulk", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Request succeeded - `401` — Authentication failed - `404` — Resource not found in the authenticated client's tenant-scoped tables - `422` — Validation error (invalid resource_type, cursor, filter, or flag payload) - `429` — Rate limit exceeded ## `POST` /api/v1/idap/{resource_type}/{resource_id}/flags **Add or remove flags on a single resource** Write one or more flag changes (add/remove) against a single resource in the authenticated client's tenant scope. The response returns the resulting full flag state. Use `/flags/bulk` when batching across many resources of the same type. **Parameters** - `resource_type` (path, string, required) - `resource_id` (path, string, required) **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/idap/{resource_type}/{resource_id}/flags' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "add": [ "string" ], "remove": [ "string" ], "flagged_by": "string", "reason": "string" }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/idap/{resource_type}/{resource_id}/flags", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"add": ["string"], "remove": ["string"], "flagged_by": "string", "reason": "string"}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/idap/{resource_type}/{resource_id}/flags", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"add": ["string"], "remove": ["string"], "flagged_by": "string", "reason": "string"}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"add": ["string"], "remove": ["string"], "flagged_by": "string", "reason": "string"}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/idap/{resource_type}/{resource_id}/flags", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Request succeeded - `401` — Authentication failed - `404` — Resource not found in the authenticated client's tenant-scoped tables - `422` — Validation error (invalid resource_type, cursor, filter, or flag payload) - `429` — Rate limit exceeded ## `GET` /api/v1/idap/{resource_type}/{resource_id} **Fetch a single resource by ID** Return the canonical IDAP record for a single resource in the authenticated client's tenant. Supports `fields` projection, `include` (comma-separated related-resource expansions), and `?format=json|yaml|md|llm` for AI-agent-friendly output. An invalid UUID is treated as a 404 rather than a 500. **Parameters** - `resource_type` (path, string, required) - `resource_id` (path, string, required) - `fields` (query, any, optional) — Comma-separated field projection - `include` (query, any, optional) — Comma-separated related types to include (e.g. 'emails,phones') - `format` (query, any, optional) — Response format. `json` (default) and `llm` return JSON; `yaml` returns text/yaml; `md` returns text/markdown. Any other value is a 422. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/idap/{resource_type}/{resource_id}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/idap/{resource_type}/{resource_id}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/idap/{resource_type}/{resource_id}", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/idap/{resource_type}/{resource_id}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Request succeeded - `401` — Authentication failed - `404` — Resource not found in the authenticated client's tenant-scoped tables - `422` — Validation error (invalid resource_type, cursor, filter, or flag payload) - `429` — Rate limit exceeded ## `GET` /api/v1/idap/{resource_type} **List resources of a given type (paged, sync-friendly)** List resources within the authenticated client's tenant, with `since`/`until` timestamp filters for incremental sync, cursor-based pagination, `fields` projection, `include` expansion, `flags` filtering, and `?format=json|yaml|md|llm` output. Designed so external systems (OPVS board-sync, CRMs) can incrementally pull new/changed records. **Parameters** - `resource_type` (path, string, required) - `since` (query, any, optional) — Return resources modified after this timestamp - `until` (query, any, optional) — Return resources modified before this timestamp - `limit` (query, integer, optional) - `cursor` (query, any, optional) - `fields` (query, any, optional) - `include` (query, any, optional) — Comma-separated related types to include (e.g. 'emails,phones') - `format` (query, any, optional) — Response format. `json` (default) and `llm` return JSON; `yaml` returns text/yaml; `md` returns text/markdown. Any other value is a 422. - `campaign_id` (query, any, optional) - `source` (query, any, optional) - `flags` (query, any, optional) - `sort` (query, string, optional) - `order` (query, string, optional) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/idap/{resource_type}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/idap/{resource_type}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/idap/{resource_type}", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/idap/{resource_type}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Request succeeded - `401` — Authentication failed - `404` — Resource not found in the authenticated client's tenant-scoped tables - `422` — Validation error (invalid resource_type, cursor, filter, or flag payload) - `429` — Rate limit exceeded ## `DELETE` /api/v1/idap/businesses/{business_id} **Delete a business record + cascade across per-tenant tables** 🔴 **Destructive.** Hard-deletes the business row in `norm_cli_.businesses` AND cascades to linked tables (pins, business_contacts, business_registry, company_registry, contacts, phones, domains, linkedin_profiles). Transactional — all or nothing. Audit row written to `public.idap_deletions_audit` in the same transaction. **Does NOT touch cs.vayapin.com.** VayaPin pin pages are permanent per VayaPin §10. Response includes `vayapin_pins_remain_external` + the orphaned `pin_data_set_id`s so the caller knows what external state remains. **Does NOT cascade **`**emails**`**.** That table is a tenant-wide canonical store keyed on `email` UNIQUE — multiple businesses share verification rows. The `business_contacts` join row is what gets removed; the email's verification metadata stays. `**booking_flows**`** + **`**services**`** auto-cascade** via Postgres ON DELETE CASCADE FK; counts surface in `auto_cascaded`. `**bookings**`** is a blocker.** If any `bookings` row references this business or its contacts, the call returns 409 with the blocking booking IDs — caller resolves those first. Tenant-owner auth only: caller's `client_id` IS the tenant scope. Idempotent: second call on the same UUID returns 404. **Parameters** - `business_id` (path, string, required) — UUID of the business to delete. Resolve alternate keys via GET /idap/businesses/resolve first. **Request body** :::codegroup ```bash curl -X DELETE 'https://spideriq.ai/api/v1/idap/businesses/{business_id}' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "reason": "string" }' ``` ```python import httpx resp = httpx.delete( "https://spideriq.ai/api/v1/idap/businesses/{business_id}", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"reason": "string"}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/idap/businesses/{business_id}", { method: "DELETE", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"reason": "string"}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"reason": "string"}`) req, _ := http.NewRequest("DELETE", "https://spideriq.ai/api/v1/idap/businesses/{business_id}", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Business + child rows deleted; audit row written. Response body reports per-table cascade counts + the VayaPin pin IDs that remain external (cs.vayapin.com pages are permanent per VayaPin §10 — we delete OUR record only). - `401` — Authentication failed - `404` — Business UUID not found in the caller's tenant schema. Also returned on idempotent re-DELETE of an already-deleted ID. - `422` — Validation error (invalid resource_type, cursor, filter, or flag payload) - `429` — Rate limit exceeded - `409` — One or more `bookings` rows reference this business or its contacts via ON DELETE NO ACTION FK. Body lists the blocking booking IDs (up to 50). Caller must re-point or delete the bookings before re-trying. ## Content Source: https://spideriq.ai/docs/api-reference/content ## `GET` /api/v1/content/help **Content Help** AI agent reference: returns all available content types, block types, Liquid filters, tags, theme structure, and data sources. Defaults to YAML (token-efficient). Use ?format=json for programmatic use. No authentication required. **Parameters** - `format` (query, any, optional) — Response format. `json` (default) and `llm` return JSON; `yaml` returns text/yaml; `md` returns text/markdown. Any other value is a 422. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/help' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/help", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/help", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/help", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/content/help/block-fields **Content Block Fields** E.2 (2026-05-22 Claude Code usability F-16) — per-block-type field map. Returns: - All block types when ``block_type`` is unset - The single block-type entry when ``block_type=hero`` (or similar) The shape mirrors the ``block_types`` section of /content/help, including: - ``fields``: canonical fields the default theme's snippet reads - ``_aliases``: agent-natural mistakes → canonical replacement - ``_anti_patterns``: shapes that 422 with a hint - ``_notes``: free-form caveats The source of truth is mirrored in: - app/api/v1/_content_help.py (this endpoint's data) - app/services/page_auditor.py (the audit warning + alias detection) Both must stay in lock-step. Drift surfaces as silent-blank-section failures the auditor catches at read-time — and as wrong-hint advice here. Update both in the same PR. **Parameters** - `block_type` (query, any, optional) — If set, return only the entry for this block_type. Omit to list all. - `format` (query, any, optional) — Response format. `json` (default) and `llm` return JSON; `yaml` returns text/yaml; `md` returns text/markdown. Any other value is a 422. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/help/block-fields' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/help/block-fields", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/help/block-fields", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/help/block-fields", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/content/variables **Content Variables** AI agent merge-tag reference: every flat variable (`{{ firstname }}`, `{{ company_name }}`, `{{ city }}`, etc.) available in dynamic-landing templates, with descriptions, source paths, selection rules, and realistic example values from the Mario's Pizzeria demo fixture. Auto-generated from `apps/liquid-renderer/merge-tags.spec.json` — the exact same JSON the TypeScript renderer imports at build time. Impossible to drift. Defaults to YAML (token-efficient for agents). Use `?format=json` for programmatic consumption. No authentication required. **Parameters** - `format` (query, any, optional) — Response format. `json` (default) and `llm` return JSON; `yaml` returns text/yaml; `md` returns text/markdown. Any other value is a 422. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/variables' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/variables", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/variables", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/variables", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/content/themes **List Themes** List available built-in themes (public, no auth). :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/themes' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/themes", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/themes", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/themes", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response ## `GET` /api/v1/content/themes/{name} **Get Theme Detail** Get a theme's details and all template files (public, no auth). **Parameters** - `name` (path, string, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/themes/{name}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/themes/{name}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/themes/{name}", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/themes/{name}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/content/site-templates **Browse public site templates** List public site templates. No auth — anyone can browse the gallery. Phase D adds 5 universal-axis filters (mood, palette, brand_fit, scene_type, agent_meta.). The /marketplace/site-templates path is an alias for the same endpoint. **Parameters** - `industry` (query, any, optional) - `use_case` (query, any, optional) - `tag` (query, any, optional) — Filter to templates that have this tag - `is_featured` (query, any, optional) - `is_single_page` (query, any, optional) — true = single-page templates (opt-in/thankyou/VSL); false = whole-site templates; omit = both. - `mood` (query, array[string], optional) — Multi-value mood filter (set-overlap). - `palette` (query, array[string], optional) — Multi-value palette filter (set-overlap). - `brand_fit` (query, array[string], optional) — Multi-value industry-fit filter (set-overlap). - `scene_type` (query, any, optional) — Single-value scene/intent filter. - `limit` (query, integer, optional) - `offset` (query, integer, optional) - `format` (query, any, optional) — Response format. `json` (default) and `llm` return JSON; `yaml` returns text/yaml; `md` returns text/markdown. Any other value is a 422. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/site-templates' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/site-templates", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/site-templates", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/site-templates", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `400` — Bad request — malformed parameter - `404` — Resource not found - `422` — Validation failed for one of the query parameters - `429` — Rate limit exceeded - `500` — Internal server error ## `GET` /api/v1/content/site-templates/{slug} **Get a site template by slug** Fetch a single public site template by slug. No auth. 404 includes ``did_you_mean`` close-match suggestions. **Parameters** - `slug` (path, string, required) - `format` (query, any, optional) — Response format. `json` (default) and `llm` return JSON; `yaml` returns text/yaml; `md` returns text/markdown. Any other value is a 422. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/site-templates/{slug}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/site-templates/{slug}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/site-templates/{slug}", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/site-templates/{slug}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `400` — Bad request — malformed parameter - `404` — Resource not found - `422` — Validation failed for one of the query parameters - `429` — Rate limit exceeded - `500` — Internal server error ## `GET` /api/v1/content/marketplace/site-templates **Browse marketplace site templates (alias)** Alias of GET /content/site-templates. Same query surface. **Parameters** - `industry` (query, any, optional) - `use_case` (query, any, optional) - `tag` (query, any, optional) - `is_featured` (query, any, optional) - `is_single_page` (query, any, optional) — true = single-page templates; false = whole-site; omit = both. - `mood` (query, array[string], optional) - `palette` (query, array[string], optional) - `brand_fit` (query, array[string], optional) - `scene_type` (query, any, optional) - `limit` (query, integer, optional) - `offset` (query, integer, optional) - `format` (query, any, optional) — Response format. `json` (default) and `llm` return JSON; `yaml` returns text/yaml; `md` returns text/markdown. Any other value is a 422. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/marketplace/site-templates' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/marketplace/site-templates", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/marketplace/site-templates", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/marketplace/site-templates", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `400` — Bad request — malformed parameter - `404` — Resource not found - `422` — Validation failed for one of the query parameters - `429` — Rate limit exceeded - `500` — Internal server error ## `GET` /api/v1/content/marketplace/category-counts **Discover marketplace categories from live DB** Returns every DISTINCT `marketplace_category` value present in the global published catalog, with row counts. Powers the frontend's auto-discovery layer so a new value added via PATCH is immediately visible in the UI without a registry source change. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/marketplace/category-counts' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/marketplace/category-counts", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/marketplace/category-counts", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/marketplace/category-counts", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response ## `GET` /api/v1/content/marketplace/search **Cross-table marketplace search** UNION search across content_bg_videos + content_components + content_site_templates with universal-axis filters. Public read. Returns a flat list of MarketplaceSearchItem rows projected to a common shape so agents can ``group_by(.asset_type)``. Backed by the GIN bitmap-AND indexes installed in migration 177; passing any of the controlled-vocab axes (mood / brand_fit / scene_type) is recommended to keep latency under 100ms even on the full catalog. **Parameters** - `asset_types` (query, array[string], optional) — Filter to specific asset tables. Allowed values: bg_video, component, site_template. Empty = all 3. - `mood` (query, array[string], optional) — Multi-value mood filter (set-overlap). - `palette` (query, array[string], optional) - `brand_fit` (query, array[string], optional) - `scene_type` (query, any, optional) - `is_featured` (query, any, optional) - `limit` (query, integer, optional) - `format` (query, any, optional) — Response format. `json` (default) and `llm` return JSON; `yaml` returns text/yaml; `md` returns text/markdown. Any other value is a 422. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/marketplace/search' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/marketplace/search", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/marketplace/search", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/marketplace/search", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `400` — Bad request — malformed parameter - `404` — Resource not found - `422` — Validation failed for one of the query parameters - `429` — Rate limit exceeded - `500` — Internal server error ## `GET` /api/v1/content/marketplace/components/{slug} **Get a marketplace component by slug** Fetch a single is_global marketplace component by slug. Public read. 404 includes did_you_mean close-match suggestions. **Parameters** - `slug` (path, string, required) - `format` (query, any, optional) — Response format. `json` (default) and `llm` return JSON; `yaml` returns text/yaml; `md` returns text/markdown. Any other value is a 422. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/marketplace/components/{slug}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/marketplace/components/{slug}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/marketplace/components/{slug}", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/marketplace/components/{slug}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `400` — Bad request — malformed parameter - `404` — Resource not found - `422` — Validation failed for one of the query parameters - `429` — Rate limit exceeded - `500` — Internal server error ## `GET` /api/v1/content/data-sources **List public data sources** Read-side projection of the content_data_sources registry. Used by the page editor's Source Picker (Phase E) and the agent help endpoint. Each row describes a dynamic-block source: posts, authors, categories, tags, idap.countries, idap.cities, idap.streets, idap.businesses, idap.lead. Hierarchical sources expose a parent_id. 🔴 **A Source Picker MUST filter on **`**is_servable**` (ISU-8). Being registered and `is_public` is not the same as being servable: `idap.cities`/`idap.countries`/`idap.streets` are declared here but nothing in the database backs them, and the items door refuses them with 422 `DATA_SOURCE_NOT_SERVABLE`. That flag comes from the SAME predicate the door applies, so the two cannot drift — which is how four global palette components previously shipped with default bindings that could never fetch. **Parameters** - `parent_id` (query, any, optional) — If provided, return only sources whose parent_id matches (hierarchy walk). - `format` (query, any, optional) — Response format. `json` (default) and `llm` return JSON; `yaml` returns text/yaml; `md` returns text/markdown. Any other value is a 422. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/data-sources' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/data-sources", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/data-sources", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/data-sources", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `400` — Bad request — malformed parameter - `404` — Resource not found - `422` — Validation failed for one of the query parameters - `429` — Rate limit exceeded - `500` — Internal server error ## `GET` /api/v1/content/data-sources/{source_id}/aggregate **Aggregate a data source into chart buckets** Marketplace V2 W5.3 — backs the dynamic `chart` block. Returns an array of `[{label, value}, ...]` produced by aggregating the registered data source per the query params. Tenant isolation: client_id resolved from the X-Content-Domain header (same as all other public content endpoints). Defence-in-depth: max_items is capped at 500 here AND in the SQL layer (server-side LIMIT clause). Pydantic enforces the same ceiling on inbound block validation. group_by_field + value_field are validated against the source's declared field types via `data_source_registry`. **Parameters** - `source_id` (path, string, required) - `group_by` (query, string, required) — Source-schema field id to group buckets by. - `agg` (query, string, optional) — Aggregation function. - `value_field` (query, any, optional) — Required when agg in (sum, avg); ignored otherwise. - `max_items` (query, integer, optional) — Max buckets to return. Hard-capped at 500 in SQL too. - `format` (query, any, optional) — Response format. `json` (default) and `llm` return JSON; `yaml` returns text/yaml; `md` returns text/markdown. Any other value is a 422. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/data-sources/{source_id}/aggregate' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/data-sources/{source_id}/aggregate", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/data-sources/{source_id}/aggregate", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/data-sources/{source_id}/aggregate", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `400` — Bad request — malformed parameter - `404` — Resource not found - `422` — Validation failed for one of the query parameters - `429` — Rate limit exceeded - `500` — Internal server error ## `GET` /api/v1/content/data-sources/{source_id}/items **List items from a registered data source** Backs the dynamic `list` / `item` blocks and any `kind='dynamic'` component bound to a collection. Returns an array of records from the registered source, filtered/sorted/paginated per the query params. Tenant isolation: client_id resolved from the X-Content-Domain header (same as all other public content endpoints); published content only. Filters are passed as arbitrary query params matching the source's `schema_json.filters` (e.g. `?tag=news&category=...`). `sort` is a single field with an optional `:asc|:desc` suffix; `limit` (1-500) + `offset` paginate; `fields=slug,title` projects. v1 sources: posts, authors, categories, tags, changelog. `**idap.businesses**`** is servable (ISU-8).** It returns the tenant's own business corpus with every declared field id from the frozen public contract — `review_count`, `phone` and `category` are the caller-facing ids for the `reviews_count`, `phone_e164` and `categories` columns, and sorting uses the resolved column. Its sibling `idap.*` collections are declared but **not servable** (nothing in the database backs them) and answer **422 **`**DATA_SOURCE_NOT_SERVABLE**` rather than an empty list, so 'no rows' and 'no table' stay distinguishable. `idap.lead` is a singleton and still answers 422. Deep paging: `offset` is capped at 10,000 and refuses past it with **422 **`**OFFSET_CAP_EXCEEDED**` — it never silently serves a different page. Send the response's `next_cursor` as `after=` to traverse further at constant cost. **Parameters** - `source_id` (path, string, required) - `limit` (query, integer, optional) — Max records to return. - `offset` (query, integer, optional) — Pagination offset (capped — see `after`). - `after` (query, any, optional) — Keyset cursor from a previous response's `next_cursor`. Pages at constant cost regardless of depth; takes precedence over `offset`. - `sort` (query, any, optional) — Sort field, optional `:asc|:desc` (or `-field`) suffix. - `fields` (query, any, optional) — Comma-separated field ids to include; omit for all fields. - `format` (query, any, optional) — Response format. `json` (default) and `llm` return JSON; `yaml` returns text/yaml; `md` returns text/markdown. Any other value is a 422. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/data-sources/{source_id}/items' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/data-sources/{source_id}/items", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/data-sources/{source_id}/items", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/data-sources/{source_id}/items", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `400` — Bad request — malformed parameter - `404` — Resource not found - `422` — Validation failed for one of the query parameters - `429` — Rate limit exceeded - `500` — Internal server error ## `GET` /api/v1/content/data-sources/{source_id}/items/{record_slug} **Get one published record of a public custom collection** The anonymous per-record detail door for a **public custom collection** (feeds the pretty-URL detail page — `//`). Returns the single record whose `slug` matches `record_slug`, with relationships hydrated one level (targets are public-only). Tenant isolation: client_id resolved from the X-Content-Domain header. Gated on the collection's `is_public` flag AND `status='published'` — a private collection, an unknown collection, and an absent/unpublished record all return **404** (a private collection is indistinguishable from an unknown one — no existence leak). All `idap.*` sources ship in Phase 2 → **501** (the decision-#29 PII structural-absence guarantee holds); built-in sources (posts/authors/…) have their own per-record endpoints and 404 here. **Parameters** - `source_id` (path, string, required) - `record_slug` (path, string, required) - `format` (query, any, optional) — Response format. `json` (default) and `llm` return JSON; `yaml` returns text/yaml; `md` returns text/markdown. Any other value is a 422. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/data-sources/{source_id}/items/{record_slug}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/data-sources/{source_id}/items/{record_slug}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/data-sources/{source_id}/items/{record_slug}", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/data-sources/{source_id}/items/{record_slug}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `400` — Bad request — malformed parameter - `404` — Resource not found - `422` — Validation failed for one of the query parameters - `429` — Rate limit exceeded - `500` — Internal server error ## `GET` /api/v1/content/data-sources/{source_id} **Get one data source by id** Fetch a single content_data_sources row by id. 404 with did_you_mean suggestions when the id doesn't exist. **Parameters** - `source_id` (path, string, required) - `format` (query, any, optional) — Response format. `json` (default) and `llm` return JSON; `yaml` returns text/yaml; `md` returns text/markdown. Any other value is a 422. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/data-sources/{source_id}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/data-sources/{source_id}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/data-sources/{source_id}", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/data-sources/{source_id}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `400` — Bad request — malformed parameter - `404` — Resource not found - `422` — Validation failed for one of the query parameters - `429` — Rate limit exceeded - `500` — Internal server error ## `GET` /api/v1/content/marketplace/components **Browse marketplace components** Browse the SpiderIQ section library. Public read — no auth. Phase D adds 5 universal-axis filters (mood, palette, brand_fit, scene_type, agent_meta.) so agents can narrow by tonal / industry / behavioural axes. The vocabulary for each axis is served by GET /content/help → marketplace.universal_axes. **Parameters** - `category` (query, any, optional) — Filter by marketplace_category (hero, features, pricing, social-proof, content, forms, team, footer, header, cta, faq). - `is_featured` (query, any, optional) — Surface featured-only sections. - `tag` (query, any, optional) — Tag filter (single tag). - `owner_client_id` (query, any, optional) — Filter to components owned by this client_id (UUID or short-id). When set, returns ONLY rows owned by that client (system-namespace components are excluded). Defaults to None = include both system + brand-owned components, current behavior. Used by marketplace authoring brands to audit just-their-own counts (Antigravity Status Report #3, 2026-05-11). - `mood` (query, array[string], optional) — Multi-value mood filter (set-overlap). See /content/help. - `palette` (query, array[string], optional) — Multi-value palette filter (set-overlap). - `brand_fit` (query, array[string], optional) — Multi-value industry-fit filter (set-overlap). - `scene_type` (query, any, optional) — Single-value scene/intent filter. - `limit` (query, integer, optional) - `offset` (query, integer, optional) - `format` (query, any, optional) — Response format. `json` (default) and `llm` return JSON; `yaml` returns text/yaml; `md` returns text/markdown. Any other value is a 422. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/marketplace/components' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/marketplace/components", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/marketplace/components", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/marketplace/components", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `400` — Bad request — malformed parameter - `404` — Resource not found - `422` — Validation failed for one of the query parameters - `429` — Rate limit exceeded - `500` — Internal server error ## `GET` /api/v1/content/marketplace/bg-videos **Browse marketplace background videos** List curated background videos. Public read — no auth. Phase D adds 5 universal-axis filters (mood, palette, brand_fit, scene_type, agent_meta.). Vocabulary served by /content/help. **Parameters** - `category` (query, any, optional) — nature | city | abstract | food | tech | people - `tag` (query, any, optional) - `is_featured` (query, any, optional) - `mood` (query, array[string], optional) — Multi-value mood filter (set-overlap). - `palette` (query, array[string], optional) — Multi-value palette filter (set-overlap). - `brand_fit` (query, array[string], optional) — Multi-value industry-fit filter (set-overlap). - `scene_type` (query, any, optional) — Single-value scene/intent filter. - `limit` (query, integer, optional) - `offset` (query, integer, optional) - `format` (query, any, optional) — Response format. `json` (default) and `llm` return JSON; `yaml` returns text/yaml; `md` returns text/markdown. Any other value is a 422. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/marketplace/bg-videos' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/marketplace/bg-videos", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/marketplace/bg-videos", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/marketplace/bg-videos", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `400` — Bad request — malformed parameter - `404` — Resource not found - `422` — Validation failed for one of the query parameters - `429` — Rate limit exceeded - `500` — Internal server error ## `GET` /api/v1/content/marketplace/bg-videos/{slug} **Get a marketplace background video by slug** Fetch a single bg-video by slug. Public read — no auth. 404 includes ``did_you_mean`` with closest-match slugs so an agent that hallucinates a slug gets a deterministic recovery path. **Parameters** - `slug` (path, string, required) - `format` (query, any, optional) — Response format. `json` (default) and `llm` return JSON; `yaml` returns text/yaml; `md` returns text/markdown. Any other value is a 422. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/marketplace/bg-videos/{slug}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/marketplace/bg-videos/{slug}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/marketplace/bg-videos/{slug}", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/marketplace/bg-videos/{slug}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `400` — Bad request — malformed parameter - `404` — Resource not found - `422` — Validation failed for one of the query parameters - `429` — Rate limit exceeded - `500` — Internal server error ## `GET` /api/v1/content/proof/recent-sales **Recent customer events (PII-filtered)** Returns recent customer events for the toast component `sys-proof-recent-sales-toast`. Tenants publish events as posts in a category (default slug `recent-sales`) with structured `custom_fields = {first_name, city, kind}`. The endpoint NEVER emits last_name, email, phone, post body, title, or excerpt — only first_name + city + kind + occurred_at. Rows where first_name OR city is empty are dropped server-side. Window: events younger than `min_age_minutes` are suppressed; events older than `max_age_hours` are dropped. **Parameters** - `category` (query, string, optional) — Post-category slug to read events from. - `min_age_minutes` (query, integer, optional) — Suppress events younger than this many minutes (default 5). - `max_age_hours` (query, integer, optional) — Drop events older than this many hours (default 72). - `limit` (query, integer, optional) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/proof/recent-sales' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/proof/recent-sales", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/proof/recent-sales", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/proof/recent-sales", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `400` — Bad request — malformed parameter - `404` — Resource not found - `422` — Validation failed for one of the query parameters - `429` — Rate limit exceeded - `500` — Internal server error ## `GET` /api/v1/content/proof/recent-activity **Recent real transactional events (PII-redacted)** Honest Social Proof: returns recent REAL customer events for the social-proof toast / ticker components — union of completed Medusa orders, closed-won CRM deals, and confirmed SpiderBook bookings for the tenant. Redacted SERVER-SIDE to first_name + optional city + an optional product/service label + occurred_at; NEVER emits last name, email, phone, address, or amount. Rows without a first_name are dropped. Empty `events` means the tenant has no qualifying activity — the component MUST hide itself rather than fabricate. `sources` selects which real feeds to union (default: all three). **Parameters** - `sources` (query, string, optional) — Comma-separated subset of orders,deals,bookings. - `min_age_minutes` (query, integer, optional) — Suppress events younger than this many minutes (default 0). - `max_age_hours` (query, integer, optional) — Drop events older than this many hours (default 72). - `limit` (query, integer, optional) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/proof/recent-activity' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/proof/recent-activity", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/proof/recent-activity", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/proof/recent-activity", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `400` — Bad request — malformed parameter - `404` — Resource not found - `422` — Validation failed for one of the query parameters - `429` — Rate limit exceeded - `500` — Internal server error ## `GET` /api/v1/content/proof/bestseller **Real bestselling product (order count)** Honest Social Proof: returns the tenant's REAL top-selling product by completed-order count over the window, for the bestseller-badge component. No PII. `product_title` is null when the tenant has no orders — the component MUST hide itself rather than claim a bestseller. **Parameters** - `max_age_hours` (query, integer, optional) — How far back to count orders (default 30d). :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/proof/bestseller' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/proof/bestseller", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/proof/bestseller", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/proof/bestseller", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `400` — Bad request — malformed parameter - `404` — Resource not found - `422` — Validation failed for one of the query parameters - `429` — Rate limit exceeded - `500` — Internal server error ## `GET` /api/v1/content/visitor-geo **Reflect Cloudflare visitor-geo headers** Returns the visitor's country / city / region / timezone as provided by Cloudflare's edge headers (`CF-IPCountry`, `CF-IPCity`, `CF-Region`, `CF-Timezone`). NO database lookup, NO IP storage — this is a stateless reflection of the edge enrichment plan. Country may be `null` when the request did not transit Cloudflare (local dev) or when CF returned `XX` / `T1` (Tor / unknown). City and region are nullable on free CF zones. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/visitor-geo' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/visitor-geo", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/visitor-geo", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/visitor-geo", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `429` — Rate limit exceeded - `500` — Internal server error ## `GET` /api/v1/content/agents/rotation **Get Agent Rotation** The project's enabled paired agents (the renderer's round-robin rotation set). Public, host-scoped — resolves client + project from X-Content-Domain exactly like the page reads, so the rotation set matches the rendered page's scope. The renderer reads this ONLY when a content row (or the project default) resolves to `round_robin`, then picks one statelessly by hashing the visitor key (no per-request write). Each entry's binding is fetched via /api/v1/booking/{id}/render. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/agents/rotation' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/agents/rotation", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/agents/rotation", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/agents/rotation", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response ## `GET` /api/v1/content/pages **List Pages** List all published marketing pages. Use ?format=json|yaml|md|llm for agent-friendly responses. **Parameters** - `page` (query, integer, optional) - `page_size` (query, integer, optional) - `format` (query, any, optional) — Response format. `json` (default) and `llm` return JSON; `yaml` returns text/yaml; `md` returns text/markdown. Any other value is a 422. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/pages' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/pages", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/pages", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/pages", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/content/pages/{slug} **Get Page** Get a published page by slug. Use ?format=json|yaml|md|llm for agent-friendly responses. **Parameters** - `slug` (path, string, required) - `format` (query, any, optional) — Response format. `json` (default) and `llm` return JSON; `yaml` returns text/yaml; `md` returns text/markdown. Any other value is a 422. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/pages/{slug}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/pages/{slug}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/pages/{slug}", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/pages/{slug}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/content/posts **List Posts** List all published blog posts. Use ?format=json|yaml|md|llm for agent-friendly responses. **Parameters** - `page` (query, integer, optional) - `page_size` (query, integer, optional) - `tag` (query, any, optional) - `category` (query, any, optional) - `format` (query, any, optional) — Response format. `json` (default) and `llm` return JSON; `yaml` returns text/yaml; `md` returns text/markdown. Any other value is a 422. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/posts' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/posts", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/posts", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/posts", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/content/posts/featured **List Featured Posts** List featured published posts. Use ?format=json|yaml|md|llm for agent-friendly responses. **Parameters** - `limit` (query, integer, optional) - `format` (query, any, optional) — Response format. `json` (default) and `llm` return JSON; `yaml` returns text/yaml; `md` returns text/markdown. Any other value is a 422. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/posts/featured' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/posts/featured", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/posts/featured", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/posts/featured", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/content/posts/search **Search Posts** Full-text search across published posts. Use ?format=json|yaml|md|llm for agent-friendly responses. **Parameters** - `q` (query, string, required) - `page` (query, integer, optional) - `page_size` (query, integer, optional) - `format` (query, any, optional) — Response format. `json` (default) and `llm` return JSON; `yaml` returns text/yaml; `md` returns text/markdown. Any other value is a 422. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/posts/search' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/posts/search", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/posts/search", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/posts/search", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/content/posts/{slug} **Get Post** Get a published blog post by slug. Increments view count. Use ?format=json|yaml|md|llm for agent-friendly responses. **Parameters** - `slug` (path, string, required) - `format` (query, any, optional) — Response format. `json` (default) and `llm` return JSON; `yaml` returns text/yaml; `md` returns text/markdown. Any other value is a 422. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/posts/{slug}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/posts/{slug}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/posts/{slug}", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/posts/{slug}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/content/categories **List Categories** List all blog categories. Use ?format=json|yaml|md|llm for agent-friendly responses. **Parameters** - `format` (query, any, optional) — Response format. `json` (default) and `llm` return JSON; `yaml` returns text/yaml; `md` returns text/markdown. Any other value is a 422. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/categories' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/categories", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/categories", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/categories", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/content/tags **List Tags** List all tags with post counts. Use ?format=json|yaml|md|llm for agent-friendly responses. **Parameters** - `format` (query, any, optional) — Response format. `json` (default) and `llm` return JSON; `yaml` returns text/yaml; `md` returns text/markdown. Any other value is a 422. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/tags' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/tags", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/tags", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/tags", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/content/authors **List Authors** List all active authors. Use ?format=json|yaml|md|llm for agent-friendly responses. **Parameters** - `page` (query, integer, optional) - `page_size` (query, integer, optional) - `format` (query, any, optional) — Response format. `json` (default) and `llm` return JSON; `yaml` returns text/yaml; `md` returns text/markdown. Any other value is a 422. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/authors' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/authors", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/authors", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/authors", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/content/authors/{slug} **Get Author** Get an author by slug (public profile). Use ?format=json|yaml|md|llm for agent-friendly responses. **Parameters** - `slug` (path, string, required) - `format` (query, any, optional) — Response format. `json` (default) and `llm` return JSON; `yaml` returns text/yaml; `md` returns text/markdown. Any other value is a 422. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/authors/{slug}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/authors/{slug}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/authors/{slug}", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/authors/{slug}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/content/components **List Components** List published components available for the site. Includes global (system) components and client-specific published components. When `slugs` is provided, returns only the latest published version of each requested slug — ignoring `page`/`page_size` caps since the caller is asking for a known bounded set. **Parameters** - `category` (query, any, optional) — Filter by component category - `slugs` (query, any, optional) — Comma-separated list of component slugs to fetch (batch). Used by the Liquid renderer's per-page prefetch so it can grab all referenced components in one round-trip regardless of pagination / total count. - `page` (query, integer, optional) - `page_size` (query, integer, optional) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/components' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/components", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/components", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/components", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/content/components/{slug} **Get Component** Get a published component by slug. Used by the Liquid renderer Worker at render time to fetch component template + CSS + props schema. **Parameters** - `slug` (path, string, required) - `version` (query, any, optional) — Specific version (default: latest published) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/components/{slug}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/components/{slug}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/components/{slug}", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/components/{slug}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/content/cdn-allowlist **List Cdn Allowlist** List active CDN libraries available for component dependencies. AI agents and the renderer use this to discover available libraries. No authentication required. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/cdn-allowlist' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/cdn-allowlist", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/cdn-allowlist", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/cdn-allowlist", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response ## `GET` /api/v1/content/docs/tree **Get Docs Tree** Get the full documentation tree structure. Client resolved from request domain. Docs Platform v2 · 3.4 — ``?version=`` scopes the tree to one docs version (omit → the tenant's default version). The response carries the tenant's published ``versions`` + the resolved ``current_version`` so the chrome can build the switcher from this single call. **Parameters** - `version` (query, any, optional) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/docs/tree' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/docs/tree", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/docs/tree", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/docs/tree", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/content/docs/versions **Get Doc Versions** List a tenant's published docs versions for the chrome switcher (3.4). Fixed-path route — MUST stay ABOVE /docs/{path:path} (the catch-all would otherwise match path='versions'). Same constraint as /docs/search. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/docs/versions' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/docs/versions", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/docs/versions", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/docs/versions", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response ## `POST` /api/v1/content/docs/event **Capture Doc Event** Docs analytics beacon (3.4) — fire-and-forget, returns 202 immediately. The capture insert runs in a background task so it never blocks the response (the doc.liquid beacon does not await meaningful work). Capture failures are swallowed inside the service. doc.liquid only ever sends ``view``; search / ask are captured server-side in their own handlers. Fixed-path route — MUST stay ABOVE /docs/{path:path}. **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/content/docs/event' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "event_type": "string", "doc_path": "string", "version": "default", "query_text": "string" }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/content/docs/event", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"event_type": "string", "doc_path": "string", "version": "default", "query_text": "string"}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/docs/event", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"event_type": "string", "doc_path": "string", "version": "default", "query_text": "string"}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"event_type": "string", "doc_path": "string", "version": "default", "query_text": "string"}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/content/docs/event", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `202` — Successful Response - `422` — Validation Error ## `GET` /api/v1/content/docs/search **Search Docs** Full-text search across the tenant's published docs. Free (not metered) keyword search over title + body_text, scoped to the resolved tenant and status='published', excluding section rows. Returns ranked hits with ts_headline snippets: {title, full_path, section_title, snippet}. Consumed by the docs chrome search box (task 1.4). **Parameters** - `q` (query, string, required) - `page` (query, integer, optional) - `page_size` (query, integer, optional) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/docs/search' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/docs/search", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/docs/search", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/docs/search", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/content/docs/semantic **Semantic Docs** Semantic (vector) search across the tenant's published docs (3.2). Embeds the query through SpiderGate and returns the top-k most similar chunks with their source doc paths — meaning-based retrieval, the half of ask-the-docs exposed on its own for agents/clients that want raw passages rather than a synthesized answer. Per-IP rate-limited (embeddings are paid). **Parameters** - `q` (query, string, required) - `top_k` (query, integer, optional) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/docs/semantic' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/docs/semantic", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/docs/semantic", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/docs/semantic", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/content/docs/ask **Ask Docs** Ask-the-docs (3.2): a grounded AI answer over the tenant's published docs. Retrieves the top-k relevant passages (semantic search), then asks a SpiderGate completion to answer using ONLY those passages, returning the answer plus the cited source links (``sources[n]`` aligns with the ``[n]`` citations in the answer). METERED — all LLM calls route through SpiderGate (``service_type='docs_ai'``). The Docs-Pro entitlement + ``docs_ai`` quota are wired in task 3.5 via ``check_docs_ai_quota`` (a no-op seam here). Per-IP rate-limited. **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/content/docs/ask' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "query": "string", "top_k": 6 }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/content/docs/ask", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"query": "string", "top_k": 6}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/docs/ask", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"query": "string", "top_k": 6}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"query": "string", "top_k": 6}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/content/docs/ask", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/content/docs/mcp **Docs Mcp** Hosted, per-tenant remote MCP server over this site's published docs (Docs Platform v2 · 3.3 — the headline moat). Streamable-HTTP transport: a single JSON-RPC 2.0 endpoint. Any external AI agent (Claude / ChatGPT / Cursor) points its MCP client at ``https:///api/v1/content/docs/mcp`` and gets four read-only, tenant-scoped, published-only tools: ``search_docs``, ``semantic_search_docs``, ``ask_docs`` (metered via the 3.5-M trusted internal path), ``get_doc``. Tenant resolved from ``X-Content-Domain`` like every other ``/content/docs/*`` route. The server never initiates messages, so each POST gets a plain JSON response (no SSE needed); a request that is all notifications gets ``202 Accepted`` with no body. :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/content/docs/mcp' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/content/docs/mcp", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/docs/mcp", { method: "POST", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/content/docs/mcp", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response ## `GET` /api/v1/content/docs/{path} **Get Doc** Get a documentation page by full path. Path can be nested (e.g., "api/authentication/oauth"). Docs Platform v2 · 3.4 — ``?version=`` selects a docs version (omit → the tenant's default). A page missing in a non-default version falls back to the default version's page rather than 404ing (graceful "only in latest"). **Parameters** - `path` (path, string, required) - `version` (query, any, optional) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/docs/{path}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/docs/{path}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/docs/{path}", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/docs/{path}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/content/navigation/{location} **Get Navigation** Get navigation menu by location. Locations: header, footer, docs_sidebar **Parameters** - `location` (path, string, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/navigation/{location}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/navigation/{location}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/navigation/{location}", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/navigation/{location}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/content/settings **Get Settings** Get site-wide content settings. Includes branding, social links, analytics IDs. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/settings' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/settings", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/settings", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/settings", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response ## `GET` /api/v1/content/members/jwks **Get Members Jwks** Public, read-only proxy for the members sidecar's JWKS (§7.6 bridge). The members sidecar (apps/auth-members) is loopback-bound + firewalled (§15 R1), so the CF edge — which cannot reach 127.0.0.1 — cannot fetch its JWKS directly. The api-gateway CAN reach `auth-members:3002` over the docker network, so it proxies the sidecar's `/api/auth/jwks` here. The renderer fetches THIS and caches it in Worker KV (TTL + `kid`-rotation). No tenant context — the keyset is global to the members system. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/members/jwks' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/members/jwks", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/members/jwks", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/members/jwks", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response ## `GET` /api/v1/content/members/gate-context **Get Members Gate Context** Per-request context the edge gate needs (project id + the two flags). Resolves the request domain → its project's public id (`proj_…`) for the §15 R4 `resolve_project(host) == jwt.project_id` assertion, plus the per-tenant `edge_auth_enabled` flag and the global `edge_auth_global` kill-switch. The renderer fetches this only when a page's `access` is non-public (the public path never calls it → R6 no-op). Fail-open posture: if the domain has no resolvable project the response is inert (project_id=null, edge_auth_enabled=false) and the edge renders the page ungated. This is safe because the renderer must ALREADY have resolved the same domain to fetch the page at all — a gate-context miss therefore only co-occurs with a page-fetch miss (the page wouldn't render either way). The global flag is always returned so an incident flip bypasses everything. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/members/gate-context' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/members/gate-context", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/members/gate-context", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/members/gate-context", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response ## `GET` /api/v1/content/members/data-sources/{source_id}/items **Get Member Data Items** Member-scoped, Data-Restriction-filtered records for a gated page. 401 — no/invalid/expired member JWT. 403 — valid token for a DIFFERENT project (§15 R4). 404 — host not resolvable to a project, or a source with no v1 enforcement adapter. 200 — the member's permitted rows only. **Parameters** - `source_id` (path, string, required) - `page` (query, integer, optional) - `page_size` (query, integer, optional) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/members/data-sources/{source_id}/items' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/members/data-sources/{source_id}/items", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/members/data-sources/{source_id}/items", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/members/data-sources/{source_id}/items", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/content/members/auth/{auth_path} **Proxy Members Auth** Public, allowlisted reverse-proxy for the members sidecar's WRITE auth endpoints (§C2.5 same-host sign-in bridge). Forwards method/body/cookies to `auth-members:3002/api/auth/*` and passes `Set-Cookie` through unchanged. A path not on the public allowlist → 404 (admin/org/token are never exposed). **Parameters** - `auth_path` (path, string, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/members/auth/{auth_path}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/members/auth/{auth_path}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/members/auth/{auth_path}", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/members/auth/{auth_path}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/content/members/auth/{auth_path} **Proxy Members Auth** Public, allowlisted reverse-proxy for the members sidecar's WRITE auth endpoints (§C2.5 same-host sign-in bridge). Forwards method/body/cookies to `auth-members:3002/api/auth/*` and passes `Set-Cookie` through unchanged. A path not on the public allowlist → 404 (admin/org/token are never exposed). **Parameters** - `auth_path` (path, string, required) :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/content/members/auth/{auth_path}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/content/members/auth/{auth_path}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/members/auth/{auth_path}", { method: "POST", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/content/members/auth/{auth_path}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `OPTIONS` /api/v1/content/members/auth/{auth_path} **Proxy Members Auth** Public, allowlisted reverse-proxy for the members sidecar's WRITE auth endpoints (§C2.5 same-host sign-in bridge). Forwards method/body/cookies to `auth-members:3002/api/auth/*` and passes `Set-Cookie` through unchanged. A path not on the public allowlist → 404 (admin/org/token are never exposed). **Parameters** - `auth_path` (path, string, required) :::codegroup ```bash curl -X OPTIONS 'https://spideriq.ai/api/v1/content/members/auth/{auth_path}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.options( "https://spideriq.ai/api/v1/content/members/auth/{auth_path}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/members/auth/{auth_path}", { method: "OPTIONS", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("OPTIONS", "https://spideriq.ai/api/v1/content/members/auth/{auth_path}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/content/redirects/check **Check Redirect** Check if a path has an active redirect. Used by Next.js middleware for redirect handling. **Parameters** - `path` (query, string, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/redirects/check' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/redirects/check", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/redirects/check", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/redirects/check", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/content/playbook **Get Playbook** Look up the canonical tool-sequence for a stated goal (Tier 4.5). Index (no intent) is ~2 KB YAML — safe to call on every session start. Full recipe for one task is typically 500-1500 bytes. **Parameters** - `intent` (query, any, optional) — Exact task key OR natural-language goal - `format` (query, any, optional) — Response format. `yaml` (default) and `md` return text; `json` and `llm` return JSON. Any other value is a 422. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/playbook' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/playbook", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/playbook", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/playbook", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/content/directory/categories **List Directory Categories** List every directory category for this tenant. **Parameters** - `page` (query, integer, optional) - `page_size` (query, integer, optional) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/directory/categories' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/directory/categories", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/directory/categories", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/directory/categories", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/content/directory/listings **List Directory Listings** Every published listing for this tenant, flat and paginated. Only `status='published'` is reachable — the status is not a parameter, so a draft listing is indistinguishable from one that does not exist. `total` is returned on page 1 only. The count is a second full-predicate scan of the table on every call and no index makes it cheap; on later pages the field is `null`, which means "not measured", not "zero". **Parameters** - `category` (query, any, optional) — Narrow to one category slug. - `city` (query, any, optional) — Narrow to one city (exact name). - `page` (query, integer, optional) - `page_size` (query, integer, optional) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/directory/listings' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/directory/listings", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/directory/listings", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/directory/listings", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/content/directory/listings/{listing_slug} **Get Directory Listing Flat** A single published listing by slug alone, with no category in the URL. The unique index is `(category_id, slug)`, so two categories in one workspace can hold the same listing slug. Locked decision (owner, 2026-08-25): resolve by deterministic first match (`ORDER BY category.sort_order, category.slug`) and return `category_slug` in the body so the caller can see which one it got — never a 400. `?category=` narrows. **Parameters** - `listing_slug` (path, string, required) - `category` (query, any, optional) — Optional — narrows to one category when a slug is ambiguous. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/directory/listings/{listing_slug}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/directory/listings/{listing_slug}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/directory/listings/{listing_slug}", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/directory/listings/{listing_slug}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/content/directory/categories/{category_slug} **Get Directory Category** Single category + the list of cities with listings inside it. **Parameters** - `category_slug` (path, string, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/directory/categories/{category_slug}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/directory/categories/{category_slug}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/directory/categories/{category_slug}", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/directory/categories/{category_slug}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/content/directory/categories/{category_slug}/cities/{city_slug} **Get Directory City** All listings in (category, city). 404 if the combo has zero published listings. **Parameters** - `category_slug` (path, string, required) - `city_slug` (path, string, required) - `page` (query, integer, optional) - `page_size` (query, integer, optional) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/directory/categories/{category_slug}/cities/{city_slug}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/directory/categories/{category_slug}/cities/{city_slug}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/directory/categories/{category_slug}/cities/{city_slug}", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/directory/categories/{category_slug}/cities/{city_slug}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/content/directory/categories/{category_slug}/cities/{city_slug}/{listing_slug} **Get Directory Listing** Single listing detail. city_slug is validated against the listing's actual city_slug. **Parameters** - `category_slug` (path, string, required) - `city_slug` (path, string, required) - `listing_slug` (path, string, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/directory/categories/{category_slug}/cities/{city_slug}/{listing_slug}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/directory/categories/{category_slug}/cities/{city_slug}/{listing_slug}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/directory/categories/{category_slug}/cities/{city_slug}/{listing_slug}", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/directory/categories/{category_slug}/cities/{city_slug}/{listing_slug}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/content/sitemap.xml **Get Sitemap** Generate XML sitemap for all published content. Includes pages, blog posts, and documentation. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/sitemap.xml' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/sitemap.xml", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/sitemap.xml", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/sitemap.xml", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response ## `GET` /api/v1/content/sitemap **Get Sitemap Json** Get sitemap data as JSON (for programmatic access). :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/sitemap' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/sitemap", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/sitemap", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/sitemap", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response ## `GET` /api/v1/content/feed.xml **Get Rss Feed** RSS 2.0 feed of published posts (sys-rss-feed extension). Empty post list serves a valid empty channel — anti-hallucination per catalog/LEARNINGS.md: never return 500 from a content extension surface, even when the tenant has no published posts. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/feed.xml' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/feed.xml", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/feed.xml", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/feed.xml", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response ## `GET` /api/v1/content/atom.xml **Get Atom Feed** Atom 1.0 feed of published posts (sys-atom-feed extension). :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/atom.xml' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/atom.xml", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/atom.xml", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/atom.xml", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response ## `GET` /api/v1/content/feed.json **Get Json Feed** JSON Feed 1.1 of published posts (sys-feed-json extension). :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/feed.json' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/feed.json", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/feed.json", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/feed.json", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response ## `GET` /api/v1/content/changelog **List Public Changelog** Published changelog entries, newest first (drafts never exposed). Paginated: `page`/`page_size` (the house pattern, echoed on the response) or the legacy `limit`/`offset`. `total` is the count of ALL matching entries, so a caller can page to the end. **Parameters** - `page` (query, any, optional) — 1-based page number (house pattern; wins over limit/offset). - `page_size` (query, any, optional) — Entries per page (1-200, default 50). - `limit` (query, any, optional) — Legacy alias for page_size (audit §9 pagination). - `offset` (query, any, optional) — Legacy alias — raw row offset. - `sort` (query, string, optional) — Ordering, newest first. `published_at` (default) = release date. `version` = semver compared NUMERICALLY per component, so v2.10.0 sorts above v2.9.0 (a TEXT sort puts it between v2.1.0 and v2.2.0). :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/changelog' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/changelog", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/changelog", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/changelog", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/content/changelog/feed.xml **Get Changelog Rss** RSS 2.0 feed of published changelog entries (per-IP rate-limited). :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/changelog/feed.xml' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/changelog/feed.xml", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/changelog/feed.xml", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/changelog/feed.xml", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response ## `GET` /api/v1/content/changelog/atom.xml **Get Changelog Atom** Atom 1.0 feed of published changelog entries (per-IP rate-limited). :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/changelog/atom.xml' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/changelog/atom.xml", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/changelog/atom.xml", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/changelog/atom.xml", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response ## `GET` /api/v1/content/changelog/{ref} **Get Public Changelog Entry** One published changelog entry, by version string or id. The detail door behind the renderer's `/changelog/{version}` route. Drafts are never resolvable — an unpublished entry 404s exactly like a missing one, so an unreleased version can't be read by guessing its URL. **Parameters** - `ref` (path, string, required) — Version string (`v2.9.0` — the human URL) or the entry's UUID. Version is matched first. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/changelog/{ref}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/changelog/{ref}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/changelog/{ref}", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/changelog/{ref}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/content/press **List published press releases** Public newsroom index — published releases only, newest first. KEYSET paginated: pass the returned `next_cursor` back as `cursor`. An empty newsroom returns `items: []` with HTTP 200, never a 404. **Parameters** - `release_type` (query, any, optional) — press_release | statement | media_alert | newsbyte - `year` (query, any, optional) — Publication year, e.g. 2026. - `cursor` (query, any, optional) — Opaque keyset cursor — NOT a page number. - `limit` (query, integer, optional) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/press' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/press", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/press", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/press", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/content/press/feed.xml **Get Press Rss Feed** RSS 2.0 feed of published press releases (press-feed extension). Empty newsroom → a valid empty channel, HTTP 200 (never a 500) — same anti-hallucination contract as the posts feed. **Parameters** - `release_type` (query, any, optional) — Optional filter: press_release | statement | media_alert | newsbyte. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/press/feed.xml' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/press/feed.xml", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/press/feed.xml", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/press/feed.xml", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/content/press/atom.xml **Get Press Atom Feed** Atom 1.0 feed of published press releases. **Parameters** - `release_type` (query, any, optional) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/press/atom.xml' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/press/atom.xml", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/press/atom.xml", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/press/atom.xml", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/content/press/feed.json **Get Press Json Feed** JSON Feed 1.1 of published press releases. **Parameters** - `release_type` (query, any, optional) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/press/feed.json' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/press/feed.json", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/press/feed.json", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/press/feed.json", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/content/press/contacts **List public press contacts** The newsroom contact block, in display order. Unpaginated by design — a newsroom has tens of contacts, not thousands. Empty → `items: []`, 200. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/press/contacts' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/press/contacts", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/press/contacts", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/press/contacts", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response ## `GET` /api/v1/content/press/kits/{slug} **Get a public media kit** A story-scoped media kit with its downloadable assets. Unknown slug → 404. **Parameters** - `slug` (path, string, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/press/kits/{slug}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/press/kits/{slug}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/press/kits/{slug}", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/press/kits/{slug}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/content/press/kits/{slug}/download **Download a media kit's full bundle** 302 to the kit's pre-built ZIP and counts the download. No auth — press assets are deliberately ungated. 404 when the kit is unknown on this site or its bundle has not been built yet. **Parameters** - `slug` (path, string, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/press/kits/{slug}/download' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/press/kits/{slug}/download", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/press/kits/{slug}/download", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/press/kits/{slug}/download", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `302` — Successful Response - `422` — Validation Error ## `GET` /api/v1/content/press/kits/{slug}/assets/{media_id}/download **Download one media-kit asset** 302 to a single asset inside a kit and counts the download. No auth. 404 when the kit or the asset is unknown on this site. **Parameters** - `slug` (path, string, required) - `media_id` (path, string, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/press/kits/{slug}/assets/{media_id}/download' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/press/kits/{slug}/assets/{media_id}/download", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/press/kits/{slug}/assets/{media_id}/download", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/press/kits/{slug}/assets/{media_id}/download", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `302` — Successful Response - `422` — Validation Error ## `POST` /api/v1/content/press/subscribe **Subscribe to a newsroom's press releases** Public, unauthenticated. Starts a DOUBLE OPT-IN: the address is stored as `pending` and a confirmation link is emailed. Nothing is sent to the address until that link is clicked. The response is identical for a new address, a repeat signup and an address already on the list — it never reveals list membership. 202 Accepted, because the confirmation mail is dispatched off the request path. **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/content/press/subscribe' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "email": "string", "topics": [ "string" ], "source": "string" }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/content/press/subscribe", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"email": "string", "topics": ["string"], "source": "string"}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/press/subscribe", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"email": "string", "topics": ["string"], "source": "string"}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"email": "string", "topics": ["string"], "source": "string"}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/content/press/subscribe", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `202` — Signup accepted; a confirmation email may have been sent. - `422` — The address is not a valid email address. - `429` — Too many signups from this IP. ## `GET` /api/v1/content/press/subscribe/confirm **Confirmation landing page for a newsroom subscription** Renders the confirm page. READ-ONLY — clicking through does not subscribe anyone; the POST to the same path does. Invalid, expired and already-used tokens all render one generic page. **Parameters** - `token` (query, string, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/press/subscribe/confirm' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/press/subscribe/confirm", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/press/subscribe/confirm", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/press/subscribe/confirm", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/content/press/subscribe/confirm **Confirm a newsroom subscription** Consumes the confirm token — SINGLE USE. Unknown, expired and already-spent tokens all return `state: invalid` with HTTP 200, so the endpoint cannot be used to probe tokens. **Parameters** - `token` (query, string, required) :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/content/press/subscribe/confirm' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/content/press/subscribe/confirm", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/press/subscribe/confirm", { method: "POST", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/content/press/subscribe/confirm", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/content/press/unsubscribe **Unsubscribe landing page** Renders the unsubscribe page. READ-ONLY — the POST to the same URL is what unsubscribes, and it is also the RFC 8058 one-click target. **Parameters** - `token` (query, string, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/press/unsubscribe' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/press/unsubscribe", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/press/unsubscribe", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/press/unsubscribe", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/content/press/unsubscribe **Unsubscribe from a newsroom (RFC 8058 one-click target)** Applies the unsubscribe. Idempotent, never expires, and the token is deliberately NOT consumed — an unsubscribe link in an old inbox must keep working. This is the URL advertised in the `List-Unsubscribe` header of every release we send. **Parameters** - `token` (query, string, required) :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/content/press/unsubscribe' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/content/press/unsubscribe", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/press/unsubscribe", { method: "POST", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/content/press/unsubscribe", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/content/press/{slug}/embargo **Preview an embargoed press release with a journalist token** Renders an EMBARGOED release before its lift for a journalist holding a valid per-recipient token. Always `robots: noindex,nofollow` — a preview URL must never be indexed. The response NEVER carries `embargo_token`. An unknown/expired/cross-tenant token, or a token whose release does not match `{slug}`, returns 404 — identical to any missing release. **Parameters** - `slug` (path, string, required) - `token` (query, string, required) — Per-journalist embargo token :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/press/{slug}/embargo' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/press/{slug}/embargo", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/press/{slug}/embargo", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/press/{slug}/embargo", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/content/press/{slug} **Get a published press release** One published release by slug. Draft, scheduled and EMBARGOED releases 404 here by design — the embargo token door is slice 3.2. The response NEVER carries `embargo_token`. **Parameters** - `slug` (path, string, required) - `format` (query, any, optional) — Response format. `json` (default) and `llm` return JSON; `md` returns text/markdown. This route has no YAML renderer, so `yaml` is a 422. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/press/{slug}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/press/{slug}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/press/{slug}", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/press/{slug}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/content/robots.txt **Get Robots Txt** Generate /robots.txt for the requesting tenant. Reads ``content_settings.extensions.robots``: - ``enabled=false`` → falls back to legacy minimal output. - ``rules=[]`` → emits one ``User-agent: * Allow: /`` group. - ``auto_sitemap_link=true`` → appends ``Sitemap: /sitemap.xml``. - ``extra_lines=[…]`` → emitted verbatim before the sitemap line. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/robots.txt' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/robots.txt", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/robots.txt", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/robots.txt", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response ## `GET` /api/v1/content/opensearch.xml **Get Opensearch Xml** OpenSearch 1.1 description document (sys-opensearch-xml extension). Reads ``content_settings.extensions.opensearch`` for short_name / description / search URL template / image overrides. Falls back to ``site_name`` / ``site_tagline`` / ``favicon_url`` when fields are unset so a fresh tenant still gets a browser-registerable description. ``enabled=false`` in config → 410 Gone (browsers stop offering the search-engine registration without retrying). :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/opensearch.xml' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/opensearch.xml", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/opensearch.xml", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/opensearch.xml", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response ## `GET` /api/v1/content/llms.txt **Get Llms Txt** llms.txt for LLM crawlers (sys-llms-txt extension). Structure preset selected via ``content_settings.extensions.llms_txt.structure``: one of ``minimal`` | ``blog-only`` | ``fastapi-style`` (default) | ``mintlify``. ``include_authors`` appends post authors to bullets. ``max_items_per_section`` is clamped to [1, 500]. Each preset only fetches the data it needs: - minimal → docs tree only - blog-only → posts only - fastapi-style → pages + posts + docs - mintlify → docs + posts (sectionable docs preferred) ``enabled=false`` in config → 410 Gone. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/llms.txt' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/llms.txt", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/llms.txt", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/llms.txt", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response ## `GET` /api/v1/content/llms-full.txt **Get Llms Full Txt** llms-full.txt — the full Markdown body of a tenant's content (SEO 2.3). Where ``/llms.txt`` is an INDEX (titles + links), this is the CONTENT: every published, indexable page/post/doc serialized to Markdown and concatenated in sitemap order (pages → posts → docs), each as ``## {title}\nSource: {url}\n\n{markdown}``. OPT-IN per tenant via ``content_settings.extensions.llms_txt.full_enabled`` (default FALSE). When the parent ``llms_txt`` surface is disabled, or ``full_enabled`` is off, this returns **404** — the full corpus is an explicit choice, not an always-on surface. Size governance (NO silent truncation): the body is capped by ``full_max_items`` and ``full_max_bytes`` (both clamped server-side); when content is dropped, a trailing ``> (truncated: N of M pages …)`` marker is appended. Hard-cached (crawler-facing, low-traffic). :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/llms-full.txt' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/llms-full.txt", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/llms-full.txt", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/llms-full.txt", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response ## `GET` /api/v1/content/leads/resolve **Resolve Lead** Resolve a lead/business by external identifier for dynamic landing pages. Used by the Liquid renderer to fetch lead data at render time. Resolves the client from X-Content-Domain header (same as all content endpoints). Returns the full business record with optional includes. **Parameters** - `place_id` (query, any, optional) — Google Place ID (e.g. 0x47e66fdad6f1cc73:0x341211b3fccd79e1) - `domain` (query, any, optional) — Domain name - `email` (query, any, optional) — Email address - `pin_name` (query, any, optional) — VayaPin pin name — resolves the business linked to this pin - `pin_data_set_id` (query, any, optional) — VayaPin pin data-set ID — resolves the linked business - `account_id` (query, any, optional) — VayaPin account ID — resolves the linked business - `pin_subscription_id` (query, any, optional) — VayaPin pin subscription ID — resolves the linked business - `vat` (query, any, optional) — VAT number — resolves the business linked to this registry record - `lei` (query, any, optional) — Legal Entity Identifier (LEI) — resolves the linked business - `tax_id` (query, any, optional) — Tax identification number — resolves the linked business - `registration_number` (query, any, optional) — Company registration number — resolves the linked business - `include` (query, any, optional) — Comma-separated related types (emails,phones,domains,contacts) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/leads/resolve' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/leads/resolve", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/leads/resolve", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/leads/resolve", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/content/forms/{form_id}/submit **Submit a dynamic form-block fill** Public form submission endpoint for the Marketplace V2 dynamic `form` block. `form_id` is composed as `:` — the service resolves the owning tenant from the page row (never from the URL or body), so the same form_id from a different tenant cannot be replayed against this site. The request body is validated against the resolved block's `props.fields` list; unknown fields are rejected. When `props.submit_url` is set the submission is also POSTed there as JSON; when `props.fallback_idap_lead` is true (default), the submission is persisted to `content_form_submissions`. **Parameters** - `form_id` (path, string, required) **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/content/forms/{form_id}/submit' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "submission": {}, "request_id": "string" }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/content/forms/{form_id}/submit", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"submission": {}, "request_id": "string"}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/forms/{form_id}/submit", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"submission": {}, "request_id": "string"}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"submission": {}, "request_id": "string"}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/content/forms/{form_id}/submit", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `400` — Bad request — malformed parameter - `404` — Resource not found - `422` — Validation failed for one of the query parameters - `429` — Rate limit exceeded - `500` — Internal server error ## `GET` /api/v1/content/vayapin/cards **Get Vayapin Cards** Resolve VayaPin "cards" for rendering on pages / blog posts. Global directory — NOT tenant-scoped, so no domain resolution. The Liquid renderer (and agents) call this to bake card data into a page at build time. Only public + listed pins are returned. - **Pinned mode** (`?pins=BB:TAPAS,BB:CHAMPERS`): exact card per named pin, in the order given, with any unresolved names reported back. - **Query mode** (`?q=`/`?country=`/`?city=`/`?category=`): a list of cards. **Parameters** - `pins` (query, any, optional) — Comma-separated pin ids (e.g. 'BB:TAPAS,BB:CHAMPERS'). Pinned mode — resolves each named pin to its exact card. - `q` (query, any, optional) — Full-text query (query mode) - `country` (query, any, optional) — 2-letter pin namespace, e.g. 'bb' (query mode) - `city` (query, any, optional) — City / settlement filter (query mode) - `category` (query, any, optional) — Category slug, e.g. 'restaurant' (query mode) - `limit` (query, integer, optional) — Max cards in query mode :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/vayapin/cards' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/vayapin/cards", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/vayapin/cards", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/vayapin/cards", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `POST` /api/v1/content/docs/feedback **Submit Doc Feedback** Record a 'was this helpful?' vote for the resolved tenant's doc. Returns 201 with a tiny ack envelope. 429 if the per-IP/tenant window is exceeded; 400/422 on bad input (handled by FastAPI/Pydantic); generic 500 on an internal error (never leaks the cause). **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/content/docs/feedback' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "doc_path": "string", "helpful": true, "comment": "string" }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/content/docs/feedback", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"doc_path": "string", "helpful": true, "comment": "string"}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/docs/feedback", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"doc_path": "string", "helpful": true, "comment": "string"}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"doc_path": "string", "helpful": true, "comment": "string"}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/content/docs/feedback", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `201` — Successful Response - `422` — Validation Error ## `POST` /api/v1/content/docs/playground/request **Playground Request** Relay one user-composed request to the imported API and return its response. The target host is fixed server-side to the imported spec's declared base URL (persisted on the doc), never the client's choice. Returns ``{status, headers, body, elapsed_ms}``. 400 if the doc has no playground base URL or the relay is refused (SSRF / method / size / timeout — all with a safe, instructive message); 429 if rate-limited; generic 500 on an internal error. **Request body** :::codegroup ```bash curl -X POST 'https://spideriq.ai/api/v1/content/docs/playground/request' \ -H 'Authorization: Bearer ' \ -H 'Content-Type: application/json' \ -d '{ "doc_path": "string", "method": "string", "path": "string", "query": {}, "headers": {}, "body": "string" }' ``` ```python import httpx resp = httpx.post( "https://spideriq.ai/api/v1/content/docs/playground/request", headers={"Authorization": "Bearer ", "Content-Type": "application/json"}, json={"doc_path": "string", "method": "string", "path": "string", "query": {}, "headers": {}, "body": "string"}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/docs/playground/request", { method: "POST", headers: { "Authorization": "Bearer ", "Content-Type": "application/json" }, body: JSON.stringify({"doc_path": "string", "method": "string", "path": "string", "query": {}, "headers": {}, "body": "string"}) }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" "strings" ) func main() { body := strings.NewReader(`{"doc_path": "string", "method": "string", "path": "string", "query": {}, "headers": {}, "body": "string"}`) req, _ := http.NewRequest("POST", "https://spideriq.ai/api/v1/content/docs/playground/request", body) req.Header.Set("Authorization", "Bearer ") req.Header.Set("Content-Type", "application/json") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## Content — Marketplace Source: https://spideriq.ai/docs/api-reference/content-marketplace ## `GET` /api/v1/content/marketplace/categories **List the marketplace category vocabulary** Returns every category in the vocabulary table. Public read so MCP tools, CLI, and AI agents can discover valid `marketplace_category` values without dashboard access. Pass `include_counts=true` to join live row counts from content_components (matches the existing /marketplace/category-counts endpoint shape). **Parameters** - `asset_type` (query, any, optional) — Filter to a specific asset class: site-template, bg-video, or component. - `include_counts` (query, boolean, optional) — When true, populates row_count with live counts from content_components. :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/marketplace/categories' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/marketplace/categories", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/marketplace/categories", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/marketplace/categories", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error ## `GET` /api/v1/content/marketplace/categories/{slug} **Get a single marketplace category by slug** **Parameters** - `slug` (path, string, required) :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/marketplace/categories/{slug}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/marketplace/categories/{slug}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/marketplace/categories/{slug}", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/marketplace/categories/{slug}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `404` — Slug not in the vocabulary. - `422` — Validation Error ## Content Presence Source: https://spideriq.ai/docs/api-reference/content-presence ## `GET` /api/v1/content/presence/{path} **Live-viewer count SSE stream for the current page** Server-Sent Events stream emitting the current viewer count for `{path}` on the resolving tenant. Used by the marketplace component `sys-proof-live-viewers-pulse`. Same-origin EventSource expected (tenant resolves via the Host / X-Content-Domain header). **Query params:** - `sid` (required): opaque session id (UUID v4 from sessionStorage). 8–64 chars, `[A-Za-z0-9_-]`. - `i` (optional): polling interval in seconds. Server-clamped to `[15, 120]` (default 30). **Events:** - `ready` — emitted once on connect with the resolved path + interval. - `presence` — emitted every `i` seconds with `{count, path, interval_s}`. **Parameters** - `path` (path, string, required) - `sid` (query, string, required) — Opaque per-tab session id from sessionStorage. - `i` (query, integer, optional) — Polling interval (seconds). :::codegroup ```bash curl -X GET 'https://spideriq.ai/api/v1/content/presence/{path}' \ -H 'Authorization: Bearer ' ``` ```python import httpx resp = httpx.get( "https://spideriq.ai/api/v1/content/presence/{path}", headers={"Authorization": "Bearer "}, ) resp.raise_for_status() print(resp.json()) ``` ```javascript const resp = await fetch("https://spideriq.ai/api/v1/content/presence/{path}", { method: "GET", headers: { "Authorization": "Bearer " } }); const data = await resp.json(); console.log(data); ``` ```go package main import ( "net/http" ) func main() { req, _ := http.NewRequest("GET", "https://spideriq.ai/api/v1/content/presence/{path}", nil) req.Header.Set("Authorization", "Bearer ") resp, _ := http.DefaultClient.Do(req) defer resp.Body.Close() } ``` ::: **Responses** - `200` — Successful Response - `422` — Validation Error > (truncated: 103 of 112 pages — hit byte budget. Raise extensions.llms_txt.full_max_bytes / full_max_items, or fetch individual pages as .md)