# Contacts &amp; Deals

URL: https://developers.uidu.org/docs/domains/contacts

> CRM — the workspace's people and organizations, and the sales deals negotiated with them.

* **Use it for:** [Server integration](https://developers.uidu.org/docs/paths/server-integration.md)
* **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](https://developers.uidu.org/docs/foundations/authentication.md)

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 &#x2A;*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`](https://developers.uidu.org/docs/foundations/pagination-and-field-values.md) 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](https://developers.uidu.org/docs/foundations/authentication.md).

## 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.

```ts
// 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](https://developers.uidu.org/docs/domains/forms.md), 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](https://developers.uidu.org/docs/foundations/writes.md).
* `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.

```ts
// 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.

```ts
'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&#x60; returns the new deal at &#x2A;*`deal.node`**, `updateDeal&#x60; at
  &#x2A;*`deal`**. Both return only the id; `getDeal` for the rest.
* The full error handling (`errors` vs a thrown `UiduError`) is in
  [Server integration](https://developers.uidu.org/docs/paths/server-integration.md).
* 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](https://developers.uidu.org/docs/reference/client.md#contacts) has the rest of the SDK.

| Function                                                                                         | Kind  | Auth   | CLI               | Description                                                                                                                                     |
| ------------------------------------------------------------------------------------------------ | ----- | ------ | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| [`createContact`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts) | write | Bearer | `contacts create` | Create a CRM contact from `{ input: { attributes } }`. Returns the payload (with `errors`) or null.                                             |
| [`createDeal`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts)    | 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`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts) | write | Bearer | `contacts delete` | Delete a contact by `{ input: { id } }`. Returns the payload (with `errors`) or null.                                                           |
| [`getContact`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/contacts.ts)     | read  | Bearer | `contacts get`    | One contact with its address and custom fields (`fieldValuesByShortname`), or null; a public token sees none.                                   |
| [`getDeal`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/contacts.ts)        | read  | Bearer | `deals get`       | One deal with its pipeline, status and owner (`user`), or null; `amount` is in cents.                                                           |
| [`listContacts`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/contacts.ts)   | 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`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/contacts.ts)      | read  | Bearer | `deals list`      | The workspace's deals as a flat array, optionally filtered with search `params`; page with `first`/`after`.                                     |
| [`updateDeal`](https://github.com/uidu-org/api.js/blob/main/packages/client/src/mutations.ts)    | 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`    |   ✓  |  ✓  |    ✓   |    ✓   |    —   |
