uidudevelopers
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 Contact carries the channels (email, phone, addresses, tags) and custom fields; the name lives on its contactable — a User for a person, an Organization for a company. ContactAttributes has no name: write contactableAttributes: { firstName, lastName } for a person, or contactableType: 'Organization' with contactableAttributes: { name } for an organization. Without contactableType, uidu builds a person (a User of kind lead). Reading, contact.name is the contactable's name.
  • listContacts isn't paginated. The field is a plain array, not a Relay connection: one call returns every contact the token may see, and there are no first/after arguments. On a very large workspace, that's a heavy call. A listed contact has id, name, email, avatar, createdAt; the address (formattedPrimaryAddress) and the custom-field map (fieldValuesByShortname) come only from getContact.
  • Deals live in a pipeline, at a status. createDeal needs pipelineId and statusId; moving a deal along is updateDeal with another statusId. 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.
  • amount is in cents, a BigInt that arrives as a string — Number(deal.amount) / 100 before you show it, with the deal's currency. winProbability is a percentage, 0–100.
  • deal.user is a person record (User), not a Contact. userId and organizationId on a deal take the id of the contact's person or organization, which the SDK's contact reads don't return today.
  • listDeals returns one page as a flat array (first/after, optional search params); walk every page with paginate and ListDealsDocument. getDeal adds the deal's user and its publicUrl.
  • 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: createContact needs the Bearer, like every write here.
  • When the form is a uidu Form, you don't call createContact yourself: pass contactAttributes (same shape — email, contactableAttributes) to createFormResponse, and the response and its contact are created together.
  • createContact returns { errors, contact: { id } }. A non-empty errors is a validation failure and nothing is written; see Writes.
  • deleteContact(uiduAdmin, { input: { id } }) removes one; the CLI does the same with uidu contacts create --attributes '{…}' and uidu 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; paginate is for totals over everything.
  • Totals assume one currency. If the workspace sells in several, group by deal.currency too.
  • 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: createDeal returns the new deal at deal.node, updateDeal at deal. Both return only the id; getDeal for the rest.
  • The full error handling (errors vs a thrown UiduError) is in Server integration.
  • From a terminal: uidu deals create --attributes '{"pipelineId":"…","statusId":"…"}' and uidu 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.

FunctionKindAuthCLIDescription
createContactwriteBearercontacts createCreate a CRM contact from { input: { attributes } }. Returns the payload (with errors) or null.
createDealwriteBearerdeals createCreate a deal in a pipeline (attributes.pipelineId, statusId). Returns the payload (with errors) or null. The new deal is at deal.node.
deleteContactwriteBearercontacts deleteDelete a contact by { input: { id } }. Returns the payload (with errors) or null.
getContactreadBearercontacts getOne contact with its address and custom fields (fieldValuesByShortname), or null; a public token sees none.
getDealreadBearerdeals getOne deal with its pipeline, status and owner (user), or null; amount is in cents.
listContactsreadBearercontacts listEvery contact the client may see, as an array: the field is not a connection, so there is no pagination.
listDealsreadBearerdeals listThe workspace's deals as a flat array, optionally filtered with search params; page with first/after.
updateDealwriteBearerdeals updateUpdate a deal, e.g. move it to another statusId: { input: { id, attributes } }. Returns the payload (with errors) or null.

Types: Contact, Deal

CLI:

Entitylistgetcreateupdatedelete
contacts✓✓✓—✓
deals✓✓✓✓—

On this page