DevelopersREST API
Projects API
List and retrieve AutoRFP.ai projects, update a project's custom fields, and list a project's requirements with the REST API.
Projects are the bids you manage in AutoRFP.ai. Use these endpoints to list and retrieve projects, update a project’s custom fields, and list the requirements in a project.
Reading projects needs the projects:read scope. Updating custom fields needs projects:write, which is a write scope.
List projects#
get/v1/projects
Returns a paginated list of projects accessible via the API key.
Required scopeprojects:read
Query parameters
page[number]integerPage number (1-indexed)
page[size]integerItems per page (1-100)
filter[createdAfter]string (date-time)ISO 8601 datetime (e.g. "2025-01-01T00:00:00Z")
filter[createdBefore]string (date-time)ISO 8601 datetime (e.g. "2025-01-01T00:00:00Z")
filter[dueAfter]string (date-time)ISO 8601 datetime (e.g. "2025-01-01T00:00:00Z")
filter[dueBefore]string (date-time)ISO 8601 datetime (e.g. "2025-01-01T00:00:00Z")
filter[status]array of stringsComma-separated list of statuses (e.g. "inProgress,completed")
Returns
Paginated list of projects
typeenumrequiredidstringrequiredattributesobjectrequiredShow child attributes
namestringrequiredProject name
statusstringrequiredCurrent project status
prospectNamestringrequiredName of the prospect or issuer
projectTypestring | nullrequiredProject type (e.g. "rfp", "rfi")
createdAtstring | nullrequiredISO 8601 datetime when the project was created
dueDatestring | nullrequiredISO 8601 datetime when the project is due
externalIdsobjectrequiredShow child attributes
salesforceIdstring | nullrequiredSalesforce external ID
opportunityIdstring | nullrequiredSalesforce opportunity ID
metaobjectrequiredShow child attributes
pageobjectrequiredShow child attributes
numbernumberrequiredsizenumberrequiredtotalItemsnumberrequiredtotalPagesnumberrequired
linksobjectrequiredShow child attributes
selfstringrequiredfirststringrequiredlaststring | nullrequiredprevstring | nullrequirednextstring | nullrequired
Errors
| Status | Meaning |
|---|---|
400 | Invalid query parameters |
401 | Unauthorized |
403 | Insufficient permissions |
429 | Rate limit exceeded |
500 | Internal server error |
Request
curl -g "https://api.autorfp.ai/v1/projects?page[size]=25&filter[status]=intake" \
-H "Authorization: Bearer $AUTORFP_API_KEY"const response = await fetch("https://api.autorfp.ai/v1/projects?page[size]=25&filter[status]=intake", {
headers: {
Authorization: `Bearer ${process.env.AUTORFP_API_KEY}`,
},
});
const data = await response.json();
console.log(data);import os
import requests
response = requests.get(
"https://api.autorfp.ai/v1/projects",
headers={"Authorization": "Bearer " + os.environ["AUTORFP_API_KEY"]},
params={"page[size]": 25, "filter[status]": "intake"},
)
print(response.json())Response
Response
{
"data": [
{
"type": "projects",
"id": "8f14e45f",
"attributes": {
"name": "Acme RFP 2026",
"status": "inProgress",
"prospectName": "Acme Corp",
"projectType": "rfp",
"createdAt": "2026-03-02T09:30:00.000Z",
"dueDate": "2026-03-02T09:30:00.000Z",
"externalIds": {
"salesforceId": "8f14e45f",
"opportunityId": "8f14e45f"
}
}
}
],
"meta": {
"page": {
"number": 1,
"size": 1,
"totalItems": 1,
"totalPages": 1
}
},
"links": {
"self": "https://api.autorfp.ai/v1/projects?page[number]=1&page[size]=25",
"first": "https://api.autorfp.ai/v1/projects?page[number]=1&page[size]=25",
"last": "https://api.autorfp.ai/v1/projects?page[number]=1&page[size]=25",
"prev": "https://api.autorfp.ai/v1/projects?page[number]=1&page[size]=25",
"next": "https://api.autorfp.ai/v1/projects?page[number]=2&page[size]=25"
}
}Get a project#
get/v1/projects/{projectId}
Returns one project: header fields, requirement summary counts, projectType, and custom field values. projectType matches GET /v1/projects list: value of the custom field with id project_type when present; otherwise null. customFields lists current values on the project. An empty array usually means none are configured; in rare cases it can briefly appear empty even when fields exist, confirm with GET /v1/organisation and retry if needed. Requires projects:read. Rate limit: 100 requests / 60 seconds.
Required scopeprojects:read
Path parameters
projectIdstringrequiredProject id
Returns
Project detail (includes customFields and projectType)
typeenumrequiredidstringrequiredattributesobjectrequiredShow child attributes
namestringrequiredProject name
issuerstringrequiredName of the issuing organisation
statusstringrequiredCurrent project status
createdAtstringrequiredISO 8601 datetime when the project was created
dueDatestring | nullrequiredISO 8601 datetime when the project is due
projectTypestring | nullrequiredSame as list: value of the custom field with id project_type when present on the project; otherwise null
customFieldsarray of objectsrequiredCurrent custom field values on the project (empty when none are configured)
Show child attributes
idstringrequiredField id (pass as id in PATCH /v1/projects/{projectId})
labelstringrequiredHuman-readable field label
typeenumrequiredField value type
externalIdstring | nullrequiredOptional CRM/external key (pass as externalId in PATCH)
optionsarray of objectsStatic options currently on the project snapshot. PATCH allowlists use the organisation template optionSource after refresh, prefer GET /v1/organisation for the live list.
Show child attributes
valuestringrequiredlabelstringrequired
optionSourceobjectDynamic option set on the project snapshot (e.g. users, usersAndTeams, teams). For known sets, PATCH values must be org members using user:<id> / team:<id> (bare id = legacy user).
Show child attributes
typeenumrequiredsetstringrequired
valuestring | number | boolean | nullScalar value for text, number, date, dropdown, or checkbox. Dates are ISO 8601 with offset (e.g. 2024-01-15T00:00:00.000+00:00).
valuesarray of stringsMultiselect values (clear with [])
summaryobjectrequiredShow child attributes
requirementsCountnumberrequiredTotal number of requirements
approvedCountnumberrequiredNumber of approved requirements
submittedCountnumberrequiredNumber of requirements submitted for review
Errors
| Status | Meaning |
|---|---|
400 | Invalid project id |
401 | Unauthorized |
403 | Insufficient scope (requires projects:read) |
404 | Project not found |
429 | Rate limit exceeded |
500 | Internal server error |
Request
curl -g "https://api.autorfp.ai/v1/projects/8f14e45f" \
-H "Authorization: Bearer $AUTORFP_API_KEY"const response = await fetch("https://api.autorfp.ai/v1/projects/8f14e45f", {
headers: {
Authorization: `Bearer ${process.env.AUTORFP_API_KEY}`,
},
});
const data = await response.json();
console.log(data);import os
import requests
response = requests.get(
"https://api.autorfp.ai/v1/projects/8f14e45f",
headers={"Authorization": "Bearer " + os.environ["AUTORFP_API_KEY"]},
)
print(response.json())Response
Response
{
"data": {
"type": "projects",
"id": "8f14e45f",
"attributes": {
"name": "Acme RFP 2026",
"issuer": "Acme Corp",
"status": "inProgress",
"createdAt": "2026-03-02T09:30:00.000Z",
"dueDate": "2026-03-02T09:30:00.000Z",
"projectType": "rfp",
"customFields": [
{
"id": "8f14e45f",
"label": "Security",
"type": "text",
"externalId": "8f14e45f",
"options": [
{
"value": "example",
"label": "Security"
}
],
"optionSource": {
"type": "dynamic",
"set": "users"
},
"value": "example",
"values": [
"example"
]
}
],
"summary": {
"requirementsCount": 1,
"approvedCount": 1,
"submittedCount": 1
}
}
}
}Update project custom fields#
patch/v1/projects/{projectId}
Updates custom project field values on one project. Request body is plain JSON (application/json), not a JSON:API envelope: { "customFields": [ { "id" and/or "externalId", "value" or "values" }, ... ] } (max 50). Match fields by id and/or externalId (case-insensitive, trimmed). If both are set and resolve to different fields, or the same field is patched twice, the request fails with 400 (field-invalid). Use value for text, number, date (ISO 8601 within 1970, 2100 UTC), dropdown, and checkbox; use values (string array) for multiselect. Null clears a scalar field; dropdown empty/whitespace clears to null; values: [] clears multiselect. Static dropdown/multiselect options must match the organisation-template option list after refresh (values are trimmed). Dynamic option sources users / usersAndTeams / teams require organisation membership: user:<id> or bare id for users, team:<id> for teams (usersAndTeams allows both). Deactivated users and unknown ids fail with field-invalid. Unknown dynamic set names still accept any non-empty string. customFields: [] is a no-op (no write, no template sync, no required check); the 200 response matches the current project. A non-empty request adds any organisation-template fields missing from the project; project-only fields are kept. On a non-empty request, optionSource for matched dropdown/multiselect fields refreshes from the organisation template (static or dynamic; values and required/manageOnly flags on existing project fields are not rewritten). Required fields: after applying patches, required fields on the project working set must be non-empty. Requiredness uses each field.required flag already on the project (or on newly cloned/appended template fields); existing project required flags are not updated from the organisation template. When the project had no custom fields yet (full template clone), every required template field must be filled. Otherwise only required fields that already existed on the project or are touched in this request are checked (newly appended untouched template fields are exempt this request). Empty means: text/dropdown null or ""; number/date null; checkbox not true; multiselect []. manageOnly is ignored by PATCH (no write-deny); required manageOnly fields still must be filled when in scope. Updates are atomic: any unknown, invalid, or missing-required field rejects the whole request with no write (unlike POST /v1/content/tags, which allows partial success). Multiple semantic failures return multiple top-level errors[] entries with code field-not-found, field-invalid, or field-required; each detail names the field (for example, "region: …"). Body/schema problems (missing customFields, unknown keys, caps, empty ids) return 400 without errors[].code. Non-empty patches when neither the organisation template nor the project has any fields return 422 with code custom-fields-not-configured. On success, returns the full project summary in the same shape as GET /v1/projects/{projectId}. Requires projects:write. Rate limit: 30 requests / 60 seconds.
Required scopeprojects:write
Path parameters
projectIdstringrequiredProject id
Request body
customFieldsarray of objectsrequiredField value patches (max 50). Each entry needs id and/or externalId plus value or values. [] is a no-op: no write and no template sync; response matches GET.
Show child attributes
idstringField id from GET /v1/organisation (or from the project customFields list)
externalIdstringExternal/CRM key for the field; may be used instead of or with id
valuestring | number | boolean | nullScalar value for text, number, date (ISO 8601), dropdown, or checkbox. Use null to clear. Dropdown empty/whitespace clears to null.
valuesarray of stringsMultiselect values only (max 100). Use [] to clear. Do not send value for multiselect.
Returns
Full project summary after update (same shape as GET /v1/projects/{projectId})
typeenumrequiredidstringrequiredattributesobjectrequiredShow child attributes
namestringrequiredProject name
issuerstringrequiredName of the issuing organisation
statusstringrequiredCurrent project status
createdAtstringrequiredISO 8601 datetime when the project was created
dueDatestring | nullrequiredISO 8601 datetime when the project is due
projectTypestring | nullrequiredSame as list: value of the custom field with id project_type when present on the project; otherwise null
customFieldsarray of objectsrequiredCurrent custom field values on the project (empty when none are configured)
Show child attributes
idstringrequiredField id (pass as id in PATCH /v1/projects/{projectId})
labelstringrequiredHuman-readable field label
typeenumrequiredField value type
externalIdstring | nullrequiredOptional CRM/external key (pass as externalId in PATCH)
optionsarray of objectsStatic options currently on the project snapshot. PATCH allowlists use the organisation template optionSource after refresh, prefer GET /v1/organisation for the live list.
Show child attributes
valuestringrequiredlabelstringrequired
optionSourceobjectDynamic option set on the project snapshot (e.g. users, usersAndTeams, teams). For known sets, PATCH values must be org members using user:<id> / team:<id> (bare id = legacy user).
Show child attributes
typeenumrequiredsetstringrequired
valuestring | number | boolean | nullScalar value for text, number, date, dropdown, or checkbox. Dates are ISO 8601 with offset (e.g. 2024-01-15T00:00:00.000+00:00).
valuesarray of stringsMultiselect values (clear with [])
summaryobjectrequiredShow child attributes
requirementsCountnumberrequiredTotal number of requirements
approvedCountnumberrequiredNumber of approved requirements
submittedCountnumberrequiredNumber of requirements submitted for review
Errors
| Status | Meaning |
|---|---|
400 | Invalid body/schema (no errors[].code), or one or more field errors with code field-not-found / field-invalid / field-required (no fields written) |
401 | Unauthorized |
403 | Insufficient scope (requires projects:write) |
404 | Project not found |
422 | No custom fields configured on the organisation or project (code: custom-fields-not-configured) |
429 | Rate limit exceeded |
500 | Internal server error |
Request
curl -g -X PATCH "https://api.autorfp.ai/v1/projects/8f14e45f" \
-H "Authorization: Bearer $AUTORFP_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"customFields": [
{
"id": "8f14e45f",
"value": "example"
}
]
}'const response = await fetch("https://api.autorfp.ai/v1/projects/8f14e45f", {
method: "PATCH",
headers: {
Authorization: `Bearer ${process.env.AUTORFP_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
"customFields": [
{
"id": "8f14e45f",
"value": "example"
}
]
}),
});
const data = await response.json();
console.log(data);import os
import requests
response = requests.patch(
"https://api.autorfp.ai/v1/projects/8f14e45f",
headers={"Authorization": "Bearer " + os.environ["AUTORFP_API_KEY"]},
json={
"customFields": [
{
"id": "8f14e45f",
"value": "example",
},
],
},
)
print(response.json())Response
Response
{
"data": {
"type": "projects",
"id": "8f14e45f",
"attributes": {
"name": "Acme RFP 2026",
"issuer": "Acme Corp",
"status": "inProgress",
"createdAt": "2026-03-02T09:30:00.000Z",
"dueDate": "2026-03-02T09:30:00.000Z",
"projectType": "rfp",
"customFields": [
{
"id": "8f14e45f",
"label": "Security",
"type": "text",
"externalId": "8f14e45f",
"options": [
{
"value": "example",
"label": "Security"
}
],
"optionSource": {
"type": "dynamic",
"set": "users"
},
"value": "example",
"values": [
"example"
]
}
],
"summary": {
"requirementsCount": 1,
"approvedCount": 1,
"submittedCount": 1
}
}
}
}List project requirements#
get/v1/projects/{projectId}/requirements
Returns a paginated list of requirements within a project.
Required scopeprojects:read
Path parameters
projectIdstringrequired
Query parameters
page[number]integerPage number (1-indexed)
page[size]integerItems per page (1-100)
Returns
Paginated list of requirements
typeenumrequiredidstringrequiredattributesobjectrequiredShow child attributes
statusstringrequiredRequirement status (drafted, assigned, submittedForReview, approved)
editorTeamIdsarray of stringsrequiredTeam IDs derived from assigned editors
reviewerTeamIdsarray of stringsrequiredTeam IDs derived from assigned reviewers
editPercentagenumber | nullrequiredEdit percentage as a 0-1 value, null if no metrics available
metaobjectrequiredShow child attributes
pageobjectrequiredShow child attributes
numbernumberrequiredsizenumberrequiredtotalItemsnumberrequiredtotalPagesnumberrequired
linksobjectrequiredShow child attributes
selfstringrequiredfirststringrequiredlaststring | nullrequiredprevstring | nullrequirednextstring | nullrequired
Errors
| Status | Meaning |
|---|---|
400 | Invalid query parameters |
401 | Unauthorized |
403 | Insufficient permissions |
404 | Project not found |
429 | Rate limit exceeded |
500 | Internal server error |
Request
curl -g "https://api.autorfp.ai/v1/projects/8f14e45f/requirements?page[size]=25" \
-H "Authorization: Bearer $AUTORFP_API_KEY"const response = await fetch("https://api.autorfp.ai/v1/projects/8f14e45f/requirements?page[size]=25", {
headers: {
Authorization: `Bearer ${process.env.AUTORFP_API_KEY}`,
},
});
const data = await response.json();
console.log(data);import os
import requests
response = requests.get(
"https://api.autorfp.ai/v1/projects/8f14e45f/requirements",
headers={"Authorization": "Bearer " + os.environ["AUTORFP_API_KEY"]},
params={"page[size]": 25},
)
print(response.json())Response
Response
{
"data": [
{
"type": "requirements",
"id": "8f14e45f",
"attributes": {
"status": "inProgress",
"editorTeamIds": [
"8f14e45f"
],
"reviewerTeamIds": [
"8f14e45f"
],
"editPercentage": 1
}
}
],
"meta": {
"page": {
"number": 1,
"size": 1,
"totalItems": 1,
"totalPages": 1
}
},
"links": {
"self": "https://api.autorfp.ai/v1/projects/8f14e45f/requirements?page[number]=1&page[size]=25",
"first": "https://api.autorfp.ai/v1/projects/8f14e45f/requirements?page[number]=1&page[size]=25",
"last": "https://api.autorfp.ai/v1/projects/8f14e45f/requirements?page[number]=1&page[size]=25",
"prev": "https://api.autorfp.ai/v1/projects/8f14e45f/requirements?page[number]=1&page[size]=25",
"next": "https://api.autorfp.ai/v1/projects/8f14e45f/requirements?page[number]=2&page[size]=25"
}
}