REST API Contracts - edmozley/freeitsm GitHub Wiki

๐Ÿ“œ REST API: Contracts

The complete usage guide for the Contracts module of the REST API โ€” contracts, term tabs, suppliers and their contacts. Mirrors the interactive documentation at System โ†’ API โ†’ Documentation (with its live "Try it" tester).

[!NOTE] New to the API? The basics table on REST API: Tickets covers base URL, authentication, the response envelope, error codes, rate limits and PATCH semantics โ€” identical across all modules.

What's distinctive about Contracts:

โณ Renewal filters built in expiring_within_days, notice_within_days and expired are the module's own dashboard/Watchtower SQL shapes exposed as parameters โ€” renewal reports are one call.
๐Ÿ”ข Contract numbers can't collide via the API The UI has no uniqueness check on contract_number; the API refuses duplicates with a 409 pointing at the existing contract.
๐Ÿญ Suppliers get their full record The lite picker list stays at GET /suppliers (under reference.read); the full CRUD โ€” registration/VAT, address, questionnaire dates, contacts โ€” lives under the new suppliers permission. The API can also set supplies_assets (the flag the Assets module filters suppliers by), which the UI never writes.
๐Ÿ“„ Documents are links, not uploads The module stores external DMS URLs (dms_link, dpia_dms_link) โ€” plain string fields, no file upload exists or is needed.
๐Ÿšช The RFP Builder is not exposed It's internal-only by design (suppliers never log in) and stays UI-only.

๐Ÿš€ Quick start

Register a supplier and its contract, with renewal tracking from day one:

B="https://your-server/api/v1"; K="Authorization: Bearer fitsm_โ€ฆ"

# 1. Supplier (flagged as an asset supplier so it appears in Assets pickers)
SUP=$(curl -s -X POST "$B/suppliers" -H "$K" -H "Content-Type: application/json" -d '{
  "legal_name": "Acme Print Ltd", "trading_name": "Acme Print", "supplies_assets": true
}' | jq -r '.data.id')

# 2. Contract with value, end date and notice date
curl -s -X POST "$B/contracts" -H "$K" -H "Content-Type: application/json" -d "{
  \"contract_number\": \"CN-2026-014\", \"title\": \"Managed print services\",
  \"supplier_id\": $SUP,
  \"contract_start\": \"2026-08-01\", \"contract_end\": \"2028-07-31\",
  \"notice_period_days\": 90, \"notice_date\": \"2028-05-02\",
  \"contract_value\": 24000, \"currency\": \"GBP\"
}"

๐Ÿ“œ Contracts

๐ŸŸข GET /contracts โ€” list / search ย  ๐Ÿ”‘ contracts.read

Parameter Description
q Search contract number, title, supplier names and customer company / person
supplier_id ยท contract_status_id ยท contract_owner_id ยท payment_schedule_id Id filters
party_type supplier / customer
company_id Contracts belonging to one company (3.5.0) โ€” 403 if the key cannot access it
customer_company_id ยท customer_user_id Customer contracts for one company or one person
is_active true / false
expiring_within_days โณ Active contracts whose contract_end falls within N days (Watchtower's 30/90-day windows as a parameter)
notice_within_days ๐Ÿ”” Active contracts whose notice_date falls within N days โ€” the "act now or auto-renew" report
expired true = contract_end already passed
ends_before / ends_after YYYY-MM-DD bounds
sort contract_end (default), contract_start, created_at, title, contract_number, contract_value, id โ€” prefix - for descending
page / per_page Pagination โ€” default 1 / 25, max 100

The contract shape:

{ "id": 4, "contract_number": "CN-2026-014", "title": "Managed print services",
  "description": null,
  "supplier": { "id": 5, "name": "Acme Print" },
  "owner": { "id": 1, "name": "Administrator" },
  "status": { "id": 2, "name": "Active" },
  "payment_schedule": { "id": 1, "name": "Annual" },
  "dates": { "start": "2026-08-01", "end": "2028-07-31",
             "notice_date": "2028-05-02", "notice_period_days": 90 },
  "value": { "amount": 24000.0, "currency": "GBP" },
  "cost_centre": null, "cost_centre_id": null, "cost_centre_name": null, "dms_link": null,
  "governance": { "terms_status": null, "personal_data_transferred": false,
                  "dpia_required": false, "dpia_completed_date": null, "dpia_dms_link": null },
  "is_active": true, "created_at": "2026-07-03T15:40:52Z" }

๐Ÿ”ต POST /contracts โ€” create ย  ๐Ÿ”‘ contracts.create

contract_number + title required; everything else optional. Who it is with: party_type supplier (default) with supplier_id, or customer with customer_tenant_id (a company), customer_user_id (a person), or both โ€” the key's analyst must be able to see that company, and the person must be in it. An unknown party_type is a 422. Lookup ids validated (422 naming the field), dates YYYY-MM-DD, currency a 3-letter code (lower-case is normalised). company_id โ€” the company the contract belongs to (3.5.0): checked against the key (403), default the key's default company. A contract_number already used in the same company โ†’ 409; two companies may share one. Returns 201.

๐ŸŸข GET /contracts/{id} ย  ๐Ÿ”‘ contracts.read ยท ๐ŸŸ  PATCH /contracts/{id} ย  ๐Ÿ”‘ contracts.update

Everything creatable is patchable; null clears; renaming to a contract_number in use โ†’ 409. Set is_active: false to retire a contract without deleting it โ€” usually the right move.

[!NOTE] Every contract belongs to a company (3.5.0) โ€” company: {id, name}, never null (the Default company when unset). A key whose company scope does not include it gets 404, as for a contract that does not exist. PATCH with company_id moves a contract, refused (409) while it has assets, projects or RFPs in its current company. Who the contract is with is separate: party_type and, for a customer, customer: {company: {id, name}, person: {id, name, email}}. See Contracts with customers.

Cost centre (3.5.0). cost_centre stays a string - the linked cost centre's code, or the free text recorded before 3.5.0 when none is linked - with cost_centre_id and cost_centre_name beside it. Write cost_centre_id (an active cost centre of the contract's company, 422 otherwise), or cost_centre as a code (422 if no such code; "" clears). Moving a contract with company_id while it holds a cost centre needs one of the new company's (or null) in the same request. See Cost centres.

๐Ÿ”ด DELETE /contracts/{id} ย  ๐Ÿ”‘ contracts.delete

[!WARNING] Permanent. The contract and its term-tab contents are removed; tasks, calendar events and RFPs that referenced it are unlinked (kept, not deleted). Prefer is_active: false for retirement.


๐Ÿ“‘ Contract terms

Term tabs (Termination, Liability, โ€ฆ) are configured in Contracts โ†’ Settings; each contract stores its own content per tab.

๐ŸŸข GET /contracts/{id}/terms ย  ๐Ÿ”‘ contract_terms.read

Every active tab with this contract's content (null where nothing recorded).

๐Ÿ”ต POST /contracts/{id}/terms ย  ๐Ÿ”‘ contract_terms.update

{ "terms": [ { "term_tab_id": 1, "content": "Termination requires 90 days written notice." } ] }

Per-tab upsert, exactly like the UI โ€” only the tabs you send are touched. Returns the full term set.


๐Ÿญ Suppliers

๐ŸŸข GET /suppliers/{id} ย  ๐Ÿ”‘ suppliers.read

The full record โ€” registration/VAT numbers, address, questionnaire dates, type/status, supplies_assets, contract + contact counts, and the contact list inline.

๐Ÿ”ต POST /suppliers ย  ๐Ÿ”‘ suppliers.create ยท ๐ŸŸ  PATCH /suppliers/{id} ย  ๐Ÿ”‘ suppliers.update

Only legal_name is required. All identity/address/questionnaire fields accepted, plus supplies_assets โ€” set it true for hardware vendors so they appear in the Assets module's supplier pickers (the UI can't set this flag; it's normally only flipped by the asset-import migration).

๐Ÿ”ด DELETE /suppliers/{id} ย  ๐Ÿ”‘ suppliers.delete

Deletes the supplier; its contracts, contacts and assets keep their rows but are unlinked (supplier set to none) โ€” the same behaviour as the UI.


๐Ÿ‘ค Supplier contacts

Endpoint Permission Notes
๐ŸŸข GET /suppliers/{id}/contacts ๐Ÿ”‘ supplier_contacts.read The supplier's people
๐Ÿ”ต POST /suppliers/{id}/contacts ๐Ÿ”‘ supplier_contacts.create first_name + surname required; email/mobile/job title/dial numbers optional
๐ŸŸ  PATCH /suppliers/{id}/contacts/{contact_id} ๐Ÿ”‘ supplier_contacts.update Send only what changes
๐Ÿ”ด DELETE /suppliers/{id}/contacts/{contact_id} ๐Ÿ”‘ supplier_contacts.delete Removes the contact

๐Ÿ“š Reference data

Under the shared ๐Ÿ”‘ reference.read permission (all populated via Contracts โ†’ Settings โ€” empty on a fresh install):

Endpoint Returns
๐ŸŸข GET /contract-statuses Contract statuses
๐ŸŸข GET /payment-schedules Payment schedules
๐ŸŸข GET /supplier-types ยท GET /supplier-statuses Supplier lookups
๐ŸŸข GET /contract-term-tabs The configured term tabs
๐ŸŸข GET /suppliers The lite supplier picker list (id, name, supplies_assets)

๐Ÿงช Worked example: renewal radar

A read-only key (contracts: read) feeds a monthly procurement report:

B="https://your-server/api/v1"; K="Authorization: Bearer fitsm_โ€ฆ"

# Contracts needing a renewal decision in the next 90 days, soonest first
curl -s -H "$K" "$B/contracts?expiring_within_days=90&sort=contract_end&per_page=100" \
  | jq -r '.data[] | [.contract_number, .dates.end, (.supplier.name // "-"),
                      ((.value.amount // 0) | tostring) + " " + (.value.currency // ""), .title] | @tsv'

# The urgent list: notice deadlines in the next 30 days (miss these and it auto-renews)
curl -s -H "$K" "$B/contracts?notice_within_days=30&sort=contract_end" \
  | jq -r '.data[] | "โš ๏ธ  \(.dates.notice_date)  \(.contract_number)  \(.title)"'

And the ERP direction โ€” syncing supplier master data in:

SUP=$(curl -s -H "$K" "$B/suppliers?" ) # lite list via GET /suppliers (reference.read)
curl -s -X PATCH "$B/suppliers/5" -H "$K" -H "Content-Type: application/json" \
  -d '{"vat_number": "GB123456789", "supplier_status_id": 2}'

Under the hood: REST API โ€” How It Works ยท Other modules: Tickets ยท Assets ยท Problems ยท Changes ยท Knowledge ยท Tasks ยท CMDB ยท Keys & permissions: System โ†’ API (System module) ยท Module docs: Contracts.