Browse the docs

Guide 01

Build a list into a table

Turn a Sales Navigator search, a LinkedIn search or a saved audience into a table with one call.

Last updated from API version 2026-10-06 at (live spec, refreshed hourly). openapi.json

Every list source is an action under actions.lists.*. It finds people (or companies) and lands them in a table of your workspace, one row per person, ready to enrich and push. Lists that read LinkedIn run on a sending identity's own account.

OperationWhat it doesClassPrice
lists.sales_navA list of people from a Sales Navigator URL.spend12 credits per call + 3 credits per record (quantity from input.maxResults)
lists.linkedin_searchA list of people from LinkedIn search criteria.spend12 credits per call + 3 credits per record (quantity from input.maxResults)
lists.icpScrape a saved audience (ICP) into its contacts.spend12 credits per call + 3 credits per record (quantity from input.maxPages)
lists.companiesA list of companies from a LinkedIn company search.spend12 credits per call + 3 credits per record (quantity from input.maxResults)
lists.company_employeesA list of one company's employees.spend12 credits per call + 3 credits per record (quantity from input.limit)
lists.decision_makersA list of one company's decision makers.spend12 credits per call + 3 credits per record (quantity from input.limit)
lists.post_engagersA list of the people who engaged with a LinkedIn post.spend12 credits per call + 3 credits per record (quantity from input.maxResults)
lists.website_visitorsA list of the companies that visited the website.readFree
lists.createBuild a list from any list source.spend12 credits per call + 3 credits per record (quantity from input.source.maxResults)

actions.lists.create is the umbrella: the same request with input.source.kind set to one of the sources above. The key needs enrich (lists spend credits) and write (the table).

1. Dry run: count and cost

Send the request with dryRun: true. Nothing is scraped or charged; the answer is a plan with the number of people, the credit range and a planId valid 15 minutes. A Sales Navigator list costs 12 credits per call + 3 credits per record (quantity from input.maxResults).

bash
curl -X POST "https://app.owlbound.ai/api/v1/actions/lists/sales_nav" \
  -H "Authorization: Bearer $OWLBOUND_API_KEY" \
  -H "Owlbound-Version: 2026-10-06" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
  "input": {
    "url": "https://www.linkedin.com/sales/search/people?query=...",
    "maxResults": 200
  },
  "output": {
    "table": {
      "create": {
        "name": "FR SaaS heads of sales"
      }
    }
  },
  "dryRun": true
}'
200 · plan
{
  "data": {
    "plan": {
      "operationId": "actions.lists.sales_nav",
      "class": "spend",
      "targets": { "total": 1, "wouldRun": 1, "wouldSkip": [] },
      "credits": { "estimatedMin": 12, "estimatedMax": 612, "available": 5400, "capRemaining": 2000 },
      "approval": { "required": false, "reason": null },
      "writes": { "table": { "tableId": null, "wouldCreateRows": 200 } },
      "planId": "plan_...",
      "expiresAt": 1761300000000
    }
  },
  "meta": { "requestId": "<requestId>", "dryRun": true, "credits": { "used": 0 } }
}

2. Apply exactly that plan

Send the same body without dryRun and with the planId (or the Owlbound-Plan header). If anything bound changed since the dry run, nothing runs and you get 409 PLAN_STALE with the new plan. Add maxCreditsto cap the spend below the plan's maximum.

bash
curl -X POST "https://app.owlbound.ai/api/v1/actions/lists/sales_nav" \
  -H "Authorization: Bearer $OWLBOUND_API_KEY" \
  -H "Owlbound-Version: 2026-10-06" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
  "input": {
    "url": "https://www.linkedin.com/sales/search/people?query=...",
    "maxResults": 200
  },
  "output": {
    "table": {
      "create": {
        "name": "FR SaaS heads of sales"
      }
    }
  },
  "planId": "plan_..."
}'

Lists always run async: the answer is 202 with the action and a Location: /api/v1/actions/{id} header.

3. Wait for it, then read the table

Poll GET /actions/{id} with ?waitSeconds=30: the request returns as soon as the action reaches a terminal status, or after 30 seconds. The action's output names the table; read its rows with GET /tables/{id}/rows.

const headers = { Authorization: `Bearer ${process.env.OWLBOUND_API_KEY}` };
let action;
do {
  const res = await fetch(`https://app.owlbound.ai/api/v1/actions/${actionId}?waitSeconds=30`, { headers });
  ({ data: action } = await res.json());
} while (["pending_approval", "queued", "running"].includes(action.status));

const tableId = action.output?.tableId;
const rows = await fetch(`https://app.owlbound.ai/api/v1/tables/${tableId}/rows?limit=50`, { headers }).then((r) => r.json());

Good to know

  • output.table is {create: {name}} for a new table with every list column, or {tableId}to fill an existing table's matching columns. Rows are matched by person: a re-run fills empty cells and never duplicates.
  • output.contacts: truealso pushes the rows to contacts when the list finishes (needs a table, and an owner, admin or leader as the key's creator).
  • LinkedIn sources run as an identity: a bound key uses its own, an unbound key passes identityId (422 IDENTITY_REQUIRED otherwise).
  • Refreshing (scheduled) lists are not available yet: schedule answers 422 SCHEDULE_UNAVAILABLE.
  • Follow progress without polling on the event feed or webhooks.