Skip to content
Signal
Dashboard
Get started

First campaign, run and monitor

Create a campaign, research one account, run a paused monitor and clean up.

This tutorial creates a campaign, starts discovery, reads the results and runs one monitor. Both scripts delete the campaign when they finish.

Paid research

Discovery and the monitor run use credits. The organization needs a positive credit balance to start. Cost depends on the research. Discovery charges one credit for each account and each person created. Research or monitor work can add charges. Read Credits.

Before you start

  • Complete the HTTP quickstart. Set LINKT_API_URL and LINKT_API_KEY.
  • Use a staging key with the effective permissions for campaign creation and deletion, discovery, monitoring, schedules and runs. Full product access covers these requirements when both the key and its owner have it. Read Permissions. Staging key settings issue staging keys.
  • Set LINKT_API_URL to https://api.sandbox.signal.linkt.ai to follow the observed example below.
  • Install curl, jq and uuidgen for Bash, or Node.js 20 or later for JavaScript.
  • Read Authentication and Retry safely.

Research target and evidence

The sample targets B2B technology, SaaS and professional services companies in North America or EMEA. Account criteria require an active website and evidence of a sales function. Person criteria target a Director, VP or executive who works on sales or growth. The script sends separate account and person criteria.

A completed staging discovery run from September 24, 2026 requested one account. The run returned one account and one person. Its recorded research duration was 618.84 seconds, about 10 minutes 19 seconds. Its run summary recorded about 10 minutes 18 seconds from start to finish. The run used two credits to create the records.

These values describe one observed run. They are not a timing range or a cost guarantee. The criteria in the sample come from that campaign's stored description and criteria, read on September 29. The historical run has no immutable criteria snapshot. The stored criteria therefore do not prove which text the agents read at run start.

Candidates that fail the target criteria can be discarded. The discarded count does not identify each rejected candidate. Successful discovery can return fewer accounts than requested.

Run the complete script

Choose Bash or Node.js. For Node.js, save the script as first-campaign.mjs and run node first-campaign.mjs. The script captures each ID from the response. It sends each paid start once with a fresh idempotency key. The monitor stays paused and runs once on request.

#!/usr/bin/env bash
set -euo pipefail
: "${LINKT_API_URL:?Set the API origin}"
: "${LINKT_API_KEY:?Set an API key}"
# Requires curl, jq and uuidgen. Each paid start runs once.
CAMPAIGN_ID=""
RUN_ID=""
cleanup() {
  if [ -n "$RUN_ID" ]; then
    curl --silent --show-error --fail-with-body -X POST "$LINKT_API_URL/v2/runs/$RUN_ID/cancel" -H "x-api-key: $LINKT_API_KEY" || true
  fi
  if [ -n "$CAMPAIGN_ID" ]; then
    curl --silent --show-error --fail-with-body --request DELETE "$LINKT_API_URL/v2/campaigns/$CAMPAIGN_ID" \
  --header "x-api-key: $LINKT_API_KEY"
  fi
}
trap cleanup EXIT
poll() {
  for ((attempt=1; attempt<=240; attempt++)); do
    RUN=$(curl --silent --show-error --fail-with-body --request GET "$LINKT_API_URL/v2/runs/$RUN_ID" \
  --header "x-api-key: $LINKT_API_KEY")
    echo "$RUN" | jq '{status, progress, error_code}'
    case $(echo "$RUN" | jq -r .status) in
      succeeded) RUN_ID=""; return 0 ;;
      failed|canceled|skipped) RUN_ID=""; return 1 ;;
    esac
    sleep 15
  done
  echo "Run timed out after 60 minutes. Cleanup cancels it." >&2
  return 1
}
CAMPAIGN_ID=$(curl --silent --show-error --fail-with-body --request POST "$LINKT_API_URL/v2/campaigns" \
  --header "x-api-key: $LINKT_API_KEY" \
  --header "Content-Type: application/json" \
  --data '{"criteria":{"account":"### Criteria\n- Must be a B2B organization within the technology, SaaS, or professional services sectors.\n- Demonstrable digital presence including a functional company website and active professional network profiles.\n- Operational evidence of a sales or go-to-market function (e.g., job postings for sales, marketing, or business development).\n- Headquartered in North America or EMEA to align with standard outbound testing parameters.\n\n### Pain Points\n- **Inefficient Prospecting**: Spending excessive manual hours researching companies to find relevant triggers, leading to low volume.\n- **Missed Timing**: Inability to track real-time events like funding, leadership changes, or product launches, resulting in missed opportunities.\n- **Stale Data**: Reliance on static databases that do not reflect current company momentum or strategic shifts.\n\n### Disqualifiers\n- B2C-only companies (e.g., local retail, consumer goods) as the value proposition is focused on B2B sales cycles.\n- Companies with no active web presence or news updates within the last 12 months.\n- Non-profit organizations or government agencies.\n\n### Ideal Intelligence\n- **recent_company_announcements**: Capture the most recent major news, press releases, or official updates found on the company website or news sections of professional platforms. This intelligence is used to identify current momentum and business focus.","person":"### Criteria\n- Find at least 1 key decision-maker per company.\n- Seniority level: Director, VP, or C-Suite (e.g., Head of Sales, VP of Marketing, Chief Revenue Officer).\n- Roles focused on Go-To-Market (GTM) strategy, sales operations, or business growth.\n- Must have a verifiable professional profile with recent activity or updated employment history.\n\n### Pain Points\n- **Cold Outreach Friction**: High bounce rates and low response rates due to lack of personalization in initial outreach.\n- **Information Overload**: Difficulty filtering relevant market signals from noise to direct their team'"'"'s efforts.\n- **Quota Pressure**: Urgent need to identify high-intent accounts to meet revenue targets.\n\n### Disqualifiers\n- Junior-level employees or individual contributors without decision-making authority.\n- Contacts who have been in their current role for less than 30 days (unless the hire itself is the signal).\n- Human Resources or Administrative roles not involved in GTM strategy.\n\n### Ideal Intelligence\n- **recent_public_activity**: Identify key themes, topics, or shared content from the individual'"'"'s recent posts and articles on professional networks. This serves as a personalization hook for sales outreach."},"description":"An ICP targeting B2B companies and decision-makers to test sales intelligence capabilities, focusing on identifying momentum through recent announcements and public activity.","entity_types":["company","person"],"name":"First campaign"}' \
  --header "Idempotency-Key: $(uuidgen)" | jq -er .id)
export CAMPAIGN_ID
RUN_ID=$(curl --silent --show-error --fail-with-body --request POST "$LINKT_API_URL/v2/runs/discovery" \
  --header "x-api-key: $LINKT_API_KEY" \
  --header "Content-Type: application/json" \
  --data "$(jq -cn --arg CAMPAIGN_ID "$CAMPAIGN_ID" '{"campaign_id":"0190a4e2-0000-7000-8000-000000000001","contacts_per_account":1,"count":1} | walk(if . == "0190a4e2-0000-7000-8000-000000000001" then $CAMPAIGN_ID else . end)')" \
  --header "Idempotency-Key: $(uuidgen)" | jq -er .id)
export RUN_ID
poll
curl --silent --show-error --fail-with-body --request GET "$LINKT_API_URL/v2/campaigns/$CAMPAIGN_ID" \
  --header "x-api-key: $LINKT_API_KEY"
curl --silent --show-error --fail-with-body --request GET "$LINKT_API_URL/v2/accounts?campaign_id=$CAMPAIGN_ID&limit=5" \
  --header "x-api-key: $LINKT_API_KEY"
curl --silent --show-error --fail-with-body --request GET "$LINKT_API_URL/v2/people?campaign_id=$CAMPAIGN_ID&limit=5" \
  --header "x-api-key: $LINKT_API_KEY"
MONITOR_ID=$(curl --silent --show-error --fail-with-body --request POST "$LINKT_API_URL/v2/campaigns/$CAMPAIGN_ID/monitors" \
  --header "x-api-key: $LINKT_API_KEY" \
  --header "Content-Type: application/json" \
  --data "$(jq -cn --arg CAMPAIGN_ID "$CAMPAIGN_ID" '{"config":{"signal_types":[{"description":"A new funding round or a new investment.","display":"Funding","type":"funding"},{"description":"A new executive in data, IT, or engineering.","display":"Leadership change","type":"leadership_change"}],"source_campaign_id":"0190a4e2-0000-7000-8000-000000000001","type":"signal-sheet"},"frequency":"weekly","name":"Funding and leadership changes","status":"paused"} | walk(if . == "0190a4e2-0000-7000-8000-000000000001" then $CAMPAIGN_ID else . end)')" \
  --header "Idempotency-Key: $(uuidgen)" | jq -er .id)
export MONITOR_ID
RUN_ID=$(curl --silent --show-error --fail-with-body --request POST "$LINKT_API_URL/v2/monitors/$MONITOR_ID/runs" \
  --header "x-api-key: $LINKT_API_KEY" \
  --header "Idempotency-Key: $(uuidgen)" | jq -er .id)
export RUN_ID
poll
curl --silent --show-error --fail-with-body --request GET "$LINKT_API_URL/v2/runs?campaign_id=$CAMPAIGN_ID&kind=monitor&limit=5" \
  --header "x-api-key: $LINKT_API_KEY"
curl --silent --show-error --fail-with-body --request GET "$LINKT_API_URL/v2/signals?limit=5&monitor_id=$MONITOR_ID" \
  --header "x-api-key: $LINKT_API_KEY"
# The EXIT trap deletes the campaign and its paused monitor.
 

The poll loop prints the status, progress and error code every 15 seconds. It stops after 240 attempts, about 60 minutes. On timeout, cleanup cancels the current run. Contact support with the run ID and request ID if research remains stuck.

Illustrative poll output:

{"status":"queued","progress":{"target":1,"queued":0,"completed":0,"discarded":0},"error_code":null}
{"status":"running","progress":{"target":1,"queued":1,"completed":0,"discarded":0},"error_code":null}
{"status":"succeeded","progress":{"target":1,"queued":0,"completed":1,"discarded":0},"error_code":null}

Read the results

The script prints campaign counts and campaign-scoped accounts and people before it starts the monitor. Each scoped record includes membership. This trimmed response is illustrative:

{
  "data": [{
    "id": "0190a4e2-0000-7000-8000-000000000002",
    "name": "Bluefin Analytics",
    "membership": {
      "campaign_id": "0190a4e2-0000-7000-8000-000000000001",
      "review_state": "new",
      "fit": {},
      "fields": {}
    }
  }],
  "next_cursor": null
}

The campaign's counts.by_entity_type and counts.by_review_state count membership copies. Discovery result.completed counts completed account targets. A discovered person adds another membership copy. See Counts and Duplicates.

The monitor reads accounts from this campaign and looks for the events in its signal config. A successful monitor run can return an empty signal list.

Clean up

The script cancels its current run on failure or timeout, then deletes the campaign. Campaign deletion cancels campaign runs and removes the owned monitor, its schedule and its signals. Deletion cannot be undone. See Update and delete.

If you stop the script before cleanup, use list_runs for the campaign and cancel active runs. Then delete the campaign:

curl --fail-with-body --request DELETE "$LINKT_API_URL/v2/campaigns/$CAMPAIGN_ID" \
  --header "x-api-key: $LINKT_API_KEY"

API reference: delete_campaign

Next steps

On this page