---
title: "The Tarkle API flow"
description: "Create an API key, find a workspace, upload a file, publish a brand page and receive signed webhooks, with curl examples."
---

> Documentation Index
> Fetch the complete documentation index at: https://tarkle.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# The Tarkle API flow

This page takes one job from start to finish: create an API key, find a workspace, put a file in it, publish a brand page and receive a signed event. Each step shows the call, a curl example and the fields it returns. Every example sends `<your-tarkle-key>`, the key you make in Settings > API. For how the key, the workspace key and roles fit together, read [Authentication](/docs/get-started/authentication).

All calls go to `https://tarkle.com/api/v1`.

## 1. Create an API key

Only an owner or admin of the organization can make keys. Open **Settings > API**, stay on the **API keys** tab and select **Create**. Enter a **Name** and choose **Access**: **All workspaces** or **Only selected workspaces**. Tarkle shows the key once, in the **API key created** dialog. Copy it then, or copy it later from the key's row menu with **Copy token**.

A key does everything the API and the MCP server can do in the workspaces it reaches, and it acts as the person who made it. A key cannot buy plans. A key limited to some workspaces gets `403 no_access` in every other workspace. Keys made before API keys had full access keep working with their old permissions and show a **Legacy** badge. Create a new key for full access.

Send the key on every call in a bearer header:

```http
Authorization: Bearer <your-tarkle-key>
```

## 2. List workspaces and pick a workspace key

`GET /workspaces` needs only a key. It lists the workspaces the key can use, each with the eight character `workspace_key` that every other call names.

```bash
curl -H "Authorization: Bearer <your-tarkle-key>" \
  https://tarkle.com/api/v1/workspaces
```

The answer is `200` with a `workspaces` list:

```json
{
  "workspaces": [
    {
      "workspace_key": "<workspace-key>",
      "name": "Acme Studio",
      "url": "acme",
      "relationship": "owner"
    }
  ]
}
```

Send the `workspace_key` in an `X-Workspace-Key` header (or as a `workspace_key` query value) on every call that works inside a workspace. A workspace id is refused with `400 workspace_key_required`.

## 3. Choose or create a folder

`GET /files` lists what is inside one folder of the library, folders first and then files. Without `folder` or `path` it lists the root. `folder` takes a folder id and `path` takes names such as `/Clients/2026`. Send one of them, not both.

```bash
curl -H "Authorization: Bearer <your-tarkle-key>" \
  -H "X-Workspace-Key: <workspace-key>" \
  "https://tarkle.com/api/v1/files?path=/Clients/2026&limit=50"
```

The answer holds `folder`, `entries` and `next_cursor`. Each entry has `id`, `type` (`file` or `folder`), `name`, `size` (bytes, `null` for a folder), `category`, `parent_id`, `created_at`, `updated_at`, `modified_at`, `sha256`, `sha256_source`, `etag`, `protected` and `protection_kind`.

To create a folder, send its name and, for a subfolder, the id of the folder it goes in. Leave out `parent_id` for the root.

```bash
curl -X POST https://tarkle.com/api/v1/folders \
  -H "Authorization: Bearer <your-tarkle-key>" \
  -H "X-Workspace-Key: <workspace-key>" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: <a-new-uuid>" \
  -d '{"name": "Invoices", "parent_id": "<folder-id>"}'
```

The answer is `201` with the new folder, in the same fields as an entry. A name already used in that folder is `409 name_taken`. The `Idempotency-Key` header makes a retry safe, as step 8 explains.

## 4. Upload a file

An upload has three calls: start it, send the bytes to the signed URLs, then finish it. The bytes go straight to storage, so a large file never passes through the API.

**Start the upload.** Send the name, the size in bytes and the folder.

```bash
curl -X POST https://tarkle.com/api/v1/uploads \
  -H "Authorization: Bearer <your-tarkle-key>" \
  -H "X-Workspace-Key: <workspace-key>" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: <a-new-uuid>" \
  -d '{"name": "contract.pdf", "size": 218342, "folder_id": "<folder-id>", "content_type": "application/pdf"}'
```

Leave out `folder_id` to upload to the root. Send `sha256` (64 hexadecimal characters) with the file. A file that is already in the folder under the same name can make Tarkle answer `409 conflict`, with your choices in `details.choices`. Send one of them as `on_conflict`: `version`, `separate` or `skip`. A file of `size` `0` is created at once and needs no upload step.

The answer is `201` with `action: "upload"`, the upload `id`, `status`, `name`, `size`, `folder_id`, `mode`, `content_type`, `expires_at`, `will_be_renamed` and `uploads`. The answer is `200` with `action: "skip"` when an identical file is already there.

```json
{
  "action": "upload",
  "id": "<upload-id>",
  "status": "pending",
  "name": "contract.pdf",
  "size": 218342,
  "folder_id": "<folder-id>",
  "mode": "single",
  "content_type": "application/pdf",
  "expires_at": "2026-11-04T12:00:00.000Z",
  "will_be_renamed": false,
  "uploads": [{ "partNumber": 1, "url": "https://storage.example/signed-url" }]
}
```

For a large file, `mode` is `multipart`, and the answer carries `part_size` and `part_count` with the first 20 part URLs in `uploads`.

**Send the bytes.** In `single` mode, send the whole file to `uploads[0].url`. In `multipart` mode, send part N, which is `part_size` bytes, to the URL of its `partNumber`. Send the same `Content-Type` you gave when you started.

```bash
curl -X PUT -H "Content-Type: application/pdf" \
  --data-binary @contract.pdf \
  "https://storage.example/signed-url"
```

A signed URL expires. To get fresh URLs for a range of parts, call `POST /uploads/{id}/parts` with `from_part` and `count` (up to 100).

```bash
curl -X POST https://tarkle.com/api/v1/uploads/<upload-id>/parts \
  -H "Authorization: Bearer <your-tarkle-key>" \
  -H "X-Workspace-Key: <workspace-key>" \
  -H "Content-Type: application/json" \
  -d '{"from_part": 21, "count": 20}'
```

**Finish the upload.** Call `POST /uploads/{id}/complete` once every part is stored.

```bash
curl -X POST https://tarkle.com/api/v1/uploads/<upload-id>/complete \
  -H "Authorization: Bearer <your-tarkle-key>" \
  -H "X-Workspace-Key: <workspace-key>"
```

The answer is `200` with the file, in the same fields as an entry in step 3. If a part is missing, the answer is `409 incomplete` and `details.missing_parts` lists the part numbers to send. Send those parts and call again.

## 5. Create a transfer or file request link

The REST API has no endpoint for a file transfer or a file request, so this step has no curl example. The MCP server does it. The tool `tarkle_create_transfer` creates a public share link for files in the library, and `tarkle_create_file_request` creates a file request. Connect with the same key as described on [the MCP server](/docs/mcp), then ask the tool for the link. Share the link the tool returns.

## 6. Create and publish a brand page

A brand is created as a private draft, and it stays off the public web until you publish it with a second call. The full set of brand fields is on [Brands](/docs/api/brands).

**Create the brand.** `name`, `summary` and `industry` are required. The `industry` must be one of the app's industries, such as `Business and Consumer Services`. The slug is made from the name, here `acme-studio`.

```bash
curl -X POST https://tarkle.com/api/v1/brands \
  -H "Authorization: Bearer <your-tarkle-key>" \
  -H "X-Workspace-Key: <workspace-key>" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: <a-new-uuid>" \
  -d '{"name": "Acme Studio", "summary": "Design studio for small brands.", "industry": "Business and Consumer Services"}'
```

The answer is `201` with the whole brand. The fields you need next are `id`, `slug`, `access` (`restricted` for a new brand), `is_draft` (`true`), `indexable` (`false`) and `urls` with `page`, `markdown`, `mark_usage` and `api`.

**Publish the brand.** Send `is_draft: false` and `access: "public"` with `confirm_public: true`. Publishing lets anyone open the brand, so the call is refused with `400 confirmation_required` until you send the confirmation.

```bash
curl -X PATCH https://tarkle.com/api/v1/brands/acme-studio \
  -H "Authorization: Bearer <your-tarkle-key>" \
  -H "X-Workspace-Key: <workspace-key>" \
  -H "Content-Type: application/json" \
  -d '{"is_draft": false, "access": "public", "confirm_public": true}'
```

The answer is the whole brand again, now with `access: "public"`, `is_draft: false` and `indexable: true`. Brand calls need the person who made the key to be an owner or admin of that workspace.

## 7. Subscribe a webhook and verify the signature

A webhook is an address you register. Tarkle sends a signed `POST` to it when something happens to a brand in the workspace.

```bash
curl -X POST https://tarkle.com/api/v1/webhooks \
  -H "Authorization: Bearer <your-tarkle-key>" \
  -H "X-Workspace-Key: <workspace-key>" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://example.com/tarkle", "events": ["brand.updated", "brand.downloaded"]}'
```

The address must be `https` and a public name, on port 443 or from 1024 up. Events are `brand.viewed`, `brand.downloaded`, `brand.updated`, `brand.request.created`, `brand.request.approved` and `brand.request.rejected`. `["*"]` sends every event except `brand.viewed`, which is sent only when an endpoint names it. A workspace can have up to 20 endpoints.

The answer is `201` with `endpoint` and `secret`. The `secret` is shown only in this answer, so store it now. A rotation makes a new secret and returns it once, and for 24 hours every delivery is signed with both secrets.

Each delivery is a `POST` with `Content-Type: application/json` and four headers: `Tarkle-Event`, `Tarkle-Event-Id`, `Tarkle-Delivery` and `Tarkle-Signature`. The signature header has the form `t=<unix seconds>,v1=<hex>`. The body has `id`, `type`, `api_version`, `created_at`, `workspace_key` and `data`. Check the signature against the raw body, before you parse it:

```js
import { createHmac, timingSafeEqual } from "node:crypto";

export function verify(rawBody, header, secret) {
  const pairs = header.split(",").map((part) => part.split("="));
  const t = pairs.find(([key]) => key === "t")?.[1];
  if (!t || Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest();
  return pairs
    .filter(([key]) => key === "v1")
    .some(([, hex]) => {
      const given = Buffer.from(hex ?? "", "hex");
      return given.length === expected.length && timingSafeEqual(given, expected);
    });
}
```

The signed string is the timestamp, a dot and the body. The timestamp must be within 300 seconds of now. To send a test event, call `POST /webhooks/{id}/test`. Tarkle tries a delivery up to 8 times. An endpoint is switched off after 5 days without one success, and `PATCH /webhooks/{id}` with `"enabled": true` turns it on again. Reference: [Webhooks](/docs/api/webhooks).

## 8. Paging, rate limits, idempotency and errors

**Paging.** A list returns one page. Send `limit` and the `next_cursor` from the last page as `cursor`. Brand lists default to 25. `GET /files` pages the same way. `next_cursor` is `null` on the last page, and the cursor is opaque, so do not build or read it. See [Paging and limits](/docs/api/paging-and-limits).

**Rate limits.** A key can make 120 brand calls a minute and 600 files calls a minute. Every answer carries `X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset` (Unix seconds). A call over the limit is `429 rate_limited` with `Retry-After` in seconds. Read the limit from the headers instead of hard-coding it.

**Idempotency.** A `POST`, `PATCH`, `PUT` or `DELETE` can carry an `Idempotency-Key` header of 1 to 255 visible characters. The first answer is kept for 24 hours. The same request again returns that answer with `Idempotent-Replayed: true`. The same key on a different request is `422 idempotency_key_reused`, and a request still running is `409 idempotency_in_progress` with `Retry-After: 1`. Reads ignore the header. See [Idempotency](/docs/api/idempotency).

**Errors.** A failure is an `application/problem+json` document with `type`, `title`, `status`, `detail` and `code`. Match on `code`, not on the message. The codes you will meet first in this flow are `400 workspace_required` and `workspace_key_required`, `401 unauthorized` (the key is missing, invalid or revoked), `403 forbidden` (the person who made the key lacks the role for the call), `403 no_access`, `409 name_taken`, `409 conflict`, `400 confirmation_required` and `429 rate_limited`. Every code is listed on [Errors](/docs/api/errors).

## Which steps an AI agent can do through MCP

An AI agent that works through the [MCP server](/docs/mcp) can do most of this flow with the same key. Coding assistants connect to `https://mcp.tarkle.com/mcp` with `Authorization: Bearer <your-tarkle-key>`. The MCP tools cover step 2 (`tarkle_list_workspaces`), step 3 (`tarkle_list_folders` and `tarkle_create_folder`), step 4 (`tarkle_upload_text`, `tarkle_upload_from_url`, `tarkle_start_upload` with `tarkle_get_upload_status`, and `tarkle_get_upload_page` for a file on the person's own device), step 5 (`tarkle_create_transfer` and `tarkle_create_file_request`) and step 6 (`tarkle_list_brands`, `tarkle_get_brand`, `tarkle_create_brand` and `tarkle_update_brand`).

For how an agent picks between the MCP tools and the REST API, read the [Brand API skill](/docs/ai-agents/brand-skill).

Source: https://tarkle.com/docs/get-started/api-flow/index.mdx
