Canonical page: https://tellenze.com/help/apps

# Connect other systems with Apps

Authorize member App creation, grant project access, and use the API with clear App and member attribution.

## Decide who can create Apps

Admins can create Apps by default. To let other active members create them, open **Administration → Settings**, enable **Allow members to create Apps**, and save. Member creation starts disabled. Turning it off stops new member-created Apps; existing Apps keep their access until their owners revoke them. Each App belongs to the member who creates it.

## Create and maintain an App

Open **Account settings → Apps**. Enter a recognizable name, choose its projects and permissions, then create it. **Workspace documentation** is a separate optional grant for Knowledge outside a project. Copy the token from the confirmation: it appears only once. Store it in the connecting system's secret store.

**Edit** changes the name, image or grants. **Rotate token** immediately invalidates the old token and displays its replacement once. **Revoke** stops further access. Other members cannot manage your credentials. If a create or rotate response is lost, check the list and deliberately rotate; plaintext tokens cannot be recovered.

Apps stay limited by their owner's current membership and project access. Archived projects, revoked Apps and suspended owners are unavailable. The settings page gives the API address for this workspace; credentials from another workspace do not work.

## Add an App image

Choose **Upload image** when creating or editing an App. PNG, JPEG, GIF and WebP images up to 2 MiB and 1024 × 1024 pixels are supported. The form previews your selection. Use **Replace image** to choose another file or **Remove image** to return to the default App symbol, then save.

Your image appears beside the App in settings, reporter and author details, document revisions and activity. Existing contributions use the current image when loaded, while their recorded App name and responsible member stay unchanged. Revoking an App preserves its image for historical attribution.

Images are scanned before saving, stored privately and available only to active members of the same workspace. They count toward member and workspace storage and share the attachment upload limits. If scanning or validation fails, the current image is preserved; correct the file or retry when scanning is available. Missing images use the default App symbol.

The dimension limit applies to the actual pixels in the file, not its displayed size or file size. A dimension error shows the detected width and height; export or resize the image to fit within 1024 × 1024. A scanning-unavailable message means the upload service could not check the file, not that the image is infected. Local developers must run ClamAV and configure its connection as described in the README's local file-upload setup.

## Choose permissions

| Permission | Access |
| --- | --- |
| `projects:read` | Read granted active projects and their workflow stages. |
| `focus-groups:read` | Read Focus groups in those projects, when Focus is enabled. |
| `work-items:read` | Read active work items in granted projects. |
| `work-items:write` | Create work items and update their supported definition fields. |
| `documents:read` | Read granted project documentation, plus workspace documentation when enabled. |
| `documents:write` | Create and update documents in those locations. |
| `signals:write` | Submit shared workspace reports or append evidence on an eligible paid subscription. |
| `signals:read` | Discover shared workspace Signals, with project-derived details restricted to App grants and current owner access. |

Projects and Focus groups are read-only. Apps cannot manage members, permissions or Required Context policies. Resource responses exclude unrelated linked work, private repository paths and attachments.

## Set up the examples

Copy the API address from **Account settings → Apps**. It ends in `/api/v1/apps`. Replace the example address and token below, and omit a trailing slash from the address. These commands use a POSIX-style shell, such as Bash or Zsh.

```sh
export TELLENZE_APP_API='https://your-workspace.example.com/api/v1/apps'
export TELLENZE_APP_TOKEN='YOUR_APP_TOKEN'
```

Every request sends `Authorization: Bearer YOUR_APP_TOKEN` and `Accept: application/json`. Writes also send `Content-Type: application/json`. The examples use `SUPPORT` as a project key: replace it with a project granted to your App. Replace sample record IDs and versions with values returned by your workspace.

Enable the permission for each operation you use. Read and write permissions are separate; for example, the read-then-update work-item example needs both `work-items:read` and `work-items:write`. Workspace documents additionally need the **Workspace documentation** grant. If Required Context applies, follow **Supply Required Context** below before sending a write.

## Read projects and work items

| Method | Path |
| --- | --- |
| GET | `/projects` or `/projects/{key-or-id}` |
| GET | `/focus-groups` or `/focus-groups/{id}` |
| GET | `/work-items` or `/work-items/{identifier-or-id}` |
| GET | `/documents` or `/documents/{id}` |

Responses use a `data` object or list. Archived records and records outside current grants are excluded. List the projects your App can access, then inspect one project's workflow stages:

```sh
curl "$TELLENZE_APP_API/projects" \
  -H "Authorization: Bearer $TELLENZE_APP_TOKEN" \
  -H 'Accept: application/json'

curl "$TELLENZE_APP_API/projects/SUPPORT" \
  -H "Authorization: Bearer $TELLENZE_APP_TOKEN" \
  -H 'Accept: application/json'
```

The project response includes `data.workflow_stages`. A stage's `id` can be used as `workflow_stage_id` when creating work; choose one whose `is_completion` is false.

List work in one project, then read a single ticket by its identifier:

```sh
curl --get "$TELLENZE_APP_API/work-items" \
  -H "Authorization: Bearer $TELLENZE_APP_TOKEN" \
  -H 'Accept: application/json' \
  --data-urlencode 'project_key=SUPPORT' \
  --data-urlencode 'per_page=50'

curl "$TELLENZE_APP_API/work-items/SUPPORT-42" \
  -H "Authorization: Bearer $TELLENZE_APP_TOKEN" \
  -H 'Accept: application/json'
```

All lists accept `per_page` from 1 to 100 (default 25). Work-item, Focus and document lists accept `project_key`. Pagination uses cursors: follow the complete URL in `links.next` with the same authorization and Accept headers until it is null. Do not construct page numbers. For example, after replacing the placeholder with a returned next-page URL:

```sh
NEXT_PAGE_URL='PASTE_THE_LINKS_NEXT_URL_HERE'
curl "$NEXT_PAGE_URL" \
  -H "Authorization: Bearer $TELLENZE_APP_TOKEN" \
  -H 'Accept: application/json'
```

## Read Focus groups and documentation

Focus must be enabled in the workspace. With `focus-groups:read`, list a project's groups, then copy an `id` from `data` to read one. The ID below is illustrative:

```sh
curl --get "$TELLENZE_APP_API/focus-groups" \
  -H "Authorization: Bearer $TELLENZE_APP_TOKEN" \
  -H 'Accept: application/json' \
  --data-urlencode 'project_key=SUPPORT'

FOCUS_GROUP_ID='01ARZ3NDEKTSV4RRFFQ69G5FAV'
curl "$TELLENZE_APP_API/focus-groups/$FOCUS_GROUP_ID" \
  -H "Authorization: Bearer $TELLENZE_APP_TOKEN" \
  -H 'Accept: application/json'
```

With `documents:read`, list project documentation or workspace Knowledge. The special `project_key=workspace` filter applies only to documents and requires the Workspace documentation grant. Without a filter, the document list includes all locations allowed by your grants.

```sh
curl --get "$TELLENZE_APP_API/documents" \
  -H "Authorization: Bearer $TELLENZE_APP_TOKEN" \
  -H 'Accept: application/json' \
  --data-urlencode 'project_key=SUPPORT'

curl --get "$TELLENZE_APP_API/documents" \
  -H "Authorization: Bearer $TELLENZE_APP_TOKEN" \
  -H 'Accept: application/json' \
  --data-urlencode 'project_key=workspace'
```

Copy a document's `id` from the response to read it. Document and Focus URLs use IDs, not titles or slugs.

```sh
DOCUMENT_ID='01ARZ3NDEKTSV4RRFFQ69G5FAW'
curl "$TELLENZE_APP_API/documents/$DOCUMENT_ID" \
  -H "Authorization: Bearer $TELLENZE_APP_TOKEN" \
  -H 'Accept: application/json'
```

## Report a ticket from another system

With `work-items:write`, the smallest creation payload contains `project_key` and `title`. Use a stable `Idempotency-Key` for each logical write, such as the provider and external ticket ID. It must contain 8–128 letters, numbers, periods, underscores, colons or hyphens.

```sh
curl "$TELLENZE_APP_API/work-items" \
  -H "Authorization: Bearer $TELLENZE_APP_TOKEN" \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: support-ticket-1842' \
  --data '{"project_key":"SUPPORT","title":"Checkout fails on mobile"}'
```

For a richer report, send Markdown in `description` and `acceptance_criteria`. This example is a separate ticket, so it uses a different key. The quoted `JSON` delimiter keeps the shell from expanding the payload.

```sh
curl "$TELLENZE_APP_API/work-items" \
  -H "Authorization: Bearer $TELLENZE_APP_TOKEN" \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: support-ticket-1843' \
  --data-binary @- <<'JSON'
{
  "project_key": "SUPPORT",
  "title": "Payment confirmation email is missing",
  "description": "## External report\nSource: [Ticket 1843](https://support.example.com/tickets/1843)\n\nPayment succeeds, but the customer receives no confirmation email.",
  "acceptance_criteria": "- [ ] A successful payment sends one confirmation email.\n- [ ] Retrying the payment callback does not send a duplicate.",
  "type": "bug",
  "due_date": "2026-10-15",
  "blocked": true,
  "blocked_reason": "Waiting for the email provider's delivery logs."
}
JSON
```

Adjust the date and choose a work type enabled for your project. A successful create returns HTTP 201. This response excerpt shows the values to keep for later requests; your identifier and other values will differ:

```json
{
  "data": {
    "identifier": "SUPPORT-42",
    "title": "Payment confirmation email is missing",
    "version": 1,
    "blocked": true
  }
}
```

Save the external ticket ID's mapping to `data.identifier` in your integration. The API assigns the reporter and App attribution automatically.

Other optional definition fields are `complexity`, `parent_id`, `assignee_ids` and `label_ids`. Complexity must use your workspace's estimation scale. Assignees are numeric member IDs with access to the project; labels and parent work items use IDs from the same project. This API does not expose member or label directories, so obtain those IDs from your existing workspace configuration.

## Choose a starting stage and Focus

Creation can also include `workflow_stage_id` from the project response. For Focus assignment, read the Focus group again and send its `data.id` as `focus_group_id` and its current `data.version` as `focus_group_version`. Replace both illustrative IDs and the version in this payload, then use it as the body of `POST /work-items` with its own idempotency key:

```json
{
  "project_key": "SUPPORT",
  "title": "Investigate delayed payment callbacks",
  "workflow_stage_id": "01ARZ3NDEKTSV4RRFFQ69G5FAX",
  "focus_group_id": "01ARZ3NDEKTSV4RRFFQ69G5FAV",
  "focus_group_version": 3
}
```

Choose a non-completion stage and a Planned or Active Focus in the same project. Focus must be enabled. Omit these fields to use the project's default starting stage with no explicit Focus assignment. You may send a supported non-completion `status` instead of `workflow_stage_id`, but do not send both.

## Update an existing work item

Read `GET /work-items/{identifier-or-id}` first and use its current `data.version`. For an item currently at version 1, this request replaces its description and unblocks it:

```sh
curl --request PATCH "$TELLENZE_APP_API/work-items/SUPPORT-42" \
  -H "Authorization: Bearer $TELLENZE_APP_TOKEN" \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: support-ticket-1843-update-1' \
  --data-binary @- <<'JSON'
{
  "version": 1,
  "description": "The provider delivered its logs. Investigation can continue.",
  "blocked": false
}
JSON
```

HTTP 200 returns the updated resource and its new version. Omitted definition fields stay unchanged; `description` and `acceptance_criteria` replace their previous text rather than append. Unblocking also clears the blocked reason. Send `due_date: null` to clear a due date, or an empty `assignee_ids` or `label_ids` array to remove those assignments. Non-empty assignment arrays replace the corresponding selection.

Stage changes, shipping, changing project and Focus reassignment remain in the existing workspace workflow controls; these are not supported by this PATCH endpoint. A stale version returns 409: read the record again, reconcile your intended changes with the current values, and use its new version and a new idempotency key for the revised edit.

## Write documentation with clear attribution

With `documents:write`, create a project document by sending its project key and title. The Markdown body, slug and folder are optional:

```sh
curl "$TELLENZE_APP_API/documents" \
  -H "Authorization: Bearer $TELLENZE_APP_TOKEN" \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: support-runbook-create-1' \
  --data-binary @- <<'JSON'
{
  "project_key": "SUPPORT",
  "title": "Payment incident runbook",
  "slug": "payment-incident-runbook",
  "body": "# Payment incidents\n\n1. Find the external ticket.\n2. Check the provider's delivery logs.\n3. Record the outcome on the work item."
}
JSON
```

HTTP 201 returns the document under `data`; save its `id` and `version`. A slug uses lowercase letters, numbers and single separating hyphens, and must be unique in that project or workspace location. Omitting it derives a slug from the title. A supplied `folder_id` must belong to the same location; omitting it creates the document at the root.

To create workspace Knowledge, use `project_key: "workspace"` and enable the App's Workspace documentation grant:

```sh
curl "$TELLENZE_APP_API/documents" \
  -H "Authorization: Bearer $TELLENZE_APP_TOKEN" \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: workspace-support-handoff-1' \
  --data-binary @- <<'JSON'
{
  "project_key": "workspace",
  "title": "Support handoff checklist",
  "body": "# Handoff\n\n- Link the external ticket.\n- Record the affected project.\n- Note the next action."
}
JSON
```

Omitting `project_key` also targets workspace Knowledge, so always include a project key when you intend to create project documentation.

Work-item reporter details show the App and responsible member. Documents show them in **Created by**, **Last updated by** and revision history. App names are captured when each change happens; renaming or revoking an App does not rewrite attribution. Clients cannot choose another reporter, author or origin.

## Update a document

Read the document first using the earlier GET example. Set `DOCUMENT_ID` to its actual `data.id`, then replace the version below with its current `data.version`:

```sh
curl --request PATCH "$TELLENZE_APP_API/documents/$DOCUMENT_ID" \
  -H "Authorization: Bearer $TELLENZE_APP_TOKEN" \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: support-runbook-update-1' \
  --data-binary @- <<'JSON'
{
  "version": 1,
  "title": "Payment incident runbook — revised",
  "body": "# Payment incidents\n\n1. Find the external ticket.\n2. Check the provider's delivery logs.\n3. Confirm the customer received the email.\n4. Record the outcome."
}
JSON
```

HTTP 200 returns the new version. Each successful create or update creates an immutable revision. A supplied body replaces the entire Markdown body; omitted fields remain unchanged. You can change `title`, `body` and `folder_id`. Use `folder_id: null` to move a document to its location's root. Project scope and slug cannot be changed through this endpoint.

## Retry a write safely

Keep each write's JSON body and idempotency key until you know its outcome. If the connection drops, send the same method, URL, key and JSON body again. For example, retrying the minimal ticket creation above with response headers visible:

```sh
curl --include "$TELLENZE_APP_API/work-items" \
  -H "Authorization: Bearer $TELLENZE_APP_TOKEN" \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: support-ticket-1842' \
  --data '{"project_key":"SUPPORT","title":"Checkout fails on mobile"}'
```

If the original write succeeded, `Idempotency-Replayed: true` identifies the replay. It returns the existing resource without duplicating a work item, document or revision. Retries recheck current grants and return current resource values, so the returned version may be newer than the original response. They never expose a cached private body.

Reusing a key for a different method, path or JSON body returns 409. Give each new logical write its own key, including updates to an existing external ticket. Do not change a key or refresh the body merely because a response was lost. A confirmed validation or version error should be corrected before sending a revised request with a new key.

## Supply Required Context

Where a policy applies, send `POST /required-context` with exactly one target:

| Write you are preparing | Context request body |
| --- | --- |
| Create a work item in SUPPORT | `{"project_key":"SUPPORT"}` |
| Update SUPPORT-42 | `{"task":"SUPPORT-42"}` |
| Create or update a SUPPORT document | `{"project_key":"SUPPORT"}` |
| Create or update workspace Knowledge | `{"workspace":true}` |

The context endpoint requires the applicable write permission but does not require an idempotency key. This example fetches guidance before creating a SUPPORT ticket. The following walkthrough uses `jq` to read JSON and safely construct a payload; you can perform the same steps with your integration's JSON library.

```sh
curl --fail --silent --show-error "$TELLENZE_APP_API/required-context" \
  -H "Authorization: Bearer $TELLENZE_APP_TOKEN" \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  --data '{"project_key":"SUPPORT"}' \
  --output required-context.json &&
  jq -r '.data.revisions[] | "# " + .title + "\n\n" + (.body // "")' required-context.json
```

Only continue if the request succeeds and you have read every returned revision. Then include the returned receipt in the write. This produces `ticket-with-context.json`; keep that file unchanged for any retry with the same key:

```sh
jq '{
  project_key: "SUPPORT",
  title: "Investigate a delayed payment callback",
  description: "Reported by the support integration after reading Required Context.",
  required_context_receipt: .data.receipt
}' required-context.json > ticket-with-context.json

curl "$TELLENZE_APP_API/work-items" \
  -H "Authorization: Bearer $TELLENZE_APP_TOKEN" \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: support-ticket-1844' \
  --data-binary @ticket-with-context.json
```

For an update, fetch context for the target in the table and add `required_context_receipt` alongside the current resource `version` and changed fields. A project receipt cannot be reused for a task-targeted update.

The receipt acknowledges delivery, not comprehension. Receipts expire after 24 hours and bind the workspace, App, owner, App settings version, target and exact policy context. Changed policies or grants require new reading. Missing or inaccessible guidance blocks the write. The App needs document-read permission and applicable project/workspace grants for the guidance. If no policy applies, `data.receipt` is null and `data.revisions` is empty; no reading is required and the null receipt can be sent or omitted.

## Handle errors

| Status | Next step |
| --- | --- |
| 401 | Check the token, workspace address, revocation and active owner. |
| 403 | Check the App's permission for the operation. The admin creation setting governs creating Apps in settings, not API access for existing Apps. |
| 404 | Check the identifier, grants and owner's current access. |
| 409 | Resolve a version/idempotency conflict or retrieve current Required Context. |
| 415 | Send the write as JSON. |
| 422 | Correct the named validation fields. |
| 429 | Respect `Retry-After` before retrying. |

For example, a create request without a title returns HTTP 422 with a field-specific error:

```json
{
  "message": "The title field is required.",
  "errors": {
    "title": ["The title field is required."]
  }
}
```

Add `--include` to a cURL command to inspect the HTTP status and headers, including `Idempotency-Replayed` and `Retry-After`. Read the error body to distinguish a stale version from an idempotency conflict or missing Required Context before retrying a 409.

Limits are 120 requests per minute per credential and 240 per minute per IP. Keep credentials on your server, out of browser code, work items and shared documents.

## Submit and read Signals

Signals requires Starter or above. Existing Apps do not receive `signals:write` or `signals:read` automatically; grant each explicitly. These permissions apply to shared workspace reports. Suggestions, linked work, and project-derived history additionally respect the App's project grants and its owner's current access.

Send `POST /signals` with `client_submission_id`, `body`, `source_key`, and `source_label`. Optional fields are `signal_id` to append evidence, `external_event_id`, an HTTP(S) `source_url`, `occurred_at`, `reported_count`, `window_start`, `window_end`, a `metadata` object, and `project_ids` for starting project associations. Send both reporting-window bounds together. Text is limited to 60,000 characters and metadata to 16 KB.

```json
{
  "client_submission_id": "accounts-monitor-event-42",
  "body": "Account login requests return HTTP 503.",
  "source_key": "accounts-monitor",
  "source_label": "Accounts monitor",
  "external_event_id": "event-42",
  "reported_count": 12,
  "window_start": "2026-09-27T10:00:00Z",
  "window_end": "2026-09-27T10:05:00Z"
}
```

`project_ids` accepts up to 20 distinct active project ULIDs, each within both the App's project grants and its owner's current access. Signals write permission alone does not grant access to project routing. These hints accompany the original report and can coexist with later Looper associations; they never create work. Reordering the same IDs is safe on retries, but changing them under the same submission or event identity returns a conflict. Read responses expose current `project_hints` and each report's original `project_hints` only within current project access. The `project_key` filter also finds raw signals associated with that project before any proposal exists.

Keep `client_submission_id` stable for delivery retries. Source/event identities also deduplicate reports across intake surfaces. Reusing an identity with different evidence returns a conflict. A submission-only App receives a minimal receipt containing signal and report identifiers and whether the report was a duplicate; it receives no shared evidence or suggestion bodies.

With `signals:read`, use `GET /signals` and `GET /signals/{id}`. The Signals list accepts `q`, `state`, `source`, `project_key`, `cursor`, and `limit` (maximum 50). Follow its returned cursor with the same filters. The App API cannot accept suggestions, discard signals, change their workflow, or request processing. Members perform those actions through the web, MCP, or Looper confirmation cards. See the [Signals guide](https://tellenze.com/help/signals) for the lifecycle and subscription cadence.

