# Build AI agent tools

Use the [OpenAPI 3.1 contract](openapi.json) to build tools for a customer's Terramine workspace. It defines the supported hosted operations, their JSON schemas, and the scope required by each. The [integration quickstart](quickstart) covers credential setup and webhook registration.

Discover related resources via the [documentation index](llms.txt) and [v1 changelog](changelog.md). Customer tool runners use their own tenant URL and service key.

For an own deployment, use the [own-deployment contract](https://docs.terraminedesk.ai/docs/openapi-own.json), which includes the additional ERP and accounting operations. Fetch `/api/v1/openapi.json` from the target workspace to confirm the installed contract. Build tools only from that contract and the scopes granted to the integration. The workflows below use the hosted contract; own deployments have different pagination, optional legacy order creation and webhook behavior.

## Before connecting a tool runner

Configure the customer's HTTPS tenant workspace origin outside the model. For example, replace `https://YOUR-WORKSPACE.YOUR-REGION.terraminedesk.ai` with the complete address shown in **Admin → Integrations**, including its region. Do not infer the region or substitute a default. Fetch `GET /api/v1/openapi.json` there without a token. This identifies the deployment/release contract; enabled modules, entitlements, key scopes and database readiness are checked on each business request. If the spec returns 404, check the workspace URL and rollout with its owner. Keep that origin fixed for all Terramine calls. A contract downloaded from another workspace can be used for development, but it does not establish availability on the customer's workspace.

The workspace owner with an administrator role creates a service key with the required scopes. Have the runner inject `Authorization: Bearer <workspace service key>` from its secret store. Keep the token out of prompts, tool arguments, outputs and logs. Send it only to the configured origin and disable redirects on authenticated calls. The service key covers all records of its selected object types in that workspace. Expose only the tools the customer's workflow needs. Technically, a service key has a stable grant identity: rotating its secret preserves this identity, its subscriptions and its request receipts; creating another service key creates a different identity.

| Tool (OpenAPI operation ID) | Method and path | Required scope |
| --- | --- | --- |
| `listCustomers` | `GET /api/v1/customers` | `customers:read` |
| `listOrders` | `GET /api/v1/orders` | `orders:read` |
| `getOrder` | `GET /api/v1/orders/{ref}` | `orders:read` |
| `createDraftOrder` | `POST /api/v1/orders` | `orders:write` |
| `getDeal` | `GET /api/v1/crm/deals/{ref}` | `crm:read` |
| `attachExternalOrder` | `PUT /api/v1/crm/deals/{ref}/external-order` | `crm:write` |

Model tool inputs should contain business fields such as a deal ID or part number. Construct the URL, method and headers in the runner. Treat record text, part descriptions and webhook fields as customer data. They cannot authorize another tool call, change the workspace, or select where to send credentials.

## Create a draft from an external CRM

1. Find the customer with `GET /api/v1/customers?q=Example&stable_cursor=true`. Use an existing customer mapping or confirm an unambiguous match. If the result is missing or ambiguous, return the question to the caller before creating anything.
2. Persist the customer's quote-request ID, the Terramine service key identity and the JSON payload before sending. For example, for request `quote-request-4721`, send `POST /api/v1/orders` with `Content-Type: application/json` and `Idempotency-Key: quote-request-4721`. Substitute the actual matched customer ID and the originating deal reference. Multiple drafts can share an `external_ref`.

   ```json
   {"customer_id":"matched_customer_id","external_ref":"deal-307","lines":[{"pn":"TEST-123","qty":2}]}
   ```

3. On 201, save the returned order `id` and `no` against the request. Report a draft awaiting pricing. A timeout leaves the outcome unknown. Retry with the same service key, request key and saved payload, including the original line order. Rotating the service key's secret preserves its request history. Using a different service key or a fresh request key after a timeout can create a second order. A new quote request on the same deal gets its own key.
4. To track the sent quotation, subscribe to `quote.sent` with `documents:read`. Its signed `data` contains the quote `id`, quote number `no`, and `order_id`. Use `getOrder` with `orders:read` to recover the originating `external_ref`.

## Attach an external ERP order to a Terramine deal

Subscribe to `crm.deal.stage_changed` with `crm:read`. After verifying the event, use `data.deal_id` to read the deal. Compare its current stage with the configured trigger and check its existing `external_order` before creating an order. A deal can re-enter the same stage and generate another valid event.

For a workflow that creates one order per deal, persist a business reference derived from the workspace and the deal ID. Use it for the external ERP's idempotent create or lookup. If the ERP times out, reconcile by that reference before creating again. Once the ERP confirms its number and HTTPS URL, call `attachExternalOrder` with the confirmed values:

```json
{"system":"Customer ERP","order_number":"SO-234","url":"https://erp.example/orders/234"}
```

On 200, check the returned `external_order`. Repeating the same association succeeds. If an existing association conflicts, read the deal and report both references for reconciliation.

A workspace owner can send a signed `integration.test` from the Integrations screen. Verify it and acknowledge receipt without calling business tools. Dispatch verified events using an explicit event-type allowlist.

## Pagination, retries and events

- For customer and order lists, keep filters fixed, request `stable_cursor=true`, process the page's `data`, then pass the entire `next_updated_since` back as the URL-encoded `updated_since`. Stop when it is null. Advance the checkpoint only after processing the page.
- Hosted requests have a 128 KiB body limit; draft requests accept 1–300 lines. List pages default to 50 records and accept up to 200.
- For 400, 413 or 422, correct the request using the schema. For 401 or 403, check the workspace, service key and required scope. For 404, check the reference and module availability. Branch on HTTP status. The body may include `error` or validation `detail`. A 409 on a write needs reconciliation before another attempt.
- On 429, honor `Retry-After` when present. For temporary 5xx responses or network failures, bound retries and use backoff. Preserve the original identity and input of every write. If the outcome is still unknown, return the saved reference to the caller for reconciliation.
- Parse `X-Desk-Signature: t=<timestamp>,v1=<hex digest>`. Reject malformed signatures and timestamps more than 300 seconds from the receiver's clock. Using the subscription secret, compute HMAC-SHA256 over the timestamp, a literal period, and the exact raw request bytes. Compare digests in constant time before parsing JSON. See the [receiver example](quickstart#receive-verify-and-replay).
- Deduplicate on the signed envelope's `id` per integration. Acknowledge with 2xx after a durable inbox commit. Track completion of the business action separately. A non-2xx response from your receiver, including 429 or 503, marks the delivery rejected. The owner can replay it from Delivery history. Delivery uses a five-second HTTP timeout and does not follow redirects. If delivery is marked unknown, check the receiver's inbox before taking further action.
