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 gets404, as for a contract that does not exist.PATCHwithcompany_idmoves a contract, refused (409) while it has assets, projects or RFPs in its current company. Who the contract is with is separate:party_typeand, for a customer,customer: {company: {id, name}, person: {id, name, email}}. See Contracts with customers.
Cost centre (3.5.0).
cost_centrestays a string - the linked cost centre's code, or the free text recorded before 3.5.0 when none is linked - withcost_centre_idandcost_centre_namebeside it. Writecost_centre_id(an active cost centre of the contract's company,422otherwise), orcost_centreas a code (422if no such code;""clears). Moving a contract withcompany_idwhile it holds a cost centre needs one of the new company's (ornull) 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: falsefor 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.