Skip to content

The Tarkle API flow

Create an API key, find a workspace, upload a file, publish a brand page and receive signed webhooks, with curl examples.

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.

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:

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.

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

The answer is 200 with a workspaces list:

{
  "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.

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.

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.

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.

{
  "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.

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).

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.

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.

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, 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.

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.

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.

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.

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:

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.

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.

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.

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.

Which steps an AI agent can do through MCP

An AI agent that works through the MCP server 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.

Type to search…

↑↓ navigate↵ selectEsc close