API reference
183 addresses, JSON, and one header with the token.
curl -X POST https://nitrino.ai/api/v1/conversations/<id>/notes \
-H "Authorization: Bearer <access token>" \
-H "Idempotency-Key: 4f9c-2a" \
-H "Content-Type: application/json" \
-d '{"text":"asked for a call on Tuesday"}'Addresses
GET/api/v1
Workspace, member and scopes of the token
200 {
"workspace": "9f1c…",
"member": "4a2b…",
"client": { "id": "d342…", "name": "Astra" },
"scopes": ["inbox:read", "inbox:write", "send"]
}GET/api/v1/conversations
Conversations, most recent first
- cursorstring
- Where to continue from: next from the previous answer
- limitinteger
- How many rows to return: 50 by default, 200 at most
- qstring
- Search across letters, people and transcripts
- folderstring
- A built-in folder or a smart folder id
- tagstring
- Tag id from /labels
- ownerstring
- Member id, nobody or agent
- statestring
- active, snoozed, closed or all
200 {
"conversations": [
{
"id": "c81f…",
"person": "77a0…",
"kind": "email",
"lastMessageAt": "2026-09-20T09:41:02.000Z",
"waitingSince": "2026-09-20T09:41:02.000Z",
"importance": "high",
"intent": "quote",
"owner": null
}
],
"next": "MjAyNi0wOS0yMFQwOTo0MTowMi4wMDBafGM4MWY"
}GET/api/v1/conversations/{id}
One conversation
PATCH/api/v1/conversations/{id}
Snooze, wake, close, reopen, assign, mark a meeting; personal, state by hand, not noise, take off 'meeting booked', no answer needed
- snoozeUntilstring | null
- Snooze until this moment; null wakes it
- whystring
- Why it is snoozed — your team reads this
- closedboolean
- true closes the conversation, false reopens it
- reasonstring
- Why it is closed: meeting_booked, not_interested, no_response, wrong_person, spam, personal, other
- ownerstring | null
- Who takes the conversation; null clears the owner
- metboolean
- A meeting was arranged outside this correspondence
- personalboolean
- Personal: no agent writes into it
- hardStopboolean
- Hard stop on the person: no agent or campaign writes to them first
- intentstring
- The conversation's state, by hand
- heedtrue
- true — not noise
- unbooktrue
- true — take off 'meeting booked'
- noReplytrue | string
- true or the call's moment — no answer needed
POST/api/v1/conversations/{id}/meeting
Tie a meeting to the conversation
- meetingstringrequired
- Meeting id from the calendar
DELETE/api/v1/conversations/{id}/agents/{agent}
Take an extra agent off the conversation
GET/api/v1/conversations/{id}/messages
Messages of a conversation, newest first
- cursorstring
- Where to continue from: next from the previous answer
- limitinteger
- How many rows to return: 50 by default, 200 at most
200 {
"messages": [
{
"id": "m4c1…",
"conversation": "c81f…",
"direction": "inbound",
"sender": { "name": "Pete", "handle": "pete@acme.example" },
"subject": "Slabs",
"text": "will you send the report?",
"at": "2026-09-20T09:41:02.000Z",
"automated": false,
"agent": null,
"voice": null
}
],
"next": null
}POST/api/v1/conversations/{id}/messages
A message to the client — into the send queue
- textstring
- What to send; files may go without words
- tostring
- The client's address, when there is more than one
- viastring
- Our mailbox, when more than one fits
- subjectstring
- Subject of a first letter; a reply keeps the thread's
- ccstring[]
- Email only: who else to put in Cc. The thread's participants are copied anyway
- replyAllboolean
- Email only: false — do not copy the thread's participants
- filesobject[]
- Files: name, type and base64 content
- shelfstring[]
- Agent shelf files, by id
- sendAtstring
- When to send; without it, as soon as the queue allows
200 {
"message": {
"id": "o7d2…",
"state": "queued",
"sendAt": null,
"to": "pete@acme.example",
"cc": ["anna@acme.example"]
}
}POST/api/v1/conversations/{id}/notes
A note inside the conversation: the client never sees it
- textstringrequired
- The note
POST/api/v1/conversations/{id}/labels
Put a label on the conversation, by name
- namestringrequired
- Label name; an unknown one is created
DELETE/api/v1/conversations/{id}/labels/{label}
Take the label off
GET/api/v1/messages/{id}/files
Files of a message
GET/api/v1/conversations/{id}/draft
The agent's draft and reminder ahead
200 {
"draft": {
"text": "Pete, the report goes out on Friday.",
"agent": "a1b2…",
"reason": "unanswered",
"note": null,
"ask": null,
"calledAt": "2026-10-08T09:41:02.000Z",
"files": [],
"blanks": [],
"route": null
},
"reminder": null
}POST/api/v1/conversations/{id}/draft
Approve the agent's draft — the letter is queued, the agent stays
- confirmedbooleanrequired
- true is the person’s yes to this letter; without it nothing is sent
- textstring
- Your own words instead of the draft; without it, the draft as it stands
- entrustboolean
- true hands the conversation to the agent: from now on it sends by itself
- shelfstring[]
- Shelf files; absent means the agent’s own, empty means none
- filesobject[]
- Files: name, type and base64 content
- sendAtstring
- When to send; without it, as soon as the queue allows
PATCH/api/v1/conversations/{id}/reminder
Change the agent's reminder
- textstring
- The reminder’s new wording
- duestring
- New due date, up to two months ahead
DELETE/api/v1/conversations/{id}/reminder
Drop the agent's reminder
- confirmedbooleanrequired
- true is the person’s yes to this deletion, in the body or as ?confirmed=true; without it nothing is deleted
GET/api/v1/conversations/{id}/pending
Letters of a conversation not sent yet
200 {
"pending": [
{
"id": "o7d2…",
"state": "queued",
"sendAt": "2026-10-09T08:00:00.000Z",
"text": "Pete, a reminder about the report.",
"subject": null,
"to": "pete@acme.example",
"via": "e5f6…",
"agent": null,
"edited": false,
"failure": null,
"files": [],
"shelf": [],
"undoUntil": null
}
]
}PATCH/api/v1/conversations/{id}/pending/{pending}
Change a waiting letter
- textstring
- The letter’s new wording
- sendAtstring
- When to send; without it, as soon as the queue allows
- nowboolean
- true lets it go with the next queue pass
DELETE/api/v1/conversations/{id}/pending/{pending}
Cancel a waiting letter
- confirmedbooleanrequired
- true is the person’s yes to this deletion, in the body or as ?confirmed=true; without it nothing is deleted
POST/api/v1/conversations/{id}/pending/undo
Take a letter back from the queue with its words and files
- pendingstringrequired
- A letter from the waiting list
POST/api/v1/messages/{id}/forward
Forward a message with its files to another conversation
- tostringrequired
- The conversation to forward into
- confirmedbooleanrequired
- true is the person’s yes to this letter; without it nothing is sent
POST/api/v1/messages/{id}/reactions
React to a message
- emojistringrequired
- An emoji from the reaction set
GET/api/v1/files/{id}
The file itself, as bytes
GET/api/v1/people
Clients, by name
- cursorstring
- Where to continue from: next from the previous answer
- limitinteger
- How many rows to return: 50 by default, 200 at most
GET/api/v1/people/{id}
One client
PATCH/api/v1/people/{id}
Change a client card
- namestring
- What you call them; empty clears the name
- citystring
- City
- jobTitlestring
- Job title
- companystring
- Their company
- emailstring
- Email address — as one more contact
- birthdaystring | null
- YYYY-MM-DD or --MM-DD; null clears it
GET/api/v1/people/{id}/card
Contacts with ids, field values, birthday
POST/api/v1/people/{id}/contacts
Add a contact; a taken one — 409 with twin
- channelstringrequired
- email, telegram, whatsapp, linkedin, instagram
- handlestringrequired
- Address, phone or nickname
PATCH/api/v1/people/{id}/contacts/{contact}
Correct a contact in its channel
- handlestringrequired
- Address, phone or nickname
POST/api/v1/people/{id}/contacts/ignored
Stop offering a contact from a letter
- channelstringrequired
- email, telegram, whatsapp, linkedin, instagram
- handlestringrequired
- Address, phone or nickname
POST/api/v1/people/{id}/colleagues
Other people's contacts — as new cards
- contactsobject[]required
- [{channel, handle}], up to 10
POST/api/v1/people/{id}/merge
Merge twin into this card — with confirmed
- twinstringrequired
- The other card's id
- confirmedbooleanrequired
- true is the person’s yes to merging exactly these two
POST/api/v1/people/{id}/unmerge
Take a merged card back out
- twinstringrequired
- The other card's id
POST/api/v1/people/{id}/different
Different people: no merge offer
- twinstringrequired
- The other card's id
PUT/api/v1/people/{id}/fields/{field}
A card field's value, by hand
- valuestring | nullrequired
- Value; empty or null clears it
- rowstring
- Row of a table block; empty — the card itself
GET/api/v1/fields
Card field schema
PUT/api/v1/fields
Replace the schema: a field left out goes with its values
- sectionsobject[]required
- Sections with fields, in order
POST/api/v1/people/{id}/tags
Mark a product on a person: client or partner
- labelstringrequired
- Product tag
- sidestringrequired
- Role: buyer — client, seller — partner
DELETE/api/v1/people/{id}/tags
Take the pairing off; it is recorded as removed, not deleted
GET/api/v1/agents
The agents of this workspace, all at once
POST/api/v1/agents
Create an agent: it is born switched on, and its first 10 letters wait for Send
- namestringrequired
- What the agent is called in the list
- goalstringrequired
- What the agent leads the conversation towards
- rulesstring
- How it must behave
- knowledgestring
- What it knows about your business
- companystring
- On whose behalf it writes; without it, the only company
- labelsstring[]
- Product tags it covers; empty means every client
- membersstring[]
- Whose behalf and channels it writes with; without it, the owners of the connected accounts
- channelsstring[]
- Connections it writes through; empty means every channel of its people
- hoursobject
- The agent's hours: timezone, from, to, weekends and replies outside the hours
- pricingstring
- What it says about money: list names the prices and terms of the products, silent names no price and calls a human
- groupsstring[]
- The group chats it leads, ids from /agents/groups; the whole list
- rankinteger
- Its place among switched-on agents when several could lead a conversation: the smaller leads
- maxAutointeger
- How many letters in a row it writes without an answer
- minScoreinteger
- Below which closeness to the goal it leaves a silent conversation alone
- ignoreAfterinteger
- After how many silent days a conversation counts as abandoned
- meetHoursobject
- From which hour to which it offers meetings, in the workspace timezone
- mannersobject
- How it handles silence: replyOnly, stopAfterLink, nudgeAfter, staleAfter
- delaysobject[]
- The wait before a letter, per channel: connection, min and max in minutes; the whole list
- confirmedboolean
- true is the person’s yes to the agent starting to write by itself; needed along with the send scope when the change switches such an agent on or sets sendMode to auto
GET/api/v1/agents/groups
The group chats an agent can lead, and who leads them
POST/api/v1/agents/bulk
Put an agent on named conversations or take agents off them, up to 200 at once
- commandstringrequired
- hand puts on, release takes off, pause mutes, resume lifts the mute
- conversationsstring[]required
- Conversation ids, up to 200
- agentstring
- The agent to put on; only with hand
- confirmedboolean
- true is the person’s yes to the agent writing to these people by itself; needed with hand and resume, along with the send scope
GET/api/v1/agents/{id}
One agent, with the words it works by
PATCH/api/v1/agents/{id}
Change an agent: send only what changes
- goalstring
- What the agent leads the conversation towards
- rulesstring
- How it must behave
- knowledgestring
- What it knows about your business
- enabledboolean
- Whether the agent works at all
- labelsstring[]
- Product tags it covers; empty means every client
- membersstring[]
- Whose behalf and channels it writes with; without it, the owners of the connected accounts
- channelsstring[]
- Connections it writes through; empty means every channel of its people
- hoursobject
- The agent's hours: timezone, from, to, weekends and replies outside the hours
- pricingstring
- What it says about money: list names the prices and terms of the products, silent names no price and calls a human
- groupsstring[]
- The group chats it leads, ids from /agents/groups; the whole list
- rankinteger
- Its place among switched-on agents when several could lead a conversation: the smaller leads
- maxAutointeger
- How many letters in a row it writes without an answer
- minScoreinteger
- Below which closeness to the goal it leaves a silent conversation alone
- ignoreAfterinteger
- After how many silent days a conversation counts as abandoned
- meetHoursobject
- From which hour to which it offers meetings, in the workspace timezone
- mannersobject
- How it handles silence: replyOnly, stopAfterLink, nudgeAfter, staleAfter
- delaysobject[]
- The wait before a letter, per channel: connection, min and max in minutes; the whole list
DELETE/api/v1/agents/{id}
Delete an agent; one switched on with live conversations only after a pause
- confirmedbooleanrequired
- true is the person’s yes to this deletion, in the body or as ?confirmed=true; without it nothing is deleted
- successorstring
- Who takes over the live conversations, in the body or as ?successor=; without it they go back to people
POST/api/v1/agents/{id}/pause
Stop an agent on one account, or let it write there again
- connectionstringrequired
- The connection to stop it on or let it back
- pausedbooleanrequired
- true stops it, false lets it back (with send and confirmed)
- confirmedboolean
- true is the person’s yes to the agent writing to these people by itself; needed with hand and resume, along with the send scope
POST/api/v1/conversations/{id}/agent
The agent on a conversation: put on, take off, mute, resume
- commandstringrequired
- hand puts on, release takes off, pause mutes, resume lifts the mute
- agentstring
- The agent to put on; only with hand
- confirmedboolean
- true is the person’s yes to the agent writing to these people by itself; needed with hand and resume, along with the send scope
GET/api/v1/products
The products of this workspace, all at once
POST/api/v1/products
Create a company product; a journal line names the program
- namestringrequired
- What the product is called
- sidestringrequired
- Selling, buying or both
- companystring
- Whose product it is; without it, the only company of the workspace
- descriptionstring
- What this product is, in your own words
- goalstring
- Deprecated: accepted and ignored. The goal of a conversation is set on the agent and the campaign
- conditionsobject[]
- Terms as key-value lines, each with its own side
- marketsstring[]
- Narrows the company's markets; empty means the company's
GET/api/v1/products/{id}
One product with its terms and markets
PATCH/api/v1/products/{id}
Change a product
- descriptionstring
- What this product is, in your own words
- sidestring
- Selling, buying or both
- goalstring
- Deprecated: accepted and ignored. The goal of a conversation is set on the agent and the campaign
- conditionsobject[]
- Terms as key-value lines, each with its own side
- marketsstring[]
- Narrows the company's markets; empty means the company's
DELETE/api/v1/products/{id}
Delete a product; its tag stays among the tags
- confirmedbooleanrequired
- true is the person’s yes to this deletion, in the body or as ?confirmed=true; without it nothing is deleted
GET/api/v1/workspace
Workspace settings: timezone and machine tagging
GET/api/v1/products/{id}/letters
A product's first letters: a conversation opened with a similar one is about it
POST/api/v1/products/{id}/letters
Keep a first letter for a product
- textstringrequired
- The letter as it is sent
DELETE/api/v1/products/{id}/letters/{id}
Remove a product's first letter; tags it already put stay
- confirmedbooleanrequired
- true is the person’s yes to this deletion, in the body or as ?confirmed=true; without it nothing is deleted
GET/api/v1/products/candidates
Goods found in conversations that no product covers yet
POST/api/v1/products/candidates/skip
“Not now”: take a found product out of the queue
- labelstringrequired
- The label of a found product
GET/api/v1/goals
The workspace's own goals and own “what the agent does” entries
POST/api/v1/goals
Add an own goal or an own “what the agent does”
- kindstringrequired
- goal is a goal, role is what the agent does
- namestringrequired
- Its name in the list, up to 80 characters
- aboutstring
- What it means, up to 500 characters
PATCH/api/v1/goals/{id}
Rename an own entry or rewrite what it means
- namestring
- Its name in the list, up to 80 characters
- aboutstring
- What it means, up to 500 characters
GET/api/v1/sources
Knowledge sources of an agent, a company or the house rules: links, notes and files
- agentstring
- Agent id: its rules and knowledge
- companystring
- Company id: what agents say about it
- fieldstring
- rules or knowledge for an agent and the house rules, pitch for a company
POST/api/v1/sources
Bring a source — a link, a note or a file; a file is parsed to text after the answer
- agentstring
- Agent id: its rules and knowledge
- companystring
- Company id: what agents say about it
- fieldstring
- rules or knowledge for an agent and the house rules, pitch for a company
- linkstring
- An http or https address
- notestring
- Text written by a person
- fileobject
- name and content in base64: xlsx, csv, pdf, docx, txt or md, up to three megabytes
DELETE/api/v1/sources/{id}
Remove a source together with its file
- confirmedbooleanrequired
- true is the person’s yes to this deletion, in the body or as ?confirmed=true; without it nothing is deleted
POST/api/v1/agents/{id}/files
Put a file on an agent's shelf: the agent attaches it to a letter on its own
- fileobjectrequired
- name, content in base64 and type; up to three megabytes
- whenToSendstring
- When the agent sends this file, up to 500 characters
PATCH/api/v1/agents/{id}/files/{id}
Rewrite a shelf file's line on when to send it
- whenToSendstringrequired
- When the agent sends this file, up to 500 characters
DELETE/api/v1/agents/{id}/files/{id}
Take a file off an agent's shelf
- confirmedbooleanrequired
- true is the person’s yes to this deletion, in the body or as ?confirmed=true; without it nothing is deleted
GET/api/v1/workspace
Workspace settings: timezone and machine tagging
PATCH/api/v1/workspace
Change settings; the name has its own route, and the people theirs
- timezonestring
- The timezone its day is counted in
- taggingboolean
- Whether the machine tags conversations itself
- findsGoodsboolean
- Whether the machine looks for goods in conversations that are not among the products
PATCH/api/v1/workspace/name
Rename the workspace
- namestringrequired
- The new name, 120 characters at most
GET/api/v1/labels
The tags of this workspace: product and correspondence
POST/api/v1/labels
Create a tag
- namestringrequired
- Tag name
- kindstring
- goods — product, talk — correspondence
- promptstring | null
- The model's question that puts the tag on by itself
- promptFromstring | null
- From which day the tag reaches older letters
- hueinteger
- Colour, 0–5
- synonymsstring[]
- Other words for the same tag
- keepsMachinesboolean
- Talk tags only: true keeps a conversation with this tag open to agents, and the tag can slice an agent. This is how Client and Partner work
PATCH/api/v1/labels/{id}
Change a tag, accept it, archive or bring back
- namestring
- Tag name
- kindstring
- goods — product, talk — correspondence
- promptstring | null
- The model's question that puts the tag on by itself
- promptFromstring | null
- From which day the tag reaches older letters
- hueinteger
- Colour, 0–5
- synonymsstring[]
- Other words for the same tag
- keepsMachinesboolean
- Talk tags only: true keeps a conversation with this tag open to agents, and the tag can slice an agent. This is how Client and Partner work
- acceptedtrue
- true — accept a tag the model proposed
- archivedboolean
- true — archive, false — bring back
POST/api/v1/labels/{id}/run
One slice of running a tag over history: costs model calls
- cursorstring
- From the previous answer, to go on
200 {
"run": { "label": "5d1e…", "looked": 40, "applied": 3, "asked": 40, "missed": 0, "done": false, "next": "c81f…" }
}GET/api/v1/folders
Smart folders this member sees
POST/api/v1/folders
Create a smart folder
- namestringrequired
- Folder name
- descriptionstring | null
- What the folder holds
- conditionsobject
- Selection conditions — keys as the folder list gives them
- sharesobject[]
- Shared with: [{member, role}]
- teamstring | null
- Whole team: viewer, editor or null
PATCH/api/v1/folders/{id}
Change a smart folder: only what is sent
- namestring
- Folder name
- descriptionstring | null
- What the folder holds
- conditionsobject
- Selection conditions — keys as the folder list gives them
- sharesobject[]
- Shared with: [{member, role}]
- teamstring | null
- Whole team: viewer, editor or null
DELETE/api/v1/folders/{id}
Delete a smart folder — by its owner, with confirmed
- confirmedbooleanrequired
- true is the person’s yes to this deletion, in the body or as ?confirmed=true; without it nothing is deleted
GET/api/v1/audit
The journal: who opened the door and what was changed through it. Owner and admins only
GET/api/v1/channels
Connections: what the workspace speaks through, and whether they are alive
GET/api/v1/members
Members and what each told the agents about themselves
POST/api/v1/members
Invite a person: a known address joins at once, any other gets a letter. Needs send and confirmed
- emailstringrequired
- The person’s email address
- rolestring
- owner, admin, operator or member; member when absent
- confirmedbooleanrequired
- true is the person’s yes to this invitation; without it no letter goes
POST/api/v1/members/{id}/role
Change a member’s role; needs confirmed
- rolestringrequired
- owner, admin, operator or member
- confirmedbooleanrequired
- true is the person’s yes to this role change
DELETE/api/v1/members/{id}
Take a member’s access away; needs confirmed, the last owner stays
- confirmedbooleanrequired
- true is the person’s yes to this member leaving, in the body or as ?confirmed=true
PATCH/api/v1/members/{id}
Change a member card
- jobTitlestring
- Their job title
- zonestring
- What they cover — what makes a question theirs
- signaturestring
- What their letters are signed with
GET/api/v1/house
The workspace-wide agent rules
PATCH/api/v1/house
Change the house rules of agents; each change is a journal line naming the program
- rulesstring
- How every agent behaves
- knowledgestring
- What every agent knows about the business
- teamstring
- Who in the team answers what
- linkstring
- The fallback booking link
- aliasesstring
- The team's own addresses, one per line: letters from them are not a contact's
- pricingFieldsstring[]
- What an agent learns from a contact before an offer — for products without a list of their own
GET/api/v1/deals
Deals: the client, the partner and where it stands
- statestring
- open or closed; omit for both
GET/api/v1/deals/{id}
One deal
POST/api/v1/deals
Open a deal
- labelstringrequired
- The product of the deal
- sellerstringrequired
- The partner, if there is one
- buyerstringrequired
- The client
- agentstringrequired
- The agent leading both sides
PATCH/api/v1/deals/{id}
Move a deal along or close it — one at a time
- stagestring
- The stage to move to
- closedboolean
- Close the deal
- outcomestring
- won or lost: close with an outcome in one call
GET/api/v1/deals/stages
The workspace’s pipeline stages in board order
200 {
"stages": [
{ "id": "paired", "name": null, "builtIn": true, "position": 0, "openDeals": 4 },
{ "id": "3b7e…", "name": "Sample sent", "builtIn": false, "position": 1, "openDeals": 2 }
]
}PATCH/api/v1/deals/stages
Rename, reorder and add stages, for an owner or an admin
- stagesobject[]required
- Every stage in order: id and name; no id means a new stage
DELETE/api/v1/deals/stages/{id}
Remove a stage of your own, moving its deals
- intostringrequired
- The stage its deals move to
- confirmedbooleanrequired
- true is the person’s yes to this deletion, in the body or as ?confirmed=true; without it nothing is deleted
GET/api/v1/companies
The companies of this workspace — who the agents write for
POST/api/v1/companies
Create a company; a journal line names the program
- namestringrequired
- What the company is called
- websitestring
- The company's website
- pitchstring
- What the company offers — why an agent writes at all
- policystring
- What it does not agree to and stays silent about
- factsobject[]
- Facts as label-value lines
- marketsstring[]
- Markets as ISO 3166-1 alpha-2 country codes, the whole list
GET/api/v1/companies/{id}
One company with its facts and markets
PATCH/api/v1/companies/{id}
Change a company card
- pitchstring
- What the company offers — why an agent writes at all
- policystring
- What it does not agree to and stays silent about
- factsobject[]
- Facts as label-value lines
- marketsstring[]
- Markets as ISO 3166-1 alpha-2 country codes, the whole list
GET/api/v1/segments
Saved segments and how many people each holds
200 {
"segments": [
{
"id": "5e0b…",
"name": "Heads of marketing, SaaS in Germany",
"query": "heads of marketing at SaaS companies in Germany",
"rules": [{ "kind": "except", "text": "agencies" }],
"refill": true,
"saved": true,
"createdAt": "2026-09-27T08:12:40.000Z",
"updatedAt": "2026-09-28T10:02:11.000Z",
"members": 412
}
]
}POST/api/v1/segments
Describe a segment in words and judge people; the rest by the same call
- querystringrequired
- Who belongs to the segment, in words
- rulesobject[]
- Refinements: only or except, and words
- refillboolean
- Whether the segment takes in new people itself
- segmentstring
- The segment to go on judging
- namestring
- Name of a new segment; without it the name follows the query
- descriptionstring
- The segment in a paragraph of up to 1,200 characters: kind of firm, headline, titles, size, countries, who is left out. New people are searched by it; null removes it
POST/api/v1/segments/agent
Say a line to the segment agent: the answer is a run id, the turn runs after
- textstring
- What to tell the agent; with a segment and no text, its conversation opens
- runstring
- The run to speak in; without it, a new one
- segmentstring
- The segment the conversation is about
- freshboolean
- A new conversation for the segment instead of the earlier one
200 {
"run": "5b2e…",
"status": "working"
}GET/api/v1/segments/agent/{run}
The segment agent’s conversation: lines, spend, people in the segment
200 {
"run": "5b2e…",
"status": "idle",
"failure": null,
"doing": null,
"talk": [{ "who": "agent", "text": "…", "at": "2026-10-22T09:14:03Z", "aside": false }],
"spent": { "usd": 1.4, "capUsd": 20, "credits": { "prospeo": 12 } },
"segment": { "id": "a91f…", "people": 48 }
}POST/api/v1/segments/agent/{run}/stop
Stop the segment agent’s turn
- ——
- No body needed
GET/api/v1/segments/{id}
A segment: its words, who is in it and its campaigns
PATCH/api/v1/segments/{id}
Keep a draft, replace a segment with it or switch refilling
- savedboolean
- true keeps the draft
- refillboolean
- Whether the segment takes in new people itself
- replacesstring
- The saved segment this draft replaces
- companiesstring[] | null
- The companies to look for new people in: domains, or names where there is no domain; search and collection from every source apply the list to email on their own; null removes the list
DELETE/api/v1/segments/{id}
Delete a segment; its people stay in the contacts, and a running campaign’s segment only after a pause
- confirmedbooleanrequired
- true is the person’s yes to this deletion, in the body or as ?confirmed=true; without it nothing is deleted
GET/api/v1/segments/{id}/people
The people of a segment
- cursorstring
- Where to continue from: next from the previous answer
- limitinteger
- How many rows to return: 50 by default, 200 at most
POST/api/v1/segments/{id}/people
Put your own contacts into a segment
- peoplestring[]required
- Contact ids
PATCH/api/v1/segments/{id}/people
Take people out of a segment or bring them back
- peoplestring[]required
- Contact ids
- keepbooleanrequired
- false takes out, true brings back
POST/api/v1/segments/{id}/stop
Stop a segment’s searches
- ——
- No body needed
POST/api/v1/segments/{id}/priority
Who a segment takes first: countries and roles
- countriesstring[]
- ISO country codes, highest first
- rolesstring[]
- Roles in words, highest first
POST/api/v1/segments/{id}/find
Look for new people: filters and how many are found
- wordsstring
- Who to look for, in words
- channelstring
- linkedin or email
- filtersobject
- Search filters, as find returned them
POST/api/v1/segments/{id}/find/add
Add found people to the contacts and the segment
- channelstring
- linkedin or email
- filtersobject
- Search filters, as find returned them
- wantinteger
- How many people to add
- runstring
- A run to continue
POST/api/v1/segments/{id}/sample
Free sample: ten people by the segment's description, with the judge's score
- filtersobject
- Search filters, as find returned them
POST/api/v1/segments/{id}/sample/nod
Nod on a sample person: ours or not ours
- personobjectrequired
- The sample person, as sample returned them
- nodstringrequired
- ours, notOurs, or null to take the nod back
POST/api/v1/segments/{id}/sample/edit
Propose a description edit from the nods, or accept one
- acceptstring
- The accepted description; without it, only a proposal
POST/api/v1/segments/{id}/search
Search every source: a block per source and the total; email also by the segment's company list
- wordsstringrequired
- Who to look for, in words
- sourcesstring[]
- Where to collect: linkedin, email, instagram, telegram
- filtersobject
- LinkedIn and email filters in place of those made from the words; email without companies of its own takes the segment's list
- emailboolean
- Look up work addresses
- seedsobject[]
- Instagram accounts where the audience gathers
- telegramobject
- Telegram: words, country, languages and group ids
200 {
"collecting": false,
"sources": [
{
"source": "linkedin",
"available": "on",
"result": "found",
"count": 1200,
"rough": false,
"perMonth": 600,
"collects": true,
"want": 600,
"unsearched": ["gender"],
"people": [
{ "name": "Mira K.", "title": "Head of Wellness", "company": "Sattva", "place": "Dubai" }
],
"room": { "left": 600, "capacity": 600, "until": "2026-11-07T00:00:00Z" },
"filters": { "country": ["AE"], "industry": ["wellness"] }
},
{
"source": "telegram",
"available": "connect",
"result": "connect",
"count": null,
"collects": false,
"want": 0,
"perDay": 0,
"look": { "words": ["wellness dubai"], "country": "AE", "languages": ["en"] },
"groups": [
{
"id": "1001",
"title": "Wellness Dubai",
"username": "wellnessdxb",
"kind": "chat",
"members": 18500,
"picked": true,
"unfit": null
}
]
}
],
"total": {
"people": 1200,
"shared": 0,
"bySource": [{ "source": "linkedin", "count": 1200, "rough": false }]
},
"unmatched": [],
"unparsed": false
}POST/api/v1/segments/{id}/search/collect
Collect from every source: a search and a run per source
- wordsstringrequired
- Who to look for, in words
- sourcesstring[]
- Where to collect: linkedin, email, instagram, telegram
- filtersobject
- LinkedIn and email filters in place of those made from the words; email without companies of its own takes the segment's list
- emailboolean
- Look up work addresses
- seedsobject[]
- Instagram accounts where the audience gathers
- telegramobject
- Telegram: words, country, languages and group ids
200 {
"search": "7d1e…",
"runs": [
{ "id": "a2c4…", "source": "linkedin", "state": "running", "want": 600, "added": 40 }
],
"waiting": [{ "id": "b3d5…", "source": "telegram", "want": 300 }],
"total": { "people": 1500, "shared": 0, "bySource": [] }
}GET/api/v1/segments/{id}/search
Search progress: count, outcome and drop reasons by source
200 {
"collecting": false,
"sources": [
{
"source": "linkedin",
"state": "done",
"added": 1140,
"want": 1200,
"resumes": null,
"eta": { "unit": "minutes", "count": 4, "kind": "took" },
"outcome": "done",
"looked": 1200,
"entered": 1140,
"addresses": null,
"places": null,
"dropped": [{ "reason": "known", "count": 60 }],
"unsearched": ["gender"]
}
],
"total": { "people": 1140, "twice": 0 },
"pace": [{ "source": "linkedin", "perDay": null }]
}GET/api/v1/campaigns
The campaigns of the workspace
POST/api/v1/campaigns
Create a campaign draft
- namestring
- Name in the list
- goalstring
- What the campaign is for, in words
- segmentstring
- A saved segment
- agentstring
- The agent who writes
GET/api/v1/campaigns/{id}
A campaign in full: letters, term and funnel
200 {
"campaign": {
"id": "a7c3…",
"name": "Webinar on 14 October",
"goal": "invite heads of marketing to the webinar",
"kind": "outreach",
"state": "running",
"segment": "5e0b…",
"agent": "2f91…",
"senders": ["c0d4…"],
"people": 412,
"missing": [],
"term": { "weeks": 5, "ends": "2026-11-02", "approx": false },
"materials": [{ "id": "9b2e…", "kind": "file", "name": "Price list.pdf", "chars": 1762, "text": "Starter — 490 € a month…" }],
"progress": { "people": 412, "written": 96, "replied": 14, "declined": 3, "interested": 6, "meetings": 2 }
}
}PATCH/api/v1/campaigns/{id}
Change a campaign; each change is a line in the journal
- goalstring
- What the campaign is for, in words
- segmentstring
- A saved segment
- agentstring
- The agent who writes
- sendersstring[]
- Connections to write from
- addRulesstring[]
- Instructions for every letter
- addTextsstring[]
- Own text for the letters, word for word, up to 4000 characters
- materialsobject[]
- Texts the letters rely on
- sendstring
- How it sends: all, first_n or auto; a running one only stricter
- acquaintedstring
- People already talked to: thread writes in their conversation, skip leaves them out
- inviteNoteboolean
- LinkedIn request with a message or without
- responsiblestring | null
- The member responsible for the campaign; null clears it
- arrivedinteger | null
- How many arrived, counted by hand; null clears it
- deadlinestring | null
- Last day of first letters, YYYY-MM-DD; null clears it
DELETE/api/v1/campaigns/{id}
Delete a campaign; a running one only after a pause
- confirmedbooleanrequired
- true is the person’s yes to this deletion, in the body or as ?confirmed=true; without it nothing is deleted
POST/api/v1/campaigns/{id}/state
Pause, resume or finish; starting a draft returns the Check and start link
- tostringrequired
- running, paused or done; running resumes a paused campaign (send and confirmed) and returns a link for a draft
- confirmedboolean
- true is the person’s yes to resuming; needed only for running on a paused campaign
POST/api/v1/campaigns/{id}/letters
Write example letters or agree to them
- writeinteger[]
- Places of the letters to write: 1, 2, 3
- agreeinteger[]
- Places of read letters to agree to
GET/api/v1/campaigns/{id}/touches
Touches of a campaign: queue and log
- statestring
- queued, sending, sent, failed or review (written and waiting for approval)
- cursorstring
- Where to continue from: next from the previous answer
- limitinteger
- How many rows to return: 50 by default, 200 at most
POST/api/v1/campaigns/{id}/touches/{touch}
A campaign letter: skip, recall or undo an approval
- dostringrequired
- skip, recall or undo
GET/api/v1/campaigns/{id}/talk
The conversation with the campaign assistant
POST/api/v1/campaigns/{id}/talk
Say a line to the campaign assistant: the answer is the whole turn with the card and letters
- textstringrequired
- What to tell the assistant, in words
200 {
"campaign": "7c0d…",
"reply": "…",
"card": { "name": "…", "goal": "…" },
"letters": [],
"trouble": null,
"untaken": [],
"handoff": null
}POST/api/v1/campaigns/{id}/review
Release letters from review: approve, send by themselves, or ten more
- dostringrequired
- send, trust or more; send and trust reach people and need send and confirmed
- touchesstring[]
- Letters for send — only the ones named go
- subjectstring
- Edited subject of one letter
- bodystring
- Edited text of one letter
- atstring
- Not before this moment — for one letter
- confirmedboolean
- true is the person’s yes to exactly these letters; without it nothing goes
200 {
"campaign": "7c0d…",
"touches": ["e41a…"],
"state": "approved",
"goes": null
}POST/api/v1/campaigns/{id}/touches/{touch}/rewrite
Rewrite a letter in review by the person’s words, without storing it
- textstringrequired
- What to change in the letter
200 {
"touch": "e41a…",
"draft": "Hi Anna, the webinar on the 14th is about…",
"subject": "Webinar on 14 October"
}GET/api/v1/campaigns/shares
Shared campaign accounts and each campaign’s share
POST/api/v1/campaigns/shares
Set campaign shares on shared accounts
- sharesobjectrequired
- Account → campaign → percent; every campaign of the account, a hundred in sum
POST/api/v1/conversations/{id}/tasks
A task for the agent: what to write and when
- textstringrequired
- What to write — in words for the agent
- duestring | null
- When; a due moment needs the send scope
GET/api/v1/notifications
Your notifications, newest first
POST/api/v1/notifications/read
Mark notifications read
- idsstring[]
- Which ones; without a list — all unread
GET/api/v1/calendar
Calendars and the meetings of the window
- fromstring
- Meetings ending after this moment
- tostring
- Meetings starting before this moment
POST/api/v1/calendar/meetings
Book a meeting; needs the send scope
- titlestringrequired
- Meeting title
- daystringrequired
- Day YYYY-MM-DD in the workspace's zone
- startstringrequired
- Start HH:MM in the workspace's zone
- minutesintegerrequired
- Length: 15, 30, 45, 60, 90 or 120
- guestsstring[]
- Guest addresses, twenty at most
- agendastring
- Agenda — the meeting's description
- calendarstring
- Calendar; without it — your own
- checkboolean
- true — only show what would happen
DELETE/api/v1/calendar/meetings/{id}
Cancel a meeting the product booked; needs the send scope
GET/api/v1/meetings
Meetings with their outcome, however long ago
- outcomestring
- Meeting outcome: held, missed, unknown, unconfirmed
- factstring
- The person's result: met, booked, missed, unconfirmed
- fromstring
- Meetings from this moment
- tostring
- Meetings before this moment
- cursorstring
- Where to continue from: next from the previous answer
- limitinteger
- How many rows to return: 50 by default, 200 at most
POST/api/v1/meetings/outcomes
Record the outcome of meetings in a batch
- meetingsobject[]required
- Rows: meeting, outcome (held or missed), source, note
- drybooleanrequired
- true — count without writing anything
200 {
"outcomes": {
"dry": true,
"counts": { "written": 280, "same": 0, "decided": 12, "unseen": 0, "bad": 0 },
"people": 274,
"rows": [{ "meeting": "9b1e…", "state": "written" }]
}
}GET/api/v1/senders/health
Account health: level, limits and why
200 {
"checked": true,
"accounts": [
{
"connection": "c0d4…",
"channel": "linkedin",
"account": "anna@acme.example",
"health": {
"level": "normal",
"limits": { "day": 15, "week": 70 },
"weakest": ["acceptance"],
"signals": [{ "key": "acceptance", "level": "caution", "score": 48, "facts": { "rate": 0.21 } }]
},
"used": { "day": 6, "week": 31 }
}
]
}POST/api/v1/senders/health/check
Check the accounts again
- connectionstring
- One connection; without it — all
GET/api/v1/senders/voices
Sender voices
GET/api/v1/channels/telegram-folders
Telegram folders with conversation counts
GET/api/v1/senders
Senders: who letters go out in the name of, their accounts and companies
POST/api/v1/senders
Add a sender without a login; the owner adds them
- namestringrequired
- What they are called
- rolestring
- Job title
- signaturestring
- How they sign a letter
POST/api/v1/senders/channels
Pin accounts to a sender, or unpin them
- senderstring | nullrequired
- Sender id; null unpins
- channelsstring[]required
- Account ids from GET /channels
POST/api/v1/senders/{member}/companies
A sender's companies — the whole list
- companiesstring[]required
- Company ids; an empty list means none
PATCH/api/v1/senders/{member}/voice
A sender's own corrections to their voice
- channelstringrequired
- Kind of channel: email, linkedin, telegram, whatsapp or instagram
- ownstringrequired
- The corrections in the person's words, up to 4000 characters
PUT/api/v1/senders/{member}/voice/borrow
The sender writes in a colleague's voice in a channel; from: null gives back their own.
- channelstringrequired
- Kind of channel: email, linkedin, telegram, whatsapp or instagram
- fromstring | nullrequired
- The sender whose voice to write with, or null for their own.
- fromChannelstring
- The colleague's voice channel; defaults to the same one.
POST/api/v1/senders/{member}/voice/learn
Retrain the voice now — teacher and judge, up to a minute
- channelstringrequired
- Kind of channel: email, linkedin, telegram, whatsapp or instagram
POST/api/v1/senders/{member}/voice/check
A dry run: one letter in the sender's voice, nothing is written
- channelstringrequired
- Kind of channel: email, linkedin, telegram, whatsapp or instagram
- turnsobject[]required
- The conversation: from — them or us, and body; up to 20 turns
- lapseboolean
- true — the contact has been silent for a week, and the sender writes again
PATCH/api/v1/channels/{connection}/limits
An account's limits and hands-off
- dailyinteger | null
- Letters a day; null — by the account's health
- hourlyinteger | null
- Letters an hour; null — by the account's health
- gapinteger | null
- Minutes between letters; null — by the account's health
- newPerDayinteger | null
- New people a day, instead of the automatic limit; null — by the account's health
- handsOffboolean
- true — agents do not write from this account; taking it off needs the send scope
- confirmedboolean
- true — the user's yes to agents writing to people
PATCH/api/v1/channels/{connection}/groups
Whether the agent replies in groups; allowing it needs the send scope
- replybooleanrequired
- true — the agent replies in groups
- confirmedboolean
- true — the user's yes to agents writing to people
POST/api/v1/channels/repair
Repair a broken connection
- connectionstringrequired
- Connection id from GET /channels
PATCH/api/v1/channels/{connection}/warmup
Warm-up of an email account at the vendor
- onbooleanrequired
- true — on, false — off
POST/api/v1/channels/zapmail
Connect Zapmail mailboxes
- onlystring[]
- Addresses of the mailboxes, when not all of them are wanted. Without it — every one not connected yet
PATCH/api/v1/channels/{connection}/country
Proxy country of a LinkedIn or Instagram account
- countrystringrequired
- Two-letter country code
- confirmedbooleanrequired
- true — the user's yes to changing the country at the vendor
GET/api/v1/channels/telegram-folders
Telegram folders with conversation counts
GET/api/v1/routing
Routing rules and the switch
POST/api/v1/routing
Change the rules
- dostringrequired
- add, drop, up, down, order or switch
- fieldstring
- label, side, product, sender, company or agent
- valuestring
- Id of the label, product, sender, company, agent, or the side
- memberstring
- Who gets the conversation
- rulestring
- One rule
- rulesstring[]
- Every rule in the new order
- enabledboolean
- Switch on or off
POST/api/v1/routing/apply
Hand out unowned conversations by the rules; needs the send scope
- rulestring
- One rule
- depthstring
- all, quarter, month or week
- cursorstring
- From the previous answer, to go on
- floorstring
- From the previous answer, to go on
POST/api/v1/people
Add a person to the contacts
- namestring
- Name
- emailstring
- Email, phone or LinkedIn — at least one
- phonestring
- Email, phone or LinkedIn — at least one
- linkedinstring
- Email, phone or LinkedIn — at least one
- companystring
- Company
- intostring
- Add to this person instead of creating one
POST/api/v1/people/upsert
A row from Clay or another table: find or add the person and write their fields
- emailstring
- Email, phone or LinkedIn — at least one
- linkedinstring
- Email, phone or LinkedIn — at least one
- phonestring
- Email, phone or LinkedIn — at least one
- namestring
- Name
- first_namestring
- First name, when it is apart from the last
- last_namestring
- Last name
- companystring
- Company
- titlestring
- Job title
- fieldsobject
- Your own fields as name: value pairs, up to 60. An unknown name adds the field; an empty value leaves what is written
- segmentstring
- The segment the person enters
- campaignstring
- A campaign: the person enters its segment, and a running campaign picks them up within the hour
200 { "person": "4b1d…", "verdict": "merge", "fields": { "written": ["Recent funding", "Opening line"], "cut": [] }, "entered": 1 }POST/api/v1/people/import
A contacts file or list: plan or write
- fileobject
- name and content in base64, up to three megabytes
- rowsobject[]
- People as a list instead of a file, up to 500 per call: name, email, phone, linkedin, company, title, city
- columnsobject[]
- The plan's columns, edited by you
- commitboolean
- true — write
POST/api/v1/people/fields
Load people's own fields from a file: the key is an email, a LinkedIn profile or a company domain; without commit — how many match and would be written
- fileobject
- name and content in base64 — a small file
- urlstring
- An https link to a CSV or XLSX up to 50 MB — for a large file
- rowsobject[]
- Rows as objects instead of a file: the key and fields by name
- keystring
- The key column's header, when it should not be guessed
- columnsobject
- Header → field (name, field:<uuid>) or null to skip it
- frominteger
- next from the previous answer — to continue a long file
- commitboolean
- true — write
200 { "rows": 8000, "matched": 7412, "people": 51230, "writes": 102460, "same": 0, "missing": { "count": 588, "sample": [{ "line": 14, "key": "acme.com" }] }, "next": null }POST/api/v1/people/refresh
Refresh cards from the LinkedIn profile: the city, and the job title and company where empty
- segmentstring
- The segment whose people to refresh
- peoplestring[]
- Or person ids as a list
- commitboolean
- true — read the profiles and write; without it — how many people and the cost
- cursorstring
- The previous answer's cursor — continue from it
GET/api/v1/archive
Where the archive review stands
POST/api/v1/archive
Go through the archive: download from a day, then review — needs the send scope; or leave it
- dostringrequired
- start — go through the archive, decline — leave it while it downloads
- sincestring
- First day to download, YYYY-MM-DD
- channelsstring[]
- Channels to download from; left out — all that keep history
- groupsboolean
- Take Telegram groups
- freshboolean
- Also review past conversations for new products
GET/api/v1/dashboard
Dashboard numbers
- daysinteger
- Days of the chart; 14 by default
GET/api/v1/billing
Subscription and accounts — read only
GET/api/v1/setup
What is set up on the way to the first campaign and what is missing
GET/api/v1/events
The journal of events about people: what came in and what the rules did
- personstring
- Only this person's events
- typestring
- Only events of this type
- sourcestring
- Only events from this source
- cursorstring
- Where to continue from: next from the previous answer
- limitinteger
- How many rows to return: 50 by default, 200 at most
200 {
"events": [
{
"id": "9b1c…",
"person": "3f2a…",
"source": "11aa…",
"externalId": "evt_1042",
"type": "signed_up",
"email": "anna@example.com",
"name": "Anna",
"data": { "plan": "free" },
"occurredAt": "2026-10-10T09:12:00.000Z",
"receivedAt": "2026-10-10T09:12:01.000Z",
"outcomes": [{ "rule": "5c0e…", "action": "segment", "outcome": "done" }],
"quiet": null
}
],
"next": null
}GET/api/v1/events/rules
Event rules in the order they run
POST/api/v1/events/rules
Create an event rule; it runs last
- typestringrequired
- The event type exactly as the source sends it: signed_up, scan_ready
- actionstringrequired
- What to do: tag, segment, campaign or task
- sourcestring
- Source; without it the rule listens to any
- tagstring
- For tag: the tag. A product tag goes on the person and needs side; a correspondence tag goes on the conversation, so a person with no correspondence cannot get it
- sidestring
- Required for a product tag: buyer or seller
- segmentstring
- For segment: the segment the person enters
- campaignstring
- For campaign: the person enters the campaign's segment, and a running campaign picks them up within the hour
- textstring
- For task: what the agent running the person's conversation is asked to do, up to 500 characters. With no conversation there is no task
- delay_minutesinteger
- For task: minutes after the event before the task is due; 0 means at once
- agentstring
- For task: the agent to put on a conversation with none when no campaign wrote to the person; the campaign's agent goes first. Empty: only that one
- enabledboolean
- Whether the rule acts; yes by default
PATCH/api/v1/events/rules/{id}
Rewrite an event rule whole
- typestringrequired
- The event type exactly as the source sends it: signed_up, scan_ready
- actionstringrequired
- What to do: tag, segment, campaign or task
- sourcestring
- Source; without it the rule listens to any
- tagstring
- For tag: the tag. A product tag goes on the person and needs side; a correspondence tag goes on the conversation, so a person with no correspondence cannot get it
- sidestring
- Required for a product tag: buyer or seller
- segmentstring
- For segment: the segment the person enters
- campaignstring
- For campaign: the person enters the campaign's segment, and a running campaign picks them up within the hour
- textstring
- For task: what the agent running the person's conversation is asked to do, up to 500 characters. With no conversation there is no task
- delay_minutesinteger
- For task: minutes after the event before the task is due; 0 means at once
- agentstring
- For task: the agent to put on a conversation with none when no campaign wrote to the person; the campaign's agent goes first. Empty: only that one
- enabledboolean
- Whether the rule acts; yes by default
DELETE/api/v1/events/rules/{id}
Remove an event rule; the journal stays
GET/api/v1/events/sources
Event sources with their address and headers, without secrets
POST/api/v1/events/sources
Create an event source; the signing secret comes in the answer once
- namestringrequired
- What the source is, up to 80 characters: Website signups
200 {
"source": {
"id": "11aa…",
"name": "Website signups",
"createdAt": "2026-10-10T09:00:00.000Z",
"revokedAt": null,
"signing": {
"endpoint": "https://nitrino.ai/events/incoming",
"headers": { "nitrino-workspace": "9f1c…", "nitrino-source": "11aa…", "nitrino-signature": "t=<unix seconds>,v1=<hex HMAC-SHA256(secret, t + '.' + raw body)>" },
"secret": "c2f0…"
}
}
}DELETE/api/v1/events/sources/{id}
Revoke a source: its events get 401 from that second on
Pages
Lists are paged by cursor. Take next from the answer and send it back as it is. An empty next means there is nothing further.
Repeats
Everything that changes requires an Idempotency-Key header. A repeat with the same key returns the earlier answer and does not do the work twice.
Campaigns write only once started
A campaign draft writes to nobody. Starting and resuming need the send scope together with campaigns:write. Letters go out on weekdays, one person at a time, within each account's health: the daily limit is set by health, not by the program.
What else needs send
Besides a message, these actions need send: approving the agent's draft, forwarding a message and reacting to one, a meeting and its cancelling (Google writes to the guests), an agent task with a due moment, handing out the pile by the routing rules, an archive review, an agent put on a conversation or let back to writing — in one conversation, in many, or on an account, letting the agent reply in groups and taking hands-off away from an account, approving campaign letters from review, letting a campaign send without review, resuming a campaign, and inviting a person into the workspace. Most of them also need confirmed, the person’s yes to exactly this; each route says what it asks.
Clay and other tables
Enrichment from Clay reaches campaign letters through the campaign's own address. On the campaign card, open Data from Clay and press Connect Clay: an address appears, and it is the key. In the Clay table, add an HTTP API column: method POST, this address, and a flat JSON body with the name on the left and a Clay column via “/” on the right. Email, LinkedIn, phone, name and company are recognised by the key's name; any other key becomes a card field under its own name, and a campaign letter reads all of the person's fields. A new person joins the campaign's segment. A field's description is your word to the letter on how to use it: Describe sits next to the field on the same screen. Sending a row again is harmless: the same person, the same values. For a table without a campaign, use POST /people/upsert with an API key.
POST https://nitrino.ai/api/v1/people/upsert
Authorization: Bearer <API key>
{
"email": "{{Work Email}}",
"linkedin": "{{LinkedIn Profile}}",
"first_name": "{{First Name}}",
"last_name": "{{Last Name}}",
"company": "{{Company Name}}",
"title": "{{Job Title}}",
"campaign": "<campaign id>",
"fields": {
"Recent funding": "{{Recent Funding}}",
"Opening line": "{{Personalized Opener}}"
}
}Events about people
Your system tells Nitrino that a person did something on your side: signed up, started a scan. A source is created in Settings, Events, or with POST /events/sources, and its signing secret is shown once. An event goes to /events/incoming with the workspace, source and signature headers: t is the time in seconds, v1 is the HMAC-SHA256 of the secret over t, a dot and the raw body. An unsigned event or one signed more than five minutes ago gets 401 and is not written. The body is one event, an array of up to a hundred, or {"events": [...]}: type and email are required, and so is id or occurred_at: they are how a repeat is recognised, and a repeat does nothing twice. With neither, the event is refused with the word id. name and data are optional. The person is found by email or created, the rules decide what to do, and the agent sees the event in its next letter.
BODY='{"id":"evt_1042","type":"signed_up","email":"anna@example.com","data":{"plan":"free"}}'
T=$(date +%s)
SIG=$(printf '%s.%s' "$T" "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | sed 's/^.* //')
curl -X POST https://nitrino.ai/events/incoming \
-H "Content-Type: application/json" \
-H "Nitrino-Workspace: <workspace id>" \
-H "Nitrino-Source: <source id>" \
-H "Nitrino-Signature: t=$T,v1=$SIG" \
-d "$BODY"const { createHmac } = require("node:crypto");
const body = JSON.stringify({ id: "evt_1042", type: "signed_up", email: "anna@example.com" });
const t = Math.floor(Date.now() / 1000);
const v1 = createHmac("sha256", process.env.NITRINO_EVENT_SECRET).update(t + "." + body).digest("hex");
fetch("https://nitrino.ai/events/incoming", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Nitrino-Workspace": "<workspace id>",
"Nitrino-Source": "<source id>",
"Nitrino-Signature": "t=" + t + ",v1=" + v1,
},
body,
});