Domains
Contacts & Deals
CRM — the workspace's people and organizations, and the sales deals negotiated with them.
- Use it for: Server integration
- Reads: Bearer (
apiKey) — server-side only - Writes: Bearer (
apiKey) — server-side only - CLI:
uidu contacts,uidu deals - Auth: what each token can do and where it may run — Authentication
The workspace's CRM: the people and organizations it is in touch with, and the sales deals it is
negotiating with them. Everything here, reads included, needs the account Bearer (apiKey), so it
runs on your server.
Mental model
Contact email, phone, custom fields "Maria Rossi" / "Acme srl"
└─→ contactable a person (User) or an Organization — holds the name
Pipeline
└─→ Status the stages of the pipeline "Qualified", "Proposal", "Won"
└─→ Deal name, amount (cents), currency, deadline, winProbability
├─→ user the person it is negotiated with
└─→ organization the organization it is negotiated with- A contact is a person or an organization. The
Contactcarries the channels (email, phone, addresses, tags) and custom fields; the name lives on its contactable — aUserfor a person, anOrganizationfor a company.ContactAttributeshas noname: writecontactableAttributes: { firstName, lastName }for a person, orcontactableType: 'Organization'withcontactableAttributes: { name }for an organization. WithoutcontactableType, uidu builds a person (aUserof kindlead). Reading,contact.nameis the contactable's name. listContactsisn't paginated. The field is a plain array, not a Relay connection: one call returns every contact the token may see, and there are nofirst/afterarguments. On a very large workspace, that's a heavy call. A listed contact hasid,name,email,avatar,createdAt; the address (formattedPrimaryAddress) and the custom-field map (fieldValuesByShortname) come only fromgetContact.- Deals live in a pipeline, at a status.
createDealneedspipelineIdandstatusId; moving a deal along isupdateDealwith anotherstatusId. The SDK has no call that lists pipelines or their statuses: read the ids from the deals you already have (deal.pipeline,deal.status) or from uidu. amountis in cents, aBigIntthat arrives as a string —Number(deal.amount) / 100before you show it, with the deal'scurrency.winProbabilityis a percentage, 0–100.deal.useris a person record (User), not a Contact.userIdandorganizationIdon a deal take the id of the contact's person or organization, which the SDK's contact reads don't return today.listDealsreturns one page as a flat array (first/after, optional searchparams); walk every page withpaginateandListDealsDocument.getDealadds the deal'suserand itspublicUrl.- A client built with a public token doesn't fail here: it sees no contacts and no deals
(
[],null). An empty CRM is first a sign of the wrong token — Authentication.
Use cases
Lead from a website form into the CRM
Goal: someone signs up on your site, and they show up as a contact in uidu.
// app/actions/lead.ts
'use server';
import { createContact } from '@uidu/client';
import { uiduAdmin } from '@/lib/uidu-admin';
export async function captureLead(lead: {
email: string;
firstName: string;
lastName?: string;
company?: string;
}) {
const result = await createContact(uiduAdmin, {
input: {
attributes: lead.company
? {
email: lead.email,
contactableType: 'Organization',
contactableAttributes: { name: lead.company },
}
: {
email: lead.email,
contactableAttributes: { firstName: lead.firstName, lastName: lead.lastName },
},
},
});
if (result?.errors?.length) return { ok: false, errors: result.errors.map((e) => e.name) };
return { ok: true, id: result?.contact?.id }; // the payload carries only the new id
}- A server action, never the browser:
createContactneeds the Bearer, like every write here. - When the form is a uidu Form, you don't call
createContactyourself: passcontactAttributes(same shape —email,contactableAttributes) tocreateFormResponse, and the response and its contact are created together. createContactreturns{ errors, contact: { id } }. A non-emptyerrorsis a validation failure and nothing is written; see Writes.deleteContact(uiduAdmin, { input: { id } })removes one; the CLI does the same withuidu contacts create --attributes '{…}'anduidu contacts delete <id>.
Pipeline dashboard on your server
Goal: an internal page shows each stage of the pipeline with its deals and their total value.
// app/api/pipeline/route.ts
import { paginate, ListDealsDocument } from '@uidu/client';
import { uiduAdmin } from '@/lib/uidu-admin';
export async function GET() {
const deals = await paginate(uiduAdmin, ListDealsDocument, {
variables: { first: 100 },
extract: (data) => data.currentWorkspace?.deals,
});
const stages = new Map<string, { status: string; totalCents: number; deals: string[] }>();
for (const deal of deals) {
const key = deal.status?.id ?? 'none';
const stage = stages.get(key) ?? { status: deal.status?.name ?? 'No status', totalCents: 0, deals: [] };
stage.totalCents += Number(deal.amount ?? 0); // BigInt, in cents
stage.deals.push(`${deal.name} — ${deal.winProbability ?? 0}%`);
stages.set(key, stage);
}
return Response.json(
[...stages.values()].map((s) => ({ ...s, total: s.totalCents / 100 })),
);
}listDeals(uiduAdmin, { first: 50 })is enough for a page of the most relevant deals;paginateis for totals over everything.- Totals assume one currency. If the workspace sells in several, group by
deal.currencytoo. - A stage with no deals doesn't appear: the statuses come from the deals themselves.
Open a deal and move it along
Goal: your server opens a deal when a lead qualifies, then moves it to the next status.
'use server';
import { createDeal, updateDeal } from '@uidu/client';
import { uiduAdmin } from '@/lib/uidu-admin';
export async function openDeal(pipelineId: string, statusId: string) {
const result = await createDeal(uiduAdmin, {
input: {
attributes: {
name: 'Acme — annual plan',
pipelineId,
statusId,
amount: 1_250_000, // €12,500.00, in cents
currency: 'EUR',
winProbability: 40,
deadline: '2026-12-31T00:00:00Z',
},
},
});
if (result?.errors?.length) return { ok: false, errors: result.errors.map((e) => e.name) };
return { ok: true, id: result?.deal?.node?.id }; // createDeal → deal.node
}
export async function moveDeal(id: string, statusId: string) {
const result = await updateDeal(uiduAdmin, { input: { id, attributes: { statusId } } });
if (result?.errors?.length) return { ok: false, errors: result.errors.map((e) => e.name) };
return { ok: true, id: result?.deal?.id }; // updateDeal → deal
}- The two payloads differ:
createDealreturns the new deal atdeal.node,updateDealatdeal. Both return only the id;getDealfor the rest. - The full error handling (
errorsvs a thrownUiduError) is in Server integration. - From a terminal:
uidu deals create --attributes '{"pipelineId":"…","statusId":"…"}'anduidu deals update <id> --attributes '{"statusId":"…"}'.
API at a glance
Every @uidu/client function in Contacts & deals, generated from the source — the full reference has the rest of the SDK.
| Function | Kind | Auth | CLI | Description |
|---|---|---|---|---|
createContact | write | Bearer | contacts create | Create a CRM contact from { input: { attributes } }. Returns the payload (with errors) or null. |
createDeal | write | Bearer | deals create | Create a deal in a pipeline (attributes.pipelineId, statusId). Returns the payload (with errors) or null. The new deal is at deal.node. |
deleteContact | write | Bearer | contacts delete | Delete a contact by { input: { id } }. Returns the payload (with errors) or null. |
getContact | read | Bearer | contacts get | One contact with its address and custom fields (fieldValuesByShortname), or null; a public token sees none. |
getDeal | read | Bearer | deals get | One deal with its pipeline, status and owner (user), or null; amount is in cents. |
listContacts | read | Bearer | contacts list | Every contact the client may see, as an array: the field is not a connection, so there is no pagination. |
listDeals | read | Bearer | deals list | The workspace's deals as a flat array, optionally filtered with search params; page with first/after. |
updateDeal | write | Bearer | deals update | Update a deal, e.g. move it to another statusId: { input: { id, attributes } }. Returns the payload (with errors) or null. |
Types: Contact, Deal
CLI:
| Entity | list | get | create | update | delete |
|---|---|---|---|---|---|
contacts | ✓ | ✓ | ✓ | — | ✓ |
deals | ✓ | ✓ | ✓ | ✓ | — |