# Add a milestone

`POST /api/v1/milestones`

Adds work a contractor is to be paid for, checked by the rules of the console's Add milestone form, and recorded in the ledger as `create_milestone` with `via: "api"` and the key's id. It is added as the key's issuer's: if the agent holds it on its own judgment, someone other than the issuer must choose Pay now, unless the issuer is the workspace's only approver.

It starts `pending`, and the API cannot verify it. A GitHub pull request in `verificationSource` is checked by the agent, which verifies the milestone once the pull request is merged; any other link is evidence for the person who verifies it on Contractors. Once it is verified, the agent decides the payment with every guardrail, and pays only to the contractor's confirmed address. A `contractorId` the workspace does not hold, or a client's, answers `400`.

Send a [workspace API key](https://www.vestiarion.xyz/docs/get-started/authentication) with read and write access as `Authorization: Bearer <key>`. A read-only key gets `403`.

## Parameters

| Name | In | Type | Required | Default | Allowed values | Description |
| --- | --- | --- | --- | --- | --- | --- |
| `Idempotency-Key` | header | string | Optional | — | — | Makes a retry safe: up to 255 printable ASCII characters, unique to the record being added, such as its id in your own system. A repeat with the same key and the same body within 24 hours gets the first answer back, with `Idempotent-Replayed: true`, and adds nothing. The same key with a different body answers `409`. |

## Request body

Example, `application/json`:

```json
{
  "contractorId": "26d6ffed-356d-474a-8d42-89bc942f6b4e",
  "title": "TypeScript SDK for the API",
  "amount": "0.10",
  "verificationSource": "https://github.com/duongnq2798/vestiarion/pull/176"
}
```

**Fields**

- `contractorId` (string, required): The `id` of the contractor or vendor to pay, from `GET /api/v1/counterparties` or from the answer that added it. A client is not paid for milestones.
- `title` (string, required): What was delivered, 3 to 160 characters.
- `amount` (string | number, required): What the work is paid, in USDC, with at most 6 decimal places. A decimal string such as `"250.00"` keeps it exact; a number is read the same way.
- `verificationSource` (string, optional): A link to the delivered work: https, up to 500 characters. A GitHub pull request (`https://github.com/<owner>/<repo>/pull/<number>`) is checked by the agent, which verifies the milestone once it is merged. Any other link is evidence for the person who verifies the milestone on Contractors.

## Code samples

```bash
curl "https://www.vestiarion.xyz/api/v1/milestones" \
  -X POST \
  -H "Authorization: Bearer $VESTIARION_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: ci-bounty-pr-176" \
  -d '{"contractorId":"26d6ffed-356d-474a-8d42-89bc942f6b4e","title":"TypeScript SDK for the API","amount":"0.10","verificationSource":"https://github.com/duongnq2798/vestiarion/pull/176"}'
```

## Response

Example, `201` `application/json`:

```json
{
  "data": {
    "id": "354132aa-e7f6-46f8-ae3c-aa6809f9b9b6",
    "title": "TypeScript SDK for the API",
    "amount": 0.1,
    "status": "pending",
    "verificationSource": "https://github.com/duongnq2798/vestiarion/pull/176",
    "verificationMethod": "unverified",
    "verificationStatus": "unverified",
    "verificationCheckedAt": null,
    "verifiedAt": null,
    "verificationDetail": {},
    "verified": false,
    "decidedAt": null,
    "settledAt": null,
    "closedAt": null,
    "closeReason": null,
    "agentReasoning": null,
    "txHash": null,
    "contractor": {
      "id": "26d6ffed-356d-474a-8d42-89bc942f6b4e",
      "name": "API Test Contractor",
      "riskLevel": "clear"
    },
    "createdAt": "2026-10-03T15:43:09.204631+00:00"
  }
}
```

**Fields**

- `data` (object, required): A contractor milestone, how it was verified, and whether it was paid.
  - `id` (string, required)
  - `title` (string, required)
  - `amount` (number, required)
  - `status` (string, required) One of `pending`, `verified`, `paid`, `held`, `closed`.
  - `verificationSource` (string, nullable, required)
  - `verificationMethod` (string, required) One of `unverified`, `github`, `manual`, `seed`.
  - `verificationStatus` (string, required) One of `unverified`, `verified`, `not_merged`, `unavailable`, `failed`.
  - `verificationCheckedAt` (string, nullable, required)
  - `verifiedAt` (string, nullable, required)
  - `verificationDetail` (object, required)
  - `verified` (boolean, required)
  - `decidedAt` (string, nullable, required)
  - `settledAt` (string, nullable, required)
  - `closedAt` (string, nullable, required): When a person closed the milestone without paying it (status closed), else null.
  - `closeReason` (string, nullable, required): The reason the person gave for closing it without paying, else null.
  - `agentReasoning` (string, nullable, required)
  - `txHash` (string, nullable, required): An on-chain hash when the payment settled on Arc, else null.
  - `contractor` (object, nullable, required)
    - `id` (string, required)
    - `name` (string, required)
    - `riskLevel` (string, required)
  - `createdAt` (string, required)

## Errors

| Status | Code | When |
| --- | --- | --- |
| 400 | `invalid_request` | An invalid `limit` or `cursor`, a filter value outside its allowed values, or a request body that does not validate. The message names the parameter or the field, and lists the accepted values. |
| 401 | `unauthorized` | No key, or a malformed, unknown or revoked one: "A valid API key is required." |
| 403 | `forbidden` | The key's scopes do not cover this route: "This key cannot do that." Or, on a write, the person who created the key can no longer add records: "This key's issuer can no longer add records in this workspace." |
| 409 | `conflict` | The `Idempotency-Key` was already used for a different request, or the first request with it is still being handled. |
| 429 | `rate_limited` | Too many requests; wait as long as `Retry-After` says before trying again. |
| 500 | `internal` | An unexpected server error. Implementation details are not exposed. |
