Indraft Interactions API
The interactions API from Indraft — 1 operation(s) for interactions.
The interactions API from Indraft — 1 operation(s) for interactions.
Every API here is available over the APIs.io API and to AI agents over MCP.
One button, every client — Claude, Cursor, VS Code and the rest.
https://apis.io/mcp
find_apisBrowse and filter every API in the catalog.get_api_artifactsOne API's artifacts, grouped by type.get_openapiThe primary OpenAPI for this API.find_similar_apisAPIs that look like this one.apis_io_searchSTART HERE — APIs, providers and tags for one query, each with its total.resolveTurn a domain, URL or GitHub org into the provider it belongs to.find_cohortsEvery scored population of providers in the catalog.curl "https://apis.io/api/v1/apis/indraft-io:indraft-io-interactions-api"
curl "https://apis.io/api/v1/apis?limit=25"
Calls without X-API-Key need no key. Calls with it need a paid plan — get a key.
Free tier, no form to fill in. Signing in shares your email address with us — we store it to create your key and to recognise you if you sign in with another provider. See our Privacy Policy and Terms.
A second provider on the same verified email joins the account you already have.
openapi: 3.2.0
info:
title: Indraft Io Interactions API
version: '1'
description: 'Operations tagged interactions across 2 of this provider''s published API definitions: indraft-io-openapi.json, indraft-io.json. Each path carries the servers of the definition it was published in.'
servers:
- url: https://api.indraft.io
security:
- bearerAuth: []
tags:
- name: Interactions
paths:
/v1/interactions:
get:
summary: Every interaction in the workspace, newest first
description: Every interaction in the workspace, newest first, across all records. Use a company's own interactions when you want one account's history rather than the whole workspace's.
tags:
- Interactions
operationId: get_v1_interactions
x-indraft-capability: crm:read
x-indraft-scopes:
- crm:read
- crm:write
- crm:configure
- crm:admin
- crm:revert
- data:export
parameters:
- name: limit
in: query
required: false
schema:
type: integer
minimum: 1
maximum: 100
description: How many to return. The page bound is published in the schema call, and asking for more is refused rather than quietly truncated.
- name: cursor
in: query
required: false
schema:
type: string
minLength: 1
description: The opaque marker from the previous page. Store it and hand it back; do not parse it. A cursor is only ever emitted when another row really exists, so a page you were given is never empty.
- name: updatedSince
in: query
required: false
schema:
type: string
minLength: 1
maxLength: 40
description: 'Only rows changed after this instant. The incremental read: without it a service syncing a large workspace has to walk all of it every time.'
- name: lastInteractionBefore
in: query
required: false
schema:
type: string
minLength: 1
maxLength: 40
description: Only records whose most recent interaction is older than this, including those with none at all.
- name: includeArchived
in: query
required: false
schema:
type: boolean
description: Include archived rows. Off by default, because a list is what you are working on and history is somewhere you go deliberately.
- name: filter
in: query
required: false
schema:
anyOf:
- type: string
minLength: 1
maxLength: 560
- type: array
items:
type: string
minLength: 1
maxLength: 560
maxItems: 8
description: Predicates, all of which must hold, written field:op:value. Only fields an index serves are accepted, and asking for one that is not names the ones that are.
- name: or
in: query
required: false
schema:
anyOf:
- type: string
minLength: 1
maxLength: 560
- type: array
items:
type: string
minLength: 1
maxLength: 560
maxItems: 4
description: 'Alternatives, ORed with each other and ANDed with filter. One level and never a tree: owned by me or owned by nobody is the question people ask, and anything deeper has a cost the caller cannot read off it.'
- name: sort
in: query
required: false
schema:
type: string
minLength: 1
maxLength: 65
description: A field to order by, with a leading minus for descending. Indexed fields only, for the same reason as filter.
responses:
'200':
description: Success
headers:
ratelimit-limit:
description: Requests permitted in the window closest to refusing this caller.
schema:
type: integer
ratelimit-remaining:
description: Requests left in that window. Zero means the next request is refused.
schema:
type: integer
ratelimit-reset:
description: Seconds until that window resets.
schema:
type: integer
ratelimit-policy:
description: Every window in force, not only the one reported above.
schema:
type: string
content:
application/json:
schema:
type: object
properties:
items:
type: array
items:
type: object
properties:
id:
type: string
interactionType:
type: string
enum:
- email
- call
- meeting
- message
- note
direction:
type: string
enum:
- inbound
- outbound
- internal
subject:
type: string
summary:
type: string
assertionKind:
type: string
enum:
- observed
- inferred
- imported
- user_supplied
occurredAt:
type: string
sourceId:
type: string
createdByActorId:
type: string
actor:
type: object
properties:
id:
type: string
kind:
type: string
enum:
- human
- agent
- api_token
- system
label:
type: string
required:
- id
- kind
- label
additionalProperties: true
links:
type: array
items:
type: object
properties:
objectType:
type: string
enum:
- company
- contact
- opportunity
objectId:
type: string
required:
- objectType
- objectId
additionalProperties: true
createdAt:
type: string
required:
- id
- interactionType
- direction
- subject
- summary
- assertionKind
- occurredAt
- sourceId
- createdByActorId
- links
- createdAt
additionalProperties: true
description: This page of results, in the order you asked for.
nextCursor:
type: string
description: 'Hand this back to get the next page, or null when there is no next page. Null is the end of the list and an empty `items` is not: a cursor is only emitted when another row really exists.'
requestId:
type: string
required:
- items
- nextCursor
- requestId
additionalProperties: true
'401':
description: No credential, or one that has been revoked.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'403':
description: Authenticated, and this capability is not held.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'422':
description: The request does not satisfy the schema on this page.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'429':
description: Infrastructure protection tripped. This is not a product quota.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
headers:
ratelimit-limit:
description: Requests permitted in the window closest to refusing this caller.
schema:
type: integer
ratelimit-remaining:
description: Requests left in that window. Zero means the next request is refused.
schema:
type: integer
ratelimit-reset:
description: Seconds until that window resets.
schema:
type: integer
ratelimit-policy:
description: Every window in force, not only the one reported above.
schema:
type: string
retry-after:
description: Seconds to wait before retrying. The request itself was fine.
schema:
type: integer
'500':
description: A defect on our side. The hint says so, and carries an incident id.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
post:
summary: Record a normalized interaction
description: 'Record what happened: an email, call, meeting, message, or note, as a SUMMARY you write. Indraft never stores a raw message body, transcript, or attachment. Link it to the companies, contacts, and opportunities it concerns, and put the structured conclusion in customFields: disposition, meeting type and next step belong in fields you can group by rather than in prose.'
tags:
- Interactions
operationId: post_v1_interactions
x-indraft-capability: crm:write
x-indraft-scopes:
- crm:write
- crm:admin
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
idempotencyKey:
type: string
minLength: 8
maxLength: 200
description: 'Your own name for this write, so retrying it cannot do it twice. Send the same key again and you get the SAME result back rather than a second record, which is what makes a re-run safe after a timeout you never saw the answer to. Derive it from what the write is about, not from the clock: `nightly-sync-halden` retries correctly and a timestamp does not.'
expectedVersion:
type: integer
minimum: 0
description: 'The version you read before deciding to write. If the record has moved since, the write is refused instead of overwriting whatever changed. Send it whenever you read, reasoned, and then wrote: without it the last writer wins and neither of you finds out.'
sourceId:
type: string
description: The recorded source this claim came from, if one exists. It attaches evidence to the write, so a person reading the field later can see where it came from rather than only who sent it.
assertionKind:
type: string
enum:
- observed
- inferred
- imported
- user_supplied
description: 'HOW YOU KNOW. This is the field the product is built around, so say what is true rather than what is strongest: `observed` you saw it stated directly, `imported` it came from a file or another system, `inferred` you worked it out, `user_supplied` a person told you in so many words. They rank, weakest first: inferred < imported < observed < user_supplied < confirmed. A weaker claim does not overwrite a stronger one; it is reported in `held[]` and the stored value stands, so an unattended re-run converges instead of eroding what somebody checked. `confirmed` is not accepted here: it means a person checked the value, and only a signed-in person can set it.'
attributionLabel:
type: string
minLength: 1
maxLength: 120
description: 'A short name for the work this write belongs to, shown beside the actor so one credential can distinguish separate jobs. It is recorded as CALLER-SUPPLIED and grants nothing: it changes no permission, no idempotency scope and no quota.'
dryRun:
type: boolean
description: Report what this write WOULD do and commit nothing. The response carries the same change list, held fields and refusals as the real call, so it is the way to check a risky write, or a whole import, before it lands. Nothing is metered.
overrideStanding:
type: boolean
description: 'Apply this write even when the value standing in the record was asserted more strongly than this one. Absent means no, which is the safe answer for a re-run. WITHOUT IT, a write can be accepted and change nothing: precedence is confirmed > user_supplied > observed > imported > inferred, and a weaker claim leaves the standing value in place and reports the field in `held[]` rather than failing. That is the whole of why a write can return 200 and not take. WITH it, this write wins regardless of what it is overruling, INCLUDING a value a person confirmed, so an agent that sets it by default has switched the provenance mechanism off. Set it per write, when the caller knows its claim supersedes what is there.'
interactionType:
type: string
enum:
- email
- call
- meeting
- message
- note
direction:
type: string
enum:
- inbound
- outbound
- internal
subject:
type: string
maxLength: 200
summary:
type: string
minLength: 1
maxLength: 4000
occurredAt:
type: string
provider:
type: string
maxLength: 200
externalId:
type: string
maxLength: 200
evidenceText:
type: string
maxLength: 2000
customFields:
type: object
additionalProperties:
anyOf:
- type: string
maxLength: 2000
- type: integer
- type: boolean
- type: 'null'
description: 'Values for fields this workspace defined, keyed by the field''s key. Read the schema first: a key that is not declared is REFUSED, with the available keys in the error, rather than stored where nobody will find it. Send null to clear one.'
companyIds:
type: array
items:
type: string
maxItems: 100
contactIds:
type: array
items:
type: string
maxItems: 100
opportunityIds:
type: array
items:
type: string
maxItems: 100
required:
- interactionType
- summary
- occurredAt
additionalProperties: false
responses:
'200':
description: Success
headers:
ratelimit-limit:
description: Requests permitted in the window closest to refusing this caller.
schema:
type: integer
ratelimit-remaining:
description: Requests left in that window. Zero means the next request is refused.
schema:
type: integer
ratelimit-reset:
description: Seconds until that window resets.
schema:
type: integer
ratelimit-policy:
description: Every window in force, not only the one reported above.
schema:
type: string
content:
application/json:
schema:
type: object
properties:
record:
type: object
properties:
id:
type: string
interactionType:
type: string
enum:
- email
- call
- meeting
- message
- note
direction:
type: string
enum:
- inbound
- outbound
- internal
subject:
type: string
summary:
type: string
assertionKind:
type: string
enum:
- observed
- inferred
- imported
- user_supplied
occurredAt:
type: string
sourceId:
type: string
createdByActorId:
type: string
actor:
type: object
properties:
id:
type: string
kind:
type: string
enum:
- human
- agent
- api_token
- system
label:
type: string
required:
- id
- kind
- label
additionalProperties: true
links:
type: array
items:
type: object
properties:
objectType:
type: string
enum:
- company
- contact
- opportunity
objectId:
type: string
required:
- objectType
- objectId
additionalProperties: true
createdAt:
type: string
required:
- id
- interactionType
- direction
- subject
- summary
- assertionKind
- occurredAt
- sourceId
- createdByActorId
- links
- createdAt
additionalProperties: true
description: The record as it stands after the write. Absent on a dry run, which commits nothing.
mutation:
type: object
properties:
mutationId:
type: string
status:
type: string
enum:
- created
- updated
- matched
- archived
- merged
- reverted
- noop
description: 'What the write did. `noop` means it was accepted and changed nothing, because every value it carried already matched what was stored. IT IS NOT METERED: a noop does not count against your monthly change allowance, so a nightly sync that finds nothing new costs nothing. Neither does a write that only REDUCES what a workspace holds. `matched` means an upsert found an existing record on real identity evidence rather than creating a second one; `reverted` and `merged` are the undo and duplicate paths, and both are ordinary writes that count.'
targetId:
type: string
record: {}
changes:
type: array
items:
type: object
properties:
field:
type: string
minLength: 1
maxLength: 128
before: {}
after: {}
required:
- field
additionalProperties: false
held:
type: array
items:
type: object
properties:
field:
type: string
minLength: 1
maxLength: 128
kept: {}
proposed: {}
standing:
type: string
enum:
- observed
- inferred
- imported
- user_supplied
- confirmed
incoming:
type: string
enum:
- observed
- inferred
- imported
- user_supplied
- confirmed
actorId:
type: string
minLength: 1
assertedAt:
type: string
minLength: 1
required:
- field
- standing
- incoming
- actorId
- assertedAt
additionalProperties: false
description: Fields this write asked to change and did not, because the value standing in the record was asserted more strongly. THIS IS WHY A WRITE CAN RETURN 200 AND CHANGE NOTHING, and a caller that ignores it will believe it wrote something it did not. Precedence is confirmed > user_supplied > observed > imported > inferred, and a write carrying no `assertionKind` is weakest of all. Each entry reports the field, what the record still says, what you proposed, which claim won, and who made it. Send `overrideStanding` to write anyway, per write, when your claim really does supersede theirs.
matchedBy:
type: string
enum:
- email
- domain
- external_id
- idempotency_key
- explicit_id
reversible:
type: boolean
replayed:
type: boolean
dryRun:
type: boolean
required:
- mutationId
- status
- reversible
additionalProperties: false
description: 'What the write actually DID. Read `status` before you conclude anything: `noop` means it was accepted and changed nothing. `changes` is the field-level diff that was recorded, and `held` names any field whose stored value outranked the claim you sent and therefore did not move. A 200 with an empty `changes` and a populated `held` is the product working, not the call failing.'
requestId:
type: string
required:
- mutation
- requestId
additionalProperties: true
'401':
description: No credential, or one that has been revoked.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'402':
description: A product quota is spent, or the subscription is not active.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'403':
description: Authenticated, and this capability is not held.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'409':
description: 'A conflict the caller can resolve: a stale expectedVersion, an idempotency key reused for different input, an ambiguous or duplicate identity, or a merge that no longer applies.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'413':
description: The body, or a single record inside it, exceeds its bound.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'422':
description: The request does not satisfy the schema on this page.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'429':
description: Infrastructure protection tripped. This is not a product quota.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
headers:
ratelimit-limit:
description: Requests permitted in the window closest to refusing this caller.
schema:
type: integer
ratelimit-remaining:
description: Requests left in that window. Zero means the next request is refused.
schema:
type: integer
ratelimit-reset:
description: Seconds until that window resets.
schema:
type: integer
ratelimit-policy:
description: Every window in force, not only the one reported above.
schema:
type: string
retry-after:
description: Seconds to wait before retrying. The request itself was fine.
schema:
type: integer
'500':
description: A defect on our side. The hint says so, and carries an incident id.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
'507':
description: The workspace is at its storage ceiling.
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
servers:
- url: https://api.indraft.io
components:
schemas:
ErrorDetail:
type: object
additionalProperties: true
description: The structured payload a recovery needs. Its shape differs per code; the keys below are the ones that recur, and anything else a code carries is present alongside them.
properties:
fieldErrors:
type: object
additionalProperties:
type: string
description: One message per field the caller sent, keyed by that field. The key `(body)` is the request body itself, which is what an unrecognised key reports under.
reason:
type: string
description: One sentence, where the refusal is about the request rather than a field. A refusal that names alternatives puts them here.
available:
type: array
items:
type: string
description: What could have been asked for instead. Present on a filter, sort or group-by the API does not serve, so a correction is one edit rather than a round trip to read the schema.
options:
type: array
items:
type: string
description: The values an enum accepts, on a value it does not.
currentVersion:
type: integer
description: What the record's version actually is, on `stale_version`. With the value you sent it is everything needed to decide whether to re-read and retry or to stop.
expectedVersion:
type: integer
description: The version you sent, echoed back on `stale_version`.
requiredScope:
type: string
description: 'The capability the credential lacks. Empty nowhere: a route no credential can reach is human-only and says so in its scopes instead.'
Error:
type: object
required:
- code
- message
- recoveryHint
- requestId
properties:
code:
type: string
enum:
- unauthorized
- forbidden_scope
- standing_override_refused
- workspace_not_found
- validation_error
- record_not_found
- ambiguous_identity
- duplicate_identity
- stale_version
- invalid_stage_transition
- idempotency_conflict
- not_reversible
- merge_conflict
- rate_limited
- quota_exceeded
- payload_too_large
- workspace_capacity
- payment_required
- billing_state_stale
- operation_in_flight
- internal_error
message:
type: string
recoveryHint:
type: string
requestId:
type: string
detail:
$ref: '#/components/schemas/ErrorDetail'
securitySchemes:
bearerAuth:
type: http
scheme: bearer
description: An Indraft API token, or a WorkOS-issued session or MCP access token.
x-refined-from:
- indraft-io-openapi.json
- indraft-io.json