---
title: "Signals API quickstart"
canonical_url: https://zentrik.ai/docs/developers/signals-api-quickstart
markdown_url: https://zentrik.ai/docs/developers/signals-api-quickstart.md
category: "Developer docs"
learning_track: null
last_reviewed: 2026-07-20
---

# Signals API quickstart

Send one transcript, ticket, review, or report to Zentrik, wait for processing, and confirm the resulting Signal in Discovery.

## Overview

If you are deciding between manual import, a connector, and the API, start with [Import evidence into Discovery](https://zentrik.ai/docs/product/evidence-to-insights). If you already know you want the API, this page shows the smallest working path:

- create one signal
- let Zentrik queue processing automatically
- confirm the signal appears in Discovery
- move to the production template when you are ready for batching or pagination

## Before you start

You need three things:

- a workspace API key from **Settings → API Keys**
- your workspace base URL including **/api**
- one real record to send, usually as a transcript, support ticket, review, or discovery report

For a product-level first test, use [manual import in Discovery](https://zentrik.ai/docs/product/evidence-to-insights#first-run) before automating anything.

## Hello World

Start with one transcript. This example creates a signal and waits for processing to finish.

~~~python
#!/usr/bin/env python3
import json
import os
import time
import urllib.request

payload = {
    "name": "Advisor onboarding call",
    "signalType": "transcript",
    "text": "Customer interview transcript goes here.",
    "additionalContext": "Imported from an onboarding call.",
    "sourceLinks": [
        {
            "name": "Open source call",
            "url": "https://meetings.example.com/calls/advisor-onboarding-call",
            "type": "transcript",
            "primary": True,
        }
    ],
}

def request_json(method, path, body=None):
    request = urllib.request.Request(
        f"{os.environ['ZENTRIK_API_BASE'].rstrip('/')}{path}",
        data=json.dumps(body).encode("utf-8") if body is not None else None,
        method=method,
        headers={
            "Authorization": f"Bearer {os.environ['ZENTRIK_API_KEY']}",
            "Content-Type": "application/json",
            "Accept": "application/json",
        },
    )
    with urllib.request.urlopen(request, timeout=60) as response:
        raw = response.read().decode("utf-8")
        return response.status, json.loads(raw) if raw else None

code, created = request_json("POST", "/external/v1/signals", payload)
public_id = created["publicId"]
print(f"Created {public_id} jobId={created['jobId']}")

for _ in range(36):
    code, signal = request_json("GET", f"/external/v1/signals/{public_id}")
    print(f"{public_id} status={signal.get('status')} insights={len(signal.get('insightIds') or [])}")
    if signal.get("status") in {"processed", "failed"}:
        print(json.dumps(signal, indent=2))
        break
    time.sleep(5)
~~~

Expected result:

- the create response returns a **publicId** and **jobId**
- the signal moves from **processing** to **processed**
- the signal appears in Discovery with linked insights when processing succeeds
- the source link appears on Signal and insight evidence surfaces so reviewers can open the original record

## Choose a signal type

Use the signal type that matches the source material:

- **transcript** for interviews, onboarding calls, sales calls, and meeting notes with direct user evidence
- **support_ticket** for support cases, ticket exports, or issue threads
- **review** for public reviews, app-store reviews, or shorter rating-backed comments
- **feedback_record** for survey rows, NPS verbatims, CSV feedback, or form responses
- **discovery_report** for synthesized research notes or analyst writeups

If you are unsure, start with the source that is closest to the original customer wording instead of a summarized derivative.

Whenever the source has a stable final URL, include **sourceLinks**. Use the primary link for the place a reviewer should land first: the exact review, ticket, Reddit comment, report, meeting, source row, or source-system record. Omit **sourceLinks** only when the provider has no durable user-facing URL.

For recurring imports, also send a stable **externalId**, source-date **occurredAt**, and canonical source fields (**providerType**, **evidenceKind**, **analysisProfile**) so filtering, source labels, and processing remain predictable as volume grows.

## After you create a signal

The create endpoint queues processing automatically. You do not need to call a second endpoint for the first run.

The main fields to watch are:

- **status**
- **insightIds**
- **processing.lastJobId**
- **processing.processedAt**
- **processing.lastError**

Use the explicit process endpoint only when you want to rerun an existing signal.

## Next steps

When the Hello World flow works, move to one of these:

- [Signals import template](https://zentrik.ai/docs/developers/signals-import-template) for batching, retries, polling, and richer payload mapping
- [Signals API reference](https://zentrik.ai/docs/api#signals) for the full endpoint contract
- [Import evidence into Discovery](https://zentrik.ai/docs/product/evidence-to-insights) if you need the product-level decision guide

## Troubleshooting

### The create call fails immediately

Check the API key, the base URL, and whether the base URL includes **/api**. A valid create response returns both a signal identifier and a queued job id.

### The signal stays in processing

Check the signal record for **processing.lastError**. Correct the reported problem before retrying, and contact support if the signal remains in processing without an error.

## Related guides

- [Signals import template](https://zentrik.ai/docs/developers/signals-import-template)
- [Set up Ideas Portal JWT SSO](https://zentrik.ai/docs/developers/ideas-portal-jwt-sso)
