---
title: "Create a signal"
method: POST
path: "/v1/signals"
tags: ["Signals"]
---

# Create a signal

`POST /v1/signals`

## Request

- Base URL: `https://api.trayo.ai`
- URL: `https://api.trayo.ai/v1/signals`
- Auth: HTTP bearer

**Required permissions:** `settings:write`.

Create a signal that describes what you want to detect. Send the returned key to `POST /v1/discoveries` immediately. After the run settles, read its `error` and `blockedSignals` before reporting results.

## Request body

- CreateSignalDto: A new signal is usable immediately: send its key to `POST /v1/discoveries` as soon as this call answers. No further call is needed before starting discovery.
  - `signalKey` string, required: Stable public identifier, lowercase alphanumeric with underscores or hyphens (`champion_move`, `funding-round`), 3-60 characters, starting and ending with a letter or digit. This is the signal's identity — it appears in the path of every other signal route and cannot be renamed. A key that is already taken is a 409 (`signal_key_conflict`); a reserved product-managed key is a 400.
  - `type` 'news' | 'jobs' | 'job_change', required: What this signal searches. `news` searches the open web. `jobs` finds hiring activity in job postings from about the last 60 days only: a longer lookback does not reach older postings, so report a jobs signal's period as about 60 days. `job_change` finds people who started a new role. While the run is active, keep polling it. After it settles, read `blockedSignals` and `error` before reporting an empty result. `technology` is system-managed and not authorable. `reddit` is not authorable here either: those signals require a competitor list and this route has no field for one.
  - `detects` string, required: Describe what counts as a match in plain language.
  - `salesIntent` string: Why a match matters commercially.
  - `includes` string[]: Examples of what counts. For `job_change`, list every title and scope you would accept: a move matches a role the definition describes, or the company-wide chief who directly owns it (a Chief Accounting Officer for Corporate Controller), never another neighbouring title. A company-wide title such as CFO does not match a regional or divisional one (CFO, Latin America) unless you list that scope. Naming senior titles too (Chief Supply Chain Officer beside VP Logistics) makes the search look for them.
  - `excludes` string[]: Cases that never count: each one vetoes a match, except one that names another signal ("this belongs to X"), which sends the match to that signal instead.
  - `weight` integer: Relative importance, 1-3. Default 1.

## Response `201`

Signal created, and usable immediately: name its key in `POST /v1/discoveries` as soon as this answers.

- SignalDto
  - `signalKey` string, required: The stable public identifier. There is no separate row id.
  - `type` string, nullable, required
  - `entityType` string, required
  - `detects` string, required
  - `salesIntent` string, nullable, required
  - `includes` string[], required
  - `excludes` string[], required
  - `weight` number, required
  - `createdAt` string, date-time, required
  - `updatedAt` string, date-time, required

## Other responses

- `400`: Malformed body; `signalKey` does not match the required pattern; `signalKey` is one of the reserved product-managed keys (the message names it); or `type` is not one of the four authorable values. `validation_failed` (`retry: fix_input`) — The request did not pass input validation: a missing or malformed body field, a bad `limit`, a path parameter that is not a UUID, a `signalKey` that already exists or is reserved. `message` lists every failed constraint; `param` names the first offending field when one can be identified.
- `401`: Missing, malformed, or incorrect bearer token. Every way authentication can fail returns the identical body, so do not try to infer which one it was, and do not repeat the call with the same key. `unauthorized` (`retry: do_not_retry`) — Missing, malformed, or incorrect bearer token. Identical body for every cause.
- `403`: The key grant and its owner's current workspace role must both include every scope required by the operation. `insufficient_scope` (`retry: do_not_retry`) — Scope denial details distinguish missing_key_scope (use a key granting every missing scope), owner_permission_limit (the owner needs the missing workspace permissions), and invalid_key_scope (replace the key with supported scopes). Membership and undeclared-route denials remain generic.
- `409`: `signal_key_conflict` (`retry: fix_input`) — A signal with this `signalKey` already exists with a different definition. Choose another key, `PATCH` the existing one, or delete it first. Repeating a create with the same definition is not a conflict: it returns the saved signal.
- `429`: Every route is rate limited per workspace across REST, MCP, all keys and OAuth users, `GET /v1/whoami` included, so a balance poll competes with the work it is watching. For `rate_limited`, wait `Retry-After` seconds (also `details.retryAfterSeconds`), then send the same request once; do not retry sooner. `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` describe the workspace's bucket, and they come back on successful responses too: read them and slow down BEFORE you are refused. A burst from one network can also be refused before the key is checked; that 429 carries `Retry-After` and no `RateLimit-*` headers. `rate_limited` (`retry: retry_later`) — The workspace or source network exhausted its request budget for one bucket. `details.retryAfterSeconds` (and `Retry-After` on REST) is the wait: wait that long, then retry the same request once. Do not poll or retry sooner; an early retry is refused again. If you cannot wait, stop and tell the user when to retry; do not fill the wait with other calls. `details.limit` and `details.bucket` name the exhausted budget. When present, RateLimit-* headers describe the active budget and reset.
- `500`: Something failed on our side. Nothing about the request needs changing. `internal_error` (`retry: retry_later`) — Something failed on our side. `message` never describes it — quote `requestId` when reporting. Retry with exponential backoff and GIVE UP after a few attempts: `retry_later` here does not mean the failure is known to be transient, only that nothing about your request needs changing. A request that fails this way repeatedly will not start working.

---

[API](https://skmtc.dev/trayo/apis/trayo-api.md) · [All operations](https://skmtc.dev/trayo/apis/trayo-api/llms.txt) · [OpenAPI document](https://skmtc.dev/trayo/apis/trayo-api/revisions/0af452dce6de?raw)
