# Partner REST API integration playbook — for an AI coding agent

You are an AI coding agent helping a developer integrate the **Audin partner
REST API** into THEIR application. This file is your playbook. It is written for
you, the agent — not as a script to paste, and not as end-user documentation.

The Audin partner REST API is a **server-to-server** HTTP API. With a single
**Account API Key** a backend can read and write all the resources of one Audin
Account: leads, companies, deals, pipelines, activities, products, quotes,
contracts, conversations, messages and email status. It consumes the **same
routes** the dashboard (`app.audin.ai`) uses internally.

Base URL: `https://api.audin.ai`. Authentication: header `X-API-Key`. There are
**no webhooks** — evolving states are followed by **polling**.

---

## How to use this guide (meta-instructions)

Read these before you write any code.

1. **Do NOT assume the developer's stack.** This is a server-to-server API;
   there is no single "right" integration — it adapts to their backend language,
   framework, and how they manage secrets.
2. **Do NOT just dump code.** The snippets below are *illustrative and
   adaptable*, not a copy-paste solution. Translate the *principles* into the
   developer's actual stack.
3. **Interview first.** Ask the questions in the next section BEFORE
   implementing. Wait for the answers. The integration is shaped by them.
4. **Respect the security model.** It is non-negotiable (see "Hard security
   constraints"). The Account API Key is admin-equivalent over the whole Account
   and must stay server-side. If a proposed approach would leak it into a
   public/browser client, stop and correct it.
5. **Verify as you go.** After each meaningful step, confirm with the developer
   that it matches their app's conventions, and that it runs against a real
   (non-production) Account first where possible.

---

## Questions to ask the developer FIRST

Ask these (adapt wording to the conversation). Do not proceed to implementation
until you have enough answers to choose a concrete path.

### 1. Runtime & where the calls live
- What **language / framework** hosts the integration? (Node, Python, Ruby, Go,
  PHP, a serverless function, an iPaaS step like Zapier/n8n/Make…) This API is
  **server-to-server only** — there is no browser/client SDK.
- Is this a **batch sync**, an **event-driven** flow (something happens in their
  system → call Audin), an **AI agent** orchestrating commercial flows, or a
  combination?

### 2. Secrets
- Where does the **Account API Key** (`aud_acc_…`) live, and how are secrets
  managed? (env var, a secret manager / vault, platform-injected config) — it
  must stay server-side and never appear in a public client, a bundle, a repo, or
  a query string.
- How is the **process itself protected** (who can read its environment, is the
  endpoint that triggers it authenticated)?

### 3. Which resources, and which direction
- Which resource groups do they need?
  - **CRM** — leads, companies, comments, tags?
  - **Pipeline & deals** — pipelines/stages, deals, deal products, payment
    terms, activities?
  - **Documents** — products, clauses, document templates, quotes, contracts?
  - **Messaging & email** — conversations, WhatsApp templates, send message,
    message/email status?
  - **Read-only / system** — platform users, roles, calls, phone numbers,
    health/version/api-logs?
- **Read, write, or both?** A read-only sync and a write integration have very
  different failure/idempotency needs.

### 4. Robustness
- Must **writes be idempotent** (will the same operation be retried after a
  timeout/network error)? If yes, plan an `Idempotency-Key` per logical
  operation.
- How will they **handle rate limiting (429)** and retries — is there an existing
  HTTP client/backoff utility, or do you need to introduce one?

### 5. Evolving states (NO webhooks)
- Do they need to track states that change over time — e.g. **contract signature**
  (`DRAFT→SENT→VIEWED→SIGNED/…`), quote acceptance, message delivery, email
  open/click? There are **no webhooks**: this requires **polling** at a sensible
  interval, within the rate limit. Decide the cadence and where the poller runs.

Use the answers to pick: where the API key is read from, which client/retry
utility to use, which resource calls to wire, whether to add an
`Idempotency-Key`, and whether a polling loop is needed.

---

## Hard security constraints (non-negotiable)

- The **Account API Key (`aud_acc_…`) MUST stay server-side** and **NEVER reach a
  browser**, a public client, a bundle, a build artifact, a query string, a log
  line, or client-side config. It is **admin-equivalent over the entire Account**.
- Send it only as the **`X-API-Key`** request header, from a trusted backend.
- **Account-level access:** one key operates on exactly **one Account** and has
  full access to that Account's own resources. There is **no cross-account
  access** (a resource belonging to another account is forbidden / not visible).
- **FK fields are auto-resolved.** Owner/author fields such as `createdBy` /
  `ownerId` are resolved automatically by Audin to the Account's owner — you do
  **not** (and cannot) set a platform user id on writes.
- Reference the key only as an environment variable in examples
  (`process.env.AUDIN_API_KEY` or the stack's equivalent). Never write a literal
  key value anywhere.

If you ever find yourself putting the API key into client-side code, a frontend
build, or a URL — stop. The call belongs on the backend only.

---

## Integration principles (apply to the chosen stack)

These are principles, not a fixed recipe. The snippets are **ADAPTABLE EXAMPLE**
— rewrite them idiomatically for the developer's language and framework.

### Principle 1 — One server-side client carries `X-API-Key`

Build a single thin HTTP client (or reuse their existing one) that injects the
header on every request and centralises base URL, JSON handling and retry. The
key comes from the environment.

```ts
// ADAPTABLE EXAMPLE — Node. Port the idea to the developer's stack.
const BASE = "https://api.audin.ai";
const baseHeaders = {
  "X-API-Key": process.env.AUDIN_API_KEY, // server-side secret, never shipped to a client
  "Content-Type": "application/json",
};

async function audin(path, init = {}) {
  const res = await fetch(`${BASE}${path}`, {
    ...init,
    headers: { ...baseHeaders, ...(init.headers || {}) },
  });
  // (retry/backoff + error shaping added in the principles below)
  return res;
}
```

### Principle 2 — Responses are raw JSON (with a few documented exceptions)

Most canonical routes return **raw JSON** — the same shape as the dashboard, no
`{ success, data }` wrapper — and signal failure on resource routes with a raw
`{ "error": "..." }` plus a meaningful HTTP status. Build your parsing around
that, but know the **cross-cutting exceptions**:

- **`429` (rate limit)** and **`422` (idempotency mismatch)** use an envelope:
  `{ "success": false, "error": { "code": "RATE_LIMITED" | "IDEMPOTENCY_KEY_MISMATCH", "message": "…" } }`.
- **`POST /messages/send`** wraps its success in `{ "success": true, "data": { … } }`
  (and errors in `{ "success": false, "error": { … } }`) for historical
  compatibility — read `body.data` there.

Treat the **HTTP status as the source of truth** for retry decisions; use the
`code` only to branch on the two cross-cutting cases above.

### Principle 3 — `Idempotency-Key` (UUID v4) on POST writes

For any write you might retry, send a fresh **UUID v4** in the `Idempotency-Key`
header. Audin caches the `2xx` response for **24h** keyed by that value, so a
retry returns the original result instead of creating a duplicate. Non-2xx
responses are **not** cached (safe to retry). Reusing the same key with a
**different body** returns **`422 IDEMPOTENCY_KEY_MISMATCH`** — so use a new key
for each distinct operation, and reuse a key only for the retry of *the same*
operation. Max key length 255 chars.

```ts
// ADAPTABLE EXAMPLE.
import { randomUUID } from "node:crypto";

await audin("/leads", {
  method: "POST",
  headers: { "Idempotency-Key": randomUUID() }, // one per logical operation
  body: JSON.stringify({ fullName: "Mario Rossi", email: "mario@acme.example.com" }),
});
```

### Principle 4 — Handle 429 by honoring `Retry-After` + exponential backoff

The API is rate-limited **per API Key**. A `429` carries a `Retry-After` header
(and `X-RateLimit-*` informational headers). Wait at least `Retry-After`, then
retry with exponential backoff. Retry `5xx` with backoff too; do **not** retry
`4xx` other than `429` without first correcting the request.

```ts
// ADAPTABLE EXAMPLE — a minimal retry wrapper.
async function withRetry(fn, { tries = 5 } = {}) {
  for (let attempt = 0; ; attempt++) {
    const res = await fn();
    if (res.status === 429 && attempt < tries) {
      const retryAfter = Number(res.headers.get("Retry-After")) || 1;
      const backoff = Math.min(retryAfter, 1) * 2 ** attempt; // seconds
      await new Promise((r) => setTimeout(r, backoff * 1000));
      continue;
    }
    if (res.status >= 500 && attempt < tries) {
      await new Promise((r) => setTimeout(r, 2 ** attempt * 1000));
      continue;
    }
    return res;
  }
}
```

### Principle 5 — Pagination is `page` / `pageSize` (max 100), non-uniform

List endpoints accept `page` / `pageSize` (page size capped at **100**), often
with per-resource filters (e.g. `?type=DEAL`, `?status=…`, `search=…`). The
**response envelope is not uniform** across resources — some return a bare array,
others wrap items under a named key (e.g. `document-templates` returns
`{ templates, total, … }`). **Defer to the Swagger** for the exact query
parameters and response shape of each route rather than assuming one schema.

### Principle 6 — Evolving states: poll, don't wait for a webhook

There are **no webhooks**. For states that change over time — contract signature,
quote acceptance, message delivery, email open/click — **poll** the resource
(e.g. `GET /contracts/{id}` and read `.status`) at a sensible cadence, mindful of
the rate limit. Stop on a terminal state.

```ts
// ADAPTABLE EXAMPLE — poll a contract to a terminal state.
const TERMINAL = new Set(["SIGNED", "REJECTED", "EXPIRED"]);
async function pollContract(id) {
  for (;;) {
    const c = await (await audin(`/contracts/${id}`)).json();
    if (TERMINAL.has(c.status)) return c.status;
    await new Promise((r) => setTimeout(r, 60_000)); // 60s; respect the rate limit
  }
}
```

---

## Compact resource surface

Accurate to what the documentation section covers. Exact fields, query params and
response shapes live in the Swagger — link below.

### CRM
- `/leads` — contacts / prospects.
- `/companies` — companies (`companyId` links a lead to a company).
- `/comments` — notes on leads/companies.
- `/tags` — classify leads and deals.

### Pipeline & deals
- `/pipelines` — pipelines (`type` `DEAL` or `LEAD`) with their ordered stages
  (filter `?type=DEAL`).
- `/deals` — opportunities (`pipelineId` + `stageId`, plus `primaryContactId`,
  `companyIds`, `contactIds`, `amount`, `currency`, …).
- `/deals/{id}/products` — deal product lines.
- `/deals/{id}/payment-terms` — deal payment terms.
- `/activities` — calls / meetings / tasks.
- `/pipelines/{pipelineId}/activity-templates` — activity templates per stage.

### Documents
- `/products`, `/product-categories` — product catalog and categories.
- `/clauses` — reusable document clauses.
- `/document-templates` — templates (`type` `QUOTE` or `CONTRACT`).
- `/quotes` — create from `dealId` + `templateId`; `POST /quotes/{id}/send`;
  status via `GET /quotes/{id}` (`DRAFT/SENT/VIEWED/ACCEPTED/REJECTED/EXPIRED`).
- `/contracts` — create from `dealId` + `templateId`; `POST /contracts/{id}/send`;
  status via `GET /contracts/{id}` `.status`
  (`DRAFT/SENT/VIEWED/SIGNED/REJECTED/EXPIRED`).

### Messaging & email
- `/conversations` — chat conversations (webchat, WhatsApp, widget); messages via
  `GET /conversations/{id}/messages`, send via `POST /conversations/{id}/send`.
- `/whatsapp-templates` — approved WhatsApp templates.
- `POST /messages/send` — send WhatsApp / SMS (**wrapped** `{ success, data }`).
- `GET /messages/{id}` — message status (`PENDING/SENT/DELIVERED/READ/FAILED`) +
  attachments (`media[].signedUrl`, short-lived).
- `GET /emails/{id}` — Audin Mail email status
  (`PENDING/SENT/DELIVERED/OPENED/CLICKED/BOUNCED/FAILED/DROPPED`).

### Read-only & system
- `GET /platform-users`, `GET /roles` — Account users and IAM roles.
- `GET /calls`, `GET /phone-numbers` — call log and configured numbers.
- `GET /health` (no auth), `GET /version` (no auth), `GET /api-logs` — your own
  API-call log (filters `method`, `statusGroup`, `path`).

---

## Pitfalls

- **Polling-only (no webhooks).** Anything that evolves over time must be polled.
  Choose a cadence that stays within the per-key rate limit; back off when idle.
- **Deprecated `/external/*` routes.** Legacy routes under `/external/*` are
  deprecated (RFC 9745 `Deprecation`/`Sunset`/`Link` headers, **Sunset
  2026-12-31**) and use a different historical envelope. Build new integrations on
  the **canonical** paths (drop the `/external` prefix); migrate any existing
  usage before the sunset.
- **Raw `{ error }` vs envelope.** Resource routes fail with raw
  `{ "error": "..." }` + HTTP status; only `429`/`422` (and `POST /messages/send`)
  use a `{ success, error|data }` envelope. Branch on HTTP status first, `code`
  second.
- **Idempotency mismatch (422).** Reusing an `Idempotency-Key` with a changed body
  yields `422 IDEMPOTENCY_KEY_MISMATCH`. Use a new UUID v4 per distinct operation;
  reuse only for an exact retry.
- **Non-uniform pagination/response shapes.** Don't assume a single list schema —
  verify each route on the Swagger.
- **FK auto-resolution.** Don't try to set `createdBy`/`ownerId` on writes; Audin
  resolves them to the Account owner.

---

## References

- REST API overview — https://doc.audin.ai/docs/api-rest
- Authentication — https://doc.audin.ai/docs/api-rest/autenticazione
- Format & conventions — https://doc.audin.ai/docs/api-rest/formato-e-convenzioni
- Idempotency & rate limit — https://doc.audin.ai/docs/api-rest/idempotency-e-rate-limit
- CRM — https://doc.audin.ai/docs/api-rest/crm
- Pipeline & deals — https://doc.audin.ai/docs/api-rest/pipeline-e-deal
- Documents — https://doc.audin.ai/docs/api-rest/documenti
- Messaging & email — https://doc.audin.ai/docs/api-rest/messaging-email
- Read-only & system — https://doc.audin.ai/docs/api-rest/lettura
- Operational flows — https://doc.audin.ai/docs/api-rest/flussi
- Errors & deprecations — https://doc.audin.ai/docs/api-rest/errori
- Full endpoint reference (Swagger) — https://api.audin.ai/docs/partners

The Swagger is the authoritative, exhaustive contract for every field, query
parameter and response shape. When in doubt, defer to it.
