> This is the Markdown of a Backed Help Centre page, https://admin.backed.giving/help/integrations/api-c03-write-api-part-two-milestones-payments-contracts.md. Every page is listed at https://admin.backed.giving/help/llms.txt.

> This page uses Backed's default terms, such as Funding Round and Application Template. An organisation may rename them, so read each as your organisation's own term if you've renamed it.

This page is for your organisation's developers. It lists the Backed API routes that create or
change milestones, payments and contracts, and that upload files, and what each one refuses. For
registrations, applications, notes, tags and assessor assignments, see
[Write API: registrations, applications, notes, tags and assignments](api-c02-write-api-part-one-registrations-applications-no.md).

No write route reads or changes a bank account number. A `bank_account_number` in a request body is
refused with `422 unknown_field`, and no write response carries one.

## Before you start

A credential can only read until you give it a write scope and an Administrator to act as:

1. Go to `Settings > Profile and terms` and open the `API Access` tab.
2. Select `Edit scopes` on the credential's row, or `New Credential` for a new one.
3. Tick the scopes the system needs. See [API access](api-access.md#scopes).
4. Choose the Administrator it acts as under `Acts as`. This needs `Can edit permissions`.
5. Select `Save`.

## The six scopes

| Scope                 | Label on the tab           | Opens                                                        |
| --------------------- | -------------------------- | ------------------------------------------------------------ |
| `read`                | `Read`                     | Every read route. Every credential has it.                   |
| `write:registrations` | `Write registrations`      | The registration routes, on the part one page.               |
| `write:applications`  | `Write applications`       | The transition, note and tag routes, on the part one page.   |
| `write:assessments`   | `Write assessments`        | The assignment route, on the part one page.                  |
| `write:post_award`    | `Write post-award records` | The milestone, payment and contract routes on this page.     |
| `write:files`         | `Write files`              | The file routes on this page.                                |

## Who a write runs as

Every write runs with the permissions, data access rules and approval limit of the credential's
`Acts as` Administrator, so it can't do anything they couldn't do in Backed. It goes through the same
checks, sends the same messages and records the same audit entries as the action on the screen.

In the record's history a write shows the credential's name, then `Acting as` and the
Administrator's name.

If that Administrator is removed, disabled or no longer an Administrator, every write answers
`403 run_as_disabled` until you choose someone else. Reads carry on.

## The Idempotency-Key header

Send your access token as a bearer token and this header on every `POST`: 1 to 255 characters your
system makes up, unique to that change.

- The same key and the same body within 24 hours answers the first response again, with the header
  `Idempotent-Replayed: true`, and changes nothing.
- The same key with a different body or path is refused with `422 idempotency_key_reused`.
- No key is refused with `400 idempotency_key_required`.
- A refused request stores nothing, so you can retry it with the same key.

No route on this page is a `PATCH`, so none takes `If-Match`. That header belongs to the `PATCH` route
on the part one page, where an out-of-date `lock_version` is refused with `412 precondition_failed`.

## Routes

Every path is on your organisation's host. Each route can also give the refusals in
[Refusals every write can give](#refusals-every-write-can-give).

| Method | Path                             | Scope              | Answers | Refusals of its own                                                                                            |
| ------ | -------------------------------- | ------------------ | ------- | -------------------------------------------------------------------------------------------------------------- |
| `POST` | `/api/v2/milestones`             | `write:post_award` | `201`   | `404 module_disabled`                                                                                          |
| `POST` | `/api/v2/milestones/:id/approve` | `write:post_award` | `200`   | `404 module_disabled`, `404 not_found`, `403 approval_limit_exceeded`, `409 invalid_transition`, `409 invalid_state` |
| `POST` | `/api/v2/payments/:id/mark_paid` | `write:post_award` | `200`   | `404 module_disabled`, `404 not_found`, `409 invalid_transition`                                               |
| `POST` | `/api/v2/contracts/:id/status`   | `write:post_award` | `200`   | `404 module_disabled`, `404 not_found`, `409 invalid_transition`                                               |
| `POST` | `/api/v2/files`                  | `write:files`      | `201`   | `404 not_found`, `409 invalid_state`, `413 payload_too_large`, `415 unsupported_media_type`                    |
| `POST` | `/api/v2/files/:id/confirm`      | `write:files`      | `200`   | `404 not_found`, `409 invalid_state`                                                                           |
| `POST` | `/api/v2/files/:id/attach`       | `write:files`      | `200`   | `404 not_found`, `409 invalid_state`, `409 file_not_scanned`, `422 file_infected`                              |

`404 module_disabled` means the module is turned off for your organisation: Contracts for the milestone
and contract routes, and Payments for the payment route. The file routes have no module of their own.

Another organisation's id answers the same `404 not_found` as an id that was never issued.

### Milestones

`POST /api/v2/milestones` creates one milestone on an application, as `Create Milestone` does. Send
`funding_application_id`, `title`, `due_date` (`YYYY-MM-DD`), `amount` (such as `"48500.00"`) and
`form_template_id`, which is the deliverables form. You can also send `milestone_template_id`,
`description` and `reviewer_user_ids`. The ids in this body are strings of digits. An application or
form template that doesn't exist or that the Administrator can't see is refused with
`422 validation_failed` on that field.

`POST /api/v2/milestones/:id/approve` does what `Approve Milestone` does and takes no body. Both
answer with the milestone as `GET /api/v2/milestones/:id` reads it.

- An amount above the Administrator's `Milestone Approval Limit` is refused with
  `403 approval_limit_exceeded`.
- A milestone that can't be approved from where it stands is refused with `409 invalid_transition`,
  and `current_status` says where it stands.
- A deliverable not yet approved, or a change made while it was being approved, is refused with
  `409 invalid_state`. The text says which.

### Payments

`POST /api/v2/payments/:id/mark_paid` does what `Mark as Paid` does, so `:id` is a payment batch, not
a single payment. Send `paid_date` (`YYYY-MM-DD`) and `reference`, both required. `paid_date` goes on
each payment marked, and `reference` on the batch's paid entry in its trail.

A batch that isn't approved, or that is already paid, is refused with `409 invalid_transition` and
nothing changes. It answers `id`, `status`, `marked_paid_at`, `paid_date`, `reference`,
`payment_count` and `amount_total`.

### Contracts

`POST /api/v2/contracts/:id/status` takes a `status` and answers with the contract as
`GET /api/v2/contracts/:id` reads it.

- `executed` is the Administrator's own counter-signature, by typed name. Backed has no offline
  signing method, so the Administrator must be a signatory of this contract and it must be their turn
  to sign. It executes the contract when theirs is the last signature.
- `terminated` needs a `reason`. The contract stays `terminating` while the termination letter is sent
  in Backed, as it does on the screen.
- Any other status, `completed` included, is refused with `409 invalid_transition`. So is a move that
  isn't open from where the contract stands, and `current_status` says where that is.

### Files

Uploading takes three steps. Send an `Idempotency-Key` on each, with no body on the last two.

1. **Reserve.** `POST /api/v2/files` takes `filename`, `content_type`, `size` in bytes and `target`.
   It answers an `id` for the next two steps, the `upload` to make, `expires_at`, `max_bytes` and
   `state`. The upload is valid for 15 minutes.
2. **Upload, then confirm.** If `upload.method` is `POST`, send a multipart form to `upload.url` with
   `upload.fields` in order and the file last, in a part named `file`. If it is `PUT`, send the bytes
   with `upload.headers`. Then `POST /api/v2/files/:id/confirm` starts the malware scan and answers
   `state: scanning`. Confirming before the upload is stored is refused with `409 invalid_state`.
3. **Attach.** `POST /api/v2/files/:id/attach` puts the clean file on its target and answers `id`,
   `state: attached` and `target`. While the scan is running it is refused with
   `409 file_not_scanned`, so wait and retry. A file that failed the scan is refused with
   `422 file_infected` and is never attached. A file never confirmed answers `404 not_found`. Attaching
   again answers the same and adds nothing.

Only the credential that reserved an upload can confirm or attach it. For anyone else the `id` answers
`404 not_found`.

`target` holds a `type`, the record's `id`, and for a deliverable the `field`:

| `target.type`           | `target.id`          | Also needs                            | The Administrator needs              |
| ----------------------- | -------------------- | ------------------------------------- | ------------------------------------ |
| `milestone_deliverable` | The milestone        | `field`: the form's File upload field | The permission to complete on behalf |
| `registration_entity`   | The registration     | Nothing                               | The permission to edit applicants    |
| `application_note`      | The application note | Nothing                               | Always answers `404 not_found`       |

Only the applicant or verifier side of an application can attach to its notes, so a request naming an
`application_note` answers `404 not_found` for an Administrator, as it does on the screen.

A file over 100 MB or over its target's own limit is refused with `413 payload_too_large`, and a type
its target doesn't accept with `415 unsupported_media_type`, both before anything is stored. A target
that is full or closed to files is refused with `409 invalid_state`.

## Refusals every write can give

A refusal's body is `{"error": "<code>", "error_description": "<text>"}`. Your system should switch
on `error`.

| Status | `error`                    | Why                                                                                   |
| ------ | -------------------------- | ------------------------------------------------------------------------------------- |
| `400`  | `invalid_request`          | The body isn't a JSON object.                                                         |
| `400`  | `idempotency_key_required` | There's no `Idempotency-Key`, or it's longer than 255 characters.                     |
| `403`  | `insufficient_scope`       | The credential doesn't hold the route's scope. The text names the scope.              |
| `403`  | `run_as_disabled`          | The credential has no enabled Administrator to act as.                                |
| `403`  | `forbidden`                | The Administrator's permissions or data access rules don't allow this on this record. |
| `413`  | `payload_too_large`        | The request body is too large.                                                        |
| `415`  | `unsupported_media_type`   | The request body isn't `application/json`.                                            |
| `422`  | `idempotency_key_reused`   | The key was already used for a different request.                                     |
| `422`  | `unknown_field`            | The body has a field the route doesn't document. The text names the first one.        |
| `422`  | `validation_failed`        | A value breaks the screen's own rules. `fields` names each failing field.             |

## See also

- [API access](api-access.md)
- [Write API: registrations, applications, notes, tags and assignments](api-c02-write-api-part-one-registrations-applications-no.md)
