Make your first authenticated request
Create a token in Developer settings, copy its value when it is shown, and keep it in a server-side secret manager. Token values are not shown again and expire within 90 days.
- 1Create a token
Start with
read-onlyunless this integration must change agreement state. - 2Check the token
Call
GET /v1/meto confirm its organization, role, and scopes. - 3See what you can create
Call
GET /v1/catalogfor the gate and action kinds available to the organization.
export TERMN_API_TOKEN="termn_live_…"
curl --request GET \
--url https://api.staging.termn.ai/v1/me \
--header "Authorization: Bearer $TERMN_API_TOKEN"{
"token_id": "11111111-1111-4111-8111-111111111111",
"actor_id": "11111111-1111-4111-8111-111111111111",
"organization_id": "22222222-2222-4222-8222-222222222222",
"token_prefix": "termn_live_…",
"scopes": ["read-write"],
"role": "owner"
}Save the request ID: keep X-Termn-Request-Id with your logs so Termn support can trace the request.
See how agreement resources fit together
An agreement contains participants and ordered stages. Gates determine when each stage is ready, actions run after readiness, and audits record each attempt. The settlement statement links the final record back to those audits.
- Agreement
- Draft, validate, publish, or cancel the governing record.
- Stage
- Order the work and define its expiration.
- Gate
- Record evidence that a condition is complete.
- Action
- Queue controlled work after readiness.
- Audit
- Retain attempts, retries, actors, and evidence.
- Settlement
- Retrieve the final record linked to gate and action audits.
Signing links belong to one participant. They are not API credentials and must never be sent as bearer tokens. Use the agreement walkthrough to create and complete an agreement.
Confirm how the agreement will be paid for
POST /v1/agreements succeeds when the organization has an active Operator or Team project start, when it supplies an unused paid Checkout Session for the Termn-selected project price, or when a general agreement without an active subscription assigns the $149 Project Pass checkout to one named participant. That participant funds the project but does not become an organization operator. If none applies, Termn returns 402 payment_required.
| Path | CreateAgreement fields | Important constraint |
|---|---|---|
| Active subscription | Omit both billing fields. | The subscription must be active or trialing for this organization. |
| Paid project checkout | payment_checkout_session_id | Termn selects the $149 Project Pass, $79 Operator overage, or $49 Team overage. The session must be paid, unused, and owned by this organization. |
| Participant-paid Project Pass | project_pass_payer_email | The email must exactly match a participant; this path is unavailable with an active subscription. |
curl --request POST \
--url https://api.staging.termn.ai/v1/agreements \
--header "Authorization: Bearer $TERMN_API_TOKEN" \
--header "Idempotency-Key: agreement-create-20260716-001" \
--header "Content-Type: application/json" \
--data '{
"name": "Vendor agreement",
"owner_email": "owner@example.com",
"participant_emails": ["signer@example.com"],
"payment_checkout_session_id": "cs_paid_one_agreement"
}'curl --request POST \
--url https://api.staging.termn.ai/v1/agreements \
--header "Authorization: Bearer $TERMN_API_TOKEN" \
--header "Idempotency-Key: agreement-create-participant-fee-001" \
--header "Content-Type: application/json" \
--data '{
"name": "Vendor agreement",
"owner_email": "owner@example.com",
"participant_emails": ["payer@example.com"],
"project_pass_payer_email": "payer@example.com"
}'Do not send both billing fields. Complete hosted Checkout on the returned Stripe URL; never collect card data through your own Termn integration.
Give the token only the access it needs
Send Authorization: Bearer <api_token>on every customer-resource request. Termn checks both the token scope and the member's organization or agreement role before allowing the request.
| Scope | Use it for | Does not grant |
|---|---|---|
read-only | Read resources permitted by the member's role. | Any mutation. |
read-write | Create and operate agreement workflow resources. | Token or webhook administration. |
token-admin | Create, update, and revoke API tokens. | Access beyond the member's organization role. |
webhook-admin | Manage endpoints, tests, event delivery, and replay. | General agreement writes. |
Roles
| Role | Authorization context | Boundary |
|---|---|---|
OrganizationOwner | The organization owner. May administer members and billing when the token scope also permits the operation. | Still limited to the token organization and any narrower agreement access check. |
OrganizationAdmin | An organization administrator for member, billing, token, and webhook operations explicitly listing this role. | Does not inherit owner-only behavior or bypass token scopes. |
OrganizationMember | An active member working with organization agreements and organization-visible resources. | Cannot administer members, billing, API tokens, or webhooks unless the operation explicitly lists another qualifying role. |
CustomerDeveloper | An active developer member operating an integration on behalf of the organization. | Administrative routes still require their named admin scope and role; a developer label is not blanket access. |
AgreementOwner | The actor recorded as owner of the scoped agreement. | Applies only to that agreement and does not grant organization administration. |
AgreementParticipant | A participant acting within the scoped agreement, such as approving a gate or paying an assigned fee. | Does not expose other agreements or owner-only mutation paths. |
Do not place API tokens in browser code, query strings, source control, logs, emails, signing links, or webhook payloads. Rotate by creating a replacement before revoking the old token.
Retry a write without creating a duplicate
Every side-effecting POST, PATCH, and DELETErequires an Idempotency-Key of at most 255 characters. A completed response is replayable for 24 hours when actor, organization, HTTP method, normalized path, and the SHA-256 fingerprint of the exact request bytesare identical. Equivalent JSON with different whitespace or key order can have a different fingerprint.
curl --request POST \
--url https://api.staging.termn.ai/v1/agreements \
--header "Authorization: Bearer $TERMN_API_TOKEN" \
--header "Idempotency-Key: agreement-create-20260716-001" \
--header "Content-Type: application/json" \
--data '{
"name": "Vendor agreement",
"owner_email": "owner@example.com",
"participant_emails": ["signer@example.com"],
"payment_checkout_session_id": "cs_paid_one_agreement"
}'- Create one key per change
- Save the key with the local command or job before sending the request.
- Retry the same body
- Save the serialized bytes and reuse them with the same key during the 24-hour window.
- Handle each conflict
- Treat
idempotency_in_progress,idempotency_reconciliation_required, andidempotency_key_reusedseparately.
A replayed success includes X-Termn-Idempotency-Replayed: true. A timeout can have no JSON body; retry the same mutation with the same key and exact bytes.
Fetch the next page with the returned cursor
Paginated resources return data, has_more,next_starting_after, and previous_ending_before. Setlimit from 1 to 100. Do not derive a cursor from position or time.
curl --get https://api.staging.termn.ai/v1/agreements \
--header "Authorization: Bearer $TERMN_API_TOKEN" \
--data-urlencode "limit=50" \
--data-urlencode "starting_after=$NEXT_CURSOR"Verify each webhook before you process it
Termn signs <timestamp>.<exact request body> with HMAC-SHA-256 using the display-once endpoint secret. Verify the signature in constant time, reject stale timestamps, and deduplicate on Termn-Webhook-Id before updating local records or starting downstream processing.
import { createHmac, timingSafeEqual } from "node:crypto";
export function verifyTermnWebhook(
rawBody: Buffer,
headers: Headers,
secret: string,
toleranceSeconds = 300
) {
const timestamp = headers.get("termn-webhook-timestamp");
const signature = headers.get("termn-webhook-signature");
if (!timestamp || !signature) return false;
const parts = Object.fromEntries(
signature.split(",").map((part) => part.trim().split("=", 2))
);
const age = Math.abs(Date.now() / 1000 - Number(timestamp));
if (parts.t !== timestamp || !Number.isFinite(age) || age > toleranceSeconds) {
return false;
}
const expected = createHmac("sha256", secret)
.update(timestamp)
.update(".")
.update(rawBody)
.digest();
const supplied = Buffer.from(parts.v1 || "", "hex");
return supplied.length === expected.length && timingSafeEqual(supplied, expected);
}Headers to save
- Termn-Webhook-Id
- Immutable event identifier and deduplication key.
- Termn-Webhook-Endpoint-Id
- The configured destination receiving this attempt.
- Termn-Webhook-Event
- Event type used for routing after verification.
- Termn-Webhook-Attempt
- Shows which delivery attempt this request belongs to.
- Termn-Webhook-Timestamp
- Unix seconds included in the signed message.
- Termn-Webhook-Signature
t=<unix-seconds>,v1=<lowercase-hex-hmac>
Delivery behavior
| Property | Guarantee |
|---|---|
| Delivery guarantee | At least once. Deduplicate with Termn-Webhook-Id before updating local state or starting downstream work. |
| Successful receipt | Any HTTP 2xx response ends delivery successfully. |
| Automatic attempts | At most 5 total attempts. |
| Network deadlines | DNS resolution is limited to 5 seconds, connection establishment to 10 seconds, and the complete delivery request to 30 seconds. |
| Network failures | DNS, connect, TLS, and request failures create an inspectable attempt with no response code and follow the bounded retry schedule. |
| Retry schedule | 30s, 60s, 120s, and 240s before ±20% jitter; delays are capped at 30 minutes. |
| Ordering | No ordering guarantee across events or endpoints. |
| Terminal failure | After attempt 5, automatic delivery stops. Correct the endpoint, inspect attempts, and explicitly replay the immutable event. |
| Secret rotation | The previous secret becomes invalid immediately. Deploy the new secret before rotation or pause delivery for the cutover. |
Respond with any 2xx only after the event is durably stored. Inspect the event envelope, payload catalog, retry policy, and secret-rotation procedure before enabling a destination.
Use the status and code to decide what happens next
API failures use application/problem+json. Show message to an operator, but use the HTTP status and stable code in program logic. Save request_id with the failed request.
{
"code": "bad_request",
"message": "Idempotency-Key is required for customer API mutations.",
"request_id": "4cfb08d9-91f6-4f30-826c-715c7f4f6a1c",
"field_errors": null
}| Status and code | Meaning | Integration response |
|---|---|---|
400 bad_request | Malformed JSON, invalid fields, or a missing/malformed Idempotency-Key. | Correct the request; do not retry unchanged. |
401 unauthorized | Missing, malformed, expired, or revoked credential. | Replace the credential before retrying. |
402 payment_required | A subscription project start, Project Pass, participant-paid Project Pass, paid Guided Close order, organization grant, or connected account prerequisite has not completed. | Complete the exact prerequisite named in the message, then retry safely. |
403 forbidden | The scope, organization role, or agreement access is insufficient. | Stop and request only the missing authorization. |
404 not_found | The scoped resource is absent or an optional operation is disabled. | Check organization, resource ID, and live-contract availability. |
408 no JSON body | The server-wide request deadline elapsed. | Retry reads; retry writes with the same key and exact bytes. |
409 conflict | A domain state conflict prevents the operation. | Reload state and reconcile before another write. |
409 idempotency_in_progress | The same key and request are still executing. | Wait and retry the exact request with the same key. |
409 idempotency_reconciliation_required | A prior attempt may have committed before interruption. | Reconcile the original intent; do not create a replacement write. |
409 idempotency_key_reused | The key was reused for different exact bytes or operation identity. | Use the original bytes, or a new key for a genuinely new intent. |
429 rate_limited | The bearer credential exceeded its request window. | Honor Retry-After and add jitter. |
503 dependency_unavailable | Termn or a required provider dependency is unavailable. | Back off; retry only a safe read or an idempotent write. |
Keep AI access narrow and review sensitive actions
Give an AI integration only the Termn operations and permissions it needs. Confirm its organization, role, and scopes before each request, and have a person approve sensitive changes.
- Use the current OpenAPI document to define the operations and fields the AI integration can access. Reject any request that does not match the schema.
- Call GET /v1/me before each request to confirm the token belongs to the intended organization and has the required role and scope.
- Require a person to approve deletion, money movement, publication, cancellation, event replay, and secret rotation.
- Keep API tokens, webhook secrets, signing tokens, protected file URLs, and decrypted agreement content out of prompts and logs.
Start with llms.txt. For authorization, events, and request schemas, use llms-full.txt with the current OpenAPI document.
Keep your client in sync with the API
Generate or update your client from the current OpenAPI file. It includes every route available to your organization and the request rules for each one.
- Listed
- You may use the route when the token also has the required role and scope.
- Organization-specific
- Some routes appear only for organizations that can use them.
- Not listed
- Do not generate or send a request for that route.
When you update: v1 may add fields and routes, so clients must ignore unknown response fields. A rename, removal, meaning change, or newly required request field requires a new API version or a migration notice. Generate clients from revision 2026-07-20.1 and review the developer changelog.