Skip to content

Partner apps

Technology partners (a CRM, a booking system, a website platform) build an app in the partner portal and connect it to many organisations. An organisation’s owners and admins see every connected app in Settings > Apps and can remove one at any time.

  1. Connect. Put a Connect Threadkite button in your product that goes to your own start URL. Your server builds the OAuth 2.1 authorisation request (authorisation code with PKCE, scope=app) to /api/auth/oauth2/authorize. The person signs in, ticks the organisations to connect and approves. Exchange the code at POST /api/auth/oauth2/token with your client id and secret (HTTP Basic): the answer lists the installations it made.
  2. App token. Get one with the client credentials grant: POST /api/auth/oauth2/token with grant_type=client_credentials and HTTP Basic. It works for 15 minutes on the app’s own endpoints (/v1/app, /v1/partner) only.
  3. Installation token. POST /v1/app/installations/{id}/access_tokens with the app token gives a one-hour tkin_ token for one organisation, with the scopes it granted (or fewer). Send it as Authorization: Bearer tkin_... to the rest of the API, as you would an API key: the same scopes, rate limits and idempotency apply, per installation.

Your app’s webhook address gets the events it chose from every installation, each with organisationId and installationId, and always app_installation.created, app_installation.updated and app_installation.removed for its own installations.

On the Free plan an organisation’s API keys only send leads in, and webhooks are not sent. Installed apps get the same, unless Threadkite has agreed with you that your apps keep the API on Free for the businesses you bring. Apps may invite people as staff (POST /v1/invitations with members:write) only while a business is on Free or a trial, where nobody is billed for them (not while a paid plan is paused). On Free they can join only when the business has room for them (Free is for one person unless we have agreed otherwise with you); on a paid plan the business adds its own team.

You can also set an organisation up for a business (POST /v1/partner/organisations, with the app installed, on a trial or straight on Free with plan: "free"; its owner is emailed an invitation; when another partner of ours brought you the business, give their referralCode and the referral is theirs, once we have agreed it with you), and email an owner or admin a link to connect (POST /v1/app/connect-invitations). Records your app creates keep your external ids under your app’s slug, and a contact links back to your product when the app has a contact address template.

Your app can offer actions that people run from a thread, such as creating a deal in your system with your own fields. Declare them in the partner portal: an actions address (https) and up to ten actions, each with a name (lower case with underscores), a label and, if it provides one, a capability from the connections directory. They show on every thread of every organisation that has your app installed and shown in threads, and run through GET /v1/threads/{id}/actions and POST /v1/threads/{id}/action-steps like Threadkite’s own.

Each step is a POST to your actions address, signed as your webhooks are (Threadkite-Signature, with your app’s webhook secret), with Threadkite-Event set to action.initialize when someone starts the action or action.submit when they submit your form:

{
"type": "action.submit",
"action": "create_deal",
"installationId": "0b7c3a52-5d9e-4f3a-9a51-2f1e8c6d4b10",
"organisationId": "5f0e2c1a-8b44-4c55-9a51-2f1e8c6d4b11",
"accountId": "C-1042",
"thread": { "id": "…", "url": "https://app.threadkite.com/t/…" },
"contact": { "id": "…", "name": "Sam Lee", "email": "sam@example.com", "phone": "+447700900123", "externalId": "123" },
"user": { "id": "…", "name": "Alex Doe", "email": "alex@business.example" },
"values": { "kind": "retail", "deposit": 150000 },
"state": "step2",
"button": null
}

contact is sent only when the organisation granted your app contacts:read (or contacts:write), with externalId your own id for them when you set one. Money is in minor units (pence). Answer within 10 seconds with JSON of 64 KB or less: either the next form,

{
"canvas": {
"title": "New deal",
"submitLabel": "Create deal",
"components": [
{ "type": "select", "id": "kind", "label": "Deal type", "required": true,
"options": [{ "value": "retail", "label": "Retail" }, { "value": "trade", "label": "Trade" }] },
{ "type": "input", "id": "deposit", "label": "Deposit", "inputType": "money" }
],
"errors": null
},
"state": "step2"
}

or what it came to, which shows on the thread as a card (and, with customerMessage, can be sent to the customer):

{
"done": {
"card": { "title": "Deal D-1042 created", "tone": "success", "lines": [{ "label": "Deposit", "value": "£1,500" }] },
"customerMessage": { "text": "Thanks, your deal is being prepared.", "editable": true },
"url": "https://dms.example/deals/D-1042"
}
}

Components are text, details, divider, link and the fields input (text, textarea, number, money, email, phone, date), select, checkbox and item (one of the organisation’s catalogue items, sent as its id). Text is plain: nothing is read as HTML or Markdown. state (4 KB at most) comes back with the next step as it was; to refuse what was sent, answer the canvas again with errors by field id (_ for the whole form). Anything else (a timeout, a status other than 2xx, a body that is neither) shows the person a plain error and is not retried. A finished action says action_run.completed on the bus; keep what you made in Threadkite as a record with POST /v1/events if you want it there.

Link to these addresses on the web app from your own screens. They stay the same, and someone who is not signed in signs in first and then lands on the page. A thread or contact they cannot see here (another organisation’s, or in an inbox they are not in) answers “not found”.

Address Opens
/t/{threadId} A thread, by the threadId the API gives you (such as POST /v1/leads)
/contacts/{contactId} A contact, by its Threadkite id
/c/by-external/{app}/{externalId} A contact, by your app’s slug and your id for them