Programmable

Docs

API

Everything the app does, from an HTTP request: for Zapier, Make, Activepieces, n8n or your own code. One endpoint, one API key. Rows are built when you ask and never stored.

The short version
URL
https://programmable.info/api/sheet
Method
POST
Header
Authorization: Bearer YOUR_API_KEY
Body (JSON)
{"domains": ["stripe.com"], "format": "json"}

Everything is on the API page in your dashboard: make an API key there, and its request builder writes the full request for whatever you pick.

1. Get an API key

There are two kinds of keys. Only API keys work for HTTP requests.

KindMade inWorks for
API keyDashboard → APIAny request: this API, live URLs, scripts. Full access to your account, so keep it in your tool's secret settings.
Link keyDashboard → LinksOnly the link it was made for (a Google Sheets formula, a shared link). It can be one-time, capped or expiring. It won't work in an HTTP step.

API keys are listed on the API page and link keys on the Links page. Generated keys start with dsk_ and are 36 characters long; a key you choose yourself on the API page (see Live URLs below) works the same way.

2. Send the request

curl -X POST https://programmable.info/api/sheet \
    -H "Authorization: Bearer YOUR_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"domains": ["stripe.com", "notion.so"], "columns": "company, tech stack", "format": "json"}'

In an automation tool, the same thing goes in its HTTP step:

ActivepiecesHTTP → Send HTTP request. Authentication: Bearer Token. Body Type: JSON.
ZapierWebhooks by Zapier → Custom Request (or POST). Add the header Authorization: Bearer … ; Data: the JSON body.
MakeHTTP → Make a request. Header Authorization: Bearer … ; Body type: Raw, JSON.
n8nHTTP Request node. Authentication: Generic → Bearer Auth (or a header). Send Body: JSON.

Everywhere: method POST, URL https://programmable.info/api/sheet, your API key as the Bearer token, and a JSON body. To use data from an earlier step, put its field inside the body, e.g. {"domains": ["{{website}}"]}.

3. Read the rows

With "format": "json" you get one document back once every row is done:

{
    "rows": [
      { "domain": "stripe.com", "company_name": "Stripe", "industry": "Financial services", "cms": "Custom", ... },
      { "domain": "notion.so", "company_name": "Notion", ... }
    ],
    "summary": { "rows": 2, "ok": 2, "failed": 0, "skipped": 0, "credits_charged": 2, "seconds": 3.1, "ai_cost_usd": null },
    "errors": []
  }

Without format the response is a CSV that streams row by row (good for files and spreadsheets). Each row keeps your input columns, then the enrichment, and a row that couldn't be enriched says why in enrich_error.

The request waits for every row. Most tools allow a few minutes; for long lists, send "webhook": "on" and let your CRM webhook receive rows in batches as they finish.

Body fields

Send one of domains, people, linkedin or products. Everything else is optional.

FieldExampleWhat it does
domains["stripe.com", "notion.so"]Companies, by website. A list, or one string separated by commas or new lines.
people["Jane Doe, stripe.com"]People as "Name, company domain". Makes a people sheet (work email and more).
linkedin["linkedin.com/in/jane-doe"]LinkedIn profiles. A domain after each helps; with Apollo, Hunter, Findymail or PDL connected the profile alone is enough.
products["https://www.allbirds.com/products/mens-tree-runners"]Product page links: one row per product with price, stock, SKU/GTIN, variants and ratings, read from the store's own product data.
list"products"With domains: list each store's products (Shopify and WooCommerce catalogs, or product sitemaps), one row per product.
per_store100With list=products: most products per store (up to 2,000).
discover"headless CMS vendors"Find companies matching a description (your Apollo or PDL key if connected, otherwise the web), then enrich them. Instead of domains.
like"stripe.com,paddle.com"Find companies like these. Works alone or with discover.
location / employees / tech"Berlin" / "11-50" / "Shopify"Discovery filters, checked against what we find on each company; misses are skipped and not charged.
revenue / founded / funding / jobs"10M-100M" / "2015+" / "5M+" / "yes"More discovery filters: annual revenue (USD), founding year, total funding raised, and companies that are hiring (or a minimum number of open jobs). Your provider applies what it can (Apollo: revenue, funding, jobs; People Data Labs: founded); the rest is checked on each company.
accounts / who / columns"target-accounts" / "buyers" / "call-prep"Presets by handle (Dashboard → Presets): a saved company list in place of domains (a list can be a saved search, resolved each run), a saved people filter (titles, seniority, departments; implies search=people), and a saved column set with its AI questions. Explicit parameters win over a preset's.
exclude / to / profile"customers" / "hubspot-flow" / "cheap"More presets by handle: an exclusion list (websites never returned, from Find, discovery or an upload; a default list applies everywhere), a destination (webhook, CRM, format) and a spending profile (providers, AI provider and model, caps, reveal). Explicit parameters win.
find"sam"Run a Find source for the companies instead of discover=: find=sam, formd, sec, contracts, web or any source on the Find tab, with that source's own parameters alongside (e.g. find=sam&naics=5415&registeredFrom=2026-07-01). The Find tab shows the exact link for any search.
describe"payment processors"With find=: plain words for what you want. Your AI screens the source's results and keeps the ones that fit.
sort"largest"Which companies to pick when there are more than the limit: largest, smallest, newest, oldest, most_funded or richest (by revenue). Needs a company-search provider; web search can't rank.
limit50With discover or like: how many companies to find (up to 500).
columns"company, tech stack, email setup"Which column groups to return, in plain words or exact keys. Leave out for the defaults.
ask["Do they offer a free plan?"]Your own questions, answered per row with your AI key. Each becomes a column.
ai_research1Your questions also search the web (news, directories, docs) and cite the page each answer came from. Per question in JSON: {"prompt": …, "research": true}.
ai_max_usd2Most this request may spend on AI. null = no cap. Leave out for your account default.
search"people"Find people at the companies in domains. Use with titles, seniority and per_company.
titles"sales, revenue"Search: job titles to look for.
seniority"vp,director"Search: seniority levels.
per_company10Search: most people per company.
reveal"email,mobile"What your data providers may unlock (and spend credits on): email, mobile, personal_email, or none.
providers"apollo,findymail"Which of your connected data providers to use. Leave out for all; none for the included sources only.
apify"none"Skip the Apify actors saved on Integrations for this sheet (they run on every company otherwise, on your Apify token).
send_to"hubspot,salesforce"Also send finished rows to destinations you've connected on Integrations: hubspot, salesforce, shopify (people → customers, product sheets → draft products). google_sheets (a new spreadsheet in your Drive) is coming soon, pending Google's verification; until then use IMPORTDATA or the add-on below.
webhook"off"on sends rows to your CRM webhook as they finish (the default), off doesn't, or a one-off https:// URL.
format"json"json returns one JSON document. Leave out for CSV.
name"Q3 prospects"A name for the run (used for the file name and receipt).

Column groups: email (email finder), company (company), leadership (leadership), site (website), tech (tech stack), email_auth (email setup), dns (dns), security (security), socials (social profiles), contacts (contacts), domain_age (domain age), providers (your data providers), product (product), product_price (price & stock), product_ids (product ids), product_variants (variants), product_market (marketplaces), product_reviews (ratings), performance (performance & third parties), hiring (hiring), gtm (go-to-market), sitemap (site map), news (news & buzz), footprint (digital footprint), public_footprint (public footprint), trust (trust & compliance), filings (filings & public record), govcon (government contracting), commerce (store signals), google (google data), changes (changes over time).

Send a CSV file

Post the file as multipart form data. Columns are recognized from their contents, whatever they're called; correct a guess with map, and say has_header=no when the first row is data.

curl -X POST https://programmable.info/api/sheet \
    -H "Authorization: Bearer YOUR_API_KEY" \
    -F file=@contacts.csv \
    -F "map=email:Contact Info|company:Employer" \
    -F format=json

Live URLs (GET)

For spreadsheets and browsers, the same options work as a URL: https://programmable.info/x/live?key=YOUR_KEY&domains=stripe.com,notion.so. In Google Sheets: =IMPORTDATA("https://programmable.info/x/live?key=…&domains=stripe.com"). A key in a URL can be seen by anyone who sees the URL, so make these on the Links page: their keys can be limited and locked to the link.

No key needed in a browser: open https://programmable.info/x/live?domains=stripe.com,notion.so&columns=company,tech and, if you're not signed in, you're asked to sign in (or create an account) and brought straight back, with the cost shown before it runs. Signed in, links you type or bookmark run at once; a link someone else sends you shows the cost and a Run button first, so nobody can spend your credits with a link. Tools like Google Sheets and Zapier have no browser session, so they still need key=. That key can be one we generate (dsk_…) or one you choose on the API page, like key=velvet-kayak-mango-orbit, so you can type links from memory; chosen keys must be hard to guess, and wrong keys are rate-limited.

Instead of naming every parameter, say what you want: https://programmable.info/x/live?request=find the CTOs at fintech startups in New York, with deep research on the AI tools they use (or the shorter q=). It's translated into the named parameters above on your own AI key, so you need Anthropic, OpenAI, Gemini or OpenRouter connected.

  • Confirmed once. The first time, you see what the link will do (what it finds, the questions it asks your AI, anything it had to guess and the exact link with named parameters) and press Run. After that the translation is remembered for your account, so the same link runs the same way every time, also from Google Sheets or Zapier. Rewording it translates and asks again.
  • Named parameters win. Mix both, e.g. request=deep research on their AI tools&domains=stripe.com,notion.so: anything the link spells out is used as is, and the request only fills in the rest.
  • From automations an unconfirmed request returns 428 with a confirm link to open once in a browser. In Settings you can choose to confirm only requests that spend your money; those (AI questions, paid provider data, sending rows to another tool) are always confirmed once.
  • Only the translation is kept, never the rows.

Google Sheets add-on

Formulas and a sidebar inside your own sheet. In Google Sheets open Extensions → Apps Script, then paste in Code.js (as Code.gs) and add an HTML file named Sidebar with Sidebar.html. Save, reload the sheet, and a Programmable menu appears. Open the sidebar and paste an API key.

  • =PROGRAMMABLE(A2, "employees") one value about a company
  • =PROGRAMMABLE_ROW(A2, "company, tech stack") several columns across the row, with =PROGRAMMABLE_HEADERS("company, tech stack") for the header
  • =PROGRAMMABLE_EMAIL("Jane Doe", "acme.com") a work email
  • Programmable → Enrich selection fills a whole column of websites at once; Refresh skips the cache.

Results are cached for 6 hours, so Sheets recalculating doesn't spend credits again. The key stays in your Google account.

Webhooks

Set a CRM webhook on the Integrations page and rows are POSTed to it as JSON batches while a sheet runs. Each delivery carries an X-Programmable-Signature header you can check with your signing secret; checking it is optional.

What a row costs

One credit a row, plus one for each of these column groups when a sheet asks for them: email (the email finder, on people sheets), leadership, performance and product_market. Find people adds one more per person found. AI columns and your own provider keys add nothing. Rows that can't be enriched are refunded at the same rate. JSON responses report credits_charged in the summary; with Accept: application/x-ndjson the response streams events, and the first (start) carries creditsPerRow.

Errors

Errors come back as {"error": "…"} with a plain-language message.

400Something in the request is wrong; the message says what.
401No key, or the key isn't recognized (not copied in full, or revoked).
402Not enough credits for the rows.
403The key's limits refuse this request, or it's a link key used outside its link.
413Too many rows for one request: send parts.
428A plain-English request (request=) that hasn't been confirmed yet: open the confirm link once in a browser.
429Too many requests this hour: wait and retry.

Rows are charged as they're built (see What a row costs); rows that fail are refunded.