FixControl/Documentation

User Guide

Public API

Tenant-scoped REST API for issues, patches, integrations, and webhooks. Bearer-token authentication.

The FixControl Public API lets you read and act on FixControl from your own tools: a custom dashboard, a planning script, an internal automation, or your organization's own chatbot. The base path is /api/public/v1/ on your FixControl host.

Before you start

  • Role — creating and revoking API keys needs the admin or manager role in the workspace.
  • One key per workspace — a key belongs to the workspace it was created in and can only reach that workspace's data.

Creating an API key

  1. Open Settings → Channels. In the Webhooks & API section, open Public API (or go straight to /settings/integrations/api).
  2. Click Create API key at the top of the page.
  3. Give the key a Name (so you remember what it's for) and tick its Scopes (see below). issues:read is ticked by default; tick at least one.
  4. Click Create key.
  5. The full token (it starts with fck_live_) is shown once. Copy it into your secret manager before you click I've copied it. Closing the dialog hides the token for good.

Afterwards the list shows the key's name, its prefix, its scopes, when it was Last used, and its status (active or revoked). The full token cannot be recovered. If you lose it, revoke the key and create a new one.

Scopes

A key carries one or more scopes. Pick the smallest set that does the job.

ScopeEndpoints
issues:readGET /api/public/v1/issues, GET /api/public/v1/issues/:key
issues:writePOST /api/public/v1/issues
patches:readGET /api/public/v1/patches, GET /api/public/v1/patches/:id
patches:writeReserved for future write actions on patches; no endpoints today.
integrations:readGET /api/public/v1/integrations/status
webhooks:readGET /api/public/v1/webhooks
webhooks:writePOST /api/public/v1/webhooks
hr:questionsPOST /api/public/v1/hr/questions — submit an HR question on behalf of an employee your chatbot has already signed in.
comm:stagePOST /api/public/v1/communications, GET /api/public/v1/communications/:id — propose a customer e-mail for human approval and read its status back.

Two scopes are a trust decision, not just a permission:

  • `hr:questions` lets the key say which employee is asking. Grant it only to a chatbot that really authenticates its users. It never widens what that employee can reach: FixControl still checks the employee's membership, their HR permission and whether HR is switched on for the project.
  • `comm:stage` lets the key propose a customer message. It cannot send anything: every proposal waits in Approvals until a person in FixControl approves or rejects it.

A request to an endpoint whose scope the key does not carry returns 403 with code: insufficient_scope.

Authentication

Every request needs an Authorization: Bearer <token> header. The token is bound to one workspace; there is no separate workspace parameter, and the workspace is always taken from the key.

curl https://<your-host>/api/public/v1/issues \
  -H "Authorization: Bearer fck_live_a1b2c3d4..."
SituationResponse
No token401, code: missing_token
Unknown or revoked token401, code: invalid_token
Valid token without the right scope403, code: insufficient_scope

Rate limits

Each key has a budget of 60 requests per minute (one per second sustained, with a burst of 60). The budget is per key, so two keys in the same workspace don't share it.

POST /api/public/v1/communications has an extra, stricter budget of 10 per minute per key on top of that, because every accepted call puts a review in front of a person.

When a budget is used up you receive 429 Too Many Requests with a Retry-After header (in seconds) and code: rate_limited. If you need a higher limit for a legitimate use case, contact us.

Endpoints

Issues

# List issues for the key's workspace
GET /api/public/v1/issues
→ { "items": PublicIssue[] }

# Fetch one issue by key
GET /api/public/v1/issues/:key
→ PublicIssue

# Create an issue
POST /api/public/v1/issues
{
  "title":    "Checkout fails on mobile",   // required
  "kind":     "BUG",                         // required: BUG | FEATURE | SUPPORT
  "priority": "high",                        // required: critical | high | medium | low
  "reporter": "alex@acme.com",               // required
  "summary":  "Optional short summary",
  "body":     "Optional long description",
  "customer": "Optional customer name",
  "flags":    ["payment"],                   // up to 32 string flags
  "assignee": "Optional internal id"
}
→ 201 Created → PublicIssue

There is no project field: an issue created through the API is created without a project. Link it to a project in FixControl afterwards if the work needs one.

PublicIssue shape:

{
  "key": "BIZ-29",
  "tenant": "acme",
  "kind": "BUG",
  "title": "Checkout fails on mobile",
  "summary": "...",
  "body": "...",
  "priority": "high",
  "status": "intake",
  "readiness": "investigate",
  "confidence": 0.82,
  "flags": ["payment"],
  "reporter": "alex@acme.com",
  "assignee": null,
  "customer": null,
  "createdAt": "2026-05-03T10:14:22.512Z",
  "updatedAt": "2026-05-03T10:14:22.512Z",
  "externalTracker": "jira",
  "externalKey": "ACME-1234",
  "externalUrl": "https://acme.atlassian.net/browse/ACME-1234",
  "externalStatus": "In Progress"
}

Patches

# List patches for the key's workspace (newest first)
GET /api/public/v1/patches?limit=100      # limit: 1..200
→ { "items": PublicPatch[] }

# Fetch one patch
GET /api/public/v1/patches/:id
→ PublicPatch

PublicPatch carries the patch id, workspace id, version, status, file count, additions and deletions, the file list, and timestamps.

Integrations

GET /api/public/v1/integrations/status
→ { "items": [
    { "id": "...", "type": "github", "name": "...", "status": "connected",
      "provider": "github", "authKind": "app",
      "createdAt": "...", "lastError": null },
    ...
  ] }

No tokens, refresh tokens, installation ids, or recipient routing rules are returned, only the public-safe summary.

Webhooks

# List outbound webhooks
GET /api/public/v1/webhooks
→ { "items": PublicWebhook[] }

# Create one. Returns the plaintext secret exactly once.
POST /api/public/v1/webhooks
{
  "name":    "Billing notifier",
  "url":     "https://hooks.acme.com/fc",
  "events":  ["issue.created", "patch.approved"],
  "enabled": true
}
→ 201 Created → { "webhook": PublicWebhook, "secret": "ws_AB3cD..." }

name, url and at least one valid event are required. Valid event values: issue.created, issue.updated, patch.ready, patch.approved, patch.failed, pr.created, integration.error. See the Webhooks guide for the delivery format and signature scheme.

HR questions

POST /api/public/v1/hr/questions
{
  "projectId":      "prj_...",               // an HR-enabled project
  "employeeEmail":  "jane@acme.com",         // the employee your chatbot signed in
  "question":       "How do I request parental leave?",
  "idempotencyKey": "chat-msg-8f3k2j1a",     // your own message id, 8–200 chars
  "locale":         "en"                     // optional: en | nl
}
→ 201 Created → { "status": "...", "decision": "...", "userMessage": "...",
                  "answerText": "...", "sources": [{ "name": "...", "version": "..." }],
                  "caseReference": "..." }

FixControl handles the question exactly as it would from Teams or Slack: it classifies it, checks the sources and decides whether to answer, ask for clarification, or route it to HR. answerText and sources are only present when an answer was given. A sensitive case does not return its reference. Sending the same idempotencyKey again returns 200 with { "status": "ALREADY_PROCESSED" }. An unknown employee, a non-member, or a project without HR all return the same 404 not_found; 409 employee_binding_missing means the employee's link to the HR system is missing and HR can fix it. See HR questions.

Customer communications

# Propose one customer e-mail for human approval
POST /api/public/v1/communications
{
  "contactEmail":   "info@acme.com",         // or "contactId"; the contact must exist
  "subject":        "Following up on your request",
  "body":           "Hello ...",
  "idempotencyKey": "growth-lead-1234",      // 8–200 chars
  "generatedByAI":  true,                    // optional, default false
  "sourceContext":  { "campaign": "q4" }     // optional, up to 20 simple values
}
→ 201 Created → PublicCommunication

# Read its status back
GET /api/public/v1/communications/:id
→ PublicCommunication

PublicCommunication holds the id, status (pending_approval, sent, rejected, failed or discarded), the approval id, contact and customer ids, the provider and message id once sent, and timestamps. It never includes the subject or body.

  • The contact must already exist in Contacts; an unknown address returns 404 contact_not_found.
  • Sending the same idempotencyKey again returns 200 with the existing communication instead of a second approval.
  • generatedByAI: true can only make the review stricter.
  • 500 stage_failed means nothing was left pending; retry with the same idempotencyKey.

Error responses

Every error returns JSON in this shape:

{ "error": "Human-readable message", "code": "stable_machine_code" }

The HTTP status carries the category; code is the stable identifier for client logic.

Statuscode examples
400missing_field, invalid_field, invalid_name, invalid_url, invalid_events, invalid_body
401missing_token, invalid_token
403insufficient_scope
404not_found, contact_not_found
409employee_binding_missing
422missing_recipient_email
429rate_limited (with Retry-After header)
5xxinternal_error, stage_failed

OpenAPI

The OpenAPI 3.1 spec is served without authentication at:

GET /api/public/v1/openapi.json

Point your code generator at it. It describes every endpoint on this page.

Revoking a key

  1. Open Settings → Channels → Public API.
  2. Click the bin icon (Revoke) on the key's row and confirm.

Revocation takes effect immediately: keys are checked against the database on every request, with no cache, so the next request with that key gets 401 invalid_token. The row stays in the list with status revoked. Then check the audit log for activity by that key.

Best practices

  • Treat the token like a password. Never commit it; never print it in logs.
  • Use one key per integration. Easier to replace, easier to audit, easier to scope.
  • Replace keys periodically. Create the new key, switch your client, then revoke the old one.
  • Prefer webhooks over polling. Subscribe an outbound webhook instead of polling GET /issues.
  • Handle 429s with backoff. Honour the Retry-After header instead of retrying immediately.

FAQ

Is the API the same as the dashboard's internal API? No. The dashboard talks to internal routes that require a session cookie and are not stable. The Public API at /api/public/v1/... is the versioned, OpenAPI-described contract.

Can a key access more than one workspace? No. A key belongs to one workspace. Working across workspaces needs a key per workspace.

Can an API key approve or send a customer message? No. A key with comm:stage can only propose one. Approving and sending is done by a person in FixControl.

Anything unclear or wrong?Let us know →

FixControl is a trade name of FixControl B.V.