Skip to content

Developers

A REST API, signed webhooks and an MCP server for AI agents, over the same records, permissions and audit trail as the app. One API key is all you need: it acts as its own account, with the role, projects and scopes you give it.

At a glance

Base URL
https://api.getkonverge.com/v1 (JSON, UTF-8, HTTPS only).
Auth
Authorization: Bearer kvg_live_… — an API key from Settings → Developers. People connecting an AI app sign in with OAuth instead.
Records
Projects, tasks, RFIs, daily logs, snags, inspections, incidents, drawings, documents, contacts, estimates, transactions and more: list, get, create, change, delete and run actions.
Webhooks
Signed HTTPS POSTs for record events, set up in Settings or through the API (REST hooks).
MCP
https://api.getkonverge.com/mcp: Claude, ChatGPT, Cursor and your own agents, with a person confirming every change.
OpenAPI
3.1, generated from the code the API runs on. Import it into Postman, Insomnia or your code generator.

47

Record types that send webhook events

6

Scopes to narrow a key

First call

GET https://api.getkonverge.com/v1/me

Quickstart

  1. Make a key. An owner or admin opens Settings → Developers → API keys → New key in the web app, names it (“Zapier”, “Power BI”), picks its role, the projects it may see and its scopes, and copies it. The key starts with kvg_live_ and is shown once: keep it in your secret store, never in code or a browser.
  2. Check it. GET /v1/me answers with the key, its company, role, scopes and rate limits.
Check the key
curl https://api.getkonverge.com/v1/me \
  -H "Authorization: Bearer $KONVERGE_API_KEY"

List open tasks, then create one. Send an Idempotency-Key with every write, so a retry after a network error never makes a second task.

List tasks
curl "https://api.getkonverge.com/v1/tasks?status=pending,in_progress&limit=50" \
  -H "Authorization: Bearer $KONVERGE_API_KEY"
Create a task
curl https://api.getkonverge.com/v1/tasks \
  -H "Authorization: Bearer $KONVERGE_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 7f1c2e0a-5b6d-4c3e-9a8b-1d2e3f4a5b6c" \
  -d '{"project_id": "3f6c2a9e-8b1d-4c7e-9a52-6d0e4b1f7a38", "title": "Fix the fire door on level 2", "priority": "high"}'

A create answers 201 with the record and a Location header. The reference lists every resource, field, filter and action.

Authentication

API keys

Send the key on every request as Authorization: Bearer <key>. Each key has its own account in your company, so everything it does is recorded as “API: <key name>” and it can never do more than a person with the same role:

  • Role — admin, manager, member or viewer. Only the company owner can make an admin key.
  • Projects — member and viewer keys see only the projects you pick.
  • Scopes — narrow the role further; each is checked on its own (write does not include read).
  • Expiry — a year by default. Rotate a key to get a new secret with the same account; revoke it to cut it off at once.
ScopeAllows
records:readList and read records.
records:writeCreate and change records, and run actions (submit, approve, set status…).
records:deleteDelete records.
files:readDownload documents and photos.
files:writeUpload documents and photos.
webhooks:manageSubscribe addresses to events and remove them (REST hooks). Needs records:read too.

A missing, mistyped, revoked or expired key always gets the same 401. Keys from a test environment start with kvg_test_ and only work there.

OAuth (acting as a person)

An app that works as the person using it, such as an AI assistant, signs them in with Konverge’s OAuth 2.1 server instead of holding a key. The person sees a consent page in Konverge, and the app then has exactly their permissions; they can remove it any time in Settings. For now OAuth is offered through the MCP server. A person in several companies names one with the Konverge-Company: <company id> header.

Records and formats

  • Paths are the record type in plural: /v1/tasks, /v1/rfis, /v1/daily_logs, /v1/punch_items (snags). GET lists, GET /{id} reads one, POST creates, PATCH /{id} changes, DELETE /{id} deletes, POST /{id}/actions/{action} runs an action such as submit, approve or set_status.
  • Fields are the database column names in snake_case; ids are UUIDs; timestamps are RFC 3339 in UTC; dates are YYYY-MM-DD; amounts are JSON numbers with at most two decimals, always next to currency.
  • One record is { "data": { … } }; a list is { "data": [ … ], "has_more": …, "next_cursor": … }.
  • Your company is implied by the key: no company id in paths. Bank details, National Insurance numbers and dates of birth are never returned.

Pagination and filters

Lists are newest first and come in pages of limit records (1 to 100, default 25). When has_more is true, pass next_cursor back as cursor with the same filters:

A page of tasks
{
  "data": [
    { "id": "b1e0d7c4-…", "title": "Fix the fire door on level 2", "status": "pending", … }
  ],
  "has_more": true,
  "next_cursor": "eyJ2IjoiMjAyNi0xMC0wNFQwOTozMDowMFoiLCJpZCI6ImIxZTBkN2M0In0"
}
The next page
curl "https://api.getkonverge.com/v1/tasks?status=pending,in_progress&limit=50&cursor=eyJ2IjoiMjAy…" \
  -H "Authorization: Bearer $KONVERGE_API_KEY"
  • Filter on the fields each resource lists: ?status=open,in_progress (a comma means “any of”), ?project_id=…, ?due_date[gte]=2026-10-01, ?assigned_to=null.
  • ?updated_since=<timestamp> returns what changed since your last sync; ?q= searches text fields.
  • An unknown parameter is an error (400 unknown_filter), never quietly ignored.

Idempotency

Send Idempotency-Key: <any unique string> (a UUID is ideal) on every POST, PATCH and DELETE. For 24 hours:

  • The same key with the same request returns the first answer again, with Idempotent-Replayed: true, and does nothing twice.
  • The same key with a different request is 409 idempotency_key_reused; while the first is still running, 409 idempotency_in_progress.
  • A request refused before anything was written (a 422, a 429, a 5xx) is not kept, so fix it and retry with the same key.

Rate limits

Every request takes a token from two buckets that refill every second: the key’s own (writes per minute as set on the key, reads five times that) and your plan’s, which all of your company’s keys share. The request goes ahead only when both have a token. GET /v1/me shows both.

HeaderMeans
RateLimit-Limit / X-RateLimit-LimitThe size of the bucket this request drew from.
RateLimit-Remaining / X-RateLimit-RemainingRequests left in it right now.
RateLimit-ResetSeconds until it is full again.
X-RateLimit-ResetThe same moment as a Unix time (seconds).
Konverge-RateLimit-Scopekey (this key’s own bucket) or plan (the bucket your plan shares between all your keys).
Retry-AfterOn a 429 or 503 only: seconds to wait before trying again.

The headers describe whichever bucket is tighter. When one is empty the answer is 429 rate_limited with Retry-After, and the message says whether it was the key or the plan. A key’s own limit is set when the key is made (120 writes a minute unless agreed otherwise; ask support for more); the plan’s grows with a larger plan, in Settings → Billing.

Errors

Every error has a stable code, a message written for people (never database text) and a request_id to quote to support; 422 adds each field’s problem.

An error
{
  "error": {
    "code": "validation_failed",
    "message": "Some fields are not valid. See details.",
    "request_id": "req_4c1f9b2d7e6a8035c2d1e9f0",
    "details": [{ "field": "project_id", "message": "No project with this id in your company, or this key cannot see it." }]
  }
}
StatusCodesMeans
400bad_request · unknown_filter · invalid_cursorThe request itself is malformed: a query parameter, the cursor, the JSON or the Idempotency-Key.
401unauthenticatedNo valid key: missing, mistyped, revoked or expired (one answer for all).
403insufficient_scope · forbidden · company_read_onlyThe key may not do this: a missing scope, its role, or a company whose subscription has ended.
404not_foundNo such record in your company, or this key cannot see it.
405method_not_allowedThe resource does not support that method or action. See the Allow header.
409conflict · idempotency_key_reused · idempotency_in_progressThe record’s state does not allow it yet, or the Idempotency-Key was reused.
422validation_failedA field is missing or wrong. details lists each one.
429rate_limitedToo many requests. Wait Retry-After seconds.
500internal_errorOur fault. A write may or may not have happened: check before retrying.
503unavailable · api_not_configuredBusy or switched off here. Retry after Retry-After seconds.

Webhooks

Konverge POSTs an event to your HTTPS address within about a minute of a change. Add a webhook in Settings → Developers → Webhooks, or from code with a key that has webhooks:manage (a REST hook: the way Zapier, Make and n8n subscribe):

Subscribe an address
curl https://api.getkonverge.com/v1/webhook_subscriptions \
  -H "Authorization: Bearer $KONVERGE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"target_url": "https://hooks.example.com/konverge", "events": ["task.created", "task.status_changed"]}'

The answer includes the signing secret once; store it. GET /v1/webhook_subscriptions lists the key’s webhooks and DELETE /v1/webhook_subscriptions/{id} removes one. The address must be public HTTPS on port 443: private, loopback and cloud-metadata addresses are refused, and so are redirects.

Events

Event names are <object>.<verb>, <object>.* or *. Verbs: created, status_changed, assigned, deleted (an edit that changes neither the status nor the assignee sends no event). Objects: project, task, daily_log, punch_item, inspection, incident, field_note, milestone, comment, contact, transaction, change_order, payment_application, purchase_order, po_receipt, subcontract, bill, location, site_induction, site_access_override, permit, schedule_baseline, task_dependency, delivery, material_request, meeting, rfi, submittal, workflow, technical_query, early_warning, compensation_event, instruction, ncr, project_contract, inbound_email, bid_package, bid, service_request, handover_pack, site_attendance, estimate, estimate_award, client_payment, selection_item, project_allowance, client_update.

An event
{
  "id": "evt_3f6c2a9e-8b1d-4c7e-9a52-6d0e4b1f7a38",
  "type": "task.status_changed",
  "api_version": "v1",
  "created_at": "2026-10-04T10:00:00Z",
  "company_id": "b1e0d7c4-5a3f-4e2b-8c19-0f6a7d2e9b54",
  "actor": { "type": "user", "id": "…", "name": "Sam Carter" },
  "data": {
    "object": "task",
    "id": "3f6c2a9e-8b1d-4c7e-9a52-6d0e4b1f7a38",
    "project_id": "b1e0d7c4-5a3f-4e2b-8c19-0f6a7d2e9b54",
    "changes": { "status": { "from": "pending", "to": "completed" } },
    "record": { "id": "3f6c2a9e-…", "title": "Fix the fire door on level 2", "status": "completed", … }
  }
}

record is read when the event is sent, as the webhook’s API key: an event about a record that key cannot see is never sent. estimate, estimate_award, client_payment and project_allowance are money records: they go only to a key that sees money figures. Deliveries are at least once, so ignore an id you have already handled.

Check the signature

Konverge-Signature: t=<unix seconds>,v1=<hex>, where v1 is HMAC-SHA256 of <t>.<raw body> keyed with the whole secret (whsec_ included). Use the body exactly as received, compare in constant time, and refuse a t more than five minutes old. For a day after you rotate the secret there are two v1 values; accept either.

Node.js
const crypto = require('crypto');

// header:  the Konverge-Signature request header
// rawBody: the request body exactly as received (Buffer or string), before JSON parsing
// secret:  the whsec_… value Konverge showed when you created the webhook
function verifyKonvergeSignature(header, rawBody, secret, toleranceSeconds = 300, nowSeconds = Math.floor(Date.now() / 1000)) {
  if (typeof header !== 'string' || !secret) return false;
  let timestamp = null;
  const signatures = [];
  for (const part of header.split(',')) {
    const eq = part.indexOf('=');
    const name = part.slice(0, eq).trim();
    const value = part.slice(eq + 1).trim();
    if (name === 't' && /^\d+$/.test(value)) timestamp = Number(value);
    if (name === 'v1' && /^[0-9a-f]{64}$/.test(value)) signatures.push(value);
  }
  if (timestamp === null || signatures.length === 0) return false;
  if (Math.abs(nowSeconds - timestamp) > toleranceSeconds) return false;
  const expected = crypto.createHmac('sha256', secret).update(`${timestamp}.`).update(rawBody).digest();
  return signatures.some((sig) => crypto.timingSafeEqual(Buffer.from(sig, 'hex'), expected));
}
Express route
app.post('/konverge', express.raw({ type: 'application/json' }), (req, res) => {
  if (!verifyKonvergeSignature(req.get('Konverge-Signature'), req.body, process.env.KONVERGE_WEBHOOK_SECRET)) {
    return res.status(400).send('bad signature');
  }
  const event = JSON.parse(req.body.toString('utf8'));
  // Ignore an event you have already handled: event.id (= Konverge-Event-Id).
  res.status(204).end();
  // …then process `event`.
});
Python
import hashlib
import hmac
import time


def verify_konverge_signature(header, raw_body, secret, tolerance_seconds=300, now=None):
    """header: the Konverge-Signature header; raw_body: the request body as bytes, exactly as received;
    secret: the whsec_... value Konverge showed when you created the webhook."""
    if not header or not secret:
        return False
    timestamp, signatures = None, []
    for part in header.split(","):
        name, _, value = part.strip().partition("=")
        if name == "t" and value.isdigit():
            timestamp = int(value)
        elif name == "v1" and len(value) == 64:
            signatures.append(value)
    if timestamp is None or not signatures:
        return False
    now = int(time.time()) if now is None else now
    if abs(now - timestamp) > tolerance_seconds:
        return False
    expected = hmac.new(secret.encode("utf-8"), str(timestamp).encode("ascii") + b"." + raw_body, hashlib.sha256).hexdigest()
    return any(hmac.compare_digest(expected, s) for s in signatures)

Retries

Answer any 2xx within 10 seconds and do slow work afterwards. A failed delivery is tried eight times in all over about three days (after 5 and 30 minutes, then 2, 6, 12, 24 and 24 hours); after 20 events in a row fail, the webhook is turned off and owners are told. You can replay any delivery from the log. Answer 410 Gone to unsubscribe: a webhook made through the API is then deleted.

MCP: connect AI agents

Konverge is a remote Model Context Protocol server at https://api.getkonverge.com/mcp. An AI app signs the person in with OAuth and then searches, reads and drafts changes as that person, with exactly their permissions. The model never writes on its own: every change is a proposal with a preview, and bigger changes (status, money, deletions) need the person to approve it in the AI app or type a short confirmation phrase.

  • Claude (claude.ai and Claude Desktop): Settings → Connectors → Add custom connector → paste the address → Connect, then allow Konverge on its consent page.
  • ChatGPT: Settings → Apps & Connectors → Advanced → turn on Developer mode → Create a connector → MCP server URL = the address, authentication OAuth.
  • Cursor: add Konverge to ~/.cursor/mcp.json, then Settings → MCP → Konverge → Log in.
Cursor: mcp.json
{
  "mcpServers": {
    "konverge": { "url": "https://api.getkonverge.com/mcp" }
  }
}
Claude Code
claude mcp add --transport http konverge https://api.getkonverge.com/mcp

An agent that runs with no person present uses an API key instead; the key’s role and scopes apply, and its changes still go through the proposal step.

Headless agent with an API key
claude mcp add --transport http konverge https://api.getkonverge.com/mcp \
  --header "Authorization: Bearer $KONVERGE_API_KEY"

Menu names change between app versions: look for “custom connector”, “MCP server” or “integrations”. Each person sees and removes the apps they connected in Konverge’s settings.

Zapier, Make and n8n

Zapier

The Konverge app for Zapier has instant triggers for new and changed projects, tasks, daily logs, snags, contacts, invoices, change orders and bills, polling triggers for RFIs, searches for projects and contacts, and actions to create tasks, snags, daily logs and contacts and to comment on or answer an RFI. Connect it with an API key that has records:read, webhooks:manage and, for actions, records:write. The app is in private testing: ask support for an invite until it is listed in the Zapier directory.

Make, n8n and everything else

  • Actions: use an HTTP module (Make: HTTP → Make a request; n8n: HTTP Request with a Header Auth credential) with Authorization: Bearer <key> and a JSON body, as in the quickstart.
  • Triggers: create a webhook in the tool (Make: Webhooks → Custom webhook; n8n: Webhook node), then subscribe its address to the events you want, in Settings → Developers or with POST /v1/webhook_subscriptions.
  • OpenAPI: getkonverge.com/openapi.json imports into Postman, Insomnia, Bruno and OpenAPI code generators.

Versioning and support

/v1 only changes by adding things: new resources, fields, filters and values. Anything retired is announced first with Deprecation and Sunset headers, and a change that would break an integration becomes /v2. Releases are in the changelog.

Questions, or something not working? Email support@getkonverge.com with the request_id (also the Request-Id header) of the call.

[A,1][F,1][A,6][F,6]

14-day free trial with your card at sign-up: cancel before day 15 and you pay nothing. Bring a live job, your drawings and your team, and see it on site the same day.