---
title: "Signals API reference"
canonical_url: https://zentrik.ai/docs/api/signals
markdown_url: https://zentrik.ai/docs/api/signals.md
last_reviewed: 2026-10-05
---

# Signals API reference

Signals are the imported transcripts, tickets, reviews, and reports that feed Discovery. Use these endpoints to bring in sources and add reviewed Evidence claims when processing misses an important point.

- Human reference: https://zentrik.ai/docs/api/signals
- Complete Markdown index: https://zentrik.ai/docs/api/index.md
- Base URL: `https://zentrik.ai/api`
- Authentication: `Authorization: Bearer YOUR_API_KEY`
- Shared pagination and rate limits: https://zentrik.ai/docs/api/index.md#shared-conventions

## GET /external/v1/signals — List signals

Operation ID: `list-signals`

List a bounded page of signals in your workspace. Signals are imported pieces of customer or market evidence such as transcripts, support tickets, reviews, and discovery reports.

Required API key scopes: `signals:read`

### Parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `limit` | number | No | Maximum number of items to return (default: 20, maximum: 100) |
| `offset` | number | No | Number of items to skip (default: 0) |
| `externalId` | string | No | Exact identifier of the record in its source system, such as a Fireflies transcript ID. |
| `integrationId` | uuid | No | Only signals imported through this workspace integration. |
| `sourceType` | string | No | Comma-separated source filters. Each value matches a provider type such as fireflies, a source facet such as fireflies:transcript, or a source type. |
| `initiativeId` | string | No | Only signals with an accepted evidence link to this Initiative. Accepts the Initiative UUID or public ID, such as INITIATIVE-7. |
| `studyId` | string | No | Only signals attached to this Study as interviews. Accepts the Study UUID or public ID, such as STUDY-172. |
| `status` | string | No | Comma-separated processing statuses: pending, processing, processed, or failed. |
| `accountId` | uuid | No | Signals linked to this account directly, or through an insight linked to it. Only direct links appear in accountIds. Comma-separate several IDs to match any of them. |
| `updatedSince` | iso-date | No | Only signals whose own fields changed at or after this ISO-8601 timestamp, oldest change first. Pass the latest updatedAt you have received to sync incrementally, and dedupe by id. Links added later to insights, Initiatives, or Studies do not count as changes. |
| `include` | string | No | Comma-separated extras. 'raw' adds rawData: the whole stored data column, including every attributed transcript turn and the provider's import response. Opt-in because it can exceed several hundred KB per signal. |

### Example request

```curl
curl -X GET https://zentrik.ai/api/external/v1/signals?limit=10 \
  -H 'Authorization: Bearer YOUR_API_KEY'
```

### Responses

| Status | Meaning | Description | Schema |
| --- | --- | --- | --- |
| 200 | Success | Signals were successfully retrieved. Pagination metadata is returned in X-Total-Count, X-Limit, X-Offset, and X-Has-More headers. | Array<Signal> |

### Example response

```json
[
  {
    "id": "uuid",
    "name": "Support Ticket - 3/20/24, 10:00 AM",
    "publicId": "SIGNAL-42",
    "workspaceId": "workspace-uuid",
    "status": "processed",
    "context": "support_request",
    "signalType": null,
    "processorType": "zendesk",
    "providerType": "zendesk",
    "evidenceKind": "support_ticket",
    "analysisProfile": "support_ticket",
    "provenance": {
      "providerType": "zendesk",
      "evidenceKind": "support_ticket",
      "analysisProfile": "support_ticket"
    },
    "sourceFacet": "zendesk:support_ticket",
    "sourceLabel": "Zendesk support ticket",
    "sourceLinks": [
      {
        "name": "Open Zendesk ticket",
        "url": "https://example.zendesk.com/agent/tickets/12345",
        "type": "support_ticket",
        "primary": true
      }
    ],
    "source": {
      "type": "zendesk",
      "external_data": {
        "ticketId": "12345"
      }
    },
    "data": {
      "subject": "Users cannot reset password",
      "severity": "high"
    },
    "transcript": null,
    "import": null,
    "insightIds": [],
    "accountIds": [],
    "initiativeIds": [],
    "studyIds": [],
    "accountId": null,
    "accountExternalId": null,
    "externalId": "external-123",
    "createdAt": "2024-03-20T10:00:00Z",
    "updatedAt": "2024-03-20T10:00:00Z"
  }
]
```

---

## GET /external/v1/signals/:publicId — Get one signal

Operation ID: `get-signal`

Retrieve a single signal by its public ID.

Required API key scopes: `signals:read`

### Parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `publicId` | string | Yes | Workspace-scoped public ID, such as SIGNAL-42 |
| `include` | string | No | Comma-separated extras. 'raw' adds rawData: the whole stored data column. Opt-in because a processed transcript's column can exceed several hundred KB. |

### Example request

```curl
curl -X GET https://zentrik.ai/api/external/v1/signals/SIGNAL-42 \
  -H 'Authorization: Bearer YOUR_API_KEY'
```

### Responses

| Status | Meaning | Description | Schema |
| --- | --- | --- | --- |
| 200 | Success | The signal was successfully retrieved. | Signal |
| 404 | Not Found | No signal found with the provided ID. | — |

### Example response

```json
{
  "id": "uuid",
  "name": "Support Ticket - 3/20/24, 10:00 AM",
  "publicId": "SIGNAL-42",
  "workspaceId": "workspace-uuid",
  "status": "processed",
  "context": "support_request",
  "signalType": null,
  "processorType": "zendesk",
  "providerType": "zendesk",
  "evidenceKind": "support_ticket",
  "analysisProfile": "support_ticket",
  "provenance": {
    "providerType": "zendesk",
    "evidenceKind": "support_ticket",
    "analysisProfile": "support_ticket"
  },
  "sourceFacet": "zendesk:support_ticket",
  "sourceLabel": "Zendesk support ticket",
  "sourceLinks": [
    {
      "name": "Open Zendesk ticket",
      "url": "https://example.zendesk.com/agent/tickets/12345",
      "type": "support_ticket",
      "primary": true
    }
  ],
  "source": {
    "type": "zendesk",
    "external_data": {
      "ticketId": "12345"
    }
  },
  "data": {
    "sessionId": "session-123"
  },
  "transcript": null,
  "import": null,
  "insightIds": [
    "insight-uuid"
  ],
  "accountIds": [],
  "initiativeIds": [
    "initiative-uuid"
  ],
  "studyIds": [],
  "accountId": null,
  "accountExternalId": null,
  "externalId": "external-123",
  "createdAt": "2024-03-20T10:00:00Z",
  "updatedAt": "2024-03-20T10:00:00Z"
}
```

---

## POST /external/v1/signals — Create a signal

Operation ID: `create-signal`

Create a new signal by sending the text you want Zentrik to analyze. This is the recommended public API flow for transcripts, support tickets, reviews, structured feedback records, and discovery reports. Advanced raw signal bodies are also accepted on this same endpoint and are auto-queued.

Required API key scopes: `signals:write`

### JSON request body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `name` | string | No | Required non-blank string for raw signals. Optional for typed text signals, which generate a name from the signal type and timestamp when omitted, null, or blank. Invalid names return 400 before an import run or signal is created. |
| `text` | string | Yes | The raw transcript, support ticket, review, structured feedback record, or discovery report text that Zentrik should process into insights. Transcript input may be copied or exported with speaker labels, timestamps, captions, markdown headings, and common meeting metadata; Zentrik preserves the source and normalizes its conversation structure automatically. |
| `signalType` | 'transcript' \| 'meeting_notes' \| 'ai_notes' \| 'reconstructed_notes' \| 'support_ticket' \| 'review' \| 'feedback_record' \| 'discovery_report' | Yes | What kind of text you are sending. Use meeting_notes for notes, ai_notes for an AI summary, and reconstructed_notes for material rebuilt from notes and recollection. These are not transcripts. New text signals are automatically queued for processing after creation. |
| `providerType` | string | No | Optional source system or channel identifier, such as google_meet, granola, app_store, reddit, g2, manual_csv, or external_api. Keep the provider separate from the content type: Google Meet can supply a transcript or notes. |
| `evidenceKind` | string | No | Optional evidence shape override. For typed text this defaults from signalType and usually does not need to be sent. |
| `analysisProfile` | 'default_transcript' \| 'support_ticket' \| 'survey_response' \| 'discovery_report' \| 'community_discussion' | No | Optional processing profile override. For typed text this defaults from signalType. |
| `additionalContext` | string | No | Optional notes to help Zentrik interpret the text correctly. Use this for things like where the text came from, what batch it belongs to, or any lightweight analyst note. This is treated as supplemental context, not an instruction override. |
| `occurredAt` | iso-date | No | Optional timestamp representing when the original signal happened. When provided, it is used as the signal creation timestamp for trend accuracy. |
| `productIds` | uuid[] | No | Optional list of products to narrow the product context used during processing. |
| `accountId` | uuid | No | Optional account to associate with this signal and the insights created from typed text processing. |
| `accountIds` | uuid[] | No | Optional list of account IDs to associate directly with this signal. Use accountId for the common single-account case. |
| `accountExternalId` | string | No | Optional stable client-side account identifier, such as crm-example-account. Prefer this for CRM automations that should not persist backend UUIDs. |
| `participants` | { email?: string; contactId?: uuid; accountId?: uuid; name?: string; role?: string; affiliation?: "external" \| "internal" \| "unknown" }[] | No | People observed in this record. Add a valid email or an existing Contact ID. Mark colleagues as internal. Zentrik changes People only when participantPolicy requests it. |
| `participantPolicy` | 'none' \| 'match_existing' \| 'create_missing' | No | Controls participant handling. Omit it or use none for no People changes. Use match_existing to link only saved People, or create_missing to create reliably identified People. |
| `createMissingAccounts` | boolean | No | When participantPolicy is create_missing, also allow one missing Account to be created from each unambiguous professional email domain. Defaults to false; public email domains never create Accounts. |
| `sourceKey` | string | No | Stable namespace for externalId, such as granola.note or crm.call. Set it when several clients send records into the same workspace. |
| `externalId` | string | No | Stable record identifier within sourceKey. Later deliveries reuse the Signal and can add newly available participant links. Use Idempotency-Key only to replay one exact request. |
| `sourceLinks` | { name?: string; url: string; type?: string; primary?: boolean }[] | No | Links users should land on from Zentrik, such as the original app store review, G2 review, Reddit thread or comment, Gartner report, support ticket, source row, or meeting link. HTTP and HTTPS URLs are normalized and returned on the Signal object. |
| `sourceExternalData` | object | No | Provider-specific metadata to keep with the signal, such as reviewId, rating, locale, reportId, threadId, productSlug, or dataset. For Granola, send noteId for a public note ID or granolaDocumentId for a legacy desktop ID; send both when known. Zentrik stores this under source.external_data. |

### Example request

```json
{
  "name": "G2 Reviews - March Batch",
  "text": "Users love the product overall, but several reviewers mention that exports are hard to find and slow to complete for large datasets.",
  "signalType": "review",
  "providerType": "g2",
  "externalId": "g2-review:review-row-1",
  "additionalContext": "Public review batch collected from G2 and Capterra.",
  "occurredAt": "2025-03-11T14:30:00Z",
  "productIds": [
    "product-uuid-1"
  ],
  "participants": [
    {
      "email": "buyer@example.com",
      "name": "Primary product contact",
      "role": "VP Product",
      "affiliation": "external"
    }
  ],
  "participantPolicy": "create_missing",
  "createMissingAccounts": false,
  "sourceExternalData": {
    "reviewId": "review-row-1",
    "rating": 3,
    "productSlug": "example-product"
  },
  "sourceLinks": [
    {
      "name": "Open G2 review",
      "url": "https://www.g2.com/products/example-product/reviews/review-row-1",
      "type": "review",
      "primary": true
    }
  ]
}
```

### Responses

| Status | Meaning | Description | Schema |
| --- | --- | --- | --- |
| 201 | Created | The signal was successfully created and queued for asynchronous processing. Create responses include a queued job id. | Signal |

### Example response

```json
{
  "id": "uuid",
  "name": "Review - 3/11/25, 2:30 PM",
  "publicId": "SIGNAL-43",
  "workspaceId": "workspace-uuid",
  "status": "processing",
  "context": null,
  "signalType": "review",
  "processorType": "manual-text",
  "providerType": "g2",
  "evidenceKind": "review",
  "analysisProfile": "survey_response",
  "provenance": {
    "providerType": "g2",
    "evidenceKind": "review",
    "analysisProfile": "survey_response"
  },
  "sourceFacet": "g2:review",
  "sourceLabel": "G2 Review",
  "sourceLinks": [
    {
      "name": "Open G2 review",
      "url": "https://www.g2.com/products/example-product/reviews/review-row-1",
      "type": "review",
      "primary": true
    }
  ],
  "source": {
    "type": "manual-text",
    "external_data": {
      "reviewId": "review-row-1",
      "rating": 3,
      "productSlug": "example-product",
      "links": [
        {
          "name": "Open G2 review",
          "url": "https://www.g2.com/products/example-product/reviews/review-row-1",
          "type": "review",
          "primary": true
        }
      ]
    }
  },
  "data": {
    "s3Key": "signals/manual-text/workspace-uuid/uuid.txt",
    "productIds": [
      "product-uuid-1"
    ],
    "accountId": "account-uuid-1",
    "accountExternalId": "crm-example-account",
    "additionalContext": "Public review batch collected from G2 and Capterra.",
    "uploadedAt": "2025-03-21T10:00:00Z"
  },
  "insightIds": [],
  "accountId": "account-uuid-1",
  "accountIds": [
    "account-uuid-1"
  ],
  "contactIds": [
    "contact-uuid-1"
  ],
  "externalId": "g2-review:review-row-1",
  "createdAt": "2025-03-11T14:30:00Z",
  "updatedAt": "2025-03-21T10:00:00Z",
  "jobId": "job-123",
  "processing": {
    "lastJobId": "job-123",
    "startedAt": "2025-03-21T10:00:00Z",
    "processedAt": null,
    "failedAt": null,
    "lastError": null
  }
}
```

---

## POST /external/v1/signals/:publicId/evidence — Create Evidence for a signal

Operation ID: `create-signal-evidence`

Add a reviewed, source-linked Evidence claim to an existing Signal when processing misses an important point. The description is the reviewer-authored claim. Optionally include extracts as one exact, continuous quotation from the stored Signal text; the API verifies it before saving. Read the Signal source and its Evidence before retrying a timed-out request to avoid duplicates.

Required API key scopes: `signals:read`, `signal-evidence:curate`

### Parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `publicId` | string | Yes | Workspace-scoped Signal ID, such as SIGNAL-42. |

### JSON request body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `title` | string | Yes | Standalone Evidence title, 3–500 characters. |
| `description` | string | Yes | Reviewed source-scoped claim and context, 3–10,000 characters. This is not stored as a verbatim extract. |
| `extracts` | string \| null | No | Optional exact, continuous quote from the persisted Signal text. The API rejects text that is not a literal passage. Omit or send null when the claim is supported by context rather than a short quote. |
| `category` | string | Yes | Evidence category, such as Product request, Product bug, Positive product feedback, Market / decision context, or Other. |
| `productId` | uuid \| null | No | Optional product assignment in this workspace. |
| `featureIds` | uuid[] | No | Optional feature assignments. Each feature must belong to the selected product and workspace. |

### Example request

```curl
curl -X POST https://zentrik.ai/api/external/v1/signals/SIGNAL-42/evidence \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{"title":"Filter reports by date range","description":"The participant needs a date range filter on reports.","category":"Product request"}'
```

### Responses

| Status | Meaning | Description | Schema |
| --- | --- | --- | --- |
| 201 | Created | The Evidence claim was attached to the Signal and returned in the same shape as Evidence detail reads. | SignalEvidence |
| 400 | Invalid request | A field is missing, unsupported, or outside its allowed length or category. | — |
| 403 | Missing scope | The API key needs signals:read and signal-evidence:curate. | — |
| 404 | Not found | The Signal does not exist in this workspace. | — |

### Example response

```json
{
  "id": "evidence-uuid",
  "publicId": "EVIDENCE-123",
  "title": "Filter reports by date range",
  "description": "The participant needs a date range filter on reports.",
  "extracts": null,
  "category": "Product request",
  "evidenceType": "need",
  "signalId": "signal-uuid",
  "resolvedAt": null,
  "resolution": null,
  "resolvedByUserId": null,
  "dismissedAt": null
}
```

---

## PATCH /external/v1/signals/:publicId — Update a signal

Operation ID: `update-signal`

Update fields of an existing signal, such as metadata or linked insights. Do not use a metadata patch to replace source text. Use the preview and source-revisions endpoints for a reviewed source correction; repeating a create request does not replace source text. Once sourceRevision is above zero, source, data, importMetadata, and status cannot be replaced through metadata updates.

Required API key scopes: `signals:write`

### Parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `publicId` | string | Yes | Workspace-scoped public ID, such as SIGNAL-43 |

### JSON request body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `source` | object | No | Updated source descriptor for the signal. |
| `data` | object | No | Updated arbitrary context payload. |
| `insightIds` | string[] | No | Replace the list of linked insight IDs. A link added here stays until it is removed here. Removing an insight also removes it from this signal's evidence, so later evidence changes do not link it again. |
| `externalId` | string | No | Updated external record identifier. |
| `status` | 'pending' \| 'processed' | No | Processing status for the signal. |
| `context` | string | No | Persisted signal context, such as user_interview or support_request. |

### Example request

```curl
curl -X PATCH https://zentrik.ai/api/external/v1/signals/SIGNAL-43 \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{"insightIds": ["insight-uuid"]}'
```

### Responses

| Status | Meaning | Description | Schema |
| --- | --- | --- | --- |
| 200 | Updated | The signal was successfully updated. | Signal |

---

## DELETE /external/v1/signals/:publicId — Delete a signal

Operation ID: `delete-signal`

Permanently delete a signal from the workspace.

Required API key scopes: `signals:delete`

### Parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `publicId` | string | Yes | Workspace-scoped public ID, such as SIGNAL-43 |

### Example request

```curl
curl -X DELETE https://zentrik.ai/api/external/v1/signals/SIGNAL-43 \
  -H 'Authorization: Bearer YOUR_API_KEY'
```

### Responses

| Status | Meaning | Description | Schema |
| --- | --- | --- | --- |
| 200 | Success | The signal was successfully deleted. | { message: string } |

### Example response

```json
{
  "message": "Deleted"
}
```

---

## POST /external/v1/signals/:publicId/process — Process a signal

Operation ID: `process-signal`

Queue signal processing for an existing signal. Use this for explicit re-runs or manual retries.

Required API key scopes: `signals:write`

### Parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `publicId` | string | Yes | Workspace-scoped public ID, such as SIGNAL-43. |

### Example request

```curl
curl -X POST https://zentrik.ai/api/external/v1/signals/SIGNAL-43/process \
  -H 'Authorization: Bearer YOUR_API_KEY'
```

### Responses

| Status | Meaning | Description | Schema |
| --- | --- | --- | --- |
| 201 | Queued | The signal was queued for asynchronous processing. | { jobId: string } |

### Example response

```json
{
  "jobId": "job-456"
}
```

---

## GET /external/v1/signals/:publicId/source-revision — Preview source correction

Operation ID: `preview-signal-source-revision`

Check whether an imported manual-text or manual-transcript Signal can be corrected. The response includes a sourceToken, revision number, Evidence counts, blockers, and up to 500 downstream target IDs for review. downstreamTruncated means the target list is incomplete. Connected providers, Granola, Studies, downstream links, and curated Evidence require review and cannot be corrected with this operation. Availability also requires the source-correction rollout to be enabled. The preview does not reserve the Signal; the write checks all conditions again, including queue state.

Required API key scopes: `signals:read`

### Parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `publicId` | string | Yes | Workspace-scoped Signal ID. |

### Responses

| Status | Meaning | Description | Schema |
| --- | --- | --- | --- |
| 200 | Preview | Current source token and correction impact. Blockers prevent a write. | SourceRevisionPreview |

### Example response

```json
{
  "publicId": "SIGNAL-43",
  "sourceRevision": 0,
  "sourceToken": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
  "eligible": true,
  "blockers": [],
  "impact": {
    "generatedEvidence": 1,
    "protectedEvidence": 0,
    "downstreamLinks": 0,
    "truncated": false
  },
  "downstream": [],
  "downstreamTruncated": false,
  "processing": "explicit_process_required"
}
```

---

## GET /external/v1/signals/:publicId/source-text — Read stored signal source text

Operation ID: `get-signal-source-text`

Read the current persisted source body in the API key workspace. This is the stored text available to processing, not a provider refresh or an analyst summary. The ordinary Signal include=raw response may expose only an S3 pointer. The response is private and not cached.

Required API key scopes: `signals:read`

### Parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `publicId` | string | Yes | Workspace-scoped Signal ID. |

### Responses

| Status | Meaning | Description | Schema |
| --- | --- | --- | --- |
| 200 | Success | Persisted source text and its current revision and date. | SourceText |
| 404 | Not found | The Signal does not exist in this workspace. | — |
| 409 | Unavailable | The stored source body is missing or empty. | — |

### Example response

```json
{
  "publicId": "SIGNAL-43",
  "sourceRevision": 0,
  "occurredAt": "2026-03-18T14:51:17.000Z",
  "sourceToken": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
  "text": "The complete imported source record."
}
```

---

## POST /external/v1/signals/:publicId/source-revisions — Correct imported source text

Operation ID: `revise-signal-source`

Correct the imported copy of one source, without changing its Signal ID, external ID, provider, product scope, Accounts, or People. Requires an Idempotency-Key header (1–200 characters), the sourceToken from a fresh preview, corrected native text, and a reason. The transaction archives the old source and uncurated, unlinked generated Evidence, removes those obsolete captures, and sets status to pending. It does not queue processing. Use the revision-specific process route from the receipt, then read the Signal status, processing receipt, and Evidence. A lost response is safe to retry with the same API key, Idempotency-Key, and exact body; a different body returns 409. Replaying returns the original acceptance receipt, not current processing status. Never remove curated links to bypass a blocker.

Required API key scopes: `signals:write`

### Parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `publicId` | string | Yes | Workspace-scoped Signal ID. |

### JSON request body

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `expectedSourceToken` | string | Yes | Exact 64-character token returned by the current preview. A stale token returns 409. |
| `text` | string | Yes | Corrected native source text. Nonblank, at most 100,000 characters. Replaces competing text aliases and derived transcript artifacts. |
| `reason` | string | Yes | Nonblank correction reason, at most 1,000 characters. Retained with the audit record. |
| `occurredAt` | iso-date | No | Verified source date, only when it also needs correction. Otherwise the date is unchanged. |

### Example request

```curl
curl -X POST https://zentrik.ai/api/external/v1/signals/SIGNAL-43/source-revisions \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: source-correction-43-v1' \
  -d '{"expectedSourceToken":"REPLACE_WITH_PREVIEW_TOKEN","text":"Original provider text, not an analyst summary.","reason":"Correct the imported text mapping."}'
```

### Responses

| Status | Meaning | Description | Schema |
| --- | --- | --- | --- |
| 201 | Correction accepted | A durable correction receipt. This is not a processing-complete receipt. | SourceRevisionReceipt |
| 400 | Invalid correction | Missing Idempotency-Key, unsupported fields, or invalid text, reason, token, or date. | — |
| 409 | Review required | Stale source, changed request key input, disabled rollout, active processing, provider ownership, curated Evidence, downstream links, or an impact above 200 Evidence rows. No correction is applied. | — |

### Example response

```json
{
  "id": "revision-uuid",
  "signalId": "signal-uuid",
  "revision": 1,
  "sourceToken": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
  "replayed": false,
  "processingQueued": false,
  "nextAction": "Use POST /signals/SIGNAL-43/source-revisions/1/process, then read the Signal status, processing receipt, and Evidence."
}
```

---

## GET /external/v1/signals/:publicId/source-revisions/:revision — Read a source correction record

Operation ID: `get-signal-source-revision`

Read the durable correction record, including its reason, acceptance time, afterToken, and beforeSnapshot. The snapshot contains the prior source, raw data, original body, source date, and removed generated Evidence. It may contain sensitive source content; protect it like the original Signal. History is deleted with the Signal or workspace. Restoring old text requires a new reviewed correction; this endpoint does not restore or reprocess anything.

Required API key scopes: `signals:read`

### Parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `publicId` | string | Yes | Workspace-scoped Signal ID. |
| `revision` | integer | Yes | Revision number from the acceptance receipt or the Signal sourceRevision field. |

### Responses

| Status | Meaning | Description | Schema |
| --- | --- | --- | --- |
| 200 | Correction record | Audit record and original source snapshot. | SourceRevisionHistory |

---

## POST /external/v1/signals/:publicId/source-revisions/:revision/process — Process a corrected source revision

Operation ID: `process-signal-source-revision`

Start processing for one accepted source revision through the existing Signal queue. The route checks that the requested revision is still current before and after the queue handoff, so a later correction cannot be mistaken for the revision being processed. It is safe to retry while the same revision is pending or already in flight. Read the Signal status, processing receipt, and Evidence after completion.

Required API key scopes: `signals:write`

### Parameters

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `publicId` | string | Yes | Workspace-scoped Signal ID. |
| `revision` | integer | Yes | Accepted source revision to process. |

### Responses

| Status | Meaning | Description | Schema |
| --- | --- | --- | --- |
| 201 | Processing started | The accepted revision was handed to the existing processing queue. | SourceRevisionProcessingReceipt |
| 409 | Revision changed | The requested revision is no longer current. Read the current Signal before processing. | — |

### Example response

```json
{
  "publicId": "SIGNAL-43",
  "sourceRevision": 1,
  "jobId": "job-uuid",
  "processingQueued": true,
  "nextAction": "Read the Signal status, processing receipt, and Evidence after the job completes."
}
```
