On this page

Make your first API request

Connect a hosted Terramine workspace to an external CRM or ERP using a service key and signed webhooks. This guide covers two workflows. Your CRM can request a draft sales order in Terramine ERP and receive a notification when the quotation is sent. Terramine CRM can notify your automation when a deal moves, then show the order number and link from your ERP on the deal card.

Building an AI agent? The agent integration guide maps tool calls to scopes and walks through draft creation, deal linking, event verification and recovery after a timeout.

Download the OpenAPI contract for the six supported API operations. The n8n workflow provides a manual CRM request you can run before connecting a live CRM trigger.

Try the customer example kit

Download the customer integration kit. It includes a Python client, a local simulation, webhook verification, the OpenAPI contract and the n8n example. Python 3.10 or newer is the only dependency; private repository access is not required. Start with the included README and run the local demo before connecting your own workspace. The demo uses synthetic records and does not create orders in Terramine.

Before you start

Have these ready:

  • Your workspace's HTTPS address, such as https://YOUR-WORKSPACE.eu.terraminedesk.ai. Copy the actual address from Admin → Integrations.
  • The current workspace owner, with an administrator role, to create credentials and subscriptions.
  • The relevant module enabled: ERP for customers and draft orders; CRM for deals and external order links. For draft orders, set up the selling entity in ERP → Reference → Our companies and an existing customer.
  • A secret store for the service key. For events, also provide a public HTTPS receiver on port 443 and a place to store its signing secret. Ask your workspace administrator to confirm that outgoing webhook delivery is available.

Fetch GET /api/v1/openapi.json from that workspace before building against it. This unauthenticated endpoint confirms the installed contract. If it returns 404, check the address and workspace rollout with the owner.

Create a service key

  1. As the workspace owner, open Admin → Integrations → Create a service key.
  2. Name the automation and select customers:read for your first read. For the quotation workflow below, also select orders:write, orders:read and documents:read.
  3. Acknowledge access to all workspace records of the selected object types, then create the key. Use a separate named key for each automation.
  4. Copy the one-time secret to the automation's credential store. Use Look up customers on the same page to check the first authenticated read.

For the curl examples below, set DESK_URL to your workspace's HTTPS origin and inject the key as DESK_KEY from the secret store. Send Authorization: Bearer <key> on every business API request. Keep authenticated requests on that origin and disable redirects in your HTTP client.

A successful lookup returns a JSON page with data and next_updated_since. Rotating a key invalidates its old secret immediately while preserving subscriptions and request receipts. Revoke the key when retiring the automation.

Make your first request

Inject DESK_KEY from your secret store into the shell environment, then replace the workspace placeholder and run this read-only request:

DESK_URL='https://YOUR-WORKSPACE.eu.terraminedesk.ai'
: "${DESK_KEY:?Inject your service key from your secret store first}"
curl --fail-with-body --get \
  -H "Authorization: Bearer $DESK_KEY" \
  --data-urlencode 'stable_cursor=true' \
  "$DESK_URL/api/v1/customers"

Expect HTTP 200 and a JSON page containing data and next_updated_since. An empty workspace returns this valid response:

{"data":[],"next_updated_since":null}

If customer records exist, data contains those records. A null cursor means there is no next page. For another page, keep stable_cursor=true and your filters fixed, and pass the entire non-null cursor as URL-encoded updated_since. For errors, use Troubleshoot a request.

Register a webhook receiver

Webhook setup is optional for your first read. To receive quote.sent, use a service key with documents:read; for crm.deal.stage_changed, use crm:read.

Under Receive signed events, select the service key, event and receiver's HTTPS URL, then create the subscription. Save its one-time signing secret on the receiver; each subscription has its own secret. To change the destination, create a new subscription and disable the old one.

Use Send synthetic test to check delivery and signing. It sends integration.test; verify it, save its event ID, and acknowledge it without taking a business action. Refresh Delivery history to see the result. A successful test checks the receiver connection; complete the business workflow below to verify order creation or linking.

Run the n8n workflow on your tenant

Import the n8n example. In Simulated CRM request, enter your tenant URL and records:

Field Value
base_url https://YOUR-WORKSPACE.eu.terraminedesk.ai
customer_name The exact name of an existing customer in your workspace
crm_deal_id The originating CRM deal reference
quote_request_id A unique reference for this quotation request; keep it unchanged for retries
currency The currency for this draft, for example USD
lines_json The requested parts and quantities, for example [{"pn":"TEST-123","qty":2}]

Replace the example tenant URL with your workspace's actual HTTPS address. Create a Header Auth credential containing Authorization: Bearer <workspace service key> and select it on both HTTP nodes. Running the workflow creates a draft in that tenant. Run the same input twice with the same credential and quote_request_id: both runs should return the same draft ID and order number. For a new quotation request, including another request for the same deal, use a new quote_request_id. Keep the saved payload unchanged during retries.

The workflow stops if customer lookup is incomplete or does not find exactly one name match. Resolve the customer in Terramine before running again. When the draft is created, open it in ERP → Customer orders for pricing and sending.

Request a Terramine draft from your CRM

  1. When a CRM deal reaches your quote-request stage, look up its customer. Match an existing Terramine customer ID, or present ambiguous name matches to a person. A customer can have several deals with different quote requests.
  2. Send the draft with the customer's ID, the deal ID in external_ref, and an Idempotency-Key stable for that quote request. Keep the same key, credential, and body through retries. A reused key with different input returns 409. For a genuinely new quote request on the same deal, choose a new request ID.
  3. Save the returned order ID on the deal. When Terramine sends quote.sent, its data.order_id identifies the order. Read the order and its external_ref to recover the originating deal even if your first request timed out.
curl --fail-with-body -H "Authorization: Bearer $DESK_KEY" \
  "$DESK_URL/api/v1/customers?q=Example&stable_cursor=true"
curl --fail-with-body -H "Authorization: Bearer $DESK_KEY" \
  -H 'Content-Type: application/json' -H 'Idempotency-Key: quote-request-4721' \
  -d '{"customer_id":"REPLACE_WITH_MATCHED_ID","external_ref":"deal-307","lines":[{"pn":"TEST-123","qty":2}]}' \
  "$DESK_URL/api/v1/orders"

The response contains the draft's id and no; creation and successful idempotent retries return 201. A draft is ready for the salesperson to price and send. For lists, request stable_cursor=true and pass the entire returned next_updated_since string, URL encoded, as the next request's updated_since. Keep filters fixed. Stop when the cursor is null.

Create a separate service key with crm:read and crm:write. Register your receiver for crm.deal.stage_changed with this key. In the receiver, compare data.to_stage with the customer's configured stage, for example negotiation. Fetch the deal with GET /api/v1/crm/deals/{deal_id} to check its current stage and company. A deal can leave and re-enter a stage.

Use a durable external ERP request reference derived from the workspace and Terramine deal ID. Reuse that reference for repeated events, including new events for the same deal. If the ERP supports idempotent creation, use its idempotency key. Otherwise reconcile by that unique external reference before retrying a timed-out create. Once the ERP confirms its order, attach its number and HTTPS link to Terramine. Inject this separate service key as CRM_KEY from your secret store. Set DEAL_ID to the Terramine deal ID from the verified event, and use the same DESK_URL as above.

curl --fail-with-body -X PUT -H "Authorization: Bearer $CRM_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"system":"Customer ERP","order_number":"SO-234","url":"https://erp.example/orders/234"}' \
  "$DESK_URL/api/v1/crm/deals/$DEAL_ID/external-order"

Repeating the same association succeeds. A different existing association returns 409 so the integrator can reconcile it. The salesperson can open the linked order from the deal.

Receive, verify, and replay

Read X-Desk-Signature: t=<timestamp>,v1=<hex digest> from the request headers. Using the subscription signing secret, verify the HMAC over the timestamp, a period, and the exact raw HTTP body before parsing. This Python example returns the signed envelope. Use a synchronized clock.

import hashlib
import hmac
import json
import time

def verified_event(raw: bytes, signature: str, secret: str):
    fields = dict(pair.split("=", 1) for pair in signature.split(","))
    timestamp = int(fields["t"])
    if abs(time.time() - timestamp) > 300:
        raise ValueError("expired webhook")
    expected = hmac.new(secret.encode(), str(timestamp).encode() + b"." + raw,
                        hashlib.sha256).hexdigest()
    if not hmac.compare_digest(fields["v1"], expected):
        raise ValueError("invalid webhook signature")
    return json.loads(raw)

Reject malformed or invalid messages. After verification, insert the signed event ID into a durable inbox with a unique constraint for this integration. Acknowledge with 2xx after that insert commits, including when the same event is already present. A worker should perform the CRM or ERP action with a stable business request ID. A crash between remote creation and recording success requires reconciliation with the receiving system.

Respond promptly: webhook delivery uses a five-second HTTP timeout and does not follow redirects. A non-2xx response is recorded as rejected; it does not schedule an automatic business retry.

Open Admin → Integrations → Delivery history to inspect the last 100 deliveries. Use Retry delivery for a rejected, expired or confirmed-unsent delivery. Replays retain event and delivery IDs. If delivery is unknown, check the receiver's inbox by event ID before deciding how to recover. A 2xx records receipt by the receiver. The business result is the order or deal association recorded by the receiving application.

Troubleshoot a request

Result Next step
400 or 422 Correct the request fields or query parameter types against the contract. The response may contain error or validation detail.
401 Check the workspace URL and whether the key is valid, expired, rotated or revoked.
403 Ask the owner to check the key's scope and the module's access and entitlement.
404 Check the record reference, workspace address and route availability.
409 Reconcile the saved request or existing external order link before another write. Do not create a new key to bypass a conflict.
413 Reduce the request body below the hosted 128 KiB limit. Drafts accept 1–300 lines.
429 Honor Retry-After when supplied, then retry with backoff.
Timeout or temporary 5xx The write outcome may be unknown. Retry with the same service key, request key and saved payload; reconcile before starting a new request.

List pages default to 50 records and accept up to 200. Use stable_cursor=true and keep filters fixed while paging; treat next_updated_since as opaque and stop when it is null. A workspace supports up to 20 active webhook subscriptions. Synthetic tests are limited to one per minute for each subscription.