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.
| Operation | What it does | Class | Price |
|---|---|---|---|
| lists.sales_nav | A list of people from a Sales Navigator URL. | spend | 12 credits per call + 3 credits per record (quantity from input.maxResults) |
| lists.linkedin_search | A list of people from LinkedIn search criteria. | spend | 12 credits per call + 3 credits per record (quantity from input.maxResults) |
| lists.icp | Scrape a saved audience (ICP) into its contacts. | spend | 12 credits per call + 3 credits per record (quantity from input.maxPages) |
| lists.companies | A list of companies from a LinkedIn company search. | spend | 12 credits per call + 3 credits per record (quantity from input.maxResults) |
| lists.company_employees | A list of one company's employees. | spend | 12 credits per call + 3 credits per record (quantity from input.limit) |
| lists.decision_makers | A list of one company's decision makers. | spend | 12 credits per call + 3 credits per record (quantity from input.limit) |
| lists.post_engagers | A list of the people who engaged with a LinkedIn post. | spend | 12 credits per call + 3 credits per record (quantity from input.maxResults) |
| lists.website_visitors | A list of the companies that visited the website. | read | Free |
| lists.create | Build a list from any list source. | spend | 12 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).
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
}'{
"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.
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.tableis{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_REQUIREDotherwise). - Refreshing (scheduled) lists are not available yet:
scheduleanswers422 SCHEDULE_UNAVAILABLE. - Follow progress without polling on the event feed or webhooks.