- GET /api/v2/milestones — List milestones.
- POST /api/v2/milestones — Create a milestone on a funding application.
- GET /api/v2/milestones/{id} — Get one milestone.
- POST /api/v2/milestones/{id}/approve — Approve a milestone.
GET /api/v2/milestones
List milestones.
Every milestone of the Organisation (application, contract and standalone) in ascending id order, at most 50 a page, each with its deliverables entry. Every filter is ANDed; any other filters[…] key is 400.
- Operation ID:
listMilestones - Authentication: A Bearer token from a credential holding the
readscope. - Also served at
/funding_system/api/v2/milestones, the documented product’s alias path.
curl "https://your-org.backed.giving/api/v2/milestones" \
-H "Authorization: Bearer $BACKED_TOKEN"Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
limit | query | No | integer | Results per page (default 50, at most 50; a larger value is read as 50). Not an integer, or below 1: 400. Example: 50. |
gt | query | No | integer | Return records with an id greater than this value. Page forward by passing the last id you received. Example: 1287. |
lt | query | No | integer | Return records with an id less than this value. |
filters[deliverables][value] | query | No | string | Match deliverable input values; % is a wildcard for partial matching. Example: %Smith%. |
filters[deliverables][tags][] | query | No | array of string | Filter by the internal_identifier of deliverable fields. Example: artist_name, song_title. |
filters[deliverables][tag] | query | No | string | Combine a tag with filters[deliverables][value] to search within one field. Example: artist_name. |
filters[ids][] | query | No | array of integer | Filter by milestone ids. Example: 2210, 2211. |
200 response
A JSON array of milestones.
| Field | Type | Required | Notes |
|---|---|---|---|
id | integer | Yes | |
created_at | string | Yes | |
updated_at | string | Yes | |
title | string | Yes | |
contract_id | integer | null | Yes | |
funding_amount | string | Yes | |
description | string | null | Yes | |
state | string | Yes | One of not_submitted, pending_review, submitted_to_verifier, submitted, resubmission_requested, approved, finance_approved. |
paid_at | string | null | Yes | |
decline_reason | string | null | Yes | |
due_date | string | Yes | |
contract | object | null | Yes | |
lock_version | integer | Yes | |
deliverables_template_entry | object | Yes |
[
{
"id": 2210,
"created_at": "2026-03-02T21:15:04.000Z",
"updated_at": "2026-06-18T03:40:11.000Z",
"title": "Final report",
"contract_id": 640,
"funding_amount": "4500.0",
"description": "A report on the outcomes the project achieved.",
"state": "submitted",
"paid_at": null,
"decline_reason": null,
"due_date": "2027-06-30",
"contract": {
"id": 640,
"title": "Funding agreement: Te Awa river restoration"
},
"lock_version": 2,
"deliverables_template_entry": {
"id": 9310,
"created_at": "2026-03-02T21:15:04.000Z",
"updated_at": "2026-06-18T03:40:11.000Z",
"inputs": [
{
"id": 77001,
"created_at": "2026-03-02T21:15:04.000Z",
"updated_at": "2026-06-18T03:40:11.000Z",
"value": "final-report.pdf",
"file": {
"filename": "final-report.pdf",
"content_type": "application/pdf",
"byte_size": 482113
},
"tahua_file_url": "https://your-org.backed.giving/api/v2/file_inputs/77001",
"field": {
"id": 950,
"created_at": "2026-03-02T21:15:04.000Z",
"updated_at": "2026-03-02T21:15:04.000Z",
"internal_identifier": "report"
}
}
]
}
}
]400 response
A query parameter is invalid, or a write’s body is not a JSON object.
{
"error": "invalid_request",
"error_description": "limit is invalid"
}401 response
The token is missing, invalid, expired or revoked, of another Organisation, or API access is not Full. Ask for a new token.
Headers: WWW-Authenticate.
{
"error": "invalid_token",
"error_description": "The access token is invalid or has expired"
}403 response
The request was sent over plain HTTP and was not processed.
{
"error": "https_required"
}404 response
An unknown host or path, or an id that is unknown, deleted or of another Organisation.
{
"error": "not_found"
}429 response
A rate limit was exceeded: 120 requests a minute per credential, 30 token requests a minute per client_id.
Headers: Retry-After.
{
"error": "rate_limited",
"error_description": "Too many requests"
}500 response
An unexpected server error.
{
"error": "server_error"
}503 response
The Organisation’s API access cannot be determined, or a dependency is unavailable. Retry after the given seconds.
Headers: Retry-After.
{
"error": "temporarily_unavailable"
}POST /api/v2/milestones
Create a milestone on a funding application.
Create Singular as the run-as Administrator (milestones.create). Needs the Contracts module.
- Operation ID:
createMilestone - Authentication: A Bearer token from a credential holding the
write:post_awardscope. Runs as the credential’s Administrator, and needs anIdempotency-Keyheader.
curl -X POST "https://your-org.backed.giving/api/v2/milestones" \
-H "Authorization: Bearer $BACKED_TOKEN" \
-H "Idempotency-Key: 7d1f0c2e-5b8a-4f3e-9c61-2a4b8e9d0f13" \
-H "Content-Type: application/json" \
-d '{"funding_application_id":"1287","title":"Progress report","due_date":"2026-12-15","amount":"5000.00","form_template_id":"61","description":"A six-month progress report."}'Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
Idempotency-Key | header | Yes | string | A key of 1 to 255 characters, unique per write. A retry with the same key, method, path and body within 24 hours answers the first response again (with Idempotent-Replayed: true) and writes nothing. Example: 7d1f0c2e-5b8a-4f3e-9c61-2a4b8e9d0f13. |
Request body
application/json, the MilestoneCreate schema. A milestone on one funding application, with its deliverables form. Ids are decimal strings and amount has two decimal places, as the product’s form takes them.
| Field | Type | Required | Notes |
|---|---|---|---|
funding_application_id | string | Yes | |
title | string | Yes | |
due_date | string | Yes | |
amount | string | Yes | |
form_template_id | string | Yes | |
milestone_template_id | string | null | No | |
description | string | null | No | |
reviewer_user_ids | array of string | No |
{
"funding_application_id": "1287",
"title": "Progress report",
"due_date": "2026-12-15",
"amount": "5000.00",
"form_template_id": "61",
"description": "A six-month progress report."
}201 response
The milestone as the single read answers it.
Headers: Idempotent-Replayed.
| Field | Type | Required | Notes |
|---|---|---|---|
id | integer | Yes | |
created_at | string | Yes | |
updated_at | string | Yes | |
title | string | Yes | |
contract_id | integer | null | Yes | |
funding_amount | string | Yes | |
description | string | null | Yes | |
state | string | Yes | One of not_submitted, pending_review, submitted_to_verifier, submitted, resubmission_requested, approved, finance_approved. |
paid_at | string | null | Yes | |
decline_reason | string | null | Yes | |
due_date | string | Yes | |
contract | object | null | Yes | |
lock_version | integer | Yes | |
deliverables_template_entry | object | Yes |
{
"id": 2210,
"created_at": "2026-03-02T21:15:04.000Z",
"updated_at": "2026-06-18T03:40:11.000Z",
"title": "Final report",
"contract_id": 640,
"funding_amount": "4500.0",
"description": "A report on the outcomes the project achieved.",
"state": "submitted",
"paid_at": null,
"decline_reason": null,
"due_date": "2027-06-30",
"contract": {
"id": 640,
"title": "Funding agreement: Te Awa river restoration"
},
"lock_version": 2,
"deliverables_template_entry": {
"id": 9310,
"created_at": "2026-03-02T21:15:04.000Z",
"updated_at": "2026-06-18T03:40:11.000Z",
"inputs": [
{
"id": 77001,
"created_at": "2026-03-02T21:15:04.000Z",
"updated_at": "2026-06-18T03:40:11.000Z",
"value": "final-report.pdf",
"file": {
"filename": "final-report.pdf",
"content_type": "application/pdf",
"byte_size": 482113
},
"tahua_file_url": "https://your-org.backed.giving/api/v2/file_inputs/77001",
"field": {
"id": 950,
"created_at": "2026-03-02T21:15:04.000Z",
"updated_at": "2026-03-02T21:15:04.000Z",
"internal_identifier": "report"
}
}
]
}
}400 response
idempotency_key_required: The write has no Idempotency-Key header of 1 to 255 characters. invalid_request: A query parameter is invalid, or a write’s body is not a JSON object.
{
"error": "idempotency_key_required",
"error_description": "A write needs an Idempotency-Key header of 1 to 255 characters"
}{
"error": "invalid_request",
"error_description": "limit is invalid"
}401 response
The token is missing, invalid, expired or revoked, of another Organisation, or API access is not Full. Ask for a new token.
Headers: WWW-Authenticate.
{
"error": "invalid_token",
"error_description": "The access token is invalid or has expired"
}403 response
https_required: The request was sent over plain HTTP and was not processed. insufficient_scope: The credential does not hold the scope the route needs. run_as_disabled: The credential has no run-as Administrator, or that person is no longer one. forbidden: The run-as Administrator lacks the permission, or the record is outside their Data Access Rules.
Headers: WWW-Authenticate.
{
"error": "https_required"
}{
"error": "insufficient_scope",
"error_description": "This credential lacks the write:applications scope"
}{
"error": "run_as_disabled",
"error_description": "This credential has no enabled run-as Administrator"
}{
"error": "forbidden",
"error_description": "The run-as Administrator may not do this to this record"
}404 response
not_found: An unknown host or path, or an id that is unknown, deleted or of another Organisation. module_disabled: The Organisation has the module this route belongs to turned off.
{
"error": "not_found"
}{
"error": "module_disabled",
"error_description": "This module is not enabled for the Organisation"
}409 response
The first request with this Idempotency-Key has not finished. Nothing was written; retry after the given seconds.
Headers: Retry-After.
{
"error": "idempotency_in_progress",
"error_description": "The first request with this Idempotency-Key has not finished. If this persists it stopped part-way: check whether the record exists before writing again with a new key"
}413 response
The body, or the declared file, is larger than the limit that applies.
{
"error": "payload_too_large",
"error_description": "The request body is too large"
}415 response
The body is not application/json, or the target does not accept the file’s content type.
{
"error": "unsupported_media_type",
"error_description": "The request body must be application/json"
}422 response
idempotency_key_reused: The Idempotency-Key was used in the last 24 hours for a different method, path or body. unknown_field: The body has a property the route does not document; the first is named. validation_failed: Body values the product’s own form would refuse; fields names each.
{
"error": "idempotency_key_reused",
"error_description": "This Idempotency-Key was used for a different request"
}{
"error": "unknown_field",
"error_description": "bank_account_number is not a writable field"
}{
"error": "validation_failed",
"error_description": "Invalid fields: due_date",
"fields": [
"due_date"
]
}429 response
A rate limit was exceeded: 120 requests a minute per credential, 30 token requests a minute per client_id.
Headers: Retry-After.
{
"error": "rate_limited",
"error_description": "Too many requests"
}500 response
An unexpected server error.
{
"error": "server_error"
}503 response
The Organisation’s API access cannot be determined, or a dependency is unavailable. Retry after the given seconds.
Headers: Retry-After.
{
"error": "temporarily_unavailable"
}GET /api/v2/milestones/{id}
Get one milestone.
Exactly the list item, its deliverables entry included. Filters are ignored.
- Operation ID:
getMilestone - Authentication: A Bearer token from a credential holding the
readscope. - Also served at
/funding_system/api/v2/milestones/{id}, the documented product’s alias path.
curl "https://your-org.backed.giving/api/v2/milestones/42" \
-H "Authorization: Bearer $BACKED_TOKEN"Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
id | path | Yes | integer | The record id. Anything but a positive integer is 404. Example: 42. |
200 response
The milestone.
| Field | Type | Required | Notes |
|---|---|---|---|
id | integer | Yes | |
created_at | string | Yes | |
updated_at | string | Yes | |
title | string | Yes | |
contract_id | integer | null | Yes | |
funding_amount | string | Yes | |
description | string | null | Yes | |
state | string | Yes | One of not_submitted, pending_review, submitted_to_verifier, submitted, resubmission_requested, approved, finance_approved. |
paid_at | string | null | Yes | |
decline_reason | string | null | Yes | |
due_date | string | Yes | |
contract | object | null | Yes | |
lock_version | integer | Yes | |
deliverables_template_entry | object | Yes |
{
"id": 2210,
"created_at": "2026-03-02T21:15:04.000Z",
"updated_at": "2026-06-18T03:40:11.000Z",
"title": "Final report",
"contract_id": 640,
"funding_amount": "4500.0",
"description": "A report on the outcomes the project achieved.",
"state": "submitted",
"paid_at": null,
"decline_reason": null,
"due_date": "2027-06-30",
"contract": {
"id": 640,
"title": "Funding agreement: Te Awa river restoration"
},
"lock_version": 2,
"deliverables_template_entry": {
"id": 9310,
"created_at": "2026-03-02T21:15:04.000Z",
"updated_at": "2026-06-18T03:40:11.000Z",
"inputs": [
{
"id": 77001,
"created_at": "2026-03-02T21:15:04.000Z",
"updated_at": "2026-06-18T03:40:11.000Z",
"value": "final-report.pdf",
"file": {
"filename": "final-report.pdf",
"content_type": "application/pdf",
"byte_size": 482113
},
"tahua_file_url": "https://your-org.backed.giving/api/v2/file_inputs/77001",
"field": {
"id": 950,
"created_at": "2026-03-02T21:15:04.000Z",
"updated_at": "2026-03-02T21:15:04.000Z",
"internal_identifier": "report"
}
}
]
}
}401 response
The token is missing, invalid, expired or revoked, of another Organisation, or API access is not Full. Ask for a new token.
Headers: WWW-Authenticate.
{
"error": "invalid_token",
"error_description": "The access token is invalid or has expired"
}403 response
The request was sent over plain HTTP and was not processed.
{
"error": "https_required"
}404 response
An unknown host or path, or an id that is unknown, deleted or of another Organisation.
{
"error": "not_found"
}429 response
A rate limit was exceeded: 120 requests a minute per credential, 30 token requests a minute per client_id.
Headers: Retry-After.
{
"error": "rate_limited",
"error_description": "Too many requests"
}500 response
An unexpected server error.
{
"error": "server_error"
}503 response
The Organisation’s API access cannot be determined, or a dependency is unavailable. Retry after the given seconds.
Headers: Retry-After.
{
"error": "temporarily_unavailable"
}POST /api/v2/milestones/{id}/approve
Approve a milestone.
Approve Milestone at its current version (milestones.approve), within the run-as Administrator’s Milestone Approval Limit. No body. Needs the Contracts module.
- Operation ID:
approveMilestone - Authentication: A Bearer token from a credential holding the
write:post_awardscope. Runs as the credential’s Administrator, and needs anIdempotency-Keyheader.
curl -X POST "https://your-org.backed.giving/api/v2/milestones/42/approve" \
-H "Authorization: Bearer $BACKED_TOKEN" \
-H "Idempotency-Key: 7d1f0c2e-5b8a-4f3e-9c61-2a4b8e9d0f13"Parameters
| Name | In | Required | Type | Description |
|---|---|---|---|---|
id | path | Yes | integer | The record id. Anything but a positive integer is 404. Example: 42. |
Idempotency-Key | header | Yes | string | A key of 1 to 255 characters, unique per write. A retry with the same key, method, path and body within 24 hours answers the first response again (with Idempotent-Replayed: true) and writes nothing. Example: 7d1f0c2e-5b8a-4f3e-9c61-2a4b8e9d0f13. |
200 response
The milestone as the single read answers it.
Headers: Idempotent-Replayed.
| Field | Type | Required | Notes |
|---|---|---|---|
id | integer | Yes | |
created_at | string | Yes | |
updated_at | string | Yes | |
title | string | Yes | |
contract_id | integer | null | Yes | |
funding_amount | string | Yes | |
description | string | null | Yes | |
state | string | Yes | One of not_submitted, pending_review, submitted_to_verifier, submitted, resubmission_requested, approved, finance_approved. |
paid_at | string | null | Yes | |
decline_reason | string | null | Yes | |
due_date | string | Yes | |
contract | object | null | Yes | |
lock_version | integer | Yes | |
deliverables_template_entry | object | Yes |
{
"id": 2210,
"created_at": "2026-03-02T21:15:04.000Z",
"updated_at": "2026-06-18T03:40:11.000Z",
"title": "Final report",
"contract_id": 640,
"funding_amount": "4500.0",
"description": "A report on the outcomes the project achieved.",
"state": "submitted",
"paid_at": null,
"decline_reason": null,
"due_date": "2027-06-30",
"contract": {
"id": 640,
"title": "Funding agreement: Te Awa river restoration"
},
"lock_version": 2,
"deliverables_template_entry": {
"id": 9310,
"created_at": "2026-03-02T21:15:04.000Z",
"updated_at": "2026-06-18T03:40:11.000Z",
"inputs": [
{
"id": 77001,
"created_at": "2026-03-02T21:15:04.000Z",
"updated_at": "2026-06-18T03:40:11.000Z",
"value": "final-report.pdf",
"file": {
"filename": "final-report.pdf",
"content_type": "application/pdf",
"byte_size": 482113
},
"tahua_file_url": "https://your-org.backed.giving/api/v2/file_inputs/77001",
"field": {
"id": 950,
"created_at": "2026-03-02T21:15:04.000Z",
"updated_at": "2026-03-02T21:15:04.000Z",
"internal_identifier": "report"
}
}
]
}
}400 response
idempotency_key_required: The write has no Idempotency-Key header of 1 to 255 characters. invalid_request: A query parameter is invalid, or a write’s body is not a JSON object.
{
"error": "idempotency_key_required",
"error_description": "A write needs an Idempotency-Key header of 1 to 255 characters"
}{
"error": "invalid_request",
"error_description": "limit is invalid"
}401 response
The token is missing, invalid, expired or revoked, of another Organisation, or API access is not Full. Ask for a new token.
Headers: WWW-Authenticate.
{
"error": "invalid_token",
"error_description": "The access token is invalid or has expired"
}403 response
https_required: The request was sent over plain HTTP and was not processed. insufficient_scope: The credential does not hold the scope the route needs. run_as_disabled: The credential has no run-as Administrator, or that person is no longer one. forbidden: The run-as Administrator lacks the permission, or the record is outside their Data Access Rules. approval_limit_exceeded: The amount is above the run-as Administrator’s approval limit.
Headers: WWW-Authenticate.
{
"error": "https_required"
}{
"error": "insufficient_scope",
"error_description": "This credential lacks the write:applications scope"
}{
"error": "run_as_disabled",
"error_description": "This credential has no enabled run-as Administrator"
}{
"error": "forbidden",
"error_description": "The run-as Administrator may not do this to this record"
}{
"error": "approval_limit_exceeded",
"error_description": "The amount is above the run-as Administrator’s approval limit"
}404 response
not_found: An unknown host or path, or an id that is unknown, deleted or of another Organisation. module_disabled: The Organisation has the module this route belongs to turned off.
{
"error": "not_found"
}{
"error": "module_disabled",
"error_description": "This module is not enabled for the Organisation"
}409 response
idempotency_in_progress: The first request with this Idempotency-Key has not finished. Nothing was written; retry after the given seconds. invalid_transition: The move is not open from where the record stands; current_status says where that is. invalid_state: The record is not in a state that admits this write.
Headers: Retry-After.
{
"error": "idempotency_in_progress",
"error_description": "The first request with this Idempotency-Key has not finished. If this persists it stopped part-way: check whether the record exists before writing again with a new key"
}{
"error": "invalid_transition",
"error_description": "The record cannot make this move from approved",
"current_status": "approved"
}{
"error": "invalid_state",
"error_description": "The target does not take files in its current state"
}413 response
The body, or the declared file, is larger than the limit that applies.
{
"error": "payload_too_large",
"error_description": "The request body is too large"
}415 response
The body is not application/json, or the target does not accept the file’s content type.
{
"error": "unsupported_media_type",
"error_description": "The request body must be application/json"
}422 response
idempotency_key_reused: The Idempotency-Key was used in the last 24 hours for a different method, path or body. unknown_field: The body has a property the route does not document; the first is named.
{
"error": "idempotency_key_reused",
"error_description": "This Idempotency-Key was used for a different request"
}{
"error": "unknown_field",
"error_description": "bank_account_number is not a writable field"
}429 response
A rate limit was exceeded: 120 requests a minute per credential, 30 token requests a minute per client_id.
Headers: Retry-After.
{
"error": "rate_limited",
"error_description": "Too many requests"
}500 response
An unexpected server error.
{
"error": "server_error"
}503 response
The Organisation’s API access cannot be determined, or a dependency is unavailable. Retry after the given seconds.
Headers: Retry-After.
{
"error": "temporarily_unavailable"
}