API reference
Build on KinCo
A REST API for managing shared ownership data programmatically. Authenticate with an API key or a session token, receive webhooks when your data changes, and integrate KinCo into your stack.
On this page
Quick start
-
Request an API key
Fill out the request form below and we'll set up a key for you. Keys are prefixed kinco_ and are read-only unless they have the write scope, so tell us if you need to make changes. Store your key securely.
-
Authenticate
Pass your key as a bearer token in every request: Authorization: Bearer kinco_…
-
Query your data
All endpoints live under https://api.kinco.io/api/v1 and return JSON. Lists that can grow are paginated.
curl https://api.kinco.io/api/v1/groups \
-H "Authorization: Bearer kinco_your_key_here" \
-H "Accept: application/json" {
"items": [
{
"id": "8c0f3b1e-5d2a-4f6b-9a7e-2c1d0e9f8a7b",
"name": "Dubai Villa Partnership",
"base_currency": "AED",
"created_by": "3e7a9c2b-1f4d-4b8e-a6c5-0d9e8f7a6b5c",
"created_at": "2024-09-01T10:00:00Z",
"updated_at": "2024-09-01T10:00:00Z",
"members": [...],
"member_count": 3
}
],
"total": 1,
"limit": 100,
"offset": 0,
"has_more": false
} Authentication
API Key
Best for server-to-server integrations. A key acts as the user it belongs to, with that user's role in each group, and can have an expiry date. Keys are issued on request: request access.
- read: every key can call
GETendpoints. A key with no scopes is read-only. - write: needed for
POST,PUT,PATCHandDELETE, including POSTs that only read, such as insights refresh and KinCoPilot. Without it:403 insufficient_scope. -
No key can use session-only endpoints,
whatever its scopes: managing API keys and webhooks, billing, linking or unlinking bank connections, managing members and invitations, sending referral emails, and deleting a group or your account. Those return
403 session_required.
Authorization: Bearer kinco_… Session (user JWT)
Best for apps acting on behalf of a signed-in user. Pass the Supabase session token in the same header. Sessions have no scopes and can use every endpoint, including the session-only ones.
Authorization: Bearer eyJ… Role-based access
On top of the rules above, each group checks your role in it, whether you use a session or a key.
- admin
Everything an editor can do, plus managing members, invitations, roles and liabilities, and deleting most records.
- editor
Create and update assets, valuations, transactions, contributions, distributions and documents, and change group settings.
- viewer
Read-only access to all group data.
Deleting a group is limited to the member who created it.
Pagination
Lists that can grow (groups, assets, valuations, asset transactions, recurring
transactions, contributions, distributions, settlements, ownership shares, documents
and activity) return this envelope. Use the limit (default
50, 100 for groups; at most 100) and offset query params to
page through them. Shorter lists, such as members, invitations, categories, liabilities,
exits, connections and insights, return a plain JSON array.
{
"items": [...],
"total": 142,
"limit": 50,
"offset": 0,
"has_more": true
} Errors
Errors are JSON with a detail field. Most are a plain
message; the ones you can act on in code carry an object with an
error code. A
422 lists each invalid field in
detail.
{ "detail": "Not authenticated" } // no Authorization header
{ "detail": "Invalid API key" } // unknown or revoked key
{ "detail": "API key expired" }
{ "detail": "Invalid token: ..." } // expired or malformed session token HTTP/1.1 403 Forbidden
WWW-Authenticate: Bearer error="insufficient_scope"
{
"detail": {
"error": "insufficient_scope",
"required_scope": "write",
"message": "This API key is read-only. Use a key with the 'write' scope to make changes."
}
} HTTP/1.1 403 Forbidden
WWW-Authenticate: Bearer error="insufficient_scope"
{
"detail": {
"error": "session_required",
"message": "API keys can't use this endpoint, whatever their scopes. Sign in to KinCo to do this."
}
} HTTP/1.1 429 Too Many Requests
Retry-After: 30
{
"detail": {
"error": "rate_limited",
"rule": "insights_refresh",
"limit": "2 per minute",
"retry_after": 30,
"message": "Too many requests. Please try again in 30 seconds."
}
} -
Other 403s are about the group or the plan: a plain message when you aren't a member
of the group or your role can't do this, and an object with
"error": "upgrade_required"or"error": "limit_reached"when the plan doesn't include a feature or one of its limits is reached (for a group's data, the group owner's plan applies). -
A 429 with
"error": "quota_exceeded"means a plan quota is used up (daily AI refreshes and KinCoPilot messages, monthly PDF exports). It has no Retry-After: the quota resets at the start of the next day or month (UTC). -
A 503 with
Authentication service temporarily unavailablemeans we couldn't check a session token just then. Retry; you aren't signed out.
Rate limits
A limit of N per period allows a burst of N requests, then refills evenly: one more request every period ÷ N. Every authenticated request counts against its caller's limit, and the endpoints listed below also have their own.
Every request
- Per API key60 per minute
The key's own limit: 60 unless a different limit was set for your key
- Per signed-in user300 per minute
Requests made with a session
- Per client IP1,200 per minute
Every request, checked before authentication
Endpoint limits
Per user: all of your API keys and sessions share them.
-
POST /groups10 per minute, 100 per hour/{group_id} /copilot /messages -
POST /groups2 per minute, 10 per hour/{group_id} /insights /refresh -
GET /groups2 per minute, 10 per hour/{group_id} /export /portfolio-pdf -
POST /groups1 per minute per account/{group_id} /connections /accounts /{account_id} /sync -
POST /groups1 per minute per group/{group_id} /connections /sync-all?force=true -
Bank balance refreshes: sync, sync-all with force=true, reactivate30 per hour, combined -
POST /groups20 per hour/{group_id} /connections /link-token -
POST /groups10 per hour/{group_id} /connections /exchange-token -
POST /groups10 per minute/{group_id} /assets /{asset_id} /valuations /sync-market -
POST /groups30 per minute, combined/{group_id} /documents and documents /upload-url -
POST /groups20 per hour, 50 per day, combined/{group_id} /invitations and invitations /{invitation_id} /resend -
POST /referrals10 per hour, 30 per day/invite
Endpoints without auth
-
GET /invitations20 per minute per IP/token /{token} -
GET /billing60 per minute per IP/plans
Over a limit, the API returns 429 Too Many Requests with
a Retry-After header in seconds and a
rate_limited body naming the
rule you hit (see Errors). Wait that long before retrying. Limits are counted on each API server separately,
so you may sometimes get a little more than the stated rate; build for the numbers
above.
Webhooks
KinCo sends an HTTPS POST to your endpoint when something
changes in a group. Webhooks are in beta and can't be set up in the KinCo app yet.
Request access, tell us the group and your HTTPS
URL, and we'll set one up with you. API keys can't manage webhooks: the
/webhooks endpoints are session only.
-
Each webhook belongs to one group (
group_idis required) and receives only that group's events, and only while the person who registered it is an active member of the group. -
Subscribe to the events you need, or leave
eventsempty to receive every event, including ones added later. - The URL must use https and point to a public internet address. Redirects aren't followed, so register the final URL.
- The signing secret is shown once, when the webhook is created. To rotate it, ask us to re-create the webhook.
-
A test ping sends a
webhook.testevent to that webhook only, at most once every 10 seconds. An account can have up to 20 webhooks.
Events
- group.updated
- member.joined
- member.invited
- asset.created
- asset.updated
- asset.deleted
- valuation.created
- valuation.updated
- contribution.created
- distribution.created
- transaction.created
- document.uploaded
Payload and headers
data is the resource as the API returns it.
id identifies the event and is the same in every delivery
of it, retries included.
POST /kinco/webhooks HTTP/1.1
Content-Type: application/json
User-Agent: KinCo-Webhooks/1.0
X-KinCo-Event: asset.created
X-KinCo-Event-Id: 6f1c2d4e-8a7b-4c3d-9e2f-1a2b3c4d5e6f
X-KinCo-Signature: sha256=<hex HMAC-SHA256 of the raw body>
{
"id": "6f1c2d4e-8a7b-4c3d-9e2f-1a2b3c4d5e6f",
"event": "asset.created",
"resource_type": "asset",
"resource_id": "2b9e4c1a-7d3f-4e8b-9c6a-5f0e1d2c3b4a",
"group_id": "8c0f3b1e-5d2a-4f6b-9a7e-2c1d0e9f8a7b",
"data": { "id": "2b9e4c1a-...", "name": "Marina apartment", ... },
"timestamp": "2026-10-13T12:22:05.123456+00:00"
} Delivery and retries
-
Delivery is at least once. A failed delivery is retried with backoff, from 10 seconds
up to 1 hour between attempts, for up to 24 hours. De-duplicate on the event
id(also sent asX-KinCo-Event-Id). -
Events can arrive out of order. Use
timestamp, or fetch the resource, when you need its latest state. -
Retried: no answer (connection failure or timeout), a DNS failure, and
408,429or any5xx. Done: any2xx. Any other4xxor a3xxisn't retried. -
Each delivery times out after 20 seconds, and only the status code is read. Answer
with a
2xxfirst, then process the event. - Retries stop if the webhook is turned off or deleted, or the person who registered it leaves the group.
- A new webhook can miss events for up to 30 seconds after it's created, while every API server picks it up.
Verify the signature
X-KinCo-Signature is
sha256= followed by the hex HMAC-SHA256 of the raw request
body, keyed with your webhook's signing secret. Use the secret string as-is (don't
hex-decode it). Compute it over the bytes you received, before parsing the JSON
(re-serialized JSON won't match), and compare in constant time.
import crypto from 'node:crypto';
import express from 'express';
const app = express();
// Shown once, when you created the webhook
const secret = process.env.KINCO_WEBHOOK_SECRET;
// express.raw keeps the exact bytes KinCo signed: verify before parsing JSON
app.post('/kinco/webhooks', express.raw({ type: 'application/json' }), (req, res) => {
const received = Buffer.from(req.get('X-KinCo-Signature') || '');
const expected = Buffer.from(
'sha256=' + crypto.createHmac('sha256', secret).update(req.body).digest('hex'),
);
if (received.length !== expected.length || !crypto.timingSafeEqual(received, expected)) {
return res.status(401).end();
}
const event = JSON.parse(req.body.toString('utf8'));
// Retries reuse event.id (also in X-KinCo-Event-Id): skip ids you've already handled
res.status(200).end(); // answer within 20 s, then do the work
}); import hashlib
import hmac
import json
import os
from flask import Flask, abort, request
app = Flask(__name__)
# Shown once, when you created the webhook
SECRET = os.environ["KINCO_WEBHOOK_SECRET"].encode()
@app.post("/kinco/webhooks")
def kinco_webhook():
body = request.get_data() # the raw bytes KinCo signed: verify before parsing JSON
expected = "sha256=" + hmac.new(SECRET, body, hashlib.sha256).hexdigest()
received = request.headers.get("X-KinCo-Signature", "")
if not hmac.compare_digest(expected.encode(), received.encode()):
abort(401)
event = json.loads(body)
# Retries reuse event["id"] (also in X-KinCo-Event-Id): skip ids you've already handled
return "", 200 # answer within 20 s, then do the work Endpoints
Base URL: https://api.kinco.io/api/v1. Paths below are
relative to it.
Unless marked, an endpoint takes a session token or an API key. Any API key can call
GET; every other method needs a key with the
write scope.
Needs a signed-in session. API keys get
403 session_required, whatever their scopes.
Public: no Authorization header needed.
Groups
- GET
/groupsList the groups you belong to, with their members (paginated)
- POST
/groupsCreate a group; you become its admin (counts toward your plan's group limit)
- GET
/groups/{group_id} Get a group with its members
- PATCH
/groups/{group_id} Update the group name or base currency (admin or editor)
- DELETE
/groupsSession only/{group_id} Delete a group (only the member who created it)
Assets
- GET
/groups/{group_id} /assets List assets (paginated; filter with asset_type, search with q)
- POST
/groups/{group_id} /assets Create an asset (real_estate, bank_account, investment_account, vehicle, business, collectible, crypto, other)
- GET
/groups/{group_id} /assets /{asset_id} Get an asset with its latest valuation
- PATCH
/groups/{group_id} /assets /{asset_id} Update asset details
- DELETE
/groups/{group_id} /assets /{asset_id} Delete an asset (admin only)
- GET
/groups/{group_id} /assets /{asset_id} /full Everything about one asset in one call: metrics, valuations, transactions, contributions, distributions, ownership, members, documents, liabilities and exit
Valuations
- GET
/groups/{group_id} /assets /{asset_id} /valuations List valuations, newest first (paginated)
- POST
/groups/{group_id} /assets /{asset_id} /valuations Add a valuation (admin or editor)
- POST
/groups/{group_id} /assets /{asset_id} /valuations /sync-market Add a valuation from the current market price (assets with a ticker)
- PATCH
/groups/{group_id} /assets /{asset_id} /valuations /{valuation_id} Update a valuation (admin or editor)
- DELETE
/groups/{group_id} /assets /{asset_id} /valuations /{valuation_id} Delete a valuation (admin only)
Asset Transactions
- GET
/groups/{group_id} /assets /{asset_id} /transactions List an asset's income and expense transactions, newest first (paginated)
- POST
/groups/{group_id} /assets /{asset_id} /transactions Record a transaction (income or expense, with a category and optional splits between members)
- GET
/groups/{group_id} /assets /{asset_id} /transactions /summary Income, expenses and net operating income for the asset, by category
- GET
/groups/{group_id} /assets /{asset_id} /transactions /{transaction_id} Get one transaction
- PATCH
/groups/{group_id} /assets /{asset_id} /transactions /{transaction_id} Update a transaction
- DELETE
/groups/{group_id} /assets /{asset_id} /transactions /{transaction_id} Delete a transaction (admin only)
Categories
- GET
/groups/{group_id} /categories List the group's income and expense categories
- POST
/groups/{group_id} /categories Create a custom category (a label, and income or expense)
- PATCH
/groups/{group_id} /categories /{category_id} Rename, reorder, or turn a category on or off (its key and type can't change)
- DELETE
/groups/{group_id} /categories /{category_id} Delete a category; one still used by transactions or recurring templates is turned off instead, so past entries keep their label
Recurring Transactions
- GET
/groups/{group_id} /recurring-transactions List recurring transaction templates (paginated)
- POST
/groups/{group_id} /recurring-transactions Create a template, e.g. monthly rent; frequency is the months between occurrences (1 = monthly, 3 = quarterly, 12 = yearly)
- GET
/groups/{group_id} /recurring-transactions /{recurring_transaction_id} Get a template
- PATCH
/groups/{group_id} /recurring-transactions /{recurring_transaction_id} Update the amount, frequency or dates, or pause it with is_active
- DELETE
/groups/{group_id} /recurring-transactions /{recurring_transaction_id} Delete a template; transactions already recorded stay (admin only)
- POST
/groups/{group_id} /recurring-transactions /{recurring_transaction_id} /record Record one occurrence as a real transaction, today or on a given date (the template needs an asset)
Asset Exits
- GET
/groups/{group_id} /exits List all exited assets with realized returns in base currency
- GET
/groups/{group_id} /assets /{asset_id} /exit Get the exit record for an asset (404 if not yet exited)
- POST
/groups/{group_id} /assets /{asset_id} /exit Record an asset sale; computes net proceeds, gain or loss, CAGR, hold period and rent yield
- DELETE
/groups/{group_id} /assets /{asset_id} /exit Undo an exit (admin or editor)
Ledger
- GET
/groups/{group_id} /ledger Contributions and distributions together, with totals and a per-member breakdown
- GET
/groups/{group_id} /contributions List contributions (paginated; filter by asset, member, category or dates)
- POST
/groups/{group_id} /contributions Record a contribution
- PATCH
/groups/{group_id} /contributions /{contribution_id} Update a contribution
- DELETE
/groups/{group_id} /contributions /{contribution_id} Delete a contribution (admin only)
- GET
/groups/{group_id} /distributions List distributions (paginated; filter by asset, member or dates)
- POST
/groups/{group_id} /distributions Record a distribution
- PATCH
/groups/{group_id} /distributions /{distribution_id} Update a distribution
- DELETE
/groups/{group_id} /distributions /{distribution_id} Delete a distribution (admin only)
Settlements & Balances
- GET
/groups/{group_id} /balances Who owes whom, from expense splits and settlements (perspective=me or all)
- GET
/groups/{group_id} /settlements List recorded settlements (paginated; filter by member or dates)
- POST
/groups/{group_id} /settlements Record a settlement payment between two members to clear a balance
- PATCH
/groups/{group_id} /settlements /{settlement_id} Update a settlement's amount, currency, date or note
- DELETE
/groups/{group_id} /settlements /{settlement_id} Delete a settlement
Liabilities
- GET
/groups/{group_id} /liabilities List liabilities with their payment history
- POST
/groups/{group_id} /liabilities Create a liability (mortgage, loan, line_of_credit, other; admin only)
- PATCH
/groups/{group_id} /liabilities /{liability_id} Update a liability (admin only)
- DELETE
/groups/{group_id} /liabilities /{liability_id} Delete a liability and its payments (admin only)
- POST
/groups/{group_id} /liabilities /{liability_id} /payments Record a payment against a liability (admin only)
- DELETE
/groups/{group_id} /liabilities /{liability_id} /payments /{payment_id} Delete a payment (admin only)
Metrics & Sheets
- GET
/groups/{group_id} /metrics Portfolio metrics: active and exited assets, P&L, ownership, realized returns, performance
- GET
/groups/{group_id} /metrics /assets /{asset_id} Per-asset metrics: CAGR, ROI, equity, ownership breakdown
- GET
/groups/{group_id} /metrics /monthly A year of monthly P&L and balance sheet figures (year required; optional asset_id)
- GET
/groups/{group_id} /metrics /portfolio-history Portfolio value over time, optionally for some assets (asset_ids, comma-separated)
Ownership
- GET
/groups/{group_id} /assets /{asset_id} /ownership List ownership shares with member details (paginated)
- PUT
/groups/{group_id} /assets /{asset_id} /ownership Set an asset's ownership shares, replacing all existing ones
- POST
/groups/{group_id} /assets /{asset_id} /ownership /calculate Work out shares from contributions; a preview unless apply=true
- DELETE
/groups/{group_id} /assets /{asset_id} /ownership Delete all of an asset's ownership shares (admin only)
Members & Invitations
- GET
/groups/{group_id} /members List active members and their roles
- PATCH
/groupsSession only/{group_id} /members /{member_id} Change a member's role or display name (admin only)
- DELETE
/groupsSession only/{group_id} /members /{member_id} Remove a member (admin only)
- GET
/groups/{group_id} /invitations List pending invitations (invite tokens are shown only to admins using a session)
- POST
/groupsSession only/{group_id} /invitations Invite someone by email (admin only)
- POST
/groupsSession only/{group_id} /invitations /{invitation_id} /resend Resend an invitation email with a new token (admin only)
- DELETE
/groupsSession only/{group_id} /invitations /{invitation_id} Cancel a pending invitation (admin only)
- GET
/invitations/pending List pending invitations sent to your email address
- GET
/invitationsNo auth/token /{token} Look up an invitation by its token: group name, role and inviter
- POST
/invitationsSession only/accept Accept an invitation by token, signed in with the invited email address
Activity
- GET
/groups/{group_id} /activity The group's activity feed, newest first, with actor, resource and metadata (paginated)
Documents
- GET
/groups/{group_id} /documents List documents (paginated; filter with asset_id or group_only, search by name with q)
- POST
/groups/{group_id} /documents Upload a file as multipart form data: PDF, image, Word, Excel, PowerPoint or CSV; the size limit depends on the group's plan
- POST
/groups/{group_id} /documents /upload-url Get a signed URL to upload a large file straight to storage
- POST
/groups/{group_id} /documents /confirm After a signed-URL upload, create the document record
- GET
/groups/{group_id} /documents /{document_id} /download Get a signed URL to download a document
- DELETE
/groups/{group_id} /documents /{document_id} Delete a document and its file (admin or editor)
Export
- GET
/groups/{group_id} /export /sheets Download a year's monthly sheet as CSV: income, expenses, net operating income, contributions, distributions, total assets and equity (year required; plan feature)
- GET
/groups/{group_id} /export /portfolio-pdf Download a PDF portfolio summary (plan feature; counts toward the plan's monthly PDF exports)
AI Insights
- GET
/groups/{group_id} /insights Get AI-generated portfolio insights: cached for 12 hours, generated again when stale
- POST
/groups/{group_id} /insights /refresh Regenerate insights now: up to 8 times per group per day, and counts toward the plan's daily AI refreshes
- PATCH
/groups/{group_id} /insights /{insight_id} /dismiss Dismiss an insight so it no longer appears in the list
KinCoPilot
- POST
/groups/{group_id} /copilot /messages Send a message to KinCoPilot and get a portfolio-aware reply (plan feature, daily message quota)
Account Connections
- POST
/groupsSession only/{group_id} /connections /link-token Create a link token for the bank-linking UI (Plaid)
- POST
/groupsSession only/{group_id} /connections /exchange-token Exchange a public token: stores the connection and creates KinCo assets
- GET
/groups/{group_id} /connections List connections and their linked accounts
- POST
/groups/{group_id} /connections /accounts /{account_id} /sync Refresh a linked account's balance (writes a new automated valuation)
- POST
/groups/{group_id} /connections /sync-all Refresh every linked account in the group; recently synced accounts are skipped unless force=true
- POST
/groups/{group_id} /connections /accounts /{account_id} /link-asset Link a connected account to an existing KinCo asset
- POST
/groupsSession only/{group_id} /connections /{connection_id} /reactivate Reactivate a connection after its bank login is re-linked
- DELETE
/groupsSession only/{group_id} /connections /{connection_id} Disconnect: removes provider access but keeps KinCo assets
Webhooks
- GET
/webhooksSession onlyList your webhooks
- POST
/webhooksSession onlyRegister a webhook: url, group_id (required) and events (empty for all); returns the signing secret once
- PATCH
/webhooksSession only/{webhook_id} Update the URL, events or active state
- DELETE
/webhooksSession only/{webhook_id} Delete a webhook
- POST
/webhooksSession only/{webhook_id} /test Send a webhook.test ping to this webhook only (once every 10 seconds)
API Keys
- GET
/api-keysSession onlyList your API keys (prefix only; secrets are never returned after creation)
- POST
/api-keysSession onlyCreate a key: name, scopes (read, write) and an optional expires_at; returns the raw key once (up to 10 keys)
- DELETE
/api-keysSession only/{key_id} Revoke an API key immediately
Account
- DELETE
/usersSession only/me Permanently delete your account: removes you from every group and cancels your invitations
Billing
- GET
/billingNo auth/plans List available plans and their features
- GET
/billing/me Your plans, subscriptions and usage
- POST
/billingSession only/checkout Create a Stripe checkout session for a paid plan
- POST
/billingSession only/change-plan Upgrade or downgrade an active subscription
- POST
/billingSession only/portal Create a Stripe customer portal session to manage payment method and invoices
Referrals
- GET
/referrals/my-code Get your personal referral code (created on the first call)
- GET
/referrals/stats Referral count, tier and the people you referred
- POST
/referralsSession only/invite Send a referral invitation by email
Ready to integrate?
Request access and we'll set you up with an API key for your KinCo account.
Request API access
Tell us a bit about what you're building and we'll get you set up.