Partner API
For software platforms that resell or embed TruTec: create and manage customer workspaces, bill sessions, mint embed sessions, and test with sandbox dev keys.
View as MarkdownNote
Partner API (for software platforms). These endpoints are for software companies that resell or embed TruTec inside their own product for their own customers. A normal contractor workspace key cannot call them. Every endpoint on this page needs a key with the Partner permission, which TruTec grants to approved partners. Other keys get 403 API key lacks partner management permission.
If you are a contractor building tools for your own business, you do not need this page. Use your workspace key with the rest of the API.
How partner access works#
A partner key belongs to the partner's own TruTec account. It can act on two kinds of customer workspaces:
| Kind | partner_access_type |
How it is created | What the partner can do |
|---|---|---|---|
| Child workspace | child |
POST /v1/partner/firms |
Everything: plans, billing, keys, settings, data |
| Linked workspace | linked |
The customer approves a link to their existing TruTec workspace | Read and write data, bill sessions. Plans, keys, billing portal and measurement settings stay with the customer |
Choosing the workspace on each call#
When a partner key calls any other endpoint in this API (jobs, measurements, webhooks, exports and so on), it must say which customer workspace to use. Pass one of these, in the query string or JSON body:
| Parameter | Meaning |
|---|---|
firm_id |
The TruTec workspace id |
linked_workspace_token |
The token returned by the link flow for a linked workspace |
partner_user_id |
Your user id, if that user has been linked to a TruTec workspace |
Without one, the call fails with 400 linked_workspace_token, firm_id, or partner_user_id is required when using a partner API key. A workspace you are not connected to returns 404 Firm not found.
Dev keys and sandbox workspaces#
Partners can test without touching live customers or live billing.
- A sandbox workspace (
is_sandbox: true) is a separate test workspace. Its data never mixes with live workspaces. Its name starts with[DEV-<your account id>]. - A dev key (
is_dev: true) is a partner key that can only reach sandbox workspaces.
What a dev key can do:
- Create workspaces. Every workspace it creates is a sandbox workspace, and that workspace's own key is also a dev key.
- List, read and manage sandbox workspaces, and call the rest of the API against them (with
firm_id). - Run billing flows in Stripe test mode: checkout pages accept Stripe test cards, and no real money moves. If test mode is not available, plan changes are applied directly with no checkout.
- Create more dev keys.
What a dev key cannot do:
- See or touch a live workspace. Any live
firm_idreturns403Dev key can only access sandbox firms.GET /v1/partner/firmsand the embed workspace picker hide live workspaces. - Create live workspaces or live keys.
- Charge real money. Sandbox workspaces never create card charges for sessions or overage.
- Delete records. Dev keys do not have the Delete records permission.
Be aware:
- Partner-wide measurement defaults (
PUT /v1/partner/measurement-defaults) are shared by your whole account. A dev key that changes them changes the defaults for live workspaces you create later. Withapply_to_existing, a dev key only updates sandbox workspaces. - A dev key has a usage limit (default 1,000 billable sessions). After that, its calls return
429. - Sandbox sessions have a second limit for your whole account: 500 new sessions in any 30 days, across all your sandbox workspaces and keys. After that, new sessions return
429sandbox_limit_reacheduntil older ones age out. Refunding a sandbox session does not free a slot. Need more for testing? Contact TruTec. GET /v1/partner/healthtells you which kind of key you are using.
A live partner key can also reach sandbox workspaces. You can create one with "sandbox": true on POST /v1/partner/firms. Sessions there count toward the same 30-day sandbox limit.
Note
Dev keys and sandbox workspaces exist only for partner accounts. A contractor's own workspace keys (created in Settings → API keys) always act on that live workspace. To test safely, ask TruTec for a separate test workspace and use its keys.
Check your key#
GET /v1/partner/health
Permission: Partner
curl https://trutec.ai/api/v1/partner/health \
-H "X-API-Key: sk_your_api_key_here"
{"ok": true, "stripe_mode": "test", "key_type": "dev"}
key_type is dev or live. stripe_mode is test for dev keys and live otherwise.
Create a dev key#
POST /v1/partner/dev-key
Permission: Partner
| Name | Type | Required | Description |
|---|---|---|---|
name |
string | No | Default Partner Dev Key |
usage_limit |
integer | No | Max billable sessions. Default 1000 |
curl -X POST https://trutec.ai/api/v1/partner/dev-key \
-H "X-API-Key: sk_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{"name": "Staging"}'
{
"id": 640,
"name": "Staging",
"key_prefix": "sk_Xy7pQ2aB",
"is_active": true,
"is_dev": true,
"usage_count": 0,
"usage_limit": 1000,
"usage_remaining": 1000,
"last_used_at": null,
"created_at": "2026-10-12T15:00:00",
"revoked_at": null,
"key": "sk_Xy7pQ2aB...",
"mode": "sandbox"
}
key is shown once. Store it securely.
Customer workspaces#
Create a workspace#
POST /v1/partner/firms
Permission: Partner
Creates a child workspace and its first API key.
| Name | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | Company name. Must be unique across TruTec |
plan |
string | No | trial, silver, gold, platinum or diamond. Default gold |
email |
string | No | Billing email for the Stripe customer |
success_url |
string | For paid plans | Where Stripe Checkout sends the customer after paying |
cancel_url |
string | For paid plans | Where Stripe Checkout sends the customer if they cancel |
sandbox |
boolean | No | true creates a sandbox workspace with no Stripe at all |
key_name |
string | No | Name for the workspace's API key |
usage_limit |
integer | No | Usage limit for the workspace's key. Default unlimited (1000 for sandbox) |
measurement_settings |
object | No | Measurement presets for this workspace. See Measurement presets |
How the plan is set up:
trial: active at once astrialing. No checkout. Includes a small number of free sessions.- Paid plans with a live key: the workspace is
pendinguntil the customer pays atcheckout_url. - Paid plans with a dev key: the same, in Stripe test mode.
sandbox: true: active at once, no Stripe.
curl -X POST https://trutec.ai/api/v1/partner/firms \
-H "X-API-Key: sk_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"name": "Northgate Paving",
"plan": "gold",
"email": "[email protected]",
"success_url": "https://app.example.com/trutec/success",
"cancel_url": "https://app.example.com/trutec/cancel"
}'
{
"firm": {
"id": 5120,
"name": "Northgate Paving",
"is_sandbox": false,
"partner_access_type": "child",
"partner_managed": true,
"linked_direct": false,
"subscription_plan": "gold",
"subscription_status": "pending",
"pending_subscription_plan": null,
"cancel_requested": false,
"stripe_customer_id": "cus_Q1w2E3r4T5",
"api_enabled": true,
"measurements_used_this_period": 0,
"measurements_limit": 30,
"measurements_remaining": 30,
"total_measurements_ever": 0,
"current_period_end": null,
"created_at": "2026-10-12T15:02:11.204512"
},
"api_key": {
"id": 641,
"name": "Northgate Paving API Key",
"key_prefix": "sk_9fKs2LmQ",
"is_active": true,
"is_dev": false,
"usage_count": 0,
"usage_limit": null,
"usage_remaining": null,
"last_used_at": null,
"created_at": "2026-10-12T15:02:11.310021",
"revoked_at": null,
"key": "sk_9fKs2LmQ..."
},
"checkout_url": "https://checkout.stripe.com/c/pay/cs_live_...",
"mode": "live"
}
mode is live, test (dev key, Stripe test mode) or sandbox (no Stripe). The workspace key can read and write jobs and measurements. It cannot delete records or use partner endpoints. It is shown once.
Errors:
400name is required,Invalid plan. Must be one of: ...,success_url and cancel_url are required,measurement_settings must be an object409A company with this name already exists.existing_firm_idis set when that workspace is already yours500Failed to create billing session: ...
List workspaces#
GET /v1/partner/firms
Permission: Partner
Your child and linked workspaces, newest first. Dev keys see sandbox workspaces only.
| Name | Type | Required | Description |
|---|---|---|---|
limit |
integer | No | Default 50, max 100 |
offset |
integer | No | Default 0 |
curl "https://trutec.ai/api/v1/partner/firms?limit=50" \
-H "X-API-Key: sk_your_api_key_here"
{
"data": [{"id": 5120, "name": "Northgate Paving", "partner_access_type": "child", "subscription_plan": "gold", "subscription_status": "active", "...": "..."}],
"pagination": {"total": 1, "limit": 50, "offset": 0}
}
Linked workspaces include a linked_workspace_token. Trial workspaces include a trial object: eligible, scope, limit, used, remaining.
Get a workspace#
GET /v1/partner/firms/{firm_id}
Permission: Partner
Returns the workspace object above plus api_key: the workspace's active key (without the secret) for child workspaces, null for linked ones.
Errors: 404 Firm not found, 403 Dev key can only access sandbox firms.
Rotate a workspace key#
POST /v1/partner/firms/{firm_id}/rotate-key
Permission: Partner
Revokes every active key on the child workspace and issues one new key. Not available for linked workspaces.
| Name | Type | Required | Description |
|---|---|---|---|
name |
string | No | Name for the new key |
usage_limit |
integer | No | Usage limit for the new key |
is_dev |
boolean | No | Only allowed for sandbox workspaces. Dev keys always create dev keys |
curl -X POST https://trutec.ai/api/v1/partner/firms/5120/rotate-key \
-H "X-API-Key: sk_your_api_key_here"
Returns 201 with the key object and key (shown once). Errors: 400 is_dev keys can only be created for sandbox firms, 400 is_dev must be a boolean when provided, 403 for linked workspaces.
Plans and billing#
Plan names are trial, silver, gold, platinum and diamond. These endpoints manage child workspaces only. Linked workspaces return 403: the customer manages their own billing in TruTec.
Change plan#
POST /v1/partner/firms/{firm_id}/change-plan
Permission: Partner
For an active paid workspace.
| Name | Type | Required | Description |
|---|---|---|---|
plan |
string | Yes | silver, gold, platinum or diamond |
success_url, cancel_url |
string | When a checkout is needed | Needed when moving to or from Silver |
- Between monthly plans (Gold, Platinum, Diamond): applied at once. Upgrades are prorated and invoiced now. Downgrades are not prorated.
checkout_urlisnull. - To or from Silver: returns a
checkout_url. The plan changes after the customer completes checkout. Until thenpending_subscription_planshows the new plan.
curl -X POST https://trutec.ai/api/v1/partner/firms/5120/change-plan \
-H "X-API-Key: sk_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{"plan": "platinum"}'
{"firm": {"id": 5120, "subscription_plan": "platinum", "...": "..."}, "checkout_url": null, "previous_plan": "gold"}
Errors (400): plan is required, Firm is already on the gold plan, Firm subscription is not active. Use /reactivate for lapsed firms., Cannot change plan for a trial firm. Use /reactivate to upgrade from trial., Cannot change plan while cancellation is scheduled. ..., success_url and cancel_url are required for this plan change.
Reactivate or upgrade from trial#
POST /v1/partner/firms/{firm_id}/reactivate
Permission: Partner
Restarts billing for a lapsed workspace, undoes a scheduled cancellation, or moves a trial workspace to a paid plan.
| Name | Type | Required | Description |
|---|---|---|---|
plan |
string | For trial workspaces | Paid plan to start. Defaults to the current plan otherwise |
success_url, cancel_url |
string | When a checkout is needed | Stripe Checkout redirects |
If a cancellation is scheduled on a live subscription, it is undone at once (checkout_url: null, reactivated: true). Otherwise the response has a checkout_url for the customer.
Errors: 400 Firm subscription is already active, 400 plan is required when upgrading a trial firm, 400 Invalid plan. ....
Billing portal#
POST /v1/partner/firms/{firm_id}/billing-portal
Permission: Partner
Returns a Stripe billing portal link where the customer can update their card or cancel.
| Name | Type | Required | Description |
|---|---|---|---|
return_url |
string | Yes | Where the portal sends the customer back to |
{"portal_url": "https://billing.stripe.com/p/session/...", "firm": {"id": 5120, "...": "..."}}
Errors: 400 return_url is required, 400 No Stripe billing account found for this firm.
Sessions: how usage is billed#
Partner usage is billed per session. A session is one takeoff or job in your product, identified by your own session_id.
- A session is billed once. Sending the same
session_idagain returns the original charge withdeduplicated: true. - Detection calls with a partner key need a
session_idand bill the session on first use. Inside the embedded editor, the first detection in a session bills it. - Child workspaces get a few free sessions first (shown in
trial). Trial-plan workspaces then stop with402trial_exhausteduntil they move to a paid plan. - Silver workspaces pay per session. Monthly plans include a number of reports per period and bill overage per report after that. See Limits & billing for prices.
- Linked workspaces are billed under the customer's own TruTec plan.
- Sandbox workspaces never create card charges. They share a limit of 500 new sessions per 30 days for your account (see Dev keys and sandbox workspaces).
Charge a session#
POST /v1/partner/firms/{firm_id}/charge
Permission: Partner
Bills one session yourself, for example when your user opens a takeoff.
| Name | Type | Required | Description |
|---|---|---|---|
session_id |
string | Yes for trial, Silver and linked workspaces | Your idempotency key for this session |
partner_user_id |
string | No | Your user id, for reporting |
partner_customer_id |
string | No | Your customer id, for reporting |
curl -X POST https://trutec.ai/api/v1/partner/firms/5120/charge \
-H "X-API-Key: sk_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{"session_id": "job-88231", "partner_user_id": "u-417", "partner_customer_id": "c-0091"}'
{
"success": true,
"usage": {"count": 12, "limit": null, "remaining": null},
"measurement_usage": {"used_this_period": 12, "limit": 30, "total_ever": 47, "partner_access_type": "child", "billing_mode": "partner_session"},
"billing": {
"session_id": "job-88231",
"status": "charged",
"trial_applied": false,
"partner_user_id": "u-417",
"partner_customer_id": "c-0091",
"deduplicated": false,
"already_refunded": false,
"charge_source": "legacy_charge_endpoint",
"partner_access_type": "child",
"billing_mode": "partner_session",
"trial": {"eligible": true, "scope": "firm", "limit": 2, "used": 2, "remaining": 0}
}
}
When a card charge was made, the response also has overage: charged, amount_cents, payment_intent_id.
Billing errors carry a machine-readable reason:
| Status | reason |
Meaning |
|---|---|---|
400 |
session_id_required |
Send a session_id |
402 |
trial_exhausted |
Free sessions used up. Upgrade with /reactivate |
402 |
billing_required, missing_payment_method, payment_failed |
The customer's payment details need attention |
402 |
subscription_inactive |
Linked workspace billing is not active |
403 |
plan_required, upgrade_required |
Linked workspace plan does not cover this |
429 |
API key usage limit exceeded |
|
429 |
sandbox_limit_reached |
Your sandbox workspaces used their 500 sessions for the last 30 days. sandbox_usage has used, limit and window_days |
Refund a session#
POST /v1/partner/firms/{firm_id}/refund-charge
Permission: Partner
Reverses a session charge, for example when a takeoff failed. Any card payment for that session is refunded, and usage counters go back down. Refunding the same session twice is safe: the second call returns already_refunded: true.
| Name | Type | Required | Description |
|---|---|---|---|
session_id |
string | Yes for linked workspaces | The session to refund |
feedback |
string | No | Why it was refunded, up to 2,000 characters. Shared with the TruTec team |
curl -X POST https://trutec.ai/api/v1/partner/firms/5120/refund-charge \
-H "X-API-Key: sk_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{"session_id": "job-88231", "feedback": "Imagery was out of date for this site."}'
Returns the same shape as a charge, with billing.status: "refunded" and feedback_received: true when feedback was saved.
Errors: 404 Billing session not found, 409 Billing session is not charged, 402 Payment refund failed: ..., 400 No charged usage to refund.
Linking an existing TruTec workspace#
Use this when your customer already has a TruTec workspace. The customer signs in to TruTec and approves the link. The first link for a workspace must be approved by a workspace owner.
Start a link#
POST /v1/partner/link/start
Permission: Partner
| Name | Type | Required | Description |
|---|---|---|---|
partner_user_id |
string | Yes | Your id for the user who is linking |
return_url |
string | Yes | http(s) URL to send the user back to |
partner_customer_id |
string | No | Your customer id |
email_hint |
string | No | Shown to the user on the TruTec page |
state |
string | No | Echoed back to return_url |
{"link_url": "https://trutec.ai/...", "expires_in": 3600}
Send the user to link_url (valid for one hour). After approval TruTec redirects to return_url?status=success&code=...&state=....
Exchange the code#
POST /v1/partner/link/exchange
Permission: Partner
Send {"code": "..."} within 10 minutes.
{
"linked": true,
"firm": {"id": 4471, "name": "Lakeside Asphalt", "partner_access_type": "linked", "linked_direct": true, "...": "..."},
"linked_workspace_token": "eyJwYXJ0bmVy...",
"user": {"id": 9021, "email": "[email protected]", "username": "lakeside-owner"},
"partner_user_id": "u-417",
"partner_customer_id": "c-0091"
}
Store firm.id or linked_workspace_token and pass it on later calls. Errors: 400 Invalid or expired code, 404 Linked workspace not found.
Embedding the takeoff editor#
Your backend mints a short-lived URL. Your front end loads it in an iframe. Never send your API key to the browser.
Mint an embed session#
POST /v1/partner/embed-token or POST /v1/partner/firms/{firm_id}/embed-token
Permission: Partner
Creates (or resumes) a job in the customer's workspace and returns a signed URL valid for 24 hours. Minting does not bill anything.
| Name | Type | Required | Description |
|---|---|---|---|
session_id |
string | Yes | Your stable id for this takeoff. The same id resumes the same job. external_id is an alias |
firm_id |
integer | No | The workspace. If left out, TruTec works it out (see below) |
partner_customer_id |
string | No | Your customer id. Remembered so later sessions find the workspace |
partner_user_id |
string | No | Your user id. The job is owned by the linked TruTec user when known |
name |
string | No | Job name. Default Parking Measurement |
address |
string | No | Site address |
Without firm_id, the workspace is chosen from: a workspace remembered for this partner_customer_id, then the workspace linked to partner_user_id, then your only connected workspace.
curl -X POST https://trutec.ai/api/v1/partner/embed-token \
-H "X-API-Key: sk_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{"session_id": "job-88231", "partner_customer_id": "c-0091", "partner_user_id": "u-417", "name": "Northgate Plaza", "address": "1200 Commerce Dr, Austin, TX"}'
{
"embed_url": "https://trutec.ai/measurements/embed/parking?token=...",
"expires_in": 86400,
"job_id": 4812,
"project_id": 3307,
"firm_id": 5120,
"workspace_name": "Northgate Paving",
"session_id": "job-88231",
"external_id": "job-88231"
}
Mint a new URL each time the user opens the editor. Do not store embed URLs.
Errors:
400reason: session_id_required409reason: workspace_ambiguous: nothing was written. The response hascandidates(firm_id,name) and aworkspace_picker_url. Passfirm_id, or loadworkspace_picker_urlin the iframe so the customer picks once.404reason: no_connected_workspace403Dev key can only access sandbox firms
Workspace picker#
POST /v1/partner/embed-token/choose-workspace
Permission: None. The picker token in the body is the credential.
The page at workspace_picker_url calls this for you. You normally do not call it directly. Body: token, firm_id. The token works once: a second use returns 410 picker_token_used. A customer already mapped to another workspace returns 409 workspace_already_mapped; mint with an explicit firm_id to move them. On success it returns the same object as minting an embed session.
Messages from the iframe#
The editor talks to your page with postMessage. Check event.origin === "https://trutec.ai".
| Message from TruTec | When |
|---|---|
trutec:autosave |
The editor saved |
trutec:elements |
Reply to trutec:request-elements |
trutec:measurement |
Reply to trutec:request-save |
trutec:save-exit |
Reply to trutec:request-save-exit |
trutec:elements-error |
A request failed (error, request_id) |
| Message you send | Effect |
|---|---|
trutec:request-elements |
Read the current measurements without saving |
trutec:request-save |
Save, then reply with trutec:measurement |
trutec:request-save-exit |
Save, then reply with trutec:save-exit |
Include a request_id and it is echoed back. Reply messages carry data with project_id, measurement_id and the measurement summary. Read the saved result server-side with GET /v1/measurements/{id}?firm_id= (Measurements).
Measurement presets#
Presets set how the takeoff editor labels and shows items in your customers' workspaces. Keys: counter_labels (object of category to label), counter_display and view_display (object of category to display mode), custom_item_defs (list), enabled_categories (list of category keys), counter_unit_rules (object of category to {basis, divisor, label}), stall_centerline_enabled (boolean). Send null or empty to clear a key. Invalid categories or modes return 400 with the valid values.
Get partner defaults#
GET /v1/partner/measurement-defaults
Permission: Partner
Returns {"measurement_defaults": {...}}: the presets copied into every workspace you create.
Set partner defaults#
PUT /v1/partner/measurement-defaults
Permission: Partner
Send one or more preset keys. Add "apply_to_existing": true to also update your existing child workspaces now (sandbox ones only, for a dev key).
curl -X PUT https://trutec.ai/api/v1/partner/measurement-defaults \
-H "X-API-Key: sk_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{"counter_labels": {"handicap": "ADA Spaces"}, "apply_to_existing": true}'
{"measurement_defaults": {"counter_labels": {"handicap": "ADA Spaces"}, "...": "..."}, "applied_to_existing_firm_ids": [5120, 5121]}
400 Provide at least one of: ... when no preset key is sent.
Get or update one workspace's presets#
GET /v1/partner/firms/{firm_id}/measurement-settings
PATCH /v1/partner/firms/{firm_id}/measurement-settings
Permission: Partner
GET returns {"firm_id": 5120, "measurement_settings": {...}}. PATCH takes the same preset keys and returns the updated presets. PATCH is not available for linked workspaces (403).