---
title: "Depot psa"
description: "Every psa operation in the Depot API, with parameters, responses and code samples in six languages."
url: "https://support.outpostplatform.com/api/generated/depot/psa/"
product: "depot"
type: "reference"
status: "reviewed"
last_reviewed: 2026-11-06
source: "https://gitlab.com/outpostplatform/docs/-/edit/main/src/content/docs/api/generated/depot/psa.mdx"
license: "CC BY 4.0"
---

# Depot psa

Registers the whole client-billing surface.

This page is generated from the Depot route table. Every operation carries the permission it needs and the anchor an agent can link to.

### GET `/api/v1/clients/{cid}/agreements` {#depot.psa.listAgreements}

**List agreements**

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `cid` | path | string | Yes |  |
| `page` | query | integer | No | One-based page number. Defaults to 1. |
| `pageSize` | query | integer | No | Rows per page, 1 to 200. Defaults to 50. |

#### Responses

| Status | Body | Description |
| --- | --- | --- |
| `200` | `application/json`, PagedResultAgreementSummaryView | Success. |
| `400` | `application/problem+json`, ProblemDetails | One or more validation errors occurred. |
| `401` | `application/problem+json`, ProblemDetails | No signed-in identity. |
| `403` | `application/problem+json`, ProblemDetails | The identity does not hold psa:read. |
| `404` | `application/problem+json`, ProblemDetails | No client matches that id. |
| `500` | `application/problem+json`, ProblemDetails | The request failed. |
| `503` | `application/problem+json`, ProblemDetails | Client billing is not available on this server. |

#### Request samples

```bash
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.
curl -X GET 'https://depot.example.com/api/v1/clients/<cid>/agreements?page=0&pageSize=0' \
  -H 'X-CSRF-Token: <csrf-token>' \
  -H 'Accept: application/json' \
  -b 'outpost_console=<console-session>'
```

```powershell
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.

$headers = @{
    'X-CSRF-Token' = '<csrf-token>'
    'Accept' = 'application/json'
    'Cookie' = 'outpost_console=<console-session>'
}

$response = Invoke-RestMethod `
    -Method Get `
    -Uri 'https://depot.example.com/api/v1/clients/<cid>/agreements?page=0&pageSize=0' `
    -Headers $headers

$response | ConvertTo-Json -Depth 10
```

```python
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.

import requests

url = "https://depot.example.com/api/v1/clients/<cid>/agreements"
params = {
    "page": "0",
    "pageSize": "0",
}
headers = {
    "X-CSRF-Token": "<csrf-token>",
    "Accept": "application/json",
}
cookies = {
    "outpost_console": "<console-session>",
}

response = requests.get(url, params=params, headers=headers, cookies=cookies, timeout=30)
response.raise_for_status()
print(response.json())
```

```go
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.
package main

import (
	"fmt"
	"io"
	"log"
	"net/http"
)

func main() {
	req, err := http.NewRequest(http.MethodGet, "https://depot.example.com/api/v1/clients/<cid>/agreements?page=0&pageSize=0", nil)
	if err != nil {
		log.Fatal(err)
	}

	req.Header.Set("X-CSRF-Token", "<csrf-token>")
	req.Header.Set("Accept", "application/json")
	req.AddCookie(&http.Cookie{Name: "outpost_console", Value: "<console-session>"})

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()

	out, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(resp.Status, string(out))
}
```

```ts
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.
// The session cookie is set by signing in. A script cannot set it itself.

const url = new URL("https://depot.example.com/api/v1/clients/<cid>/agreements");
url.searchParams.set("page", "0");
url.searchParams.set("pageSize", "0");

const response = await fetch(url, {
	method: "GET",
	headers: {
		"X-CSRF-Token": "<csrf-token>",
		"Accept": "application/json",
	},
	credentials: "include",
});

if (!response.ok) {
	throw new Error(`Request failed with status ${response.status}`);
}

const data = await response.json();
console.log(data);
```

```csharp
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.

using System;
using System.Net.Http;

using var client = new HttpClient();

var request = new HttpRequestMessage(HttpMethod.Get, "https://depot.example.com/api/v1/clients/<cid>/agreements?page=0&pageSize=0");
request.Headers.TryAddWithoutValidation("X-CSRF-Token", "<csrf-token>");
request.Headers.TryAddWithoutValidation("Accept", "application/json");
request.Headers.TryAddWithoutValidation("Cookie", "outpost_console=<console-session>");

var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
Console.WriteLine(await response.Content.ReadAsStringAsync());
```

### POST `/api/v1/clients/{cid}/agreements` {#depot.psa.createAgreement}

**Create agreement**

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `cid` | path | string | Yes |  |

#### Request body

Required, sent as `application/json`.

#### Responses

| Status | Body | Description |
| --- | --- | --- |
| `201` | `application/json`, AgreementView | Created. |
| `400` | `application/problem+json`, ProblemDetails | One or more validation errors occurred. |
| `401` | `application/problem+json`, ProblemDetails | No signed-in identity. |
| `403` | `application/problem+json`, ProblemDetails | The identity does not hold psa:write. |
| `404` | `application/problem+json`, ProblemDetails | No client matches that id. |
| `500` | `application/problem+json`, ProblemDetails | The request failed. |
| `503` | `application/problem+json`, ProblemDetails | Client billing is not available on this server. |

#### Request samples

```bash
# Sign in first. outpost_console is a session cookie, so send it with every request.
curl -X POST 'https://depot.example.com/api/v1/clients/<cid>/agreements' \
  -H 'X-CSRF-Token: <csrf-token>' \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -b 'outpost_console=<console-session>' \
  --data '{
  "billingDay": 0,
  "defaultChargeTypeCode": "string",
  "defaultRateCents": 0,
  "endsOn": "string",
  "lines": [
    {
      "active": false,
      "balanceMinutes": 0,
      "description": "string",
      "exclusive": false,
      "filter": "string",
      "includedDevicesPerSeat": 0,
      "includedMinutes": 0,
      "kind": "string",
      "overageUnitAmountCents": 0,
      "publicId": "string",
      "seatRequiresEndpoint": false,
      "sortOrder": 0,
      "unitAmountCents": 0,
      "usageMethod": "string"
    }
  ],
  "name": "string",
  "notes": "string",
  "rules": [
    {
      "chargeTypeCode": "string",
      "description": "string",
      "outOfHours": false,
      "outcome": "string",
      "position": 0,
      "prepaidLinePublicId": "string",
      "projectWork": false,
      "publicId": "string",
      "rateCents": 0,
      "ticketTypeCode": "string"
    }
  ],
  "startsOn": "string",
  "templateId": "string"
}'
```

```powershell
# Sign in first. outpost_console is a session cookie, so send it with every request.

$headers = @{
    'X-CSRF-Token' = '<csrf-token>'
    'Accept' = 'application/json'
    'Content-Type' = 'application/json'
    'Cookie' = 'outpost_console=<console-session>'
}

$body = @'
{
  "billingDay": 0,
  "defaultChargeTypeCode": "string",
  "defaultRateCents": 0,
  "endsOn": "string",
  "lines": [
    {
      "active": false,
      "balanceMinutes": 0,
      "description": "string",
      "exclusive": false,
      "filter": "string",
      "includedDevicesPerSeat": 0,
      "includedMinutes": 0,
      "kind": "string",
      "overageUnitAmountCents": 0,
      "publicId": "string",
      "seatRequiresEndpoint": false,
      "sortOrder": 0,
      "unitAmountCents": 0,
      "usageMethod": "string"
    }
  ],
  "name": "string",
  "notes": "string",
  "rules": [
    {
      "chargeTypeCode": "string",
      "description": "string",
      "outOfHours": false,
      "outcome": "string",
      "position": 0,
      "prepaidLinePublicId": "string",
      "projectWork": false,
      "publicId": "string",
      "rateCents": 0,
      "ticketTypeCode": "string"
    }
  ],
  "startsOn": "string",
  "templateId": "string"
}
'@

$response = Invoke-RestMethod `
    -Method Post `
    -Uri 'https://depot.example.com/api/v1/clients/<cid>/agreements' `
    -Headers $headers `
    -ContentType 'application/json' `
    -Body $body

$response | ConvertTo-Json -Depth 10
```

```python
# Sign in first. outpost_console is a session cookie, so send it with every request.

import requests

url = "https://depot.example.com/api/v1/clients/<cid>/agreements"
headers = {
    "X-CSRF-Token": "<csrf-token>",
    "Accept": "application/json",
    "Content-Type": "application/json",
}
cookies = {
    "outpost_console": "<console-session>",
}
payload = {
    "billingDay": 0,
    "defaultChargeTypeCode": "string",
    "defaultRateCents": 0,
    "endsOn": "string",
    "lines": [
        {
            "active": False,
            "balanceMinutes": 0,
            "description": "string",
            "exclusive": False,
            "filter": "string",
            "includedDevicesPerSeat": 0,
            "includedMinutes": 0,
            "kind": "string",
            "overageUnitAmountCents": 0,
            "publicId": "string",
            "seatRequiresEndpoint": False,
            "sortOrder": 0,
            "unitAmountCents": 0,
            "usageMethod": "string",
        },
    ],
    "name": "string",
    "notes": "string",
    "rules": [
        {
            "chargeTypeCode": "string",
            "description": "string",
            "outOfHours": False,
            "outcome": "string",
            "position": 0,
            "prepaidLinePublicId": "string",
            "projectWork": False,
            "publicId": "string",
            "rateCents": 0,
            "ticketTypeCode": "string",
        },
    ],
    "startsOn": "string",
    "templateId": "string",
}

response = requests.post(url, headers=headers, cookies=cookies, json=payload, timeout=30)
response.raise_for_status()
print(response.json())
```

```go
// Sign in first. outpost_console is a session cookie, so send it with every request.
package main

import (
	"bytes"
	"fmt"
	"io"
	"log"
	"net/http"
)

func main() {
	body := []byte(`{
  "billingDay": 0,
  "defaultChargeTypeCode": "string",
  "defaultRateCents": 0,
  "endsOn": "string",
  "lines": [
    {
      "active": false,
      "balanceMinutes": 0,
      "description": "string",
      "exclusive": false,
      "filter": "string",
      "includedDevicesPerSeat": 0,
      "includedMinutes": 0,
      "kind": "string",
      "overageUnitAmountCents": 0,
      "publicId": "string",
      "seatRequiresEndpoint": false,
      "sortOrder": 0,
      "unitAmountCents": 0,
      "usageMethod": "string"
    }
  ],
  "name": "string",
  "notes": "string",
  "rules": [
    {
      "chargeTypeCode": "string",
      "description": "string",
      "outOfHours": false,
      "outcome": "string",
      "position": 0,
      "prepaidLinePublicId": "string",
      "projectWork": false,
      "publicId": "string",
      "rateCents": 0,
      "ticketTypeCode": "string"
    }
  ],
  "startsOn": "string",
  "templateId": "string"
}`)

	req, err := http.NewRequest(http.MethodPost, "https://depot.example.com/api/v1/clients/<cid>/agreements", bytes.NewReader(body))
	if err != nil {
		log.Fatal(err)
	}

	req.Header.Set("X-CSRF-Token", "<csrf-token>")
	req.Header.Set("Accept", "application/json")
	req.Header.Set("Content-Type", "application/json")
	req.AddCookie(&http.Cookie{Name: "outpost_console", Value: "<console-session>"})

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()

	out, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(resp.Status, string(out))
}
```

```ts
// Sign in first. outpost_console is a session cookie, so send it with every request.
// The session cookie is set by signing in. A script cannot set it itself.

const url = "https://depot.example.com/api/v1/clients/<cid>/agreements";

const response = await fetch(url, {
	method: "POST",
	headers: {
		"X-CSRF-Token": "<csrf-token>",
		"Accept": "application/json",
		"Content-Type": "application/json",
	},
	credentials: "include",
	body: JSON.stringify({
		"billingDay": 0,
		"defaultChargeTypeCode": "string",
		"defaultRateCents": 0,
		"endsOn": "string",
		"lines": [
			{
				"active": false,
				"balanceMinutes": 0,
				"description": "string",
				"exclusive": false,
				"filter": "string",
				"includedDevicesPerSeat": 0,
				"includedMinutes": 0,
				"kind": "string",
				"overageUnitAmountCents": 0,
				"publicId": "string",
				"seatRequiresEndpoint": false,
				"sortOrder": 0,
				"unitAmountCents": 0,
				"usageMethod": "string"
			}
		],
		"name": "string",
		"notes": "string",
		"rules": [
			{
				"chargeTypeCode": "string",
				"description": "string",
				"outOfHours": false,
				"outcome": "string",
				"position": 0,
				"prepaidLinePublicId": "string",
				"projectWork": false,
				"publicId": "string",
				"rateCents": 0,
				"ticketTypeCode": "string"
			}
		],
		"startsOn": "string",
		"templateId": "string"
	}),
});

if (!response.ok) {
	throw new Error(`Request failed with status ${response.status}`);
}

const data = await response.json();
console.log(data);
```

```csharp
// Sign in first. outpost_console is a session cookie, so send it with every request.

using System;
using System.Net.Http;
using System.Text;

using var client = new HttpClient();

const string body = """
{
  "billingDay": 0,
  "defaultChargeTypeCode": "string",
  "defaultRateCents": 0,
  "endsOn": "string",
  "lines": [
    {
      "active": false,
      "balanceMinutes": 0,
      "description": "string",
      "exclusive": false,
      "filter": "string",
      "includedDevicesPerSeat": 0,
      "includedMinutes": 0,
      "kind": "string",
      "overageUnitAmountCents": 0,
      "publicId": "string",
      "seatRequiresEndpoint": false,
      "sortOrder": 0,
      "unitAmountCents": 0,
      "usageMethod": "string"
    }
  ],
  "name": "string",
  "notes": "string",
  "rules": [
    {
      "chargeTypeCode": "string",
      "description": "string",
      "outOfHours": false,
      "outcome": "string",
      "position": 0,
      "prepaidLinePublicId": "string",
      "projectWork": false,
      "publicId": "string",
      "rateCents": 0,
      "ticketTypeCode": "string"
    }
  ],
  "startsOn": "string",
  "templateId": "string"
}
""";

var request = new HttpRequestMessage(HttpMethod.Post, "https://depot.example.com/api/v1/clients/<cid>/agreements");
request.Headers.TryAddWithoutValidation("X-CSRF-Token", "<csrf-token>");
request.Headers.TryAddWithoutValidation("Accept", "application/json");
request.Headers.TryAddWithoutValidation("Cookie", "outpost_console=<console-session>");
request.Content = new StringContent(body, Encoding.UTF8, "application/json");

var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
Console.WriteLine(await response.Content.ReadAsStringAsync());
```

### GET `/api/v1/clients/{cid}/agreements/{agreementID}` {#depot.psa.getAgreement}

**Get agreement**

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `agreementID` | path | string | Yes | A uuid. |
| `cid` | path | string | Yes |  |

#### Responses

| Status | Body | Description |
| --- | --- | --- |
| `200` | `application/json`, AgreementView | Success. |
| `400` | `application/problem+json`, ProblemDetails | One or more validation errors occurred. |
| `401` | `application/problem+json`, ProblemDetails | No signed-in identity. |
| `403` | `application/problem+json`, ProblemDetails | The identity does not hold psa:read. |
| `404` | `application/problem+json`, ProblemDetails | No client matches that id. |
| `500` | `application/problem+json`, ProblemDetails | The request failed. |
| `503` | `application/problem+json`, ProblemDetails | Client billing is not available on this server. |

#### Request samples

```bash
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.
curl -X GET 'https://depot.example.com/api/v1/clients/<cid>/agreements/<agreementID>' \
  -H 'X-CSRF-Token: <csrf-token>' \
  -H 'Accept: application/json' \
  -b 'outpost_console=<console-session>'
```

```powershell
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.

$headers = @{
    'X-CSRF-Token' = '<csrf-token>'
    'Accept' = 'application/json'
    'Cookie' = 'outpost_console=<console-session>'
}

$response = Invoke-RestMethod `
    -Method Get `
    -Uri 'https://depot.example.com/api/v1/clients/<cid>/agreements/<agreementID>' `
    -Headers $headers

$response | ConvertTo-Json -Depth 10
```

```python
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.

import requests

url = "https://depot.example.com/api/v1/clients/<cid>/agreements/<agreementID>"
headers = {
    "X-CSRF-Token": "<csrf-token>",
    "Accept": "application/json",
}
cookies = {
    "outpost_console": "<console-session>",
}

response = requests.get(url, headers=headers, cookies=cookies, timeout=30)
response.raise_for_status()
print(response.json())
```

```go
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.
package main

import (
	"fmt"
	"io"
	"log"
	"net/http"
)

func main() {
	req, err := http.NewRequest(http.MethodGet, "https://depot.example.com/api/v1/clients/<cid>/agreements/<agreementID>", nil)
	if err != nil {
		log.Fatal(err)
	}

	req.Header.Set("X-CSRF-Token", "<csrf-token>")
	req.Header.Set("Accept", "application/json")
	req.AddCookie(&http.Cookie{Name: "outpost_console", Value: "<console-session>"})

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()

	out, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(resp.Status, string(out))
}
```

```ts
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.
// The session cookie is set by signing in. A script cannot set it itself.

const url = "https://depot.example.com/api/v1/clients/<cid>/agreements/<agreementID>";

const response = await fetch(url, {
	method: "GET",
	headers: {
		"X-CSRF-Token": "<csrf-token>",
		"Accept": "application/json",
	},
	credentials: "include",
});

if (!response.ok) {
	throw new Error(`Request failed with status ${response.status}`);
}

const data = await response.json();
console.log(data);
```

```csharp
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.

using System;
using System.Net.Http;

using var client = new HttpClient();

var request = new HttpRequestMessage(HttpMethod.Get, "https://depot.example.com/api/v1/clients/<cid>/agreements/<agreementID>");
request.Headers.TryAddWithoutValidation("X-CSRF-Token", "<csrf-token>");
request.Headers.TryAddWithoutValidation("Accept", "application/json");
request.Headers.TryAddWithoutValidation("Cookie", "outpost_console=<console-session>");

var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
Console.WriteLine(await response.Content.ReadAsStringAsync());
```

### PATCH `/api/v1/clients/{cid}/agreements/{agreementID}` {#depot.psa.patchAgreement}

**patchAgreement merges the body onto the stored agreement.**

RULES ARE NOT TOUCHED HERE even when the body carries them. Replacing the
ordered array is a different operation with a different consequence, it
reprices unbilled work, and it has its own route so that consequence is
something an operator asked for rather than a side effect of renaming a
contract.

LINES MERGE BY publicId AND ARE NEVER DESTROYED. A line the caller sends with
a known id is updated in place; one with no id is inserted; one the caller
OMITS is RETIRED, not deleted. The store cannot delete a line even if it
wanted to: FK_PrepaidLedger_Line cascades, so dropping a prepaid_block would
take the customer's whole ledger with it and silently zero minutes they paid
for, and FK_AgreementBillingRule_PrepaidLine would leave a prepaid rule
violating its own CHECK.

The consequence for the console is the one worth stating: IT MUST ROUND-TRIP
EVERY LINE'S publicId. A save that posts the lines back without their ids
retires the whole set and inserts fresh rows, which loses every prepaid
balance attached to them.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `agreementID` | path | string | Yes | A uuid. |
| `cid` | path | string | Yes |  |

#### Request body

Required, sent as `application/json`.

#### Responses

| Status | Body | Description |
| --- | --- | --- |
| `200` | `application/json`, AgreementView | Success. |
| `400` | `application/problem+json`, ProblemDetails | One or more validation errors occurred. |
| `401` | `application/problem+json`, ProblemDetails | No signed-in identity. |
| `403` | `application/problem+json`, ProblemDetails | The identity does not hold psa:write. |
| `404` | `application/problem+json`, ProblemDetails | No client matches that id. |
| `500` | `application/problem+json`, ProblemDetails | The request failed. |
| `503` | `application/problem+json`, ProblemDetails | Client billing is not available on this server. |

#### Request samples

```bash
# Sign in first. outpost_console is a session cookie, so send it with every request.
curl -X PATCH 'https://depot.example.com/api/v1/clients/<cid>/agreements/<agreementID>' \
  -H 'X-CSRF-Token: <csrf-token>' \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -b 'outpost_console=<console-session>' \
  --data '{
  "billingDay": 0,
  "defaultChargeTypeCode": "string",
  "defaultRateCents": 0,
  "endsOn": "string",
  "lines": [
    {
      "active": false,
      "balanceMinutes": 0,
      "description": "string",
      "exclusive": false,
      "filter": "string",
      "includedDevicesPerSeat": 0,
      "includedMinutes": 0,
      "kind": "string",
      "overageUnitAmountCents": 0,
      "publicId": "string",
      "seatRequiresEndpoint": false,
      "sortOrder": 0,
      "unitAmountCents": 0,
      "usageMethod": "string"
    }
  ],
  "name": "string",
  "notes": "string",
  "rules": [
    {
      "chargeTypeCode": "string",
      "description": "string",
      "outOfHours": false,
      "outcome": "string",
      "position": 0,
      "prepaidLinePublicId": "string",
      "projectWork": false,
      "publicId": "string",
      "rateCents": 0,
      "ticketTypeCode": "string"
    }
  ],
  "startsOn": "string",
  "templateId": "string"
}'
```

```powershell
# Sign in first. outpost_console is a session cookie, so send it with every request.

$headers = @{
    'X-CSRF-Token' = '<csrf-token>'
    'Accept' = 'application/json'
    'Content-Type' = 'application/json'
    'Cookie' = 'outpost_console=<console-session>'
}

$body = @'
{
  "billingDay": 0,
  "defaultChargeTypeCode": "string",
  "defaultRateCents": 0,
  "endsOn": "string",
  "lines": [
    {
      "active": false,
      "balanceMinutes": 0,
      "description": "string",
      "exclusive": false,
      "filter": "string",
      "includedDevicesPerSeat": 0,
      "includedMinutes": 0,
      "kind": "string",
      "overageUnitAmountCents": 0,
      "publicId": "string",
      "seatRequiresEndpoint": false,
      "sortOrder": 0,
      "unitAmountCents": 0,
      "usageMethod": "string"
    }
  ],
  "name": "string",
  "notes": "string",
  "rules": [
    {
      "chargeTypeCode": "string",
      "description": "string",
      "outOfHours": false,
      "outcome": "string",
      "position": 0,
      "prepaidLinePublicId": "string",
      "projectWork": false,
      "publicId": "string",
      "rateCents": 0,
      "ticketTypeCode": "string"
    }
  ],
  "startsOn": "string",
  "templateId": "string"
}
'@

$response = Invoke-RestMethod `
    -Method Patch `
    -Uri 'https://depot.example.com/api/v1/clients/<cid>/agreements/<agreementID>' `
    -Headers $headers `
    -ContentType 'application/json' `
    -Body $body

$response | ConvertTo-Json -Depth 10
```

```python
# Sign in first. outpost_console is a session cookie, so send it with every request.

import requests

url = "https://depot.example.com/api/v1/clients/<cid>/agreements/<agreementID>"
headers = {
    "X-CSRF-Token": "<csrf-token>",
    "Accept": "application/json",
    "Content-Type": "application/json",
}
cookies = {
    "outpost_console": "<console-session>",
}
payload = {
    "billingDay": 0,
    "defaultChargeTypeCode": "string",
    "defaultRateCents": 0,
    "endsOn": "string",
    "lines": [
        {
            "active": False,
            "balanceMinutes": 0,
            "description": "string",
            "exclusive": False,
            "filter": "string",
            "includedDevicesPerSeat": 0,
            "includedMinutes": 0,
            "kind": "string",
            "overageUnitAmountCents": 0,
            "publicId": "string",
            "seatRequiresEndpoint": False,
            "sortOrder": 0,
            "unitAmountCents": 0,
            "usageMethod": "string",
        },
    ],
    "name": "string",
    "notes": "string",
    "rules": [
        {
            "chargeTypeCode": "string",
            "description": "string",
            "outOfHours": False,
            "outcome": "string",
            "position": 0,
            "prepaidLinePublicId": "string",
            "projectWork": False,
            "publicId": "string",
            "rateCents": 0,
            "ticketTypeCode": "string",
        },
    ],
    "startsOn": "string",
    "templateId": "string",
}

response = requests.patch(url, headers=headers, cookies=cookies, json=payload, timeout=30)
response.raise_for_status()
print(response.json())
```

```go
// Sign in first. outpost_console is a session cookie, so send it with every request.
package main

import (
	"bytes"
	"fmt"
	"io"
	"log"
	"net/http"
)

func main() {
	body := []byte(`{
  "billingDay": 0,
  "defaultChargeTypeCode": "string",
  "defaultRateCents": 0,
  "endsOn": "string",
  "lines": [
    {
      "active": false,
      "balanceMinutes": 0,
      "description": "string",
      "exclusive": false,
      "filter": "string",
      "includedDevicesPerSeat": 0,
      "includedMinutes": 0,
      "kind": "string",
      "overageUnitAmountCents": 0,
      "publicId": "string",
      "seatRequiresEndpoint": false,
      "sortOrder": 0,
      "unitAmountCents": 0,
      "usageMethod": "string"
    }
  ],
  "name": "string",
  "notes": "string",
  "rules": [
    {
      "chargeTypeCode": "string",
      "description": "string",
      "outOfHours": false,
      "outcome": "string",
      "position": 0,
      "prepaidLinePublicId": "string",
      "projectWork": false,
      "publicId": "string",
      "rateCents": 0,
      "ticketTypeCode": "string"
    }
  ],
  "startsOn": "string",
  "templateId": "string"
}`)

	req, err := http.NewRequest(http.MethodPatch, "https://depot.example.com/api/v1/clients/<cid>/agreements/<agreementID>", bytes.NewReader(body))
	if err != nil {
		log.Fatal(err)
	}

	req.Header.Set("X-CSRF-Token", "<csrf-token>")
	req.Header.Set("Accept", "application/json")
	req.Header.Set("Content-Type", "application/json")
	req.AddCookie(&http.Cookie{Name: "outpost_console", Value: "<console-session>"})

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()

	out, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(resp.Status, string(out))
}
```

```ts
// Sign in first. outpost_console is a session cookie, so send it with every request.
// The session cookie is set by signing in. A script cannot set it itself.

const url = "https://depot.example.com/api/v1/clients/<cid>/agreements/<agreementID>";

const response = await fetch(url, {
	method: "PATCH",
	headers: {
		"X-CSRF-Token": "<csrf-token>",
		"Accept": "application/json",
		"Content-Type": "application/json",
	},
	credentials: "include",
	body: JSON.stringify({
		"billingDay": 0,
		"defaultChargeTypeCode": "string",
		"defaultRateCents": 0,
		"endsOn": "string",
		"lines": [
			{
				"active": false,
				"balanceMinutes": 0,
				"description": "string",
				"exclusive": false,
				"filter": "string",
				"includedDevicesPerSeat": 0,
				"includedMinutes": 0,
				"kind": "string",
				"overageUnitAmountCents": 0,
				"publicId": "string",
				"seatRequiresEndpoint": false,
				"sortOrder": 0,
				"unitAmountCents": 0,
				"usageMethod": "string"
			}
		],
		"name": "string",
		"notes": "string",
		"rules": [
			{
				"chargeTypeCode": "string",
				"description": "string",
				"outOfHours": false,
				"outcome": "string",
				"position": 0,
				"prepaidLinePublicId": "string",
				"projectWork": false,
				"publicId": "string",
				"rateCents": 0,
				"ticketTypeCode": "string"
			}
		],
		"startsOn": "string",
		"templateId": "string"
	}),
});

if (!response.ok) {
	throw new Error(`Request failed with status ${response.status}`);
}

const data = await response.json();
console.log(data);
```

```csharp
// Sign in first. outpost_console is a session cookie, so send it with every request.

using System;
using System.Net.Http;
using System.Text;

using var client = new HttpClient();

const string body = """
{
  "billingDay": 0,
  "defaultChargeTypeCode": "string",
  "defaultRateCents": 0,
  "endsOn": "string",
  "lines": [
    {
      "active": false,
      "balanceMinutes": 0,
      "description": "string",
      "exclusive": false,
      "filter": "string",
      "includedDevicesPerSeat": 0,
      "includedMinutes": 0,
      "kind": "string",
      "overageUnitAmountCents": 0,
      "publicId": "string",
      "seatRequiresEndpoint": false,
      "sortOrder": 0,
      "unitAmountCents": 0,
      "usageMethod": "string"
    }
  ],
  "name": "string",
  "notes": "string",
  "rules": [
    {
      "chargeTypeCode": "string",
      "description": "string",
      "outOfHours": false,
      "outcome": "string",
      "position": 0,
      "prepaidLinePublicId": "string",
      "projectWork": false,
      "publicId": "string",
      "rateCents": 0,
      "ticketTypeCode": "string"
    }
  ],
  "startsOn": "string",
  "templateId": "string"
}
""";

var request = new HttpRequestMessage(HttpMethod.Patch, "https://depot.example.com/api/v1/clients/<cid>/agreements/<agreementID>");
request.Headers.TryAddWithoutValidation("X-CSRF-Token", "<csrf-token>");
request.Headers.TryAddWithoutValidation("Accept", "application/json");
request.Headers.TryAddWithoutValidation("Cookie", "outpost_console=<console-session>");
request.Content = new StringContent(body, Encoding.UTF8, "application/json");

var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
Console.WriteLine(await response.Content.ReadAsStringAsync());
```

### DELETE `/api/v1/clients/{cid}/agreements/{agreementID}` {#depot.psa.deleteAgreement}

**Delete agreement**

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `agreementID` | path | string | Yes | A uuid. |
| `cid` | path | string | Yes |  |

#### Responses

| Status | Body | Description |
| --- | --- | --- |
| `204` | None | Success, with no body. |
| `400` | `application/problem+json`, ProblemDetails | One or more validation errors occurred. |
| `401` | `application/problem+json`, ProblemDetails | No signed-in identity. |
| `403` | `application/problem+json`, ProblemDetails | The identity does not hold psa:write. |
| `404` | `application/problem+json`, ProblemDetails | No client matches that id. |
| `500` | `application/problem+json`, ProblemDetails | The request failed. |
| `503` | `application/problem+json`, ProblemDetails | Client billing is not available on this server. |

#### Request samples

```bash
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.
curl -X DELETE 'https://depot.example.com/api/v1/clients/<cid>/agreements/<agreementID>' \
  -H 'X-CSRF-Token: <csrf-token>' \
  -H 'Accept: application/json' \
  -b 'outpost_console=<console-session>'
```

```powershell
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.

$headers = @{
    'X-CSRF-Token' = '<csrf-token>'
    'Accept' = 'application/json'
    'Cookie' = 'outpost_console=<console-session>'
}

$response = Invoke-RestMethod `
    -Method Delete `
    -Uri 'https://depot.example.com/api/v1/clients/<cid>/agreements/<agreementID>' `
    -Headers $headers

$response | ConvertTo-Json -Depth 10
```

```python
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.

import requests

url = "https://depot.example.com/api/v1/clients/<cid>/agreements/<agreementID>"
headers = {
    "X-CSRF-Token": "<csrf-token>",
    "Accept": "application/json",
}
cookies = {
    "outpost_console": "<console-session>",
}

response = requests.delete(url, headers=headers, cookies=cookies, timeout=30)
response.raise_for_status()
print(response.json())
```

```go
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.
package main

import (
	"fmt"
	"io"
	"log"
	"net/http"
)

func main() {
	req, err := http.NewRequest(http.MethodDelete, "https://depot.example.com/api/v1/clients/<cid>/agreements/<agreementID>", nil)
	if err != nil {
		log.Fatal(err)
	}

	req.Header.Set("X-CSRF-Token", "<csrf-token>")
	req.Header.Set("Accept", "application/json")
	req.AddCookie(&http.Cookie{Name: "outpost_console", Value: "<console-session>"})

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()

	out, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(resp.Status, string(out))
}
```

```ts
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.
// The session cookie is set by signing in. A script cannot set it itself.

const url = "https://depot.example.com/api/v1/clients/<cid>/agreements/<agreementID>";

const response = await fetch(url, {
	method: "DELETE",
	headers: {
		"X-CSRF-Token": "<csrf-token>",
		"Accept": "application/json",
	},
	credentials: "include",
});

if (!response.ok) {
	throw new Error(`Request failed with status ${response.status}`);
}

const data = await response.json();
console.log(data);
```

```csharp
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.

using System;
using System.Net.Http;

using var client = new HttpClient();

var request = new HttpRequestMessage(HttpMethod.Delete, "https://depot.example.com/api/v1/clients/<cid>/agreements/<agreementID>");
request.Headers.TryAddWithoutValidation("X-CSRF-Token", "<csrf-token>");
request.Headers.TryAddWithoutValidation("Accept", "application/json");
request.Headers.TryAddWithoutValidation("Cookie", "outpost_console=<console-session>");

var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
Console.WriteLine(await response.Content.ReadAsStringAsync());
```

### POST `/api/v1/clients/{cid}/agreements/{agreementID}/activate` {#depot.psa.activateAgreement}

**Activate agreement**

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `agreementID` | path | string | Yes | A uuid. |
| `cid` | path | string | Yes |  |

#### Responses

| Status | Body | Description |
| --- | --- | --- |
| `200` | `application/json`, AgreementView | Success. |
| `400` | `application/problem+json`, ProblemDetails | One or more validation errors occurred. |
| `401` | `application/problem+json`, ProblemDetails | No signed-in identity. |
| `403` | `application/problem+json`, ProblemDetails | The identity does not hold psa:write. |
| `404` | `application/problem+json`, ProblemDetails | No client matches that id. |
| `500` | `application/problem+json`, ProblemDetails | The request failed. |
| `503` | `application/problem+json`, ProblemDetails | Client billing is not available on this server. |

#### Request samples

```bash
# Sign in first. outpost_console is a session cookie, so send it with every request.
curl -X POST 'https://depot.example.com/api/v1/clients/<cid>/agreements/<agreementID>/activate' \
  -H 'X-CSRF-Token: <csrf-token>' \
  -H 'Accept: application/json' \
  -b 'outpost_console=<console-session>'
```

```powershell
# Sign in first. outpost_console is a session cookie, so send it with every request.

$headers = @{
    'X-CSRF-Token' = '<csrf-token>'
    'Accept' = 'application/json'
    'Cookie' = 'outpost_console=<console-session>'
}

$response = Invoke-RestMethod `
    -Method Post `
    -Uri 'https://depot.example.com/api/v1/clients/<cid>/agreements/<agreementID>/activate' `
    -Headers $headers

$response | ConvertTo-Json -Depth 10
```

```python
# Sign in first. outpost_console is a session cookie, so send it with every request.

import requests

url = "https://depot.example.com/api/v1/clients/<cid>/agreements/<agreementID>/activate"
headers = {
    "X-CSRF-Token": "<csrf-token>",
    "Accept": "application/json",
}
cookies = {
    "outpost_console": "<console-session>",
}

response = requests.post(url, headers=headers, cookies=cookies, timeout=30)
response.raise_for_status()
print(response.json())
```

```go
// Sign in first. outpost_console is a session cookie, so send it with every request.
package main

import (
	"fmt"
	"io"
	"log"
	"net/http"
)

func main() {
	req, err := http.NewRequest(http.MethodPost, "https://depot.example.com/api/v1/clients/<cid>/agreements/<agreementID>/activate", nil)
	if err != nil {
		log.Fatal(err)
	}

	req.Header.Set("X-CSRF-Token", "<csrf-token>")
	req.Header.Set("Accept", "application/json")
	req.AddCookie(&http.Cookie{Name: "outpost_console", Value: "<console-session>"})

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()

	out, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(resp.Status, string(out))
}
```

```ts
// Sign in first. outpost_console is a session cookie, so send it with every request.
// The session cookie is set by signing in. A script cannot set it itself.

const url = "https://depot.example.com/api/v1/clients/<cid>/agreements/<agreementID>/activate";

const response = await fetch(url, {
	method: "POST",
	headers: {
		"X-CSRF-Token": "<csrf-token>",
		"Accept": "application/json",
	},
	credentials: "include",
});

if (!response.ok) {
	throw new Error(`Request failed with status ${response.status}`);
}

const data = await response.json();
console.log(data);
```

```csharp
// Sign in first. outpost_console is a session cookie, so send it with every request.

using System;
using System.Net.Http;

using var client = new HttpClient();

var request = new HttpRequestMessage(HttpMethod.Post, "https://depot.example.com/api/v1/clients/<cid>/agreements/<agreementID>/activate");
request.Headers.TryAddWithoutValidation("X-CSRF-Token", "<csrf-token>");
request.Headers.TryAddWithoutValidation("Accept", "application/json");
request.Headers.TryAddWithoutValidation("Cookie", "outpost_console=<console-session>");

var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
Console.WriteLine(await response.Content.ReadAsStringAsync());
```

### GET `/api/v1/clients/{cid}/agreements/{agreementID}/adjustments` {#depot.psa.listPriceAdjustments}

**listPriceAdjustments lists the agreement's discounts and escalators.**

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `agreementID` | path | string | Yes | A uuid. |
| `cid` | path | string | Yes |  |

#### Responses

| Status | Body | Description |
| --- | --- | --- |
| `200` | `application/json`, PagedResultPriceAdjustmentView | Success. |
| `400` | `application/problem+json`, ProblemDetails | One or more validation errors occurred. |
| `401` | `application/problem+json`, ProblemDetails | No signed-in identity. |
| `403` | `application/problem+json`, ProblemDetails | The identity does not hold psa:read. |
| `404` | `application/problem+json`, ProblemDetails | No client matches that id. |
| `500` | `application/problem+json`, ProblemDetails | The request failed. |
| `503` | `application/problem+json`, ProblemDetails | Client billing is not available on this server. |

#### Request samples

```bash
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.
curl -X GET 'https://depot.example.com/api/v1/clients/<cid>/agreements/<agreementID>/adjustments' \
  -H 'X-CSRF-Token: <csrf-token>' \
  -H 'Accept: application/json' \
  -b 'outpost_console=<console-session>'
```

```powershell
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.

$headers = @{
    'X-CSRF-Token' = '<csrf-token>'
    'Accept' = 'application/json'
    'Cookie' = 'outpost_console=<console-session>'
}

$response = Invoke-RestMethod `
    -Method Get `
    -Uri 'https://depot.example.com/api/v1/clients/<cid>/agreements/<agreementID>/adjustments' `
    -Headers $headers

$response | ConvertTo-Json -Depth 10
```

```python
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.

import requests

url = "https://depot.example.com/api/v1/clients/<cid>/agreements/<agreementID>/adjustments"
headers = {
    "X-CSRF-Token": "<csrf-token>",
    "Accept": "application/json",
}
cookies = {
    "outpost_console": "<console-session>",
}

response = requests.get(url, headers=headers, cookies=cookies, timeout=30)
response.raise_for_status()
print(response.json())
```

```go
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.
package main

import (
	"fmt"
	"io"
	"log"
	"net/http"
)

func main() {
	req, err := http.NewRequest(http.MethodGet, "https://depot.example.com/api/v1/clients/<cid>/agreements/<agreementID>/adjustments", nil)
	if err != nil {
		log.Fatal(err)
	}

	req.Header.Set("X-CSRF-Token", "<csrf-token>")
	req.Header.Set("Accept", "application/json")
	req.AddCookie(&http.Cookie{Name: "outpost_console", Value: "<console-session>"})

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()

	out, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(resp.Status, string(out))
}
```

```ts
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.
// The session cookie is set by signing in. A script cannot set it itself.

const url = "https://depot.example.com/api/v1/clients/<cid>/agreements/<agreementID>/adjustments";

const response = await fetch(url, {
	method: "GET",
	headers: {
		"X-CSRF-Token": "<csrf-token>",
		"Accept": "application/json",
	},
	credentials: "include",
});

if (!response.ok) {
	throw new Error(`Request failed with status ${response.status}`);
}

const data = await response.json();
console.log(data);
```

```csharp
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.

using System;
using System.Net.Http;

using var client = new HttpClient();

var request = new HttpRequestMessage(HttpMethod.Get, "https://depot.example.com/api/v1/clients/<cid>/agreements/<agreementID>/adjustments");
request.Headers.TryAddWithoutValidation("X-CSRF-Token", "<csrf-token>");
request.Headers.TryAddWithoutValidation("Accept", "application/json");
request.Headers.TryAddWithoutValidation("Cookie", "outpost_console=<console-session>");

var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
Console.WriteLine(await response.Content.ReadAsStringAsync());
```

### PUT `/api/v1/clients/{cid}/agreements/{agreementID}/adjustments` {#depot.psa.putPriceAdjustments}

**putPriceAdjustments replaces the agreement's discounts and escalators with the array sent.**

Rows with a known publicId are updated, new rows created, rows left out removed. A new escalator's first step is its startsOn; moving startsOn re-schedules it.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `agreementID` | path | string | Yes | A uuid. |
| `cid` | path | string | Yes |  |

#### Request body

Required, sent as `application/json`.

#### Responses

| Status | Body | Description |
| --- | --- | --- |
| `200` | `application/json`, PagedResultPriceAdjustmentView | Success. |
| `400` | `application/problem+json`, ProblemDetails | One or more validation errors occurred. |
| `401` | `application/problem+json`, ProblemDetails | No signed-in identity. |
| `403` | `application/problem+json`, ProblemDetails | The identity does not hold psa:write. |
| `404` | `application/problem+json`, ProblemDetails | No client matches that id. |
| `500` | `application/problem+json`, ProblemDetails | The request failed. |
| `503` | `application/problem+json`, ProblemDetails | Client billing is not available on this server. |

#### Request samples

```bash
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.
curl -X PUT 'https://depot.example.com/api/v1/clients/<cid>/agreements/<agreementID>/adjustments' \
  -H 'X-CSRF-Token: <csrf-token>' \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -b 'outpost_console=<console-session>' \
  --data '{
  "adjustments": [
    {
      "active": false,
      "agreementLinePublicId": "string",
      "amountCents": 0,
      "basisPoints": 0,
      "description": "string",
      "endsOn": "string",
      "everyMonths": 0,
      "kind": "string",
      "mode": "string",
      "nextEffectiveOn": "string",
      "noticeDays": 0,
      "publicId": "string",
      "startsOn": "string"
    }
  ]
}'
```

```powershell
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.

$headers = @{
    'X-CSRF-Token' = '<csrf-token>'
    'Accept' = 'application/json'
    'Content-Type' = 'application/json'
    'Cookie' = 'outpost_console=<console-session>'
}

$body = @'
{
  "adjustments": [
    {
      "active": false,
      "agreementLinePublicId": "string",
      "amountCents": 0,
      "basisPoints": 0,
      "description": "string",
      "endsOn": "string",
      "everyMonths": 0,
      "kind": "string",
      "mode": "string",
      "nextEffectiveOn": "string",
      "noticeDays": 0,
      "publicId": "string",
      "startsOn": "string"
    }
  ]
}
'@

$response = Invoke-RestMethod `
    -Method Put `
    -Uri 'https://depot.example.com/api/v1/clients/<cid>/agreements/<agreementID>/adjustments' `
    -Headers $headers `
    -ContentType 'application/json' `
    -Body $body

$response | ConvertTo-Json -Depth 10
```

```python
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.

import requests

url = "https://depot.example.com/api/v1/clients/<cid>/agreements/<agreementID>/adjustments"
headers = {
    "X-CSRF-Token": "<csrf-token>",
    "Accept": "application/json",
    "Content-Type": "application/json",
}
cookies = {
    "outpost_console": "<console-session>",
}
payload = {
    "adjustments": [
        {
            "active": False,
            "agreementLinePublicId": "string",
            "amountCents": 0,
            "basisPoints": 0,
            "description": "string",
            "endsOn": "string",
            "everyMonths": 0,
            "kind": "string",
            "mode": "string",
            "nextEffectiveOn": "string",
            "noticeDays": 0,
            "publicId": "string",
            "startsOn": "string",
        },
    ],
}

response = requests.put(url, headers=headers, cookies=cookies, json=payload, timeout=30)
response.raise_for_status()
print(response.json())
```

```go
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.
package main

import (
	"bytes"
	"fmt"
	"io"
	"log"
	"net/http"
)

func main() {
	body := []byte(`{
  "adjustments": [
    {
      "active": false,
      "agreementLinePublicId": "string",
      "amountCents": 0,
      "basisPoints": 0,
      "description": "string",
      "endsOn": "string",
      "everyMonths": 0,
      "kind": "string",
      "mode": "string",
      "nextEffectiveOn": "string",
      "noticeDays": 0,
      "publicId": "string",
      "startsOn": "string"
    }
  ]
}`)

	req, err := http.NewRequest(http.MethodPut, "https://depot.example.com/api/v1/clients/<cid>/agreements/<agreementID>/adjustments", bytes.NewReader(body))
	if err != nil {
		log.Fatal(err)
	}

	req.Header.Set("X-CSRF-Token", "<csrf-token>")
	req.Header.Set("Accept", "application/json")
	req.Header.Set("Content-Type", "application/json")
	req.AddCookie(&http.Cookie{Name: "outpost_console", Value: "<console-session>"})

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()

	out, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(resp.Status, string(out))
}
```

```ts
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.
// The session cookie is set by signing in. A script cannot set it itself.

const url = "https://depot.example.com/api/v1/clients/<cid>/agreements/<agreementID>/adjustments";

const response = await fetch(url, {
	method: "PUT",
	headers: {
		"X-CSRF-Token": "<csrf-token>",
		"Accept": "application/json",
		"Content-Type": "application/json",
	},
	credentials: "include",
	body: JSON.stringify({
		"adjustments": [
			{
				"active": false,
				"agreementLinePublicId": "string",
				"amountCents": 0,
				"basisPoints": 0,
				"description": "string",
				"endsOn": "string",
				"everyMonths": 0,
				"kind": "string",
				"mode": "string",
				"nextEffectiveOn": "string",
				"noticeDays": 0,
				"publicId": "string",
				"startsOn": "string"
			}
		]
	}),
});

if (!response.ok) {
	throw new Error(`Request failed with status ${response.status}`);
}

const data = await response.json();
console.log(data);
```

```csharp
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.

using System;
using System.Net.Http;
using System.Text;

using var client = new HttpClient();

const string body = """
{
  "adjustments": [
    {
      "active": false,
      "agreementLinePublicId": "string",
      "amountCents": 0,
      "basisPoints": 0,
      "description": "string",
      "endsOn": "string",
      "everyMonths": 0,
      "kind": "string",
      "mode": "string",
      "nextEffectiveOn": "string",
      "noticeDays": 0,
      "publicId": "string",
      "startsOn": "string"
    }
  ]
}
""";

var request = new HttpRequestMessage(HttpMethod.Put, "https://depot.example.com/api/v1/clients/<cid>/agreements/<agreementID>/adjustments");
request.Headers.TryAddWithoutValidation("X-CSRF-Token", "<csrf-token>");
request.Headers.TryAddWithoutValidation("Accept", "application/json");
request.Headers.TryAddWithoutValidation("Cookie", "outpost_console=<console-session>");
request.Content = new StringContent(body, Encoding.UTF8, "application/json");

var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
Console.WriteLine(await response.Content.ReadAsStringAsync());
```

### POST `/api/v1/clients/{cid}/agreements/{agreementID}/end` {#depot.psa.endAgreement}

**End agreement**

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `agreementID` | path | string | Yes | A uuid. |
| `cid` | path | string | Yes |  |

#### Request body

Required, sent as `application/json`.

#### Responses

| Status | Body | Description |
| --- | --- | --- |
| `200` | `application/json`, AgreementView | Success. |
| `400` | `application/problem+json`, ProblemDetails | One or more validation errors occurred. |
| `401` | `application/problem+json`, ProblemDetails | No signed-in identity. |
| `403` | `application/problem+json`, ProblemDetails | The identity does not hold psa:write. |
| `404` | `application/problem+json`, ProblemDetails | No client matches that id. |
| `500` | `application/problem+json`, ProblemDetails | The request failed. |
| `503` | `application/problem+json`, ProblemDetails | Client billing is not available on this server. |

#### Request samples

```bash
# Sign in first. outpost_console is a session cookie, so send it with every request.
curl -X POST 'https://depot.example.com/api/v1/clients/<cid>/agreements/<agreementID>/end' \
  -H 'X-CSRF-Token: <csrf-token>' \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -b 'outpost_console=<console-session>' \
  --data '{
  "endsOn": "string"
}'
```

```powershell
# Sign in first. outpost_console is a session cookie, so send it with every request.

$headers = @{
    'X-CSRF-Token' = '<csrf-token>'
    'Accept' = 'application/json'
    'Content-Type' = 'application/json'
    'Cookie' = 'outpost_console=<console-session>'
}

$body = @'
{
  "endsOn": "string"
}
'@

$response = Invoke-RestMethod `
    -Method Post `
    -Uri 'https://depot.example.com/api/v1/clients/<cid>/agreements/<agreementID>/end' `
    -Headers $headers `
    -ContentType 'application/json' `
    -Body $body

$response | ConvertTo-Json -Depth 10
```

```python
# Sign in first. outpost_console is a session cookie, so send it with every request.

import requests

url = "https://depot.example.com/api/v1/clients/<cid>/agreements/<agreementID>/end"
headers = {
    "X-CSRF-Token": "<csrf-token>",
    "Accept": "application/json",
    "Content-Type": "application/json",
}
cookies = {
    "outpost_console": "<console-session>",
}
payload = {
    "endsOn": "string",
}

response = requests.post(url, headers=headers, cookies=cookies, json=payload, timeout=30)
response.raise_for_status()
print(response.json())
```

```go
// Sign in first. outpost_console is a session cookie, so send it with every request.
package main

import (
	"bytes"
	"fmt"
	"io"
	"log"
	"net/http"
)

func main() {
	body := []byte(`{
  "endsOn": "string"
}`)

	req, err := http.NewRequest(http.MethodPost, "https://depot.example.com/api/v1/clients/<cid>/agreements/<agreementID>/end", bytes.NewReader(body))
	if err != nil {
		log.Fatal(err)
	}

	req.Header.Set("X-CSRF-Token", "<csrf-token>")
	req.Header.Set("Accept", "application/json")
	req.Header.Set("Content-Type", "application/json")
	req.AddCookie(&http.Cookie{Name: "outpost_console", Value: "<console-session>"})

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()

	out, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(resp.Status, string(out))
}
```

```ts
// Sign in first. outpost_console is a session cookie, so send it with every request.
// The session cookie is set by signing in. A script cannot set it itself.

const url = "https://depot.example.com/api/v1/clients/<cid>/agreements/<agreementID>/end";

const response = await fetch(url, {
	method: "POST",
	headers: {
		"X-CSRF-Token": "<csrf-token>",
		"Accept": "application/json",
		"Content-Type": "application/json",
	},
	credentials: "include",
	body: JSON.stringify({
		"endsOn": "string"
	}),
});

if (!response.ok) {
	throw new Error(`Request failed with status ${response.status}`);
}

const data = await response.json();
console.log(data);
```

```csharp
// Sign in first. outpost_console is a session cookie, so send it with every request.

using System;
using System.Net.Http;
using System.Text;

using var client = new HttpClient();

const string body = """
{
  "endsOn": "string"
}
""";

var request = new HttpRequestMessage(HttpMethod.Post, "https://depot.example.com/api/v1/clients/<cid>/agreements/<agreementID>/end");
request.Headers.TryAddWithoutValidation("X-CSRF-Token", "<csrf-token>");
request.Headers.TryAddWithoutValidation("Accept", "application/json");
request.Headers.TryAddWithoutValidation("Cookie", "outpost_console=<console-session>");
request.Content = new StringContent(body, Encoding.UTF8, "application/json");

var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
Console.WriteLine(await response.Content.ReadAsStringAsync());
```

### GET `/api/v1/clients/{cid}/agreements/{agreementID}/estimate` {#depot.psa.getAgreementEstimate}

**getAgreementEstimate is the agreement's next invoice before it exists: every line with the devices and people it counted, the change since the previous invoice, the devices no line bills, and what the engine could not settle.**

It is computed by the invoice run's own code, so it is the invoice the run would draft now.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `agreementID` | path | string | Yes | A uuid. |
| `cid` | path | string | Yes |  |

#### Responses

| Status | Body | Description |
| --- | --- | --- |
| `200` | `application/json`, AgreementEstimateView | Success. |
| `400` | `application/problem+json`, ProblemDetails | One or more validation errors occurred. |
| `401` | `application/problem+json`, ProblemDetails | No signed-in identity. |
| `403` | `application/problem+json`, ProblemDetails | The identity does not hold psa:read. |
| `404` | `application/problem+json`, ProblemDetails | No client matches that id. |
| `500` | `application/problem+json`, ProblemDetails | The request failed. |
| `503` | `application/problem+json`, ProblemDetails | Client billing is not available on this server. |

#### Request samples

```bash
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.
curl -X GET 'https://depot.example.com/api/v1/clients/<cid>/agreements/<agreementID>/estimate' \
  -H 'X-CSRF-Token: <csrf-token>' \
  -H 'Accept: application/json' \
  -b 'outpost_console=<console-session>'
```

```powershell
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.

$headers = @{
    'X-CSRF-Token' = '<csrf-token>'
    'Accept' = 'application/json'
    'Cookie' = 'outpost_console=<console-session>'
}

$response = Invoke-RestMethod `
    -Method Get `
    -Uri 'https://depot.example.com/api/v1/clients/<cid>/agreements/<agreementID>/estimate' `
    -Headers $headers

$response | ConvertTo-Json -Depth 10
```

```python
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.

import requests

url = "https://depot.example.com/api/v1/clients/<cid>/agreements/<agreementID>/estimate"
headers = {
    "X-CSRF-Token": "<csrf-token>",
    "Accept": "application/json",
}
cookies = {
    "outpost_console": "<console-session>",
}

response = requests.get(url, headers=headers, cookies=cookies, timeout=30)
response.raise_for_status()
print(response.json())
```

```go
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.
package main

import (
	"fmt"
	"io"
	"log"
	"net/http"
)

func main() {
	req, err := http.NewRequest(http.MethodGet, "https://depot.example.com/api/v1/clients/<cid>/agreements/<agreementID>/estimate", nil)
	if err != nil {
		log.Fatal(err)
	}

	req.Header.Set("X-CSRF-Token", "<csrf-token>")
	req.Header.Set("Accept", "application/json")
	req.AddCookie(&http.Cookie{Name: "outpost_console", Value: "<console-session>"})

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()

	out, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(resp.Status, string(out))
}
```

```ts
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.
// The session cookie is set by signing in. A script cannot set it itself.

const url = "https://depot.example.com/api/v1/clients/<cid>/agreements/<agreementID>/estimate";

const response = await fetch(url, {
	method: "GET",
	headers: {
		"X-CSRF-Token": "<csrf-token>",
		"Accept": "application/json",
	},
	credentials: "include",
});

if (!response.ok) {
	throw new Error(`Request failed with status ${response.status}`);
}

const data = await response.json();
console.log(data);
```

```csharp
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.

using System;
using System.Net.Http;

using var client = new HttpClient();

var request = new HttpRequestMessage(HttpMethod.Get, "https://depot.example.com/api/v1/clients/<cid>/agreements/<agreementID>/estimate");
request.Headers.TryAddWithoutValidation("X-CSRF-Token", "<csrf-token>");
request.Headers.TryAddWithoutValidation("Accept", "application/json");
request.Headers.TryAddWithoutValidation("Cookie", "outpost_console=<console-session>");

var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
Console.WriteLine(await response.Content.ReadAsStringAsync());
```

### GET `/api/v1/clients/{cid}/agreements/{agreementID}/prepaid` {#depot.psa.listPrepaid}

**List prepaid**

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `agreementID` | path | string | Yes | A uuid. |
| `cid` | path | string | Yes |  |
| `page` | query | integer | No | One-based page number. Defaults to 1. |
| `pageSize` | query | integer | No | Rows per page, 1 to 200. Defaults to 50. |

#### Responses

| Status | Body | Description |
| --- | --- | --- |
| `200` | `application/json`, PagedResultPrepaidBlockView | Success. |
| `400` | `application/problem+json`, ProblemDetails | One or more validation errors occurred. |
| `401` | `application/problem+json`, ProblemDetails | No signed-in identity. |
| `403` | `application/problem+json`, ProblemDetails | The identity does not hold psa:read. |
| `404` | `application/problem+json`, ProblemDetails | No client matches that id. |
| `500` | `application/problem+json`, ProblemDetails | The request failed. |
| `503` | `application/problem+json`, ProblemDetails | Client billing is not available on this server. |

#### Request samples

```bash
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.
curl -X GET 'https://depot.example.com/api/v1/clients/<cid>/agreements/<agreementID>/prepaid?page=0&pageSize=0' \
  -H 'X-CSRF-Token: <csrf-token>' \
  -H 'Accept: application/json' \
  -b 'outpost_console=<console-session>'
```

```powershell
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.

$headers = @{
    'X-CSRF-Token' = '<csrf-token>'
    'Accept' = 'application/json'
    'Cookie' = 'outpost_console=<console-session>'
}

$response = Invoke-RestMethod `
    -Method Get `
    -Uri 'https://depot.example.com/api/v1/clients/<cid>/agreements/<agreementID>/prepaid?page=0&pageSize=0' `
    -Headers $headers

$response | ConvertTo-Json -Depth 10
```

```python
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.

import requests

url = "https://depot.example.com/api/v1/clients/<cid>/agreements/<agreementID>/prepaid"
params = {
    "page": "0",
    "pageSize": "0",
}
headers = {
    "X-CSRF-Token": "<csrf-token>",
    "Accept": "application/json",
}
cookies = {
    "outpost_console": "<console-session>",
}

response = requests.get(url, params=params, headers=headers, cookies=cookies, timeout=30)
response.raise_for_status()
print(response.json())
```

```go
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.
package main

import (
	"fmt"
	"io"
	"log"
	"net/http"
)

func main() {
	req, err := http.NewRequest(http.MethodGet, "https://depot.example.com/api/v1/clients/<cid>/agreements/<agreementID>/prepaid?page=0&pageSize=0", nil)
	if err != nil {
		log.Fatal(err)
	}

	req.Header.Set("X-CSRF-Token", "<csrf-token>")
	req.Header.Set("Accept", "application/json")
	req.AddCookie(&http.Cookie{Name: "outpost_console", Value: "<console-session>"})

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()

	out, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(resp.Status, string(out))
}
```

```ts
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.
// The session cookie is set by signing in. A script cannot set it itself.

const url = new URL("https://depot.example.com/api/v1/clients/<cid>/agreements/<agreementID>/prepaid");
url.searchParams.set("page", "0");
url.searchParams.set("pageSize", "0");

const response = await fetch(url, {
	method: "GET",
	headers: {
		"X-CSRF-Token": "<csrf-token>",
		"Accept": "application/json",
	},
	credentials: "include",
});

if (!response.ok) {
	throw new Error(`Request failed with status ${response.status}`);
}

const data = await response.json();
console.log(data);
```

```csharp
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.

using System;
using System.Net.Http;

using var client = new HttpClient();

var request = new HttpRequestMessage(HttpMethod.Get, "https://depot.example.com/api/v1/clients/<cid>/agreements/<agreementID>/prepaid?page=0&pageSize=0");
request.Headers.TryAddWithoutValidation("X-CSRF-Token", "<csrf-token>");
request.Headers.TryAddWithoutValidation("Accept", "application/json");
request.Headers.TryAddWithoutValidation("Cookie", "outpost_console=<console-session>");

var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
Console.WriteLine(await response.Content.ReadAsStringAsync());
```

### POST `/api/v1/clients/{cid}/agreements/{agreementID}/prepaid/{lineID}/purchase` {#depot.psa.purchasePrepaid}

**purchasePrepaid sells another block of minutes against an existing prepaid line.**

psa:invoice and not psa:write: this changes what a customer owes. It writes a
LEDGER ROW and nothing else, because the balance is the sum of the ledger and
there is no cached copy to keep in step. Billing the purchase is the invoice
run's job through the agreement's prepaid_block line; adding minutes here
does not raise a charge, which is why the body has no price on it.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `agreementID` | path | string | Yes | A uuid. |
| `cid` | path | string | Yes |  |
| `lineID` | path | string | Yes | A uuid. |

#### Request body

Required, sent as `application/json`.

#### Responses

| Status | Body | Description |
| --- | --- | --- |
| `200` | `application/json`, PrepaidBlockView | Success. |
| `400` | `application/problem+json`, ProblemDetails | One or more validation errors occurred. |
| `401` | `application/problem+json`, ProblemDetails | No signed-in identity. |
| `403` | `application/problem+json`, ProblemDetails | The identity does not hold psa:invoice. |
| `404` | `application/problem+json`, ProblemDetails | No client matches that id. |
| `500` | `application/problem+json`, ProblemDetails | The request failed. |
| `503` | `application/problem+json`, ProblemDetails | Client billing is not available on this server. |

#### Request samples

```bash
# Sign in first. outpost_console is a session cookie, so send it with every request.
curl -X POST 'https://depot.example.com/api/v1/clients/<cid>/agreements/<agreementID>/prepaid/<lineID>/purchase' \
  -H 'X-CSRF-Token: <csrf-token>' \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -b 'outpost_console=<console-session>' \
  --data '{
  "minutes": 0,
  "note": "string"
}'
```

```powershell
# Sign in first. outpost_console is a session cookie, so send it with every request.

$headers = @{
    'X-CSRF-Token' = '<csrf-token>'
    'Accept' = 'application/json'
    'Content-Type' = 'application/json'
    'Cookie' = 'outpost_console=<console-session>'
}

$body = @'
{
  "minutes": 0,
  "note": "string"
}
'@

$response = Invoke-RestMethod `
    -Method Post `
    -Uri 'https://depot.example.com/api/v1/clients/<cid>/agreements/<agreementID>/prepaid/<lineID>/purchase' `
    -Headers $headers `
    -ContentType 'application/json' `
    -Body $body

$response | ConvertTo-Json -Depth 10
```

```python
# Sign in first. outpost_console is a session cookie, so send it with every request.

import requests

url = "https://depot.example.com/api/v1/clients/<cid>/agreements/<agreementID>/prepaid/<lineID>/purchase"
headers = {
    "X-CSRF-Token": "<csrf-token>",
    "Accept": "application/json",
    "Content-Type": "application/json",
}
cookies = {
    "outpost_console": "<console-session>",
}
payload = {
    "minutes": 0,
    "note": "string",
}

response = requests.post(url, headers=headers, cookies=cookies, json=payload, timeout=30)
response.raise_for_status()
print(response.json())
```

```go
// Sign in first. outpost_console is a session cookie, so send it with every request.
package main

import (
	"bytes"
	"fmt"
	"io"
	"log"
	"net/http"
)

func main() {
	body := []byte(`{
  "minutes": 0,
  "note": "string"
}`)

	req, err := http.NewRequest(http.MethodPost, "https://depot.example.com/api/v1/clients/<cid>/agreements/<agreementID>/prepaid/<lineID>/purchase", bytes.NewReader(body))
	if err != nil {
		log.Fatal(err)
	}

	req.Header.Set("X-CSRF-Token", "<csrf-token>")
	req.Header.Set("Accept", "application/json")
	req.Header.Set("Content-Type", "application/json")
	req.AddCookie(&http.Cookie{Name: "outpost_console", Value: "<console-session>"})

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()

	out, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(resp.Status, string(out))
}
```

```ts
// Sign in first. outpost_console is a session cookie, so send it with every request.
// The session cookie is set by signing in. A script cannot set it itself.

const url = "https://depot.example.com/api/v1/clients/<cid>/agreements/<agreementID>/prepaid/<lineID>/purchase";

const response = await fetch(url, {
	method: "POST",
	headers: {
		"X-CSRF-Token": "<csrf-token>",
		"Accept": "application/json",
		"Content-Type": "application/json",
	},
	credentials: "include",
	body: JSON.stringify({
		"minutes": 0,
		"note": "string"
	}),
});

if (!response.ok) {
	throw new Error(`Request failed with status ${response.status}`);
}

const data = await response.json();
console.log(data);
```

```csharp
// Sign in first. outpost_console is a session cookie, so send it with every request.

using System;
using System.Net.Http;
using System.Text;

using var client = new HttpClient();

const string body = """
{
  "minutes": 0,
  "note": "string"
}
""";

var request = new HttpRequestMessage(HttpMethod.Post, "https://depot.example.com/api/v1/clients/<cid>/agreements/<agreementID>/prepaid/<lineID>/purchase");
request.Headers.TryAddWithoutValidation("X-CSRF-Token", "<csrf-token>");
request.Headers.TryAddWithoutValidation("Accept", "application/json");
request.Headers.TryAddWithoutValidation("Cookie", "outpost_console=<console-session>");
request.Content = new StringContent(body, Encoding.UTF8, "application/json");

var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
Console.WriteLine(await response.Content.ReadAsStringAsync());
```

### GET `/api/v1/clients/{cid}/agreements/{agreementID}/prices` {#depot.psa.listLinePrices}

**listLinePrices is the agreement's effective-dated price history, by line then date.**

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `agreementID` | path | string | Yes | A uuid. |
| `cid` | path | string | Yes |  |

#### Responses

| Status | Body | Description |
| --- | --- | --- |
| `200` | `application/json`, PagedResultLinePriceView | Success. |
| `400` | `application/problem+json`, ProblemDetails | One or more validation errors occurred. |
| `401` | `application/problem+json`, ProblemDetails | No signed-in identity. |
| `403` | `application/problem+json`, ProblemDetails | The identity does not hold psa:read. |
| `404` | `application/problem+json`, ProblemDetails | No client matches that id. |
| `500` | `application/problem+json`, ProblemDetails | The request failed. |
| `503` | `application/problem+json`, ProblemDetails | Client billing is not available on this server. |

#### Request samples

```bash
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.
curl -X GET 'https://depot.example.com/api/v1/clients/<cid>/agreements/<agreementID>/prices' \
  -H 'X-CSRF-Token: <csrf-token>' \
  -H 'Accept: application/json' \
  -b 'outpost_console=<console-session>'
```

```powershell
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.

$headers = @{
    'X-CSRF-Token' = '<csrf-token>'
    'Accept' = 'application/json'
    'Cookie' = 'outpost_console=<console-session>'
}

$response = Invoke-RestMethod `
    -Method Get `
    -Uri 'https://depot.example.com/api/v1/clients/<cid>/agreements/<agreementID>/prices' `
    -Headers $headers

$response | ConvertTo-Json -Depth 10
```

```python
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.

import requests

url = "https://depot.example.com/api/v1/clients/<cid>/agreements/<agreementID>/prices"
headers = {
    "X-CSRF-Token": "<csrf-token>",
    "Accept": "application/json",
}
cookies = {
    "outpost_console": "<console-session>",
}

response = requests.get(url, headers=headers, cookies=cookies, timeout=30)
response.raise_for_status()
print(response.json())
```

```go
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.
package main

import (
	"fmt"
	"io"
	"log"
	"net/http"
)

func main() {
	req, err := http.NewRequest(http.MethodGet, "https://depot.example.com/api/v1/clients/<cid>/agreements/<agreementID>/prices", nil)
	if err != nil {
		log.Fatal(err)
	}

	req.Header.Set("X-CSRF-Token", "<csrf-token>")
	req.Header.Set("Accept", "application/json")
	req.AddCookie(&http.Cookie{Name: "outpost_console", Value: "<console-session>"})

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()

	out, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(resp.Status, string(out))
}
```

```ts
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.
// The session cookie is set by signing in. A script cannot set it itself.

const url = "https://depot.example.com/api/v1/clients/<cid>/agreements/<agreementID>/prices";

const response = await fetch(url, {
	method: "GET",
	headers: {
		"X-CSRF-Token": "<csrf-token>",
		"Accept": "application/json",
	},
	credentials: "include",
});

if (!response.ok) {
	throw new Error(`Request failed with status ${response.status}`);
}

const data = await response.json();
console.log(data);
```

```csharp
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.

using System;
using System.Net.Http;

using var client = new HttpClient();

var request = new HttpRequestMessage(HttpMethod.Get, "https://depot.example.com/api/v1/clients/<cid>/agreements/<agreementID>/prices");
request.Headers.TryAddWithoutValidation("X-CSRF-Token", "<csrf-token>");
request.Headers.TryAddWithoutValidation("Accept", "application/json");
request.Headers.TryAddWithoutValidation("Cookie", "outpost_console=<console-session>");

var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
Console.WriteLine(await response.Content.ReadAsStringAsync());
```

### PUT `/api/v1/clients/{cid}/agreements/{agreementID}/rules` {#depot.psa.putAgreementRules}

**putAgreementRules replaces the whole ordered rule set and then reprices the unbilled work it governs.**

TWO STEPS AND NOT ONE, and the split is the store's instruction rather than
this handler's preference: ReplaceAgreementRules holds a transaction while it
renumbers, and repricing is a loop over rows that may take a while. Doing
both inside that transaction would block every technician logging time
against this client for the length of the loop.

A REPRICE FAILURE DOES NOT FAIL THE SAVE. The rules are already committed by
the time the loop runs, so returning an error would report a failure for work
that succeeded and invite a retry that re-saved rules that were already
there. Entries that could not be repriced are counted and logged, and the
next invoice run classifies anything still unclassified anyway.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `agreementID` | path | string | Yes | A uuid. |
| `cid` | path | string | Yes |  |

#### Request body

Required, sent as `application/json`.

#### Responses

| Status | Body | Description |
| --- | --- | --- |
| `200` | `application/json`, AgreementView | Success. |
| `400` | `application/problem+json`, ProblemDetails | One or more validation errors occurred. |
| `401` | `application/problem+json`, ProblemDetails | No signed-in identity. |
| `403` | `application/problem+json`, ProblemDetails | The identity does not hold psa:write. |
| `404` | `application/problem+json`, ProblemDetails | No client matches that id. |
| `500` | `application/problem+json`, ProblemDetails | The request failed. |
| `503` | `application/problem+json`, ProblemDetails | Client billing is not available on this server. |

#### Request samples

```bash
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.
curl -X PUT 'https://depot.example.com/api/v1/clients/<cid>/agreements/<agreementID>/rules' \
  -H 'X-CSRF-Token: <csrf-token>' \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -b 'outpost_console=<console-session>' \
  --data '{
  "rules": [
    {
      "chargeTypeCode": "string",
      "description": "string",
      "outOfHours": false,
      "outcome": "string",
      "position": 0,
      "prepaidLinePublicId": "string",
      "projectWork": false,
      "publicId": "string",
      "rateCents": 0,
      "ticketTypeCode": "string"
    }
  ]
}'
```

```powershell
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.

$headers = @{
    'X-CSRF-Token' = '<csrf-token>'
    'Accept' = 'application/json'
    'Content-Type' = 'application/json'
    'Cookie' = 'outpost_console=<console-session>'
}

$body = @'
{
  "rules": [
    {
      "chargeTypeCode": "string",
      "description": "string",
      "outOfHours": false,
      "outcome": "string",
      "position": 0,
      "prepaidLinePublicId": "string",
      "projectWork": false,
      "publicId": "string",
      "rateCents": 0,
      "ticketTypeCode": "string"
    }
  ]
}
'@

$response = Invoke-RestMethod `
    -Method Put `
    -Uri 'https://depot.example.com/api/v1/clients/<cid>/agreements/<agreementID>/rules' `
    -Headers $headers `
    -ContentType 'application/json' `
    -Body $body

$response | ConvertTo-Json -Depth 10
```

```python
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.

import requests

url = "https://depot.example.com/api/v1/clients/<cid>/agreements/<agreementID>/rules"
headers = {
    "X-CSRF-Token": "<csrf-token>",
    "Accept": "application/json",
    "Content-Type": "application/json",
}
cookies = {
    "outpost_console": "<console-session>",
}
payload = {
    "rules": [
        {
            "chargeTypeCode": "string",
            "description": "string",
            "outOfHours": False,
            "outcome": "string",
            "position": 0,
            "prepaidLinePublicId": "string",
            "projectWork": False,
            "publicId": "string",
            "rateCents": 0,
            "ticketTypeCode": "string",
        },
    ],
}

response = requests.put(url, headers=headers, cookies=cookies, json=payload, timeout=30)
response.raise_for_status()
print(response.json())
```

```go
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.
package main

import (
	"bytes"
	"fmt"
	"io"
	"log"
	"net/http"
)

func main() {
	body := []byte(`{
  "rules": [
    {
      "chargeTypeCode": "string",
      "description": "string",
      "outOfHours": false,
      "outcome": "string",
      "position": 0,
      "prepaidLinePublicId": "string",
      "projectWork": false,
      "publicId": "string",
      "rateCents": 0,
      "ticketTypeCode": "string"
    }
  ]
}`)

	req, err := http.NewRequest(http.MethodPut, "https://depot.example.com/api/v1/clients/<cid>/agreements/<agreementID>/rules", bytes.NewReader(body))
	if err != nil {
		log.Fatal(err)
	}

	req.Header.Set("X-CSRF-Token", "<csrf-token>")
	req.Header.Set("Accept", "application/json")
	req.Header.Set("Content-Type", "application/json")
	req.AddCookie(&http.Cookie{Name: "outpost_console", Value: "<console-session>"})

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()

	out, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(resp.Status, string(out))
}
```

```ts
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.
// The session cookie is set by signing in. A script cannot set it itself.

const url = "https://depot.example.com/api/v1/clients/<cid>/agreements/<agreementID>/rules";

const response = await fetch(url, {
	method: "PUT",
	headers: {
		"X-CSRF-Token": "<csrf-token>",
		"Accept": "application/json",
		"Content-Type": "application/json",
	},
	credentials: "include",
	body: JSON.stringify({
		"rules": [
			{
				"chargeTypeCode": "string",
				"description": "string",
				"outOfHours": false,
				"outcome": "string",
				"position": 0,
				"prepaidLinePublicId": "string",
				"projectWork": false,
				"publicId": "string",
				"rateCents": 0,
				"ticketTypeCode": "string"
			}
		]
	}),
});

if (!response.ok) {
	throw new Error(`Request failed with status ${response.status}`);
}

const data = await response.json();
console.log(data);
```

```csharp
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.

using System;
using System.Net.Http;
using System.Text;

using var client = new HttpClient();

const string body = """
{
  "rules": [
    {
      "chargeTypeCode": "string",
      "description": "string",
      "outOfHours": false,
      "outcome": "string",
      "position": 0,
      "prepaidLinePublicId": "string",
      "projectWork": false,
      "publicId": "string",
      "rateCents": 0,
      "ticketTypeCode": "string"
    }
  ]
}
""";

var request = new HttpRequestMessage(HttpMethod.Put, "https://depot.example.com/api/v1/clients/<cid>/agreements/<agreementID>/rules");
request.Headers.TryAddWithoutValidation("X-CSRF-Token", "<csrf-token>");
request.Headers.TryAddWithoutValidation("Accept", "application/json");
request.Headers.TryAddWithoutValidation("Cookie", "outpost_console=<console-session>");
request.Content = new StringContent(body, Encoding.UTF8, "application/json");

var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
Console.WriteLine(await response.Content.ReadAsStringAsync());
```

### GET `/api/v1/clients/{cid}/billing-profile` {#depot.psa.getBillingProfile}

**Get billing profile**

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `cid` | path | string | Yes |  |

#### Responses

| Status | Body | Description |
| --- | --- | --- |
| `200` | `application/json`, ClientBillingProfileView | Success. |
| `400` | `application/problem+json`, ProblemDetails | One or more validation errors occurred. |
| `401` | `application/problem+json`, ProblemDetails | No signed-in identity. |
| `403` | `application/problem+json`, ProblemDetails | The identity does not hold psa:read. |
| `404` | `application/problem+json`, ProblemDetails | No client matches that id. |
| `500` | `application/problem+json`, ProblemDetails | The request failed. |
| `503` | `application/problem+json`, ProblemDetails | Client billing is not available on this server. |

#### Request samples

```bash
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.
curl -X GET 'https://depot.example.com/api/v1/clients/<cid>/billing-profile' \
  -H 'X-CSRF-Token: <csrf-token>' \
  -H 'Accept: application/json' \
  -b 'outpost_console=<console-session>'
```

```powershell
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.

$headers = @{
    'X-CSRF-Token' = '<csrf-token>'
    'Accept' = 'application/json'
    'Cookie' = 'outpost_console=<console-session>'
}

$response = Invoke-RestMethod `
    -Method Get `
    -Uri 'https://depot.example.com/api/v1/clients/<cid>/billing-profile' `
    -Headers $headers

$response | ConvertTo-Json -Depth 10
```

```python
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.

import requests

url = "https://depot.example.com/api/v1/clients/<cid>/billing-profile"
headers = {
    "X-CSRF-Token": "<csrf-token>",
    "Accept": "application/json",
}
cookies = {
    "outpost_console": "<console-session>",
}

response = requests.get(url, headers=headers, cookies=cookies, timeout=30)
response.raise_for_status()
print(response.json())
```

```go
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.
package main

import (
	"fmt"
	"io"
	"log"
	"net/http"
)

func main() {
	req, err := http.NewRequest(http.MethodGet, "https://depot.example.com/api/v1/clients/<cid>/billing-profile", nil)
	if err != nil {
		log.Fatal(err)
	}

	req.Header.Set("X-CSRF-Token", "<csrf-token>")
	req.Header.Set("Accept", "application/json")
	req.AddCookie(&http.Cookie{Name: "outpost_console", Value: "<console-session>"})

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()

	out, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(resp.Status, string(out))
}
```

```ts
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.
// The session cookie is set by signing in. A script cannot set it itself.

const url = "https://depot.example.com/api/v1/clients/<cid>/billing-profile";

const response = await fetch(url, {
	method: "GET",
	headers: {
		"X-CSRF-Token": "<csrf-token>",
		"Accept": "application/json",
	},
	credentials: "include",
});

if (!response.ok) {
	throw new Error(`Request failed with status ${response.status}`);
}

const data = await response.json();
console.log(data);
```

```csharp
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.

using System;
using System.Net.Http;

using var client = new HttpClient();

var request = new HttpRequestMessage(HttpMethod.Get, "https://depot.example.com/api/v1/clients/<cid>/billing-profile");
request.Headers.TryAddWithoutValidation("X-CSRF-Token", "<csrf-token>");
request.Headers.TryAddWithoutValidation("Accept", "application/json");
request.Headers.TryAddWithoutValidation("Cookie", "outpost_console=<console-session>");

var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
Console.WriteLine(await response.Content.ReadAsStringAsync());
```

### PUT `/api/v1/clients/{cid}/billing-profile` {#depot.psa.putBillingProfile}

**Put billing profile**

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `cid` | path | string | Yes |  |

#### Request body

Required, sent as `application/json`.

#### Responses

| Status | Body | Description |
| --- | --- | --- |
| `200` | `application/json`, ClientBillingProfileView | Success. |
| `400` | `application/problem+json`, ProblemDetails | The request body is not valid JSON. |
| `401` | `application/problem+json`, ProblemDetails | No signed-in identity. |
| `403` | `application/problem+json`, ProblemDetails | The identity does not hold psa:write. |
| `404` | `application/problem+json`, ProblemDetails | No client matches that id. |
| `500` | `application/problem+json`, ProblemDetails | The request failed. |
| `503` | `application/problem+json`, ProblemDetails | Client billing is not available on this server. |

#### Request samples

```bash
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.
curl -X PUT 'https://depot.example.com/api/v1/clients/<cid>/billing-profile' \
  -H 'X-CSRF-Token: <csrf-token>' \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -b 'outpost_console=<console-session>' \
  --data '{
  "address": "string",
  "contactEmail": "string",
  "contactName": "string",
  "netDays": 0,
  "taxRateBasisPoints": 0,
  "timeZone": "string",
  "usageMethod": "string"
}'
```

```powershell
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.

$headers = @{
    'X-CSRF-Token' = '<csrf-token>'
    'Accept' = 'application/json'
    'Content-Type' = 'application/json'
    'Cookie' = 'outpost_console=<console-session>'
}

$body = @'
{
  "address": "string",
  "contactEmail": "string",
  "contactName": "string",
  "netDays": 0,
  "taxRateBasisPoints": 0,
  "timeZone": "string",
  "usageMethod": "string"
}
'@

$response = Invoke-RestMethod `
    -Method Put `
    -Uri 'https://depot.example.com/api/v1/clients/<cid>/billing-profile' `
    -Headers $headers `
    -ContentType 'application/json' `
    -Body $body

$response | ConvertTo-Json -Depth 10
```

```python
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.

import requests

url = "https://depot.example.com/api/v1/clients/<cid>/billing-profile"
headers = {
    "X-CSRF-Token": "<csrf-token>",
    "Accept": "application/json",
    "Content-Type": "application/json",
}
cookies = {
    "outpost_console": "<console-session>",
}
payload = {
    "address": "string",
    "contactEmail": "string",
    "contactName": "string",
    "netDays": 0,
    "taxRateBasisPoints": 0,
    "timeZone": "string",
    "usageMethod": "string",
}

response = requests.put(url, headers=headers, cookies=cookies, json=payload, timeout=30)
response.raise_for_status()
print(response.json())
```

```go
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.
package main

import (
	"bytes"
	"fmt"
	"io"
	"log"
	"net/http"
)

func main() {
	body := []byte(`{
  "address": "string",
  "contactEmail": "string",
  "contactName": "string",
  "netDays": 0,
  "taxRateBasisPoints": 0,
  "timeZone": "string",
  "usageMethod": "string"
}`)

	req, err := http.NewRequest(http.MethodPut, "https://depot.example.com/api/v1/clients/<cid>/billing-profile", bytes.NewReader(body))
	if err != nil {
		log.Fatal(err)
	}

	req.Header.Set("X-CSRF-Token", "<csrf-token>")
	req.Header.Set("Accept", "application/json")
	req.Header.Set("Content-Type", "application/json")
	req.AddCookie(&http.Cookie{Name: "outpost_console", Value: "<console-session>"})

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()

	out, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(resp.Status, string(out))
}
```

```ts
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.
// The session cookie is set by signing in. A script cannot set it itself.

const url = "https://depot.example.com/api/v1/clients/<cid>/billing-profile";

const response = await fetch(url, {
	method: "PUT",
	headers: {
		"X-CSRF-Token": "<csrf-token>",
		"Accept": "application/json",
		"Content-Type": "application/json",
	},
	credentials: "include",
	body: JSON.stringify({
		"address": "string",
		"contactEmail": "string",
		"contactName": "string",
		"netDays": 0,
		"taxRateBasisPoints": 0,
		"timeZone": "string",
		"usageMethod": "string"
	}),
});

if (!response.ok) {
	throw new Error(`Request failed with status ${response.status}`);
}

const data = await response.json();
console.log(data);
```

```csharp
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.

using System;
using System.Net.Http;
using System.Text;

using var client = new HttpClient();

const string body = """
{
  "address": "string",
  "contactEmail": "string",
  "contactName": "string",
  "netDays": 0,
  "taxRateBasisPoints": 0,
  "timeZone": "string",
  "usageMethod": "string"
}
""";

var request = new HttpRequestMessage(HttpMethod.Put, "https://depot.example.com/api/v1/clients/<cid>/billing-profile");
request.Headers.TryAddWithoutValidation("X-CSRF-Token", "<csrf-token>");
request.Headers.TryAddWithoutValidation("Accept", "application/json");
request.Headers.TryAddWithoutValidation("Cookie", "outpost_console=<console-session>");
request.Content = new StringContent(body, Encoding.UTF8, "application/json");

var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
Console.WriteLine(await response.Content.ReadAsStringAsync());
```

### POST `/api/v1/clients/{cid}/invoices/generate` {#depot.psa.generateInvoice}

**generateInvoice runs the invoice run for one client, right now.**

It calls billingworker through PsaDeps.GenerateNow rather than re-implementing
the run: the run holds the claim discipline, the proration and the two-commit
ordering, and a second implementation of any of those is a second answer to
what a month costs.

409 WHEN THE PERIOD IS ALREADY LIVE. On a multi-node deployment that is an
ordinary outcome and not a fault: another node got there first and the draft
it made is the one to open.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `cid` | path | string | Yes |  |

#### Request body

Required, sent as `application/json`.

#### Responses

| Status | Body | Description |
| --- | --- | --- |
| `201` | `application/json`, InvoiceView | Created. |
| `400` | `application/problem+json`, ProblemDetails | One or more validation errors occurred. |
| `401` | `application/problem+json`, ProblemDetails | No signed-in identity. |
| `403` | `application/problem+json`, ProblemDetails | The identity does not hold psa:invoice. |
| `404` | `application/problem+json`, ProblemDetails | No client matches that id. |
| `500` | `application/problem+json`, ProblemDetails | The request failed. |
| `503` | `application/problem+json`, ProblemDetails | Client billing is not available on this server. |

#### Request samples

```bash
# Sign in first. outpost_console is a session cookie, so send it with every request.
curl -X POST 'https://depot.example.com/api/v1/clients/<cid>/invoices/generate' \
  -H 'X-CSRF-Token: <csrf-token>' \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -b 'outpost_console=<console-session>' \
  --data '{
  "periodStart": "string"
}'
```

```powershell
# Sign in first. outpost_console is a session cookie, so send it with every request.

$headers = @{
    'X-CSRF-Token' = '<csrf-token>'
    'Accept' = 'application/json'
    'Content-Type' = 'application/json'
    'Cookie' = 'outpost_console=<console-session>'
}

$body = @'
{
  "periodStart": "string"
}
'@

$response = Invoke-RestMethod `
    -Method Post `
    -Uri 'https://depot.example.com/api/v1/clients/<cid>/invoices/generate' `
    -Headers $headers `
    -ContentType 'application/json' `
    -Body $body

$response | ConvertTo-Json -Depth 10
```

```python
# Sign in first. outpost_console is a session cookie, so send it with every request.

import requests

url = "https://depot.example.com/api/v1/clients/<cid>/invoices/generate"
headers = {
    "X-CSRF-Token": "<csrf-token>",
    "Accept": "application/json",
    "Content-Type": "application/json",
}
cookies = {
    "outpost_console": "<console-session>",
}
payload = {
    "periodStart": "string",
}

response = requests.post(url, headers=headers, cookies=cookies, json=payload, timeout=30)
response.raise_for_status()
print(response.json())
```

```go
// Sign in first. outpost_console is a session cookie, so send it with every request.
package main

import (
	"bytes"
	"fmt"
	"io"
	"log"
	"net/http"
)

func main() {
	body := []byte(`{
  "periodStart": "string"
}`)

	req, err := http.NewRequest(http.MethodPost, "https://depot.example.com/api/v1/clients/<cid>/invoices/generate", bytes.NewReader(body))
	if err != nil {
		log.Fatal(err)
	}

	req.Header.Set("X-CSRF-Token", "<csrf-token>")
	req.Header.Set("Accept", "application/json")
	req.Header.Set("Content-Type", "application/json")
	req.AddCookie(&http.Cookie{Name: "outpost_console", Value: "<console-session>"})

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()

	out, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(resp.Status, string(out))
}
```

```ts
// Sign in first. outpost_console is a session cookie, so send it with every request.
// The session cookie is set by signing in. A script cannot set it itself.

const url = "https://depot.example.com/api/v1/clients/<cid>/invoices/generate";

const response = await fetch(url, {
	method: "POST",
	headers: {
		"X-CSRF-Token": "<csrf-token>",
		"Accept": "application/json",
		"Content-Type": "application/json",
	},
	credentials: "include",
	body: JSON.stringify({
		"periodStart": "string"
	}),
});

if (!response.ok) {
	throw new Error(`Request failed with status ${response.status}`);
}

const data = await response.json();
console.log(data);
```

```csharp
// Sign in first. outpost_console is a session cookie, so send it with every request.

using System;
using System.Net.Http;
using System.Text;

using var client = new HttpClient();

const string body = """
{
  "periodStart": "string"
}
""";

var request = new HttpRequestMessage(HttpMethod.Post, "https://depot.example.com/api/v1/clients/<cid>/invoices/generate");
request.Headers.TryAddWithoutValidation("X-CSRF-Token", "<csrf-token>");
request.Headers.TryAddWithoutValidation("Accept", "application/json");
request.Headers.TryAddWithoutValidation("Cookie", "outpost_console=<console-session>");
request.Content = new StringContent(body, Encoding.UTF8, "application/json");

var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
Console.WriteLine(await response.Content.ReadAsStringAsync());
```

### GET `/api/v1/clients/{cid}/ticket-types` {#depot.psa.listTicketTypes}

**listTicketTypes answers the vocabulary a billing rule may match on.**

It is NOT paged, deliberately, unlike every other list on this face. A
tenant's ticket types are a handful of rows that fill one select, and a picker
that could be truncated by a page size is a picker that silently offers fewer
options than the desk actually has, which reads to an operator as the type
they wanted not existing.

Soft-deleted types are excluded and the order is SortOrder then Code, both by
the store's own query: a retired type is not something a NEW rule should match
on, while an existing rule that already names one keeps working, because the
reference is by code and has no foreign key.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `cid` | path | string | Yes |  |

#### Responses

| Status | Body | Description |
| --- | --- | --- |
| `200` | `application/json`, TicketTypeListView | Success. |
| `400` | `application/problem+json`, ProblemDetails | One or more validation errors occurred. |
| `401` | `application/problem+json`, ProblemDetails | No signed-in identity. |
| `403` | `application/problem+json`, ProblemDetails | The identity does not hold psa:read. |
| `404` | `application/problem+json`, ProblemDetails | No client matches that id. |
| `500` | `application/problem+json`, ProblemDetails | The request failed. |
| `503` | `application/problem+json`, ProblemDetails | Client billing is not available on this server. |

#### Request samples

```bash
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.
curl -X GET 'https://depot.example.com/api/v1/clients/<cid>/ticket-types' \
  -H 'X-CSRF-Token: <csrf-token>' \
  -H 'Accept: application/json' \
  -b 'outpost_console=<console-session>'
```

```powershell
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.

$headers = @{
    'X-CSRF-Token' = '<csrf-token>'
    'Accept' = 'application/json'
    'Cookie' = 'outpost_console=<console-session>'
}

$response = Invoke-RestMethod `
    -Method Get `
    -Uri 'https://depot.example.com/api/v1/clients/<cid>/ticket-types' `
    -Headers $headers

$response | ConvertTo-Json -Depth 10
```

```python
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.

import requests

url = "https://depot.example.com/api/v1/clients/<cid>/ticket-types"
headers = {
    "X-CSRF-Token": "<csrf-token>",
    "Accept": "application/json",
}
cookies = {
    "outpost_console": "<console-session>",
}

response = requests.get(url, headers=headers, cookies=cookies, timeout=30)
response.raise_for_status()
print(response.json())
```

```go
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.
package main

import (
	"fmt"
	"io"
	"log"
	"net/http"
)

func main() {
	req, err := http.NewRequest(http.MethodGet, "https://depot.example.com/api/v1/clients/<cid>/ticket-types", nil)
	if err != nil {
		log.Fatal(err)
	}

	req.Header.Set("X-CSRF-Token", "<csrf-token>")
	req.Header.Set("Accept", "application/json")
	req.AddCookie(&http.Cookie{Name: "outpost_console", Value: "<console-session>"})

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()

	out, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(resp.Status, string(out))
}
```

```ts
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.
// The session cookie is set by signing in. A script cannot set it itself.

const url = "https://depot.example.com/api/v1/clients/<cid>/ticket-types";

const response = await fetch(url, {
	method: "GET",
	headers: {
		"X-CSRF-Token": "<csrf-token>",
		"Accept": "application/json",
	},
	credentials: "include",
});

if (!response.ok) {
	throw new Error(`Request failed with status ${response.status}`);
}

const data = await response.json();
console.log(data);
```

```csharp
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.

using System;
using System.Net.Http;

using var client = new HttpClient();

var request = new HttpRequestMessage(HttpMethod.Get, "https://depot.example.com/api/v1/clients/<cid>/ticket-types");
request.Headers.TryAddWithoutValidation("X-CSRF-Token", "<csrf-token>");
request.Headers.TryAddWithoutValidation("Accept", "application/json");
request.Headers.TryAddWithoutValidation("Cookie", "outpost_console=<console-session>");

var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
Console.WriteLine(await response.Content.ReadAsStringAsync());
```

### POST `/api/v1/clients/{cid}/tickets/{ticketID}/time/{entryID}/reprice` {#depot.psa.repriceTimeEntry}

**repriceTimeEntry re-runs the agreement's rules over one logged entry and restamps what they decide.**

psa:write and NOT ticket:write, unlike the PATCH and DELETE beside it: editing
what you logged is part of working a ticket, and changing what a customer is
charged for it is not.

409 ONCE THE ENTRY IS INVOICED, which is the store's ErrConflict under a row
lock rather than a check here. The check and the write cannot race a finalize
that way, and repricing work a customer has already been billed for makes a
sent invoice disagree with the database.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `cid` | path | string | Yes |  |
| `entryID` | path | string | Yes | A uuid. |
| `ticketID` | path | string | Yes | A uuid. |

#### Responses

| Status | Body | Description |
| --- | --- | --- |
| `200` | `application/json`, TicketTimeEntryView | Success. |
| `400` | `application/problem+json`, ProblemDetails | One or more validation errors occurred. |
| `401` | `application/problem+json`, ProblemDetails | No signed-in identity. |
| `403` | `application/problem+json`, ProblemDetails | The identity does not hold psa:write. |
| `404` | `application/problem+json`, ProblemDetails | No client matches that id. |
| `409` | `application/problem+json`, ProblemDetails | This client has no active agreement, so there are no rules to price this work against. |
| `500` | `application/problem+json`, ProblemDetails | The request failed. |
| `503` | `application/problem+json`, ProblemDetails | Client billing is not available on this server. |

#### Request samples

```bash
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.
curl -X POST 'https://depot.example.com/api/v1/clients/<cid>/tickets/<ticketID>/time/<entryID>/reprice' \
  -H 'X-CSRF-Token: <csrf-token>' \
  -H 'Accept: application/json' \
  -b 'outpost_console=<console-session>'
```

```powershell
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.

$headers = @{
    'X-CSRF-Token' = '<csrf-token>'
    'Accept' = 'application/json'
    'Cookie' = 'outpost_console=<console-session>'
}

$response = Invoke-RestMethod `
    -Method Post `
    -Uri 'https://depot.example.com/api/v1/clients/<cid>/tickets/<ticketID>/time/<entryID>/reprice' `
    -Headers $headers

$response | ConvertTo-Json -Depth 10
```

```python
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.

import requests

url = "https://depot.example.com/api/v1/clients/<cid>/tickets/<ticketID>/time/<entryID>/reprice"
headers = {
    "X-CSRF-Token": "<csrf-token>",
    "Accept": "application/json",
}
cookies = {
    "outpost_console": "<console-session>",
}

response = requests.post(url, headers=headers, cookies=cookies, timeout=30)
response.raise_for_status()
print(response.json())
```

```go
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.
package main

import (
	"fmt"
	"io"
	"log"
	"net/http"
)

func main() {
	req, err := http.NewRequest(http.MethodPost, "https://depot.example.com/api/v1/clients/<cid>/tickets/<ticketID>/time/<entryID>/reprice", nil)
	if err != nil {
		log.Fatal(err)
	}

	req.Header.Set("X-CSRF-Token", "<csrf-token>")
	req.Header.Set("Accept", "application/json")
	req.AddCookie(&http.Cookie{Name: "outpost_console", Value: "<console-session>"})

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()

	out, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(resp.Status, string(out))
}
```

```ts
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.
// The session cookie is set by signing in. A script cannot set it itself.

const url = "https://depot.example.com/api/v1/clients/<cid>/tickets/<ticketID>/time/<entryID>/reprice";

const response = await fetch(url, {
	method: "POST",
	headers: {
		"X-CSRF-Token": "<csrf-token>",
		"Accept": "application/json",
	},
	credentials: "include",
});

if (!response.ok) {
	throw new Error(`Request failed with status ${response.status}`);
}

const data = await response.json();
console.log(data);
```

```csharp
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.

using System;
using System.Net.Http;

using var client = new HttpClient();

var request = new HttpRequestMessage(HttpMethod.Post, "https://depot.example.com/api/v1/clients/<cid>/tickets/<ticketID>/time/<entryID>/reprice");
request.Headers.TryAddWithoutValidation("X-CSRF-Token", "<csrf-token>");
request.Headers.TryAddWithoutValidation("Accept", "application/json");
request.Headers.TryAddWithoutValidation("Cookie", "outpost_console=<console-session>");

var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
Console.WriteLine(await response.Content.ReadAsStringAsync());
```

### GET `/api/v1/clients/{cid}/unbilled-time` {#depot.psa.listClientUnbilledTime}

**List client unbilled time**

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `cid` | path | string | Yes |  |
| `outcome` | query | string, one of covered, excluded, fixed, payg, prepaid, unclassified | No | Comma-separated set. An unrecognised value is refused. |
| `page` | query | integer | No | One-based page number. Defaults to 1. |
| `pageSize` | query | integer | No | Rows per page, 1 to 200. Defaults to 50. |
| `project` | query | string | No |  |
| `subject` | query | string | No |  |

#### Responses

| Status | Body | Description |
| --- | --- | --- |
| `200` | `application/json`, PagedResultUnbilledTimeEntryView | Success. |
| `400` | `application/problem+json`, ProblemDetails | One or more validation errors occurred. |
| `401` | `application/problem+json`, ProblemDetails | No signed-in identity. |
| `403` | `application/problem+json`, ProblemDetails | The identity does not hold psa:read. |
| `404` | `application/problem+json`, ProblemDetails | No client matches that id. |
| `500` | `application/problem+json`, ProblemDetails | The request failed. |
| `503` | `application/problem+json`, ProblemDetails | Client billing is not available on this server. |

#### Request samples

```bash
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.
curl -X GET 'https://depot.example.com/api/v1/clients/<cid>/unbilled-time?outcome=covered&page=0&pageSize=0&project=<project>&subject=<subject>' \
  -H 'X-CSRF-Token: <csrf-token>' \
  -H 'Accept: application/json' \
  -b 'outpost_console=<console-session>'
```

```powershell
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.

$headers = @{
    'X-CSRF-Token' = '<csrf-token>'
    'Accept' = 'application/json'
    'Cookie' = 'outpost_console=<console-session>'
}

$response = Invoke-RestMethod `
    -Method Get `
    -Uri 'https://depot.example.com/api/v1/clients/<cid>/unbilled-time?outcome=covered&page=0&pageSize=0&project=<project>&subject=<subject>' `
    -Headers $headers

$response | ConvertTo-Json -Depth 10
```

```python
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.

import requests

url = "https://depot.example.com/api/v1/clients/<cid>/unbilled-time"
params = {
    "outcome": "covered",
    "page": "0",
    "pageSize": "0",
    "project": "<project>",
    "subject": "<subject>",
}
headers = {
    "X-CSRF-Token": "<csrf-token>",
    "Accept": "application/json",
}
cookies = {
    "outpost_console": "<console-session>",
}

response = requests.get(url, params=params, headers=headers, cookies=cookies, timeout=30)
response.raise_for_status()
print(response.json())
```

```go
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.
package main

import (
	"fmt"
	"io"
	"log"
	"net/http"
)

func main() {
	req, err := http.NewRequest(http.MethodGet, "https://depot.example.com/api/v1/clients/<cid>/unbilled-time?outcome=covered&page=0&pageSize=0&project=<project>&subject=<subject>", nil)
	if err != nil {
		log.Fatal(err)
	}

	req.Header.Set("X-CSRF-Token", "<csrf-token>")
	req.Header.Set("Accept", "application/json")
	req.AddCookie(&http.Cookie{Name: "outpost_console", Value: "<console-session>"})

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()

	out, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(resp.Status, string(out))
}
```

```ts
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.
// The session cookie is set by signing in. A script cannot set it itself.

const url = new URL("https://depot.example.com/api/v1/clients/<cid>/unbilled-time");
url.searchParams.set("outcome", "covered");
url.searchParams.set("page", "0");
url.searchParams.set("pageSize", "0");
url.searchParams.set("project", "<project>");
url.searchParams.set("subject", "<subject>");

const response = await fetch(url, {
	method: "GET",
	headers: {
		"X-CSRF-Token": "<csrf-token>",
		"Accept": "application/json",
	},
	credentials: "include",
});

if (!response.ok) {
	throw new Error(`Request failed with status ${response.status}`);
}

const data = await response.json();
console.log(data);
```

```csharp
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.

using System;
using System.Net.Http;

using var client = new HttpClient();

var request = new HttpRequestMessage(HttpMethod.Get, "https://depot.example.com/api/v1/clients/<cid>/unbilled-time?outcome=covered&page=0&pageSize=0&project=<project>&subject=<subject>");
request.Headers.TryAddWithoutValidation("X-CSRF-Token", "<csrf-token>");
request.Headers.TryAddWithoutValidation("Accept", "application/json");
request.Headers.TryAddWithoutValidation("Cookie", "outpost_console=<console-session>");

var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
Console.WriteLine(await response.Content.ReadAsStringAsync());
```

### GET `/api/v1/clients/{cid}/usage` {#depot.psa.listClientUsage}

**listClientUsage is the client's daily billable inventory between from and to (inclusive dates; default the last 31 days), oldest first, paged.**

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `cid` | path | string | Yes |  |
| `page` | query | integer | No | One-based page number. Defaults to 1. |
| `pageSize` | query | integer | No | Rows per page, 1 to 200. Defaults to 50. |

#### Responses

| Status | Body | Description |
| --- | --- | --- |
| `200` | `application/json`, PagedResultUsageDayView | Success. |
| `400` | `application/problem+json`, ProblemDetails | One or more validation errors occurred. |
| `401` | `application/problem+json`, ProblemDetails | No signed-in identity. |
| `403` | `application/problem+json`, ProblemDetails | The identity does not hold psa:read. |
| `404` | `application/problem+json`, ProblemDetails | No client matches that id. |
| `500` | `application/problem+json`, ProblemDetails | The request failed. |
| `503` | `application/problem+json`, ProblemDetails | Client billing is not available on this server. |

#### Request samples

```bash
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.
curl -X GET 'https://depot.example.com/api/v1/clients/<cid>/usage?page=0&pageSize=0' \
  -H 'X-CSRF-Token: <csrf-token>' \
  -H 'Accept: application/json' \
  -b 'outpost_console=<console-session>'
```

```powershell
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.

$headers = @{
    'X-CSRF-Token' = '<csrf-token>'
    'Accept' = 'application/json'
    'Cookie' = 'outpost_console=<console-session>'
}

$response = Invoke-RestMethod `
    -Method Get `
    -Uri 'https://depot.example.com/api/v1/clients/<cid>/usage?page=0&pageSize=0' `
    -Headers $headers

$response | ConvertTo-Json -Depth 10
```

```python
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.

import requests

url = "https://depot.example.com/api/v1/clients/<cid>/usage"
params = {
    "page": "0",
    "pageSize": "0",
}
headers = {
    "X-CSRF-Token": "<csrf-token>",
    "Accept": "application/json",
}
cookies = {
    "outpost_console": "<console-session>",
}

response = requests.get(url, params=params, headers=headers, cookies=cookies, timeout=30)
response.raise_for_status()
print(response.json())
```

```go
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.
package main

import (
	"fmt"
	"io"
	"log"
	"net/http"
)

func main() {
	req, err := http.NewRequest(http.MethodGet, "https://depot.example.com/api/v1/clients/<cid>/usage?page=0&pageSize=0", nil)
	if err != nil {
		log.Fatal(err)
	}

	req.Header.Set("X-CSRF-Token", "<csrf-token>")
	req.Header.Set("Accept", "application/json")
	req.AddCookie(&http.Cookie{Name: "outpost_console", Value: "<console-session>"})

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()

	out, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(resp.Status, string(out))
}
```

```ts
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.
// The session cookie is set by signing in. A script cannot set it itself.

const url = new URL("https://depot.example.com/api/v1/clients/<cid>/usage");
url.searchParams.set("page", "0");
url.searchParams.set("pageSize", "0");

const response = await fetch(url, {
	method: "GET",
	headers: {
		"X-CSRF-Token": "<csrf-token>",
		"Accept": "application/json",
	},
	credentials: "include",
});

if (!response.ok) {
	throw new Error(`Request failed with status ${response.status}`);
}

const data = await response.json();
console.log(data);
```

```csharp
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.

using System;
using System.Net.Http;

using var client = new HttpClient();

var request = new HttpRequestMessage(HttpMethod.Get, "https://depot.example.com/api/v1/clients/<cid>/usage?page=0&pageSize=0");
request.Headers.TryAddWithoutValidation("X-CSRF-Token", "<csrf-token>");
request.Headers.TryAddWithoutValidation("Accept", "application/json");
request.Headers.TryAddWithoutValidation("Cookie", "outpost_console=<console-session>");

var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
Console.WriteLine(await response.Content.ReadAsStringAsync());
```

### GET `/api/v1/psa/agreements` {#depot.psa.listAllAgreements}

**List all agreements**

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `client` | query | string | No |  |
| `clientId` | query | string | No |  |
| `page` | query | integer | No | One-based page number. Defaults to 1. |
| `pageSize` | query | integer | No | Rows per page, 1 to 200. Defaults to 50. |
| `status` | query | string, one of active, draft, ended | No | Comma-separated set. An unrecognised value is refused. |

#### Responses

| Status | Body | Description |
| --- | --- | --- |
| `200` | `application/json`, PsaFanOutPagePracticeAgreementView | Success. |
| `400` | `application/problem+json`, ProblemDetails | One or more validation errors occurred. |
| `401` | `application/problem+json`, ProblemDetails | No signed-in identity. |
| `403` | `application/problem+json`, ProblemDetails | The identity does not hold psa:read. |
| `500` | `application/problem+json`, ProblemDetails | Could not list clients. |
| `503` | `application/problem+json`, ProblemDetails | Client billing is not available on this server. |

#### Request samples

```bash
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.
curl -X GET 'https://depot.example.com/api/v1/psa/agreements?client=<client>&clientId=<clientId>&page=0&pageSize=0&status=active' \
  -H 'X-CSRF-Token: <csrf-token>' \
  -H 'Accept: application/json' \
  -b 'outpost_console=<console-session>'
```

```powershell
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.

$headers = @{
    'X-CSRF-Token' = '<csrf-token>'
    'Accept' = 'application/json'
    'Cookie' = 'outpost_console=<console-session>'
}

$response = Invoke-RestMethod `
    -Method Get `
    -Uri 'https://depot.example.com/api/v1/psa/agreements?client=<client>&clientId=<clientId>&page=0&pageSize=0&status=active' `
    -Headers $headers

$response | ConvertTo-Json -Depth 10
```

```python
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.

import requests

url = "https://depot.example.com/api/v1/psa/agreements"
params = {
    "client": "<client>",
    "clientId": "<clientId>",
    "page": "0",
    "pageSize": "0",
    "status": "active",
}
headers = {
    "X-CSRF-Token": "<csrf-token>",
    "Accept": "application/json",
}
cookies = {
    "outpost_console": "<console-session>",
}

response = requests.get(url, params=params, headers=headers, cookies=cookies, timeout=30)
response.raise_for_status()
print(response.json())
```

```go
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.
package main

import (
	"fmt"
	"io"
	"log"
	"net/http"
)

func main() {
	req, err := http.NewRequest(http.MethodGet, "https://depot.example.com/api/v1/psa/agreements?client=<client>&clientId=<clientId>&page=0&pageSize=0&status=active", nil)
	if err != nil {
		log.Fatal(err)
	}

	req.Header.Set("X-CSRF-Token", "<csrf-token>")
	req.Header.Set("Accept", "application/json")
	req.AddCookie(&http.Cookie{Name: "outpost_console", Value: "<console-session>"})

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()

	out, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(resp.Status, string(out))
}
```

```ts
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.
// The session cookie is set by signing in. A script cannot set it itself.

const url = new URL("https://depot.example.com/api/v1/psa/agreements");
url.searchParams.set("client", "<client>");
url.searchParams.set("clientId", "<clientId>");
url.searchParams.set("page", "0");
url.searchParams.set("pageSize", "0");
url.searchParams.set("status", "active");

const response = await fetch(url, {
	method: "GET",
	headers: {
		"X-CSRF-Token": "<csrf-token>",
		"Accept": "application/json",
	},
	credentials: "include",
});

if (!response.ok) {
	throw new Error(`Request failed with status ${response.status}`);
}

const data = await response.json();
console.log(data);
```

```csharp
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.

using System;
using System.Net.Http;

using var client = new HttpClient();

var request = new HttpRequestMessage(HttpMethod.Get, "https://depot.example.com/api/v1/psa/agreements?client=<client>&clientId=<clientId>&page=0&pageSize=0&status=active");
request.Headers.TryAddWithoutValidation("X-CSRF-Token", "<csrf-token>");
request.Headers.TryAddWithoutValidation("Accept", "application/json");
request.Headers.TryAddWithoutValidation("Cookie", "outpost_console=<console-session>");

var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
Console.WriteLine(await response.Content.ReadAsStringAsync());
```

### GET `/api/v1/psa/ar-aging` {#depot.psa.arAging}

**arAging answers what is owed, bucketed by how far past due it is.**

THE TOTALS ARE SUMMED HERE AND NOT ASKED OF THE STORE. The buckets are
disjoint by construction, so the footer is the column sums of the rows the
screen is showing, and a separately-queried total is one that can disagree
with the table above it after a payment lands between the two reads.

#### Responses

| Status | Body | Description |
| --- | --- | --- |
| `200` | `application/json`, ArAgingView | Success. |
| `400` | `application/problem+json`, ProblemDetails | One or more validation errors occurred. |
| `401` | `application/problem+json`, ProblemDetails | No signed-in identity. |
| `403` | `application/problem+json`, ProblemDetails | The identity does not hold psa:read. |
| `404` | `application/problem+json`, ProblemDetails | No resource matches. |
| `500` | `application/problem+json`, ProblemDetails | The request failed. |
| `503` | `application/problem+json`, ProblemDetails | Client billing is not available on this server. |

#### Request samples

```bash
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.
curl -X GET 'https://depot.example.com/api/v1/psa/ar-aging' \
  -H 'X-CSRF-Token: <csrf-token>' \
  -H 'Accept: application/json' \
  -b 'outpost_console=<console-session>'
```

```powershell
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.

$headers = @{
    'X-CSRF-Token' = '<csrf-token>'
    'Accept' = 'application/json'
    'Cookie' = 'outpost_console=<console-session>'
}

$response = Invoke-RestMethod `
    -Method Get `
    -Uri 'https://depot.example.com/api/v1/psa/ar-aging' `
    -Headers $headers

$response | ConvertTo-Json -Depth 10
```

```python
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.

import requests

url = "https://depot.example.com/api/v1/psa/ar-aging"
headers = {
    "X-CSRF-Token": "<csrf-token>",
    "Accept": "application/json",
}
cookies = {
    "outpost_console": "<console-session>",
}

response = requests.get(url, headers=headers, cookies=cookies, timeout=30)
response.raise_for_status()
print(response.json())
```

```go
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.
package main

import (
	"fmt"
	"io"
	"log"
	"net/http"
)

func main() {
	req, err := http.NewRequest(http.MethodGet, "https://depot.example.com/api/v1/psa/ar-aging", nil)
	if err != nil {
		log.Fatal(err)
	}

	req.Header.Set("X-CSRF-Token", "<csrf-token>")
	req.Header.Set("Accept", "application/json")
	req.AddCookie(&http.Cookie{Name: "outpost_console", Value: "<console-session>"})

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()

	out, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(resp.Status, string(out))
}
```

```ts
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.
// The session cookie is set by signing in. A script cannot set it itself.

const url = "https://depot.example.com/api/v1/psa/ar-aging";

const response = await fetch(url, {
	method: "GET",
	headers: {
		"X-CSRF-Token": "<csrf-token>",
		"Accept": "application/json",
	},
	credentials: "include",
});

if (!response.ok) {
	throw new Error(`Request failed with status ${response.status}`);
}

const data = await response.json();
console.log(data);
```

```csharp
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.

using System;
using System.Net.Http;

using var client = new HttpClient();

var request = new HttpRequestMessage(HttpMethod.Get, "https://depot.example.com/api/v1/psa/ar-aging");
request.Headers.TryAddWithoutValidation("X-CSRF-Token", "<csrf-token>");
request.Headers.TryAddWithoutValidation("Accept", "application/json");
request.Headers.TryAddWithoutValidation("Cookie", "outpost_console=<console-session>");

var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
Console.WriteLine(await response.Content.ReadAsStringAsync());
```

### GET `/api/v1/psa/charge-types` {#depot.psa.listChargeTypes}

**List charge types**

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `page` | query | integer | No | One-based page number. Defaults to 1. |
| `pageSize` | query | integer | No | Rows per page, 1 to 200. Defaults to 50. |

#### Responses

| Status | Body | Description |
| --- | --- | --- |
| `200` | `application/json`, PagedResultChargeTypeView | Success. |
| `400` | `application/problem+json`, ProblemDetails | One or more validation errors occurred. |
| `401` | `application/problem+json`, ProblemDetails | No signed-in identity. |
| `403` | `application/problem+json`, ProblemDetails | The identity does not hold psa:read. |
| `404` | `application/problem+json`, ProblemDetails | No resource matches. |
| `500` | `application/problem+json`, ProblemDetails | The request failed. |
| `503` | `application/problem+json`, ProblemDetails | Client billing is not available on this server. |

#### Request samples

```bash
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.
curl -X GET 'https://depot.example.com/api/v1/psa/charge-types?page=0&pageSize=0' \
  -H 'X-CSRF-Token: <csrf-token>' \
  -H 'Accept: application/json' \
  -b 'outpost_console=<console-session>'
```

```powershell
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.

$headers = @{
    'X-CSRF-Token' = '<csrf-token>'
    'Accept' = 'application/json'
    'Cookie' = 'outpost_console=<console-session>'
}

$response = Invoke-RestMethod `
    -Method Get `
    -Uri 'https://depot.example.com/api/v1/psa/charge-types?page=0&pageSize=0' `
    -Headers $headers

$response | ConvertTo-Json -Depth 10
```

```python
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.

import requests

url = "https://depot.example.com/api/v1/psa/charge-types"
params = {
    "page": "0",
    "pageSize": "0",
}
headers = {
    "X-CSRF-Token": "<csrf-token>",
    "Accept": "application/json",
}
cookies = {
    "outpost_console": "<console-session>",
}

response = requests.get(url, params=params, headers=headers, cookies=cookies, timeout=30)
response.raise_for_status()
print(response.json())
```

```go
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.
package main

import (
	"fmt"
	"io"
	"log"
	"net/http"
)

func main() {
	req, err := http.NewRequest(http.MethodGet, "https://depot.example.com/api/v1/psa/charge-types?page=0&pageSize=0", nil)
	if err != nil {
		log.Fatal(err)
	}

	req.Header.Set("X-CSRF-Token", "<csrf-token>")
	req.Header.Set("Accept", "application/json")
	req.AddCookie(&http.Cookie{Name: "outpost_console", Value: "<console-session>"})

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()

	out, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(resp.Status, string(out))
}
```

```ts
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.
// The session cookie is set by signing in. A script cannot set it itself.

const url = new URL("https://depot.example.com/api/v1/psa/charge-types");
url.searchParams.set("page", "0");
url.searchParams.set("pageSize", "0");

const response = await fetch(url, {
	method: "GET",
	headers: {
		"X-CSRF-Token": "<csrf-token>",
		"Accept": "application/json",
	},
	credentials: "include",
});

if (!response.ok) {
	throw new Error(`Request failed with status ${response.status}`);
}

const data = await response.json();
console.log(data);
```

```csharp
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.

using System;
using System.Net.Http;

using var client = new HttpClient();

var request = new HttpRequestMessage(HttpMethod.Get, "https://depot.example.com/api/v1/psa/charge-types?page=0&pageSize=0");
request.Headers.TryAddWithoutValidation("X-CSRF-Token", "<csrf-token>");
request.Headers.TryAddWithoutValidation("Accept", "application/json");
request.Headers.TryAddWithoutValidation("Cookie", "outpost_console=<console-session>");

var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
Console.WriteLine(await response.Content.ReadAsStringAsync());
```

### POST `/api/v1/psa/charge-types` {#depot.psa.createChargeType}

**Create charge type**

#### Request body

Required, sent as `application/json`.

#### Responses

| Status | Body | Description |
| --- | --- | --- |
| `201` | `application/json`, ChargeTypeView | Created. |
| `400` | `application/problem+json`, ProblemDetails | The request body is not valid JSON. |
| `401` | `application/problem+json`, ProblemDetails | No signed-in identity. |
| `403` | `application/problem+json`, ProblemDetails | The identity does not hold psa:admin. |
| `404` | `application/problem+json`, ProblemDetails | No resource matches. |
| `500` | `application/problem+json`, ProblemDetails | The request failed. |
| `503` | `application/problem+json`, ProblemDetails | Client billing is not available on this server. |

#### Request samples

```bash
# Sign in first. outpost_console is a session cookie, so send it with every request.
curl -X POST 'https://depot.example.com/api/v1/psa/charge-types' \
  -H 'X-CSRF-Token: <csrf-token>' \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -b 'outpost_console=<console-session>' \
  --data '{
  "code": "string",
  "defaultRateCents": 0,
  "name": "string",
  "sortOrder": 0,
  "unit": "string"
}'
```

```powershell
# Sign in first. outpost_console is a session cookie, so send it with every request.

$headers = @{
    'X-CSRF-Token' = '<csrf-token>'
    'Accept' = 'application/json'
    'Content-Type' = 'application/json'
    'Cookie' = 'outpost_console=<console-session>'
}

$body = @'
{
  "code": "string",
  "defaultRateCents": 0,
  "name": "string",
  "sortOrder": 0,
  "unit": "string"
}
'@

$response = Invoke-RestMethod `
    -Method Post `
    -Uri 'https://depot.example.com/api/v1/psa/charge-types' `
    -Headers $headers `
    -ContentType 'application/json' `
    -Body $body

$response | ConvertTo-Json -Depth 10
```

```python
# Sign in first. outpost_console is a session cookie, so send it with every request.

import requests

url = "https://depot.example.com/api/v1/psa/charge-types"
headers = {
    "X-CSRF-Token": "<csrf-token>",
    "Accept": "application/json",
    "Content-Type": "application/json",
}
cookies = {
    "outpost_console": "<console-session>",
}
payload = {
    "code": "string",
    "defaultRateCents": 0,
    "name": "string",
    "sortOrder": 0,
    "unit": "string",
}

response = requests.post(url, headers=headers, cookies=cookies, json=payload, timeout=30)
response.raise_for_status()
print(response.json())
```

```go
// Sign in first. outpost_console is a session cookie, so send it with every request.
package main

import (
	"bytes"
	"fmt"
	"io"
	"log"
	"net/http"
)

func main() {
	body := []byte(`{
  "code": "string",
  "defaultRateCents": 0,
  "name": "string",
  "sortOrder": 0,
  "unit": "string"
}`)

	req, err := http.NewRequest(http.MethodPost, "https://depot.example.com/api/v1/psa/charge-types", bytes.NewReader(body))
	if err != nil {
		log.Fatal(err)
	}

	req.Header.Set("X-CSRF-Token", "<csrf-token>")
	req.Header.Set("Accept", "application/json")
	req.Header.Set("Content-Type", "application/json")
	req.AddCookie(&http.Cookie{Name: "outpost_console", Value: "<console-session>"})

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()

	out, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(resp.Status, string(out))
}
```

```ts
// Sign in first. outpost_console is a session cookie, so send it with every request.
// The session cookie is set by signing in. A script cannot set it itself.

const url = "https://depot.example.com/api/v1/psa/charge-types";

const response = await fetch(url, {
	method: "POST",
	headers: {
		"X-CSRF-Token": "<csrf-token>",
		"Accept": "application/json",
		"Content-Type": "application/json",
	},
	credentials: "include",
	body: JSON.stringify({
		"code": "string",
		"defaultRateCents": 0,
		"name": "string",
		"sortOrder": 0,
		"unit": "string"
	}),
});

if (!response.ok) {
	throw new Error(`Request failed with status ${response.status}`);
}

const data = await response.json();
console.log(data);
```

```csharp
// Sign in first. outpost_console is a session cookie, so send it with every request.

using System;
using System.Net.Http;
using System.Text;

using var client = new HttpClient();

const string body = """
{
  "code": "string",
  "defaultRateCents": 0,
  "name": "string",
  "sortOrder": 0,
  "unit": "string"
}
""";

var request = new HttpRequestMessage(HttpMethod.Post, "https://depot.example.com/api/v1/psa/charge-types");
request.Headers.TryAddWithoutValidation("X-CSRF-Token", "<csrf-token>");
request.Headers.TryAddWithoutValidation("Accept", "application/json");
request.Headers.TryAddWithoutValidation("Cookie", "outpost_console=<console-session>");
request.Content = new StringContent(body, Encoding.UTF8, "application/json");

var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
Console.WriteLine(await response.Content.ReadAsStringAsync());
```

### PATCH `/api/v1/psa/charge-types/{code}` {#depot.psa.patchChargeType}

**Patch charge type**

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `code` | path | string | Yes |  |

#### Request body

Required, sent as `application/json`.

#### Responses

| Status | Body | Description |
| --- | --- | --- |
| `200` | `application/json`, ChargeTypeView | Success. |
| `400` | `application/problem+json`, ProblemDetails | The request body is not valid JSON. |
| `401` | `application/problem+json`, ProblemDetails | No signed-in identity. |
| `403` | `application/problem+json`, ProblemDetails | The identity does not hold psa:admin. |
| `404` | `application/problem+json`, ProblemDetails | No charge type matches that code. |
| `500` | `application/problem+json`, ProblemDetails | The request failed. |
| `503` | `application/problem+json`, ProblemDetails | Client billing is not available on this server. |

#### Request samples

```bash
# Sign in first. outpost_console is a session cookie, so send it with every request.
curl -X PATCH 'https://depot.example.com/api/v1/psa/charge-types/<code>' \
  -H 'X-CSRF-Token: <csrf-token>' \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -b 'outpost_console=<console-session>' \
  --data '{
  "active": false,
  "defaultRateCents": 0,
  "name": "string",
  "sortOrder": 0,
  "unit": "string"
}'
```

```powershell
# Sign in first. outpost_console is a session cookie, so send it with every request.

$headers = @{
    'X-CSRF-Token' = '<csrf-token>'
    'Accept' = 'application/json'
    'Content-Type' = 'application/json'
    'Cookie' = 'outpost_console=<console-session>'
}

$body = @'
{
  "active": false,
  "defaultRateCents": 0,
  "name": "string",
  "sortOrder": 0,
  "unit": "string"
}
'@

$response = Invoke-RestMethod `
    -Method Patch `
    -Uri 'https://depot.example.com/api/v1/psa/charge-types/<code>' `
    -Headers $headers `
    -ContentType 'application/json' `
    -Body $body

$response | ConvertTo-Json -Depth 10
```

```python
# Sign in first. outpost_console is a session cookie, so send it with every request.

import requests

url = "https://depot.example.com/api/v1/psa/charge-types/<code>"
headers = {
    "X-CSRF-Token": "<csrf-token>",
    "Accept": "application/json",
    "Content-Type": "application/json",
}
cookies = {
    "outpost_console": "<console-session>",
}
payload = {
    "active": False,
    "defaultRateCents": 0,
    "name": "string",
    "sortOrder": 0,
    "unit": "string",
}

response = requests.patch(url, headers=headers, cookies=cookies, json=payload, timeout=30)
response.raise_for_status()
print(response.json())
```

```go
// Sign in first. outpost_console is a session cookie, so send it with every request.
package main

import (
	"bytes"
	"fmt"
	"io"
	"log"
	"net/http"
)

func main() {
	body := []byte(`{
  "active": false,
  "defaultRateCents": 0,
  "name": "string",
  "sortOrder": 0,
  "unit": "string"
}`)

	req, err := http.NewRequest(http.MethodPatch, "https://depot.example.com/api/v1/psa/charge-types/<code>", bytes.NewReader(body))
	if err != nil {
		log.Fatal(err)
	}

	req.Header.Set("X-CSRF-Token", "<csrf-token>")
	req.Header.Set("Accept", "application/json")
	req.Header.Set("Content-Type", "application/json")
	req.AddCookie(&http.Cookie{Name: "outpost_console", Value: "<console-session>"})

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()

	out, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(resp.Status, string(out))
}
```

```ts
// Sign in first. outpost_console is a session cookie, so send it with every request.
// The session cookie is set by signing in. A script cannot set it itself.

const url = "https://depot.example.com/api/v1/psa/charge-types/<code>";

const response = await fetch(url, {
	method: "PATCH",
	headers: {
		"X-CSRF-Token": "<csrf-token>",
		"Accept": "application/json",
		"Content-Type": "application/json",
	},
	credentials: "include",
	body: JSON.stringify({
		"active": false,
		"defaultRateCents": 0,
		"name": "string",
		"sortOrder": 0,
		"unit": "string"
	}),
});

if (!response.ok) {
	throw new Error(`Request failed with status ${response.status}`);
}

const data = await response.json();
console.log(data);
```

```csharp
// Sign in first. outpost_console is a session cookie, so send it with every request.

using System;
using System.Net.Http;
using System.Text;

using var client = new HttpClient();

const string body = """
{
  "active": false,
  "defaultRateCents": 0,
  "name": "string",
  "sortOrder": 0,
  "unit": "string"
}
""";

var request = new HttpRequestMessage(HttpMethod.Patch, "https://depot.example.com/api/v1/psa/charge-types/<code>");
request.Headers.TryAddWithoutValidation("X-CSRF-Token", "<csrf-token>");
request.Headers.TryAddWithoutValidation("Accept", "application/json");
request.Headers.TryAddWithoutValidation("Cookie", "outpost_console=<console-session>");
request.Content = new StringContent(body, Encoding.UTF8, "application/json");

var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
Console.WriteLine(await response.Content.ReadAsStringAsync());
```

### DELETE `/api/v1/psa/charge-types/{code}` {#depot.psa.deleteChargeType}

**deleteChargeType RETIRES a charge type.**

There is no delete, deliberately: a code on a historical time entry has to keep resolving, so the row stays and Active goes false. The verb is DELETE because that is what the console's button means, and the answer is the 204 a delete gives.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `code` | path | string | Yes |  |

#### Responses

| Status | Body | Description |
| --- | --- | --- |
| `204` | None | Success, with no body. |
| `400` | `application/problem+json`, ProblemDetails | One or more validation errors occurred. |
| `401` | `application/problem+json`, ProblemDetails | No signed-in identity. |
| `403` | `application/problem+json`, ProblemDetails | The identity does not hold psa:admin. |
| `404` | `application/problem+json`, ProblemDetails | No resource matches. |
| `500` | `application/problem+json`, ProblemDetails | The request failed. |
| `503` | `application/problem+json`, ProblemDetails | Client billing is not available on this server. |

#### Request samples

```bash
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.
curl -X DELETE 'https://depot.example.com/api/v1/psa/charge-types/<code>' \
  -H 'X-CSRF-Token: <csrf-token>' \
  -H 'Accept: application/json' \
  -b 'outpost_console=<console-session>'
```

```powershell
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.

$headers = @{
    'X-CSRF-Token' = '<csrf-token>'
    'Accept' = 'application/json'
    'Cookie' = 'outpost_console=<console-session>'
}

$response = Invoke-RestMethod `
    -Method Delete `
    -Uri 'https://depot.example.com/api/v1/psa/charge-types/<code>' `
    -Headers $headers

$response | ConvertTo-Json -Depth 10
```

```python
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.

import requests

url = "https://depot.example.com/api/v1/psa/charge-types/<code>"
headers = {
    "X-CSRF-Token": "<csrf-token>",
    "Accept": "application/json",
}
cookies = {
    "outpost_console": "<console-session>",
}

response = requests.delete(url, headers=headers, cookies=cookies, timeout=30)
response.raise_for_status()
print(response.json())
```

```go
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.
package main

import (
	"fmt"
	"io"
	"log"
	"net/http"
)

func main() {
	req, err := http.NewRequest(http.MethodDelete, "https://depot.example.com/api/v1/psa/charge-types/<code>", nil)
	if err != nil {
		log.Fatal(err)
	}

	req.Header.Set("X-CSRF-Token", "<csrf-token>")
	req.Header.Set("Accept", "application/json")
	req.AddCookie(&http.Cookie{Name: "outpost_console", Value: "<console-session>"})

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()

	out, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(resp.Status, string(out))
}
```

```ts
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.
// The session cookie is set by signing in. A script cannot set it itself.

const url = "https://depot.example.com/api/v1/psa/charge-types/<code>";

const response = await fetch(url, {
	method: "DELETE",
	headers: {
		"X-CSRF-Token": "<csrf-token>",
		"Accept": "application/json",
	},
	credentials: "include",
});

if (!response.ok) {
	throw new Error(`Request failed with status ${response.status}`);
}

const data = await response.json();
console.log(data);
```

```csharp
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.

using System;
using System.Net.Http;

using var client = new HttpClient();

var request = new HttpRequestMessage(HttpMethod.Delete, "https://depot.example.com/api/v1/psa/charge-types/<code>");
request.Headers.TryAddWithoutValidation("X-CSRF-Token", "<csrf-token>");
request.Headers.TryAddWithoutValidation("Accept", "application/json");
request.Headers.TryAddWithoutValidation("Cookie", "outpost_console=<console-session>");

var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
Console.WriteLine(await response.Content.ReadAsStringAsync());
```

### GET `/api/v1/psa/export.csv` {#depot.psa.exportCsv}

**exportCSV writes the finalized, non-void invoices of a window in the named accounting package's own column order.**

psa:admin and not psa:read, deliberately: one file carries every customer's
name, address, invoice number and amount, which is a different disclosure from
reading one invoice on a screen.

The body is BUILT IN MEMORY BEFORE ANYTHING IS WRITTEN, so a failure halfway
through becomes a problem document rather than a truncated CSV with a 200 on
it. A half-written file imported into an accounting package is worse than no
file, because nothing about it looks wrong.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `format` | query | string | No |  |

#### Responses

| Status | Body | Description |
| --- | --- | --- |
| `200` | `application/octet-stream`, string (binary) | Success. The body shape was not resolved; see x-outpost-unresolved. |
| `400` | `application/problem+json`, ProblemDetails | One or more validation errors occurred. |
| `401` | `application/problem+json`, ProblemDetails | No signed-in identity. |
| `403` | `application/problem+json`, ProblemDetails | The identity does not hold psa:admin. |
| `404` | `application/problem+json`, ProblemDetails | No resource matches. |
| `500` | `application/problem+json`, ProblemDetails | The request failed. |
| `503` | `application/problem+json`, ProblemDetails | Client billing is not available on this server. |

#### Request samples

```bash
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.
curl -X GET 'https://depot.example.com/api/v1/psa/export.csv?format=<format>' \
  -H 'X-CSRF-Token: <csrf-token>' \
  -H 'Accept: application/json' \
  -b 'outpost_console=<console-session>'
```

```powershell
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.

$headers = @{
    'X-CSRF-Token' = '<csrf-token>'
    'Accept' = 'application/json'
    'Cookie' = 'outpost_console=<console-session>'
}

$response = Invoke-RestMethod `
    -Method Get `
    -Uri 'https://depot.example.com/api/v1/psa/export.csv?format=<format>' `
    -Headers $headers

$response | ConvertTo-Json -Depth 10
```

```python
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.

import requests

url = "https://depot.example.com/api/v1/psa/export.csv"
params = {
    "format": "<format>",
}
headers = {
    "X-CSRF-Token": "<csrf-token>",
    "Accept": "application/json",
}
cookies = {
    "outpost_console": "<console-session>",
}

response = requests.get(url, params=params, headers=headers, cookies=cookies, timeout=30)
response.raise_for_status()
print(response.json())
```

```go
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.
package main

import (
	"fmt"
	"io"
	"log"
	"net/http"
)

func main() {
	req, err := http.NewRequest(http.MethodGet, "https://depot.example.com/api/v1/psa/export.csv?format=<format>", nil)
	if err != nil {
		log.Fatal(err)
	}

	req.Header.Set("X-CSRF-Token", "<csrf-token>")
	req.Header.Set("Accept", "application/json")
	req.AddCookie(&http.Cookie{Name: "outpost_console", Value: "<console-session>"})

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()

	out, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(resp.Status, string(out))
}
```

```ts
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.
// The session cookie is set by signing in. A script cannot set it itself.

const url = new URL("https://depot.example.com/api/v1/psa/export.csv");
url.searchParams.set("format", "<format>");

const response = await fetch(url, {
	method: "GET",
	headers: {
		"X-CSRF-Token": "<csrf-token>",
		"Accept": "application/json",
	},
	credentials: "include",
});

if (!response.ok) {
	throw new Error(`Request failed with status ${response.status}`);
}

const data = await response.json();
console.log(data);
```

```csharp
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.

using System;
using System.Net.Http;

using var client = new HttpClient();

var request = new HttpRequestMessage(HttpMethod.Get, "https://depot.example.com/api/v1/psa/export.csv?format=<format>");
request.Headers.TryAddWithoutValidation("X-CSRF-Token", "<csrf-token>");
request.Headers.TryAddWithoutValidation("Accept", "application/json");
request.Headers.TryAddWithoutValidation("Cookie", "outpost_console=<console-session>");

var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
Console.WriteLine(await response.Content.ReadAsStringAsync());
```

### GET `/api/v1/psa/invoices` {#depot.psa.listInvoices}

**List invoices**

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `client` | query | string | No |  |
| `page` | query | integer | No | One-based page number. Defaults to 1. |
| `pageSize` | query | integer | No | Rows per page, 1 to 200. Defaults to 50. |
| `q` | query | string | No | Free-text search term. |
| `sort` | query | string, one of balance, due, issued, number, total | No | Sort key. |
| `status` | query | string, one of draft, issued, paid, partial, past_due, sent, void | No | Comma-separated set. An unrecognised value is refused. |

#### Responses

| Status | Body | Description |
| --- | --- | --- |
| `200` | `application/json`, PagedResultInvoiceSummaryView | Success. |
| `400` | `application/problem+json`, ProblemDetails | One or more validation errors occurred. |
| `401` | `application/problem+json`, ProblemDetails | No signed-in identity. |
| `403` | `application/problem+json`, ProblemDetails | The identity does not hold psa:read. |
| `404` | `application/problem+json`, ProblemDetails | No resource matches. |
| `500` | `application/problem+json`, ProblemDetails | The request failed. |
| `503` | `application/problem+json`, ProblemDetails | Client billing is not available on this server. |

#### Request samples

```bash
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.
curl -X GET 'https://depot.example.com/api/v1/psa/invoices?client=<client>&page=0&pageSize=0&q=<q>&sort=balance&status=draft' \
  -H 'X-CSRF-Token: <csrf-token>' \
  -H 'Accept: application/json' \
  -b 'outpost_console=<console-session>'
```

```powershell
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.

$headers = @{
    'X-CSRF-Token' = '<csrf-token>'
    'Accept' = 'application/json'
    'Cookie' = 'outpost_console=<console-session>'
}

$response = Invoke-RestMethod `
    -Method Get `
    -Uri 'https://depot.example.com/api/v1/psa/invoices?client=<client>&page=0&pageSize=0&q=<q>&sort=balance&status=draft' `
    -Headers $headers

$response | ConvertTo-Json -Depth 10
```

```python
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.

import requests

url = "https://depot.example.com/api/v1/psa/invoices"
params = {
    "client": "<client>",
    "page": "0",
    "pageSize": "0",
    "q": "<q>",
    "sort": "balance",
    "status": "draft",
}
headers = {
    "X-CSRF-Token": "<csrf-token>",
    "Accept": "application/json",
}
cookies = {
    "outpost_console": "<console-session>",
}

response = requests.get(url, params=params, headers=headers, cookies=cookies, timeout=30)
response.raise_for_status()
print(response.json())
```

```go
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.
package main

import (
	"fmt"
	"io"
	"log"
	"net/http"
)

func main() {
	req, err := http.NewRequest(http.MethodGet, "https://depot.example.com/api/v1/psa/invoices?client=<client>&page=0&pageSize=0&q=<q>&sort=balance&status=draft", nil)
	if err != nil {
		log.Fatal(err)
	}

	req.Header.Set("X-CSRF-Token", "<csrf-token>")
	req.Header.Set("Accept", "application/json")
	req.AddCookie(&http.Cookie{Name: "outpost_console", Value: "<console-session>"})

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()

	out, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(resp.Status, string(out))
}
```

```ts
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.
// The session cookie is set by signing in. A script cannot set it itself.

const url = new URL("https://depot.example.com/api/v1/psa/invoices");
url.searchParams.set("client", "<client>");
url.searchParams.set("page", "0");
url.searchParams.set("pageSize", "0");
url.searchParams.set("q", "<q>");
url.searchParams.set("sort", "balance");
url.searchParams.set("status", "draft");

const response = await fetch(url, {
	method: "GET",
	headers: {
		"X-CSRF-Token": "<csrf-token>",
		"Accept": "application/json",
	},
	credentials: "include",
});

if (!response.ok) {
	throw new Error(`Request failed with status ${response.status}`);
}

const data = await response.json();
console.log(data);
```

```csharp
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.

using System;
using System.Net.Http;

using var client = new HttpClient();

var request = new HttpRequestMessage(HttpMethod.Get, "https://depot.example.com/api/v1/psa/invoices?client=<client>&page=0&pageSize=0&q=<q>&sort=balance&status=draft");
request.Headers.TryAddWithoutValidation("X-CSRF-Token", "<csrf-token>");
request.Headers.TryAddWithoutValidation("Accept", "application/json");
request.Headers.TryAddWithoutValidation("Cookie", "outpost_console=<console-session>");

var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
Console.WriteLine(await response.Content.ReadAsStringAsync());
```

### POST `/api/v1/psa/invoices` {#depot.psa.createInvoice}

**createInvoice raises a DRAFT invoice for something that is not on a contract.**

TWO CALLERS AND ONE OF THEM IS A WORKER. The console gets an "invoice
something that is not on an agreement" it did not have, and the quote
conversion worker needs it for an accepted quote's one-time lines: acceptance
creates a draft AGREEMENT for the recurring half and a draft INVOICE for the
rest, and without this route the second half would have no door.

IT NAMES NO AGREEMENT AND NO PERIOD, so CreateDraftInvoice writes no
BillingRunClaim row and nothing can collide with the month the invoice run is
billing. That is the whole reason this is safe beside a running worker, and it
is why the request type has no period fields to fill in by accident.

psa:invoice and not psa:write, matching finalize and send: the split in this
package is that whoever writes a contract is not automatically whoever bills
against it, and raising a document a customer will be asked to pay is billing.

#### Request body

Required, sent as `application/json`.

#### Responses

| Status | Body | Description |
| --- | --- | --- |
| `201` | `application/json`, InvoiceView | Created. |
| `400` | `application/problem+json`, ProblemDetails | The request body is not valid JSON. |
| `401` | `application/problem+json`, ProblemDetails | No signed-in identity. |
| `403` | `application/problem+json`, ProblemDetails | The identity does not hold psa:invoice. |
| `404` | `application/problem+json`, ProblemDetails | No resource matches. |
| `500` | `application/problem+json`, ProblemDetails | The request failed. |
| `503` | `application/problem+json`, ProblemDetails | Client billing is not available on this server. |

#### Request samples

```bash
# Sign in first. outpost_console is a session cookie, so send it with every request.
curl -X POST 'https://depot.example.com/api/v1/psa/invoices' \
  -H 'X-CSRF-Token: <csrf-token>' \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -b 'outpost_console=<console-session>' \
  --data '{
  "clientId": "string",
  "lines": [
    {
      "agreementLinePublicId": "string",
      "amountCents": 0,
      "changeOrderPublicId": "string",
      "description": "string",
      "kind": "string",
      "milestonePublicId": "string",
      "projectPublicId": "string",
      "publicId": "string",
      "quantity": "string",
      "quantitySnapshot": {
        "count": 0,
        "ids": [
          "string"
        ]
      },
      "sortOrder": 0,
      "timeEntryPublicId": "string",
      "unitAmountCents": 0
    }
  ],
  "notes": "string",
  "taxRateBasisPoints": 0
}'
```

```powershell
# Sign in first. outpost_console is a session cookie, so send it with every request.

$headers = @{
    'X-CSRF-Token' = '<csrf-token>'
    'Accept' = 'application/json'
    'Content-Type' = 'application/json'
    'Cookie' = 'outpost_console=<console-session>'
}

$body = @'
{
  "clientId": "string",
  "lines": [
    {
      "agreementLinePublicId": "string",
      "amountCents": 0,
      "changeOrderPublicId": "string",
      "description": "string",
      "kind": "string",
      "milestonePublicId": "string",
      "projectPublicId": "string",
      "publicId": "string",
      "quantity": "string",
      "quantitySnapshot": {
        "count": 0,
        "ids": [
          "string"
        ]
      },
      "sortOrder": 0,
      "timeEntryPublicId": "string",
      "unitAmountCents": 0
    }
  ],
  "notes": "string",
  "taxRateBasisPoints": 0
}
'@

$response = Invoke-RestMethod `
    -Method Post `
    -Uri 'https://depot.example.com/api/v1/psa/invoices' `
    -Headers $headers `
    -ContentType 'application/json' `
    -Body $body

$response | ConvertTo-Json -Depth 10
```

```python
# Sign in first. outpost_console is a session cookie, so send it with every request.

import requests

url = "https://depot.example.com/api/v1/psa/invoices"
headers = {
    "X-CSRF-Token": "<csrf-token>",
    "Accept": "application/json",
    "Content-Type": "application/json",
}
cookies = {
    "outpost_console": "<console-session>",
}
payload = {
    "clientId": "string",
    "lines": [
        {
            "agreementLinePublicId": "string",
            "amountCents": 0,
            "changeOrderPublicId": "string",
            "description": "string",
            "kind": "string",
            "milestonePublicId": "string",
            "projectPublicId": "string",
            "publicId": "string",
            "quantity": "string",
            "quantitySnapshot": {
                "count": 0,
                "ids": [
                    "string",
                ],
            },
            "sortOrder": 0,
            "timeEntryPublicId": "string",
            "unitAmountCents": 0,
        },
    ],
    "notes": "string",
    "taxRateBasisPoints": 0,
}

response = requests.post(url, headers=headers, cookies=cookies, json=payload, timeout=30)
response.raise_for_status()
print(response.json())
```

```go
// Sign in first. outpost_console is a session cookie, so send it with every request.
package main

import (
	"bytes"
	"fmt"
	"io"
	"log"
	"net/http"
)

func main() {
	body := []byte(`{
  "clientId": "string",
  "lines": [
    {
      "agreementLinePublicId": "string",
      "amountCents": 0,
      "changeOrderPublicId": "string",
      "description": "string",
      "kind": "string",
      "milestonePublicId": "string",
      "projectPublicId": "string",
      "publicId": "string",
      "quantity": "string",
      "quantitySnapshot": {
        "count": 0,
        "ids": [
          "string"
        ]
      },
      "sortOrder": 0,
      "timeEntryPublicId": "string",
      "unitAmountCents": 0
    }
  ],
  "notes": "string",
  "taxRateBasisPoints": 0
}`)

	req, err := http.NewRequest(http.MethodPost, "https://depot.example.com/api/v1/psa/invoices", bytes.NewReader(body))
	if err != nil {
		log.Fatal(err)
	}

	req.Header.Set("X-CSRF-Token", "<csrf-token>")
	req.Header.Set("Accept", "application/json")
	req.Header.Set("Content-Type", "application/json")
	req.AddCookie(&http.Cookie{Name: "outpost_console", Value: "<console-session>"})

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()

	out, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(resp.Status, string(out))
}
```

```ts
// Sign in first. outpost_console is a session cookie, so send it with every request.
// The session cookie is set by signing in. A script cannot set it itself.

const url = "https://depot.example.com/api/v1/psa/invoices";

const response = await fetch(url, {
	method: "POST",
	headers: {
		"X-CSRF-Token": "<csrf-token>",
		"Accept": "application/json",
		"Content-Type": "application/json",
	},
	credentials: "include",
	body: JSON.stringify({
		"clientId": "string",
		"lines": [
			{
				"agreementLinePublicId": "string",
				"amountCents": 0,
				"changeOrderPublicId": "string",
				"description": "string",
				"kind": "string",
				"milestonePublicId": "string",
				"projectPublicId": "string",
				"publicId": "string",
				"quantity": "string",
				"quantitySnapshot": {
					"count": 0,
					"ids": [
						"string"
					]
				},
				"sortOrder": 0,
				"timeEntryPublicId": "string",
				"unitAmountCents": 0
			}
		],
		"notes": "string",
		"taxRateBasisPoints": 0
	}),
});

if (!response.ok) {
	throw new Error(`Request failed with status ${response.status}`);
}

const data = await response.json();
console.log(data);
```

```csharp
// Sign in first. outpost_console is a session cookie, so send it with every request.

using System;
using System.Net.Http;
using System.Text;

using var client = new HttpClient();

const string body = """
{
  "clientId": "string",
  "lines": [
    {
      "agreementLinePublicId": "string",
      "amountCents": 0,
      "changeOrderPublicId": "string",
      "description": "string",
      "kind": "string",
      "milestonePublicId": "string",
      "projectPublicId": "string",
      "publicId": "string",
      "quantity": "string",
      "quantitySnapshot": {
        "count": 0,
        "ids": [
          "string"
        ]
      },
      "sortOrder": 0,
      "timeEntryPublicId": "string",
      "unitAmountCents": 0
    }
  ],
  "notes": "string",
  "taxRateBasisPoints": 0
}
""";

var request = new HttpRequestMessage(HttpMethod.Post, "https://depot.example.com/api/v1/psa/invoices");
request.Headers.TryAddWithoutValidation("X-CSRF-Token", "<csrf-token>");
request.Headers.TryAddWithoutValidation("Accept", "application/json");
request.Headers.TryAddWithoutValidation("Cookie", "outpost_console=<console-session>");
request.Content = new StringContent(body, Encoding.UTF8, "application/json");

var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
Console.WriteLine(await response.Content.ReadAsStringAsync());
```

### GET `/api/v1/psa/invoices/{invoiceID}` {#depot.psa.getInvoice}

**Get invoice**

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `invoiceID` | path | string | Yes | A uuid. |

#### Responses

| Status | Body | Description |
| --- | --- | --- |
| `200` | `application/json`, InvoiceView | Success. |
| `400` | `application/problem+json`, ProblemDetails | One or more validation errors occurred. |
| `401` | `application/problem+json`, ProblemDetails | No signed-in identity. |
| `403` | `application/problem+json`, ProblemDetails | The identity does not hold psa:read. |
| `404` | `application/problem+json`, ProblemDetails | No resource matches that id. |
| `500` | `application/problem+json`, ProblemDetails | The request failed. |
| `503` | `application/problem+json`, ProblemDetails | Client billing is not available on this server. |

#### Request samples

```bash
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.
curl -X GET 'https://depot.example.com/api/v1/psa/invoices/<invoiceID>' \
  -H 'X-CSRF-Token: <csrf-token>' \
  -H 'Accept: application/json' \
  -b 'outpost_console=<console-session>'
```

```powershell
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.

$headers = @{
    'X-CSRF-Token' = '<csrf-token>'
    'Accept' = 'application/json'
    'Cookie' = 'outpost_console=<console-session>'
}

$response = Invoke-RestMethod `
    -Method Get `
    -Uri 'https://depot.example.com/api/v1/psa/invoices/<invoiceID>' `
    -Headers $headers

$response | ConvertTo-Json -Depth 10
```

```python
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.

import requests

url = "https://depot.example.com/api/v1/psa/invoices/<invoiceID>"
headers = {
    "X-CSRF-Token": "<csrf-token>",
    "Accept": "application/json",
}
cookies = {
    "outpost_console": "<console-session>",
}

response = requests.get(url, headers=headers, cookies=cookies, timeout=30)
response.raise_for_status()
print(response.json())
```

```go
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.
package main

import (
	"fmt"
	"io"
	"log"
	"net/http"
)

func main() {
	req, err := http.NewRequest(http.MethodGet, "https://depot.example.com/api/v1/psa/invoices/<invoiceID>", nil)
	if err != nil {
		log.Fatal(err)
	}

	req.Header.Set("X-CSRF-Token", "<csrf-token>")
	req.Header.Set("Accept", "application/json")
	req.AddCookie(&http.Cookie{Name: "outpost_console", Value: "<console-session>"})

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()

	out, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(resp.Status, string(out))
}
```

```ts
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.
// The session cookie is set by signing in. A script cannot set it itself.

const url = "https://depot.example.com/api/v1/psa/invoices/<invoiceID>";

const response = await fetch(url, {
	method: "GET",
	headers: {
		"X-CSRF-Token": "<csrf-token>",
		"Accept": "application/json",
	},
	credentials: "include",
});

if (!response.ok) {
	throw new Error(`Request failed with status ${response.status}`);
}

const data = await response.json();
console.log(data);
```

```csharp
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.

using System;
using System.Net.Http;

using var client = new HttpClient();

var request = new HttpRequestMessage(HttpMethod.Get, "https://depot.example.com/api/v1/psa/invoices/<invoiceID>");
request.Headers.TryAddWithoutValidation("X-CSRF-Token", "<csrf-token>");
request.Headers.TryAddWithoutValidation("Accept", "application/json");
request.Headers.TryAddWithoutValidation("Cookie", "outpost_console=<console-session>");

var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
Console.WriteLine(await response.Content.ReadAsStringAsync());
```

### PATCH `/api/v1/psa/invoices/{invoiceID}` {#depot.psa.patchInvoice}

**patchInvoice edits a DRAFT and nothing else.**

The 409 past draft is the store's, decided inside the transaction that takes
the row FOR UPDATE, and NOT a status check here. A check here would race a
concurrent finalize and let an edit land on a document that had already been
numbered, rendered and sent.

A non-nil lines array REPLACES the whole set and the store recomputes
subtotal, tax and total from the result. Omitting it leaves the lines alone,
which is the difference between "I renamed the notes" and "this invoice has
no lines".

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `invoiceID` | path | string | Yes | A uuid. |

#### Request body

Required, sent as `application/json`.

#### Responses

| Status | Body | Description |
| --- | --- | --- |
| `200` | `application/json`, InvoiceView | Success. |
| `400` | `application/problem+json`, ProblemDetails | The request body is not valid JSON. |
| `401` | `application/problem+json`, ProblemDetails | No signed-in identity. |
| `403` | `application/problem+json`, ProblemDetails | The identity does not hold psa:write. |
| `404` | `application/problem+json`, ProblemDetails | No resource matches that id. |
| `500` | `application/problem+json`, ProblemDetails | The request failed. |
| `503` | `application/problem+json`, ProblemDetails | Client billing is not available on this server. |

#### Request samples

```bash
# Sign in first. outpost_console is a session cookie, so send it with every request.
curl -X PATCH 'https://depot.example.com/api/v1/psa/invoices/<invoiceID>' \
  -H 'X-CSRF-Token: <csrf-token>' \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -b 'outpost_console=<console-session>' \
  --data '{
  "lines": [
    {
      "agreementLinePublicId": "string",
      "amountCents": 0,
      "changeOrderPublicId": "string",
      "description": "string",
      "kind": "string",
      "milestonePublicId": "string",
      "projectPublicId": "string",
      "publicId": "string",
      "quantity": "string",
      "quantitySnapshot": {
        "count": 0,
        "ids": [
          "string"
        ]
      },
      "sortOrder": 0,
      "timeEntryPublicId": "string",
      "unitAmountCents": 0
    }
  ],
  "notes": "string",
  "taxRateBasisPoints": 0
}'
```

```powershell
# Sign in first. outpost_console is a session cookie, so send it with every request.

$headers = @{
    'X-CSRF-Token' = '<csrf-token>'
    'Accept' = 'application/json'
    'Content-Type' = 'application/json'
    'Cookie' = 'outpost_console=<console-session>'
}

$body = @'
{
  "lines": [
    {
      "agreementLinePublicId": "string",
      "amountCents": 0,
      "changeOrderPublicId": "string",
      "description": "string",
      "kind": "string",
      "milestonePublicId": "string",
      "projectPublicId": "string",
      "publicId": "string",
      "quantity": "string",
      "quantitySnapshot": {
        "count": 0,
        "ids": [
          "string"
        ]
      },
      "sortOrder": 0,
      "timeEntryPublicId": "string",
      "unitAmountCents": 0
    }
  ],
  "notes": "string",
  "taxRateBasisPoints": 0
}
'@

$response = Invoke-RestMethod `
    -Method Patch `
    -Uri 'https://depot.example.com/api/v1/psa/invoices/<invoiceID>' `
    -Headers $headers `
    -ContentType 'application/json' `
    -Body $body

$response | ConvertTo-Json -Depth 10
```

```python
# Sign in first. outpost_console is a session cookie, so send it with every request.

import requests

url = "https://depot.example.com/api/v1/psa/invoices/<invoiceID>"
headers = {
    "X-CSRF-Token": "<csrf-token>",
    "Accept": "application/json",
    "Content-Type": "application/json",
}
cookies = {
    "outpost_console": "<console-session>",
}
payload = {
    "lines": [
        {
            "agreementLinePublicId": "string",
            "amountCents": 0,
            "changeOrderPublicId": "string",
            "description": "string",
            "kind": "string",
            "milestonePublicId": "string",
            "projectPublicId": "string",
            "publicId": "string",
            "quantity": "string",
            "quantitySnapshot": {
                "count": 0,
                "ids": [
                    "string",
                ],
            },
            "sortOrder": 0,
            "timeEntryPublicId": "string",
            "unitAmountCents": 0,
        },
    ],
    "notes": "string",
    "taxRateBasisPoints": 0,
}

response = requests.patch(url, headers=headers, cookies=cookies, json=payload, timeout=30)
response.raise_for_status()
print(response.json())
```

```go
// Sign in first. outpost_console is a session cookie, so send it with every request.
package main

import (
	"bytes"
	"fmt"
	"io"
	"log"
	"net/http"
)

func main() {
	body := []byte(`{
  "lines": [
    {
      "agreementLinePublicId": "string",
      "amountCents": 0,
      "changeOrderPublicId": "string",
      "description": "string",
      "kind": "string",
      "milestonePublicId": "string",
      "projectPublicId": "string",
      "publicId": "string",
      "quantity": "string",
      "quantitySnapshot": {
        "count": 0,
        "ids": [
          "string"
        ]
      },
      "sortOrder": 0,
      "timeEntryPublicId": "string",
      "unitAmountCents": 0
    }
  ],
  "notes": "string",
  "taxRateBasisPoints": 0
}`)

	req, err := http.NewRequest(http.MethodPatch, "https://depot.example.com/api/v1/psa/invoices/<invoiceID>", bytes.NewReader(body))
	if err != nil {
		log.Fatal(err)
	}

	req.Header.Set("X-CSRF-Token", "<csrf-token>")
	req.Header.Set("Accept", "application/json")
	req.Header.Set("Content-Type", "application/json")
	req.AddCookie(&http.Cookie{Name: "outpost_console", Value: "<console-session>"})

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()

	out, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(resp.Status, string(out))
}
```

```ts
// Sign in first. outpost_console is a session cookie, so send it with every request.
// The session cookie is set by signing in. A script cannot set it itself.

const url = "https://depot.example.com/api/v1/psa/invoices/<invoiceID>";

const response = await fetch(url, {
	method: "PATCH",
	headers: {
		"X-CSRF-Token": "<csrf-token>",
		"Accept": "application/json",
		"Content-Type": "application/json",
	},
	credentials: "include",
	body: JSON.stringify({
		"lines": [
			{
				"agreementLinePublicId": "string",
				"amountCents": 0,
				"changeOrderPublicId": "string",
				"description": "string",
				"kind": "string",
				"milestonePublicId": "string",
				"projectPublicId": "string",
				"publicId": "string",
				"quantity": "string",
				"quantitySnapshot": {
					"count": 0,
					"ids": [
						"string"
					]
				},
				"sortOrder": 0,
				"timeEntryPublicId": "string",
				"unitAmountCents": 0
			}
		],
		"notes": "string",
		"taxRateBasisPoints": 0
	}),
});

if (!response.ok) {
	throw new Error(`Request failed with status ${response.status}`);
}

const data = await response.json();
console.log(data);
```

```csharp
// Sign in first. outpost_console is a session cookie, so send it with every request.

using System;
using System.Net.Http;
using System.Text;

using var client = new HttpClient();

const string body = """
{
  "lines": [
    {
      "agreementLinePublicId": "string",
      "amountCents": 0,
      "changeOrderPublicId": "string",
      "description": "string",
      "kind": "string",
      "milestonePublicId": "string",
      "projectPublicId": "string",
      "publicId": "string",
      "quantity": "string",
      "quantitySnapshot": {
        "count": 0,
        "ids": [
          "string"
        ]
      },
      "sortOrder": 0,
      "timeEntryPublicId": "string",
      "unitAmountCents": 0
    }
  ],
  "notes": "string",
  "taxRateBasisPoints": 0
}
""";

var request = new HttpRequestMessage(HttpMethod.Patch, "https://depot.example.com/api/v1/psa/invoices/<invoiceID>");
request.Headers.TryAddWithoutValidation("X-CSRF-Token", "<csrf-token>");
request.Headers.TryAddWithoutValidation("Accept", "application/json");
request.Headers.TryAddWithoutValidation("Cookie", "outpost_console=<console-session>");
request.Content = new StringContent(body, Encoding.UTF8, "application/json");

var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
Console.WriteLine(await response.Content.ReadAsStringAsync());
```

### DELETE `/api/v1/psa/invoices/{invoiceID}` {#depot.psa.deleteInvoice}

**deleteInvoice discards a DRAFT and gives back everything it was holding.**

DELETE IS FOR A DRAFT AND VOID IS FOR A DOCUMENT, and they are not two spellings
of one act. A draft never reached a customer, has no number and has no rendered
PDF, so there is nothing for an auditor to find a gap in and the row goes. A
numbered invoice is a record: it keeps its row and its number forever, and the
store answers ErrConflict here to say so.

AFTER THE CONTROL-DATABASE COMMIT, and only after it, the client database is
put back, on the same two-commit shape as void:

  - DetachTimeEntriesFromDraft, not UnmarkTimeEntriesInvoiced. A draft stamps
    InvoicePublicID and never InvoicedAt, so detach is the method written for
    exactly this case; unmark would additionally clear an InvoicedAt that a
    draft's entries cannot have, and reaching for it here would suggest they
    can. Entries whose InvoicedAt IS set are left alone by both.
  - ReversePrepaidForInvoice, because building the draft wrote purchase and
    drawdown rows against it. Leaving those behind would hand the customer
    minutes an invoice nobody ever sent had paid for.

THE PERIOD FREES ITSELF. Nothing here touches BillingRunClaim and nothing
needs to: the claim's upsert in CreateDraftInvoice reclaims any row whose
InvoicePublicId names no live invoice, and after this the invoice is gone
entirely, so the next generate-now for the same period takes the claim and
passes UQ_Invoice_AgreementPeriod. The claim was never the real protection.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `invoiceID` | path | string | Yes | A uuid. |

#### Responses

| Status | Body | Description |
| --- | --- | --- |
| `204` | None | Success, with no body. |
| `400` | `application/problem+json`, ProblemDetails | One or more validation errors occurred. |
| `401` | `application/problem+json`, ProblemDetails | No signed-in identity. |
| `403` | `application/problem+json`, ProblemDetails | The identity does not hold psa:write. |
| `404` | `application/problem+json`, ProblemDetails | No resource matches that id. |
| `500` | `application/problem+json`, ProblemDetails | The request failed. |
| `503` | `application/problem+json`, ProblemDetails | Client billing is not available on this server. |

#### Request samples

```bash
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.
curl -X DELETE 'https://depot.example.com/api/v1/psa/invoices/<invoiceID>' \
  -H 'X-CSRF-Token: <csrf-token>' \
  -H 'Accept: application/json' \
  -b 'outpost_console=<console-session>'
```

```powershell
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.

$headers = @{
    'X-CSRF-Token' = '<csrf-token>'
    'Accept' = 'application/json'
    'Cookie' = 'outpost_console=<console-session>'
}

$response = Invoke-RestMethod `
    -Method Delete `
    -Uri 'https://depot.example.com/api/v1/psa/invoices/<invoiceID>' `
    -Headers $headers

$response | ConvertTo-Json -Depth 10
```

```python
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.

import requests

url = "https://depot.example.com/api/v1/psa/invoices/<invoiceID>"
headers = {
    "X-CSRF-Token": "<csrf-token>",
    "Accept": "application/json",
}
cookies = {
    "outpost_console": "<console-session>",
}

response = requests.delete(url, headers=headers, cookies=cookies, timeout=30)
response.raise_for_status()
print(response.json())
```

```go
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.
package main

import (
	"fmt"
	"io"
	"log"
	"net/http"
)

func main() {
	req, err := http.NewRequest(http.MethodDelete, "https://depot.example.com/api/v1/psa/invoices/<invoiceID>", nil)
	if err != nil {
		log.Fatal(err)
	}

	req.Header.Set("X-CSRF-Token", "<csrf-token>")
	req.Header.Set("Accept", "application/json")
	req.AddCookie(&http.Cookie{Name: "outpost_console", Value: "<console-session>"})

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()

	out, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(resp.Status, string(out))
}
```

```ts
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.
// The session cookie is set by signing in. A script cannot set it itself.

const url = "https://depot.example.com/api/v1/psa/invoices/<invoiceID>";

const response = await fetch(url, {
	method: "DELETE",
	headers: {
		"X-CSRF-Token": "<csrf-token>",
		"Accept": "application/json",
	},
	credentials: "include",
});

if (!response.ok) {
	throw new Error(`Request failed with status ${response.status}`);
}

const data = await response.json();
console.log(data);
```

```csharp
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.

using System;
using System.Net.Http;

using var client = new HttpClient();

var request = new HttpRequestMessage(HttpMethod.Delete, "https://depot.example.com/api/v1/psa/invoices/<invoiceID>");
request.Headers.TryAddWithoutValidation("X-CSRF-Token", "<csrf-token>");
request.Headers.TryAddWithoutValidation("Accept", "application/json");
request.Headers.TryAddWithoutValidation("Cookie", "outpost_console=<console-session>");

var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
Console.WriteLine(await response.Content.ReadAsStringAsync());
```

### POST `/api/v1/psa/invoices/{invoiceID}/credit-notes` {#depot.psa.createCreditNote}

**createCreditNote reduces what a customer owes, with a number of its own.**

amountCents IS THE TOTAL CREDITED, which is what the balance goes down by and
what the operator typed into a box beside a balance. It is split into a
subtotal and a tax component at the invoice's OWN snapshotted rate, so the
credit reconciles against the document it reduces line for line; the split is
exact by construction, since the tax is the remainder rather than a second
rounding.

The store refuses a credit larger than the outstanding balance with
ErrConflict: crediting more than is owed is a refund, which this slice does
not have.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `invoiceID` | path | string | Yes | A uuid. |

#### Request body

Required, sent as `application/json`.

#### Responses

| Status | Body | Description |
| --- | --- | --- |
| `201` | `application/json`, CreditNoteView | Created. |
| `400` | `application/problem+json`, ProblemDetails | The request body is not valid JSON. |
| `401` | `application/problem+json`, ProblemDetails | No signed-in identity. |
| `403` | `application/problem+json`, ProblemDetails | The identity does not hold psa:invoice. |
| `404` | `application/problem+json`, ProblemDetails | No resource matches that id. |
| `500` | `application/problem+json`, ProblemDetails | The request failed. |
| `503` | `application/problem+json`, ProblemDetails | Client billing is not available on this server. |

#### Request samples

```bash
# Sign in first. outpost_console is a session cookie, so send it with every request.
curl -X POST 'https://depot.example.com/api/v1/psa/invoices/<invoiceID>/credit-notes' \
  -H 'X-CSRF-Token: <csrf-token>' \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -b 'outpost_console=<console-session>' \
  --data '{
  "amountCents": 0,
  "kind": "string",
  "reason": "string"
}'
```

```powershell
# Sign in first. outpost_console is a session cookie, so send it with every request.

$headers = @{
    'X-CSRF-Token' = '<csrf-token>'
    'Accept' = 'application/json'
    'Content-Type' = 'application/json'
    'Cookie' = 'outpost_console=<console-session>'
}

$body = @'
{
  "amountCents": 0,
  "kind": "string",
  "reason": "string"
}
'@

$response = Invoke-RestMethod `
    -Method Post `
    -Uri 'https://depot.example.com/api/v1/psa/invoices/<invoiceID>/credit-notes' `
    -Headers $headers `
    -ContentType 'application/json' `
    -Body $body

$response | ConvertTo-Json -Depth 10
```

```python
# Sign in first. outpost_console is a session cookie, so send it with every request.

import requests

url = "https://depot.example.com/api/v1/psa/invoices/<invoiceID>/credit-notes"
headers = {
    "X-CSRF-Token": "<csrf-token>",
    "Accept": "application/json",
    "Content-Type": "application/json",
}
cookies = {
    "outpost_console": "<console-session>",
}
payload = {
    "amountCents": 0,
    "kind": "string",
    "reason": "string",
}

response = requests.post(url, headers=headers, cookies=cookies, json=payload, timeout=30)
response.raise_for_status()
print(response.json())
```

```go
// Sign in first. outpost_console is a session cookie, so send it with every request.
package main

import (
	"bytes"
	"fmt"
	"io"
	"log"
	"net/http"
)

func main() {
	body := []byte(`{
  "amountCents": 0,
  "kind": "string",
  "reason": "string"
}`)

	req, err := http.NewRequest(http.MethodPost, "https://depot.example.com/api/v1/psa/invoices/<invoiceID>/credit-notes", bytes.NewReader(body))
	if err != nil {
		log.Fatal(err)
	}

	req.Header.Set("X-CSRF-Token", "<csrf-token>")
	req.Header.Set("Accept", "application/json")
	req.Header.Set("Content-Type", "application/json")
	req.AddCookie(&http.Cookie{Name: "outpost_console", Value: "<console-session>"})

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()

	out, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(resp.Status, string(out))
}
```

```ts
// Sign in first. outpost_console is a session cookie, so send it with every request.
// The session cookie is set by signing in. A script cannot set it itself.

const url = "https://depot.example.com/api/v1/psa/invoices/<invoiceID>/credit-notes";

const response = await fetch(url, {
	method: "POST",
	headers: {
		"X-CSRF-Token": "<csrf-token>",
		"Accept": "application/json",
		"Content-Type": "application/json",
	},
	credentials: "include",
	body: JSON.stringify({
		"amountCents": 0,
		"kind": "string",
		"reason": "string"
	}),
});

if (!response.ok) {
	throw new Error(`Request failed with status ${response.status}`);
}

const data = await response.json();
console.log(data);
```

```csharp
// Sign in first. outpost_console is a session cookie, so send it with every request.

using System;
using System.Net.Http;
using System.Text;

using var client = new HttpClient();

const string body = """
{
  "amountCents": 0,
  "kind": "string",
  "reason": "string"
}
""";

var request = new HttpRequestMessage(HttpMethod.Post, "https://depot.example.com/api/v1/psa/invoices/<invoiceID>/credit-notes");
request.Headers.TryAddWithoutValidation("X-CSRF-Token", "<csrf-token>");
request.Headers.TryAddWithoutValidation("Accept", "application/json");
request.Headers.TryAddWithoutValidation("Cookie", "outpost_console=<console-session>");
request.Content = new StringContent(body, Encoding.UTF8, "application/json");

var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
Console.WriteLine(await response.Content.ReadAsStringAsync());
```

### POST `/api/v1/psa/invoices/{invoiceID}/finalize` {#depot.psa.finalizeInvoice}

**finalizeInvoice turns a draft into a numbered, rendered, frozen document, then marks the work it billed as invoiced in the client's database.**

EVERY INPUT THE RENDERER NEEDS IS GATHERED BEFORE THE STORE CALL, because the
render closure runs INSIDE the store's transaction and must be pure: a
database call from it would deadlock against the row it is rendering.

IDEMPOTENT. Called on an invoice already past draft the store returns it
unchanged with no error, so a retried request after a timeout nobody saw the
answer to cannot double-number a document. The marking below runs on that path
too, which is deliberate: a retry is exactly when the first attempt's second
commit is the one that went missing.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `invoiceID` | path | string | Yes | A uuid. |

#### Responses

| Status | Body | Description |
| --- | --- | --- |
| `200` | `application/json`, InvoiceView | Success. |
| `400` | `application/problem+json`, ProblemDetails | One or more validation errors occurred. |
| `401` | `application/problem+json`, ProblemDetails | No signed-in identity. |
| `403` | `application/problem+json`, ProblemDetails | The identity does not hold psa:invoice. |
| `404` | `application/problem+json`, ProblemDetails | No resource matches that id. |
| `500` | `application/problem+json`, ProblemDetails | The request failed. |
| `503` | `application/problem+json`, ProblemDetails | Client billing is not available on this server. |

#### Request samples

```bash
# Sign in first. outpost_console is a session cookie, so send it with every request.
curl -X POST 'https://depot.example.com/api/v1/psa/invoices/<invoiceID>/finalize' \
  -H 'X-CSRF-Token: <csrf-token>' \
  -H 'Accept: application/json' \
  -b 'outpost_console=<console-session>'
```

```powershell
# Sign in first. outpost_console is a session cookie, so send it with every request.

$headers = @{
    'X-CSRF-Token' = '<csrf-token>'
    'Accept' = 'application/json'
    'Cookie' = 'outpost_console=<console-session>'
}

$response = Invoke-RestMethod `
    -Method Post `
    -Uri 'https://depot.example.com/api/v1/psa/invoices/<invoiceID>/finalize' `
    -Headers $headers

$response | ConvertTo-Json -Depth 10
```

```python
# Sign in first. outpost_console is a session cookie, so send it with every request.

import requests

url = "https://depot.example.com/api/v1/psa/invoices/<invoiceID>/finalize"
headers = {
    "X-CSRF-Token": "<csrf-token>",
    "Accept": "application/json",
}
cookies = {
    "outpost_console": "<console-session>",
}

response = requests.post(url, headers=headers, cookies=cookies, timeout=30)
response.raise_for_status()
print(response.json())
```

```go
// Sign in first. outpost_console is a session cookie, so send it with every request.
package main

import (
	"fmt"
	"io"
	"log"
	"net/http"
)

func main() {
	req, err := http.NewRequest(http.MethodPost, "https://depot.example.com/api/v1/psa/invoices/<invoiceID>/finalize", nil)
	if err != nil {
		log.Fatal(err)
	}

	req.Header.Set("X-CSRF-Token", "<csrf-token>")
	req.Header.Set("Accept", "application/json")
	req.AddCookie(&http.Cookie{Name: "outpost_console", Value: "<console-session>"})

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()

	out, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(resp.Status, string(out))
}
```

```ts
// Sign in first. outpost_console is a session cookie, so send it with every request.
// The session cookie is set by signing in. A script cannot set it itself.

const url = "https://depot.example.com/api/v1/psa/invoices/<invoiceID>/finalize";

const response = await fetch(url, {
	method: "POST",
	headers: {
		"X-CSRF-Token": "<csrf-token>",
		"Accept": "application/json",
	},
	credentials: "include",
});

if (!response.ok) {
	throw new Error(`Request failed with status ${response.status}`);
}

const data = await response.json();
console.log(data);
```

```csharp
// Sign in first. outpost_console is a session cookie, so send it with every request.

using System;
using System.Net.Http;

using var client = new HttpClient();

var request = new HttpRequestMessage(HttpMethod.Post, "https://depot.example.com/api/v1/psa/invoices/<invoiceID>/finalize");
request.Headers.TryAddWithoutValidation("X-CSRF-Token", "<csrf-token>");
request.Headers.TryAddWithoutValidation("Accept", "application/json");
request.Headers.TryAddWithoutValidation("Cookie", "outpost_console=<console-session>");

var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
Console.WriteLine(await response.Content.ReadAsStringAsync());
```

### GET `/api/v1/psa/invoices/{invoiceID}/payments` {#depot.psa.listPayments}

**List payments**

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `invoiceID` | path | string | Yes | A uuid. |
| `page` | query | integer | No | One-based page number. Defaults to 1. |
| `pageSize` | query | integer | No | Rows per page, 1 to 200. Defaults to 50. |

#### Responses

| Status | Body | Description |
| --- | --- | --- |
| `200` | `application/json`, PagedResultPaymentView | Success. |
| `400` | `application/problem+json`, ProblemDetails | One or more validation errors occurred. |
| `401` | `application/problem+json`, ProblemDetails | No signed-in identity. |
| `403` | `application/problem+json`, ProblemDetails | The identity does not hold psa:read. |
| `404` | `application/problem+json`, ProblemDetails | No resource matches that id. |
| `500` | `application/problem+json`, ProblemDetails | The request failed. |
| `503` | `application/problem+json`, ProblemDetails | Client billing is not available on this server. |

#### Request samples

```bash
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.
curl -X GET 'https://depot.example.com/api/v1/psa/invoices/<invoiceID>/payments?page=0&pageSize=0' \
  -H 'X-CSRF-Token: <csrf-token>' \
  -H 'Accept: application/json' \
  -b 'outpost_console=<console-session>'
```

```powershell
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.

$headers = @{
    'X-CSRF-Token' = '<csrf-token>'
    'Accept' = 'application/json'
    'Cookie' = 'outpost_console=<console-session>'
}

$response = Invoke-RestMethod `
    -Method Get `
    -Uri 'https://depot.example.com/api/v1/psa/invoices/<invoiceID>/payments?page=0&pageSize=0' `
    -Headers $headers

$response | ConvertTo-Json -Depth 10
```

```python
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.

import requests

url = "https://depot.example.com/api/v1/psa/invoices/<invoiceID>/payments"
params = {
    "page": "0",
    "pageSize": "0",
}
headers = {
    "X-CSRF-Token": "<csrf-token>",
    "Accept": "application/json",
}
cookies = {
    "outpost_console": "<console-session>",
}

response = requests.get(url, params=params, headers=headers, cookies=cookies, timeout=30)
response.raise_for_status()
print(response.json())
```

```go
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.
package main

import (
	"fmt"
	"io"
	"log"
	"net/http"
)

func main() {
	req, err := http.NewRequest(http.MethodGet, "https://depot.example.com/api/v1/psa/invoices/<invoiceID>/payments?page=0&pageSize=0", nil)
	if err != nil {
		log.Fatal(err)
	}

	req.Header.Set("X-CSRF-Token", "<csrf-token>")
	req.Header.Set("Accept", "application/json")
	req.AddCookie(&http.Cookie{Name: "outpost_console", Value: "<console-session>"})

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()

	out, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(resp.Status, string(out))
}
```

```ts
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.
// The session cookie is set by signing in. A script cannot set it itself.

const url = new URL("https://depot.example.com/api/v1/psa/invoices/<invoiceID>/payments");
url.searchParams.set("page", "0");
url.searchParams.set("pageSize", "0");

const response = await fetch(url, {
	method: "GET",
	headers: {
		"X-CSRF-Token": "<csrf-token>",
		"Accept": "application/json",
	},
	credentials: "include",
});

if (!response.ok) {
	throw new Error(`Request failed with status ${response.status}`);
}

const data = await response.json();
console.log(data);
```

```csharp
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.

using System;
using System.Net.Http;

using var client = new HttpClient();

var request = new HttpRequestMessage(HttpMethod.Get, "https://depot.example.com/api/v1/psa/invoices/<invoiceID>/payments?page=0&pageSize=0");
request.Headers.TryAddWithoutValidation("X-CSRF-Token", "<csrf-token>");
request.Headers.TryAddWithoutValidation("Accept", "application/json");
request.Headers.TryAddWithoutValidation("Cookie", "outpost_console=<console-session>");

var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
Console.WriteLine(await response.Content.ReadAsStringAsync());
```

### POST `/api/v1/psa/invoices/{invoiceID}/payments` {#depot.psa.createPayment}

**createPayment records money that arrived.**

The store recomputes the invoice's status in the SAME transaction, so a bill
can never be paid in full and still read past_due. A zero or negative payment
is refused here and by CHECK: a refund is a credit note and a zero payment is
a typo.

AN OVERPAYMENT IS ALSO REFUSED, by the store, as ErrConflict. The status walk
reads any non-positive balance as paid, so money beyond the total would drive
the balance negative and disappear from every report rather than landing
somewhere anybody could refund it, and this slice has no refund. The 409 below
names the balance so an operator can correct the figure rather than guess at
which of the two conflicts they hit.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `invoiceID` | path | string | Yes | A uuid. |

#### Request body

Required, sent as `application/json`.

#### Responses

| Status | Body | Description |
| --- | --- | --- |
| `201` | `application/json`, PaymentView | Created. |
| `400` | `application/problem+json`, ProblemDetails | The request body is not valid JSON. |
| `401` | `application/problem+json`, ProblemDetails | No signed-in identity. |
| `403` | `application/problem+json`, ProblemDetails | The identity does not hold psa:invoice. |
| `404` | `application/problem+json`, ProblemDetails | No resource matches that id. |
| `500` | `application/problem+json`, ProblemDetails | The request failed. |
| `503` | `application/problem+json`, ProblemDetails | Client billing is not available on this server. |

#### Request samples

```bash
# Sign in first. outpost_console is a session cookie, so send it with every request.
curl -X POST 'https://depot.example.com/api/v1/psa/invoices/<invoiceID>/payments' \
  -H 'X-CSRF-Token: <csrf-token>' \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -b 'outpost_console=<console-session>' \
  --data '{
  "amountCents": 0,
  "method": "string",
  "note": "string",
  "receivedOn": "string",
  "reference": "string"
}'
```

```powershell
# Sign in first. outpost_console is a session cookie, so send it with every request.

$headers = @{
    'X-CSRF-Token' = '<csrf-token>'
    'Accept' = 'application/json'
    'Content-Type' = 'application/json'
    'Cookie' = 'outpost_console=<console-session>'
}

$body = @'
{
  "amountCents": 0,
  "method": "string",
  "note": "string",
  "receivedOn": "string",
  "reference": "string"
}
'@

$response = Invoke-RestMethod `
    -Method Post `
    -Uri 'https://depot.example.com/api/v1/psa/invoices/<invoiceID>/payments' `
    -Headers $headers `
    -ContentType 'application/json' `
    -Body $body

$response | ConvertTo-Json -Depth 10
```

```python
# Sign in first. outpost_console is a session cookie, so send it with every request.

import requests

url = "https://depot.example.com/api/v1/psa/invoices/<invoiceID>/payments"
headers = {
    "X-CSRF-Token": "<csrf-token>",
    "Accept": "application/json",
    "Content-Type": "application/json",
}
cookies = {
    "outpost_console": "<console-session>",
}
payload = {
    "amountCents": 0,
    "method": "string",
    "note": "string",
    "receivedOn": "string",
    "reference": "string",
}

response = requests.post(url, headers=headers, cookies=cookies, json=payload, timeout=30)
response.raise_for_status()
print(response.json())
```

```go
// Sign in first. outpost_console is a session cookie, so send it with every request.
package main

import (
	"bytes"
	"fmt"
	"io"
	"log"
	"net/http"
)

func main() {
	body := []byte(`{
  "amountCents": 0,
  "method": "string",
  "note": "string",
  "receivedOn": "string",
  "reference": "string"
}`)

	req, err := http.NewRequest(http.MethodPost, "https://depot.example.com/api/v1/psa/invoices/<invoiceID>/payments", bytes.NewReader(body))
	if err != nil {
		log.Fatal(err)
	}

	req.Header.Set("X-CSRF-Token", "<csrf-token>")
	req.Header.Set("Accept", "application/json")
	req.Header.Set("Content-Type", "application/json")
	req.AddCookie(&http.Cookie{Name: "outpost_console", Value: "<console-session>"})

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()

	out, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(resp.Status, string(out))
}
```

```ts
// Sign in first. outpost_console is a session cookie, so send it with every request.
// The session cookie is set by signing in. A script cannot set it itself.

const url = "https://depot.example.com/api/v1/psa/invoices/<invoiceID>/payments";

const response = await fetch(url, {
	method: "POST",
	headers: {
		"X-CSRF-Token": "<csrf-token>",
		"Accept": "application/json",
		"Content-Type": "application/json",
	},
	credentials: "include",
	body: JSON.stringify({
		"amountCents": 0,
		"method": "string",
		"note": "string",
		"receivedOn": "string",
		"reference": "string"
	}),
});

if (!response.ok) {
	throw new Error(`Request failed with status ${response.status}`);
}

const data = await response.json();
console.log(data);
```

```csharp
// Sign in first. outpost_console is a session cookie, so send it with every request.

using System;
using System.Net.Http;
using System.Text;

using var client = new HttpClient();

const string body = """
{
  "amountCents": 0,
  "method": "string",
  "note": "string",
  "receivedOn": "string",
  "reference": "string"
}
""";

var request = new HttpRequestMessage(HttpMethod.Post, "https://depot.example.com/api/v1/psa/invoices/<invoiceID>/payments");
request.Headers.TryAddWithoutValidation("X-CSRF-Token", "<csrf-token>");
request.Headers.TryAddWithoutValidation("Accept", "application/json");
request.Headers.TryAddWithoutValidation("Cookie", "outpost_console=<console-session>");
request.Content = new StringContent(body, Encoding.UTF8, "application/json");

var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
Console.WriteLine(await response.Content.ReadAsStringAsync());
```

### GET `/api/v1/psa/invoices/{invoiceID}/pdf` {#depot.psa.getInvoicePdf}

**getInvoicePdf serves the bytes stored at finalize.**

404 FOR A DRAFT, which has no row in InvoicePdf. Rendering one on demand would
hand somebody a document that looks like an invoice, carries no number, and
would be a different document tomorrow.

Content-Disposition is INLINE and not attachment, unlike the report downloads:
the console opens this in a tab so the browser's own viewer renders it, with
the browser's own save control beside it.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `invoiceID` | path | string | Yes | A uuid. |

#### Responses

| Status | Body | Description |
| --- | --- | --- |
| `200` | `application/octet-stream`, string (binary) | Success. The body shape was not resolved; see x-outpost-unresolved. |
| `400` | `application/problem+json`, ProblemDetails | One or more validation errors occurred. |
| `401` | `application/problem+json`, ProblemDetails | No signed-in identity. |
| `403` | `application/problem+json`, ProblemDetails | The identity does not hold psa:read. |
| `404` | `application/problem+json`, ProblemDetails | No resource matches that id. |
| `500` | `application/problem+json`, ProblemDetails | The request failed. |
| `503` | `application/problem+json`, ProblemDetails | Client billing is not available on this server. |

#### Request samples

```bash
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.
curl -X GET 'https://depot.example.com/api/v1/psa/invoices/<invoiceID>/pdf' \
  -H 'X-CSRF-Token: <csrf-token>' \
  -H 'Accept: application/json' \
  -b 'outpost_console=<console-session>'
```

```powershell
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.

$headers = @{
    'X-CSRF-Token' = '<csrf-token>'
    'Accept' = 'application/json'
    'Cookie' = 'outpost_console=<console-session>'
}

$response = Invoke-RestMethod `
    -Method Get `
    -Uri 'https://depot.example.com/api/v1/psa/invoices/<invoiceID>/pdf' `
    -Headers $headers

$response | ConvertTo-Json -Depth 10
```

```python
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.

import requests

url = "https://depot.example.com/api/v1/psa/invoices/<invoiceID>/pdf"
headers = {
    "X-CSRF-Token": "<csrf-token>",
    "Accept": "application/json",
}
cookies = {
    "outpost_console": "<console-session>",
}

response = requests.get(url, headers=headers, cookies=cookies, timeout=30)
response.raise_for_status()
print(response.json())
```

```go
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.
package main

import (
	"fmt"
	"io"
	"log"
	"net/http"
)

func main() {
	req, err := http.NewRequest(http.MethodGet, "https://depot.example.com/api/v1/psa/invoices/<invoiceID>/pdf", nil)
	if err != nil {
		log.Fatal(err)
	}

	req.Header.Set("X-CSRF-Token", "<csrf-token>")
	req.Header.Set("Accept", "application/json")
	req.AddCookie(&http.Cookie{Name: "outpost_console", Value: "<console-session>"})

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()

	out, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(resp.Status, string(out))
}
```

```ts
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.
// The session cookie is set by signing in. A script cannot set it itself.

const url = "https://depot.example.com/api/v1/psa/invoices/<invoiceID>/pdf";

const response = await fetch(url, {
	method: "GET",
	headers: {
		"X-CSRF-Token": "<csrf-token>",
		"Accept": "application/json",
	},
	credentials: "include",
});

if (!response.ok) {
	throw new Error(`Request failed with status ${response.status}`);
}

const data = await response.json();
console.log(data);
```

```csharp
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.

using System;
using System.Net.Http;

using var client = new HttpClient();

var request = new HttpRequestMessage(HttpMethod.Get, "https://depot.example.com/api/v1/psa/invoices/<invoiceID>/pdf");
request.Headers.TryAddWithoutValidation("X-CSRF-Token", "<csrf-token>");
request.Headers.TryAddWithoutValidation("Accept", "application/json");
request.Headers.TryAddWithoutValidation("Cookie", "outpost_console=<console-session>");

var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
Console.WriteLine(await response.Content.ReadAsStringAsync());
```

### POST `/api/v1/psa/invoices/{invoiceID}/send` {#depot.psa.sendInvoice}

**sendInvoice enqueues the document to the client's accounts-payable contact.**

IT DOES NOT MOVE THE STATUS. The mail drain calls MarkInvoiceSent after the
message actually leaves, which is what makes "did we send it" answerable
without reading the outbox. Stamping 'sent' here would say yes for a message
still sitting in a queue behind a broken SMTP password.

A DRAFT CANNOT BE SENT: it has no number and no rendered document, so there is
nothing to attach and nothing for the customer to quote back.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `invoiceID` | path | string | Yes | A uuid. |

#### Request body

Required, sent as `application/json`.

#### Responses

| Status | Body | Description |
| --- | --- | --- |
| `200` | `application/json`, InvoiceView | Success. |
| `400` | `application/problem+json`, ProblemDetails | The request body is not valid JSON. |
| `401` | `application/problem+json`, ProblemDetails | No signed-in identity. |
| `403` | `application/problem+json`, ProblemDetails | The identity does not hold psa:invoice. |
| `404` | `application/problem+json`, ProblemDetails | No resource matches that id. |
| `409` | `application/problem+json`, ProblemDetails | A draft has no number and no document. Finalize it first. |
| `500` | `application/problem+json`, ProblemDetails | The request failed. |
| `503` | `application/problem+json`, ProblemDetails | Client billing is not available on this server. |

#### Request samples

```bash
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.
curl -X POST 'https://depot.example.com/api/v1/psa/invoices/<invoiceID>/send' \
  -H 'X-CSRF-Token: <csrf-token>' \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -b 'outpost_console=<console-session>' \
  --data '{
  "to": "string"
}'
```

```powershell
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.

$headers = @{
    'X-CSRF-Token' = '<csrf-token>'
    'Accept' = 'application/json'
    'Content-Type' = 'application/json'
    'Cookie' = 'outpost_console=<console-session>'
}

$body = @'
{
  "to": "string"
}
'@

$response = Invoke-RestMethod `
    -Method Post `
    -Uri 'https://depot.example.com/api/v1/psa/invoices/<invoiceID>/send' `
    -Headers $headers `
    -ContentType 'application/json' `
    -Body $body

$response | ConvertTo-Json -Depth 10
```

```python
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.

import requests

url = "https://depot.example.com/api/v1/psa/invoices/<invoiceID>/send"
headers = {
    "X-CSRF-Token": "<csrf-token>",
    "Accept": "application/json",
    "Content-Type": "application/json",
}
cookies = {
    "outpost_console": "<console-session>",
}
payload = {
    "to": "string",
}

response = requests.post(url, headers=headers, cookies=cookies, json=payload, timeout=30)
response.raise_for_status()
print(response.json())
```

```go
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.
package main

import (
	"bytes"
	"fmt"
	"io"
	"log"
	"net/http"
)

func main() {
	body := []byte(`{
  "to": "string"
}`)

	req, err := http.NewRequest(http.MethodPost, "https://depot.example.com/api/v1/psa/invoices/<invoiceID>/send", bytes.NewReader(body))
	if err != nil {
		log.Fatal(err)
	}

	req.Header.Set("X-CSRF-Token", "<csrf-token>")
	req.Header.Set("Accept", "application/json")
	req.Header.Set("Content-Type", "application/json")
	req.AddCookie(&http.Cookie{Name: "outpost_console", Value: "<console-session>"})

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()

	out, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(resp.Status, string(out))
}
```

```ts
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.
// The session cookie is set by signing in. A script cannot set it itself.

const url = "https://depot.example.com/api/v1/psa/invoices/<invoiceID>/send";

const response = await fetch(url, {
	method: "POST",
	headers: {
		"X-CSRF-Token": "<csrf-token>",
		"Accept": "application/json",
		"Content-Type": "application/json",
	},
	credentials: "include",
	body: JSON.stringify({
		"to": "string"
	}),
});

if (!response.ok) {
	throw new Error(`Request failed with status ${response.status}`);
}

const data = await response.json();
console.log(data);
```

```csharp
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.

using System;
using System.Net.Http;
using System.Text;

using var client = new HttpClient();

const string body = """
{
  "to": "string"
}
""";

var request = new HttpRequestMessage(HttpMethod.Post, "https://depot.example.com/api/v1/psa/invoices/<invoiceID>/send");
request.Headers.TryAddWithoutValidation("X-CSRF-Token", "<csrf-token>");
request.Headers.TryAddWithoutValidation("Accept", "application/json");
request.Headers.TryAddWithoutValidation("Cookie", "outpost_console=<console-session>");
request.Content = new StringContent(body, Encoding.UTF8, "application/json");

var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
Console.WriteLine(await response.Content.ReadAsStringAsync());
```

### POST `/api/v1/psa/invoices/{invoiceID}/void` {#depot.psa.voidInvoice}

**voidInvoice cancels a document, keeps its number, and gives the work back.**

THE ROW AND THE NUMBER STAY FOREVER. A document that can disappear is not a
record, and a gap in the series is what an auditor asks about. Voiding does
free the PERIOD, because UQ_Invoice_AgreementPeriod excludes void, so a
replacement can be drafted for the same month.

After the control-database commit, and only after it, the client database is
put back: the time entries are unstamped so the work returns to the unbilled
queue, and every prepaid drawdown this invoice caused is reversed with a
compensating adjustment. Both are idempotent, because the reconciliation sweep
can reach the same rows.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `invoiceID` | path | string | Yes | A uuid. |

#### Request body

Required, sent as `application/json`.

#### Responses

| Status | Body | Description |
| --- | --- | --- |
| `200` | `application/json`, InvoiceView | Success. |
| `400` | `application/problem+json`, ProblemDetails | The request body is not valid JSON. |
| `401` | `application/problem+json`, ProblemDetails | No signed-in identity. |
| `403` | `application/problem+json`, ProblemDetails | The identity does not hold psa:invoice. |
| `404` | `application/problem+json`, ProblemDetails | No resource matches that id. |
| `500` | `application/problem+json`, ProblemDetails | The request failed. |
| `503` | `application/problem+json`, ProblemDetails | Client billing is not available on this server. |

#### Request samples

```bash
# Sign in first. outpost_console is a session cookie, so send it with every request.
curl -X POST 'https://depot.example.com/api/v1/psa/invoices/<invoiceID>/void' \
  -H 'X-CSRF-Token: <csrf-token>' \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -b 'outpost_console=<console-session>' \
  --data '{
  "reason": "string"
}'
```

```powershell
# Sign in first. outpost_console is a session cookie, so send it with every request.

$headers = @{
    'X-CSRF-Token' = '<csrf-token>'
    'Accept' = 'application/json'
    'Content-Type' = 'application/json'
    'Cookie' = 'outpost_console=<console-session>'
}

$body = @'
{
  "reason": "string"
}
'@

$response = Invoke-RestMethod `
    -Method Post `
    -Uri 'https://depot.example.com/api/v1/psa/invoices/<invoiceID>/void' `
    -Headers $headers `
    -ContentType 'application/json' `
    -Body $body

$response | ConvertTo-Json -Depth 10
```

```python
# Sign in first. outpost_console is a session cookie, so send it with every request.

import requests

url = "https://depot.example.com/api/v1/psa/invoices/<invoiceID>/void"
headers = {
    "X-CSRF-Token": "<csrf-token>",
    "Accept": "application/json",
    "Content-Type": "application/json",
}
cookies = {
    "outpost_console": "<console-session>",
}
payload = {
    "reason": "string",
}

response = requests.post(url, headers=headers, cookies=cookies, json=payload, timeout=30)
response.raise_for_status()
print(response.json())
```

```go
// Sign in first. outpost_console is a session cookie, so send it with every request.
package main

import (
	"bytes"
	"fmt"
	"io"
	"log"
	"net/http"
)

func main() {
	body := []byte(`{
  "reason": "string"
}`)

	req, err := http.NewRequest(http.MethodPost, "https://depot.example.com/api/v1/psa/invoices/<invoiceID>/void", bytes.NewReader(body))
	if err != nil {
		log.Fatal(err)
	}

	req.Header.Set("X-CSRF-Token", "<csrf-token>")
	req.Header.Set("Accept", "application/json")
	req.Header.Set("Content-Type", "application/json")
	req.AddCookie(&http.Cookie{Name: "outpost_console", Value: "<console-session>"})

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()

	out, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(resp.Status, string(out))
}
```

```ts
// Sign in first. outpost_console is a session cookie, so send it with every request.
// The session cookie is set by signing in. A script cannot set it itself.

const url = "https://depot.example.com/api/v1/psa/invoices/<invoiceID>/void";

const response = await fetch(url, {
	method: "POST",
	headers: {
		"X-CSRF-Token": "<csrf-token>",
		"Accept": "application/json",
		"Content-Type": "application/json",
	},
	credentials: "include",
	body: JSON.stringify({
		"reason": "string"
	}),
});

if (!response.ok) {
	throw new Error(`Request failed with status ${response.status}`);
}

const data = await response.json();
console.log(data);
```

```csharp
// Sign in first. outpost_console is a session cookie, so send it with every request.

using System;
using System.Net.Http;
using System.Text;

using var client = new HttpClient();

const string body = """
{
  "reason": "string"
}
""";

var request = new HttpRequestMessage(HttpMethod.Post, "https://depot.example.com/api/v1/psa/invoices/<invoiceID>/void");
request.Headers.TryAddWithoutValidation("X-CSRF-Token", "<csrf-token>");
request.Headers.TryAddWithoutValidation("Accept", "application/json");
request.Headers.TryAddWithoutValidation("Cookie", "outpost_console=<console-session>");
request.Content = new StringContent(body, Encoding.UTF8, "application/json");

var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
Console.WriteLine(await response.Content.ReadAsStringAsync());
```

### GET `/api/v1/psa/ready-to-invoice` {#depot.psa.readyToInvoice}

**readyToInvoice answers which clients have a period worth billing and what it comes to.**

EVERY FIGURE IS PRODUCED BY billing.BuildDraft, the same pure function the
invoice run uses, so the number on this screen and the number on the draft
that follows it cannot disagree. Summing the lines here instead would be a
second implementation of proration, and an operator reading two answers to
"what is this month worth" would be right to trust neither.

A client with no active agreement, or whose billing day has not arrived, is
simply absent. It is a work queue, not a roster.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `client` | query | string | No |  |
| `clientId` | query | string | No |  |
| `page` | query | integer | No | One-based page number. Defaults to 1. |
| `pageSize` | query | integer | No | Rows per page, 1 to 200. Defaults to 50. |

#### Responses

| Status | Body | Description |
| --- | --- | --- |
| `200` | `application/json`, PsaFanOutPageReadyToInvoiceClientView | Success. |
| `401` | `application/problem+json`, ProblemDetails | No signed-in identity. |
| `403` | `application/problem+json`, ProblemDetails | The identity does not hold psa:read. |
| `500` | `application/problem+json`, ProblemDetails | Could not list clients. |
| `503` | `application/problem+json`, ProblemDetails | Client billing is not available on this server. |

#### Request samples

```bash
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.
curl -X GET 'https://depot.example.com/api/v1/psa/ready-to-invoice?client=<client>&clientId=<clientId>&page=0&pageSize=0' \
  -H 'X-CSRF-Token: <csrf-token>' \
  -H 'Accept: application/json' \
  -b 'outpost_console=<console-session>'
```

```powershell
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.

$headers = @{
    'X-CSRF-Token' = '<csrf-token>'
    'Accept' = 'application/json'
    'Cookie' = 'outpost_console=<console-session>'
}

$response = Invoke-RestMethod `
    -Method Get `
    -Uri 'https://depot.example.com/api/v1/psa/ready-to-invoice?client=<client>&clientId=<clientId>&page=0&pageSize=0' `
    -Headers $headers

$response | ConvertTo-Json -Depth 10
```

```python
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.

import requests

url = "https://depot.example.com/api/v1/psa/ready-to-invoice"
params = {
    "client": "<client>",
    "clientId": "<clientId>",
    "page": "0",
    "pageSize": "0",
}
headers = {
    "X-CSRF-Token": "<csrf-token>",
    "Accept": "application/json",
}
cookies = {
    "outpost_console": "<console-session>",
}

response = requests.get(url, params=params, headers=headers, cookies=cookies, timeout=30)
response.raise_for_status()
print(response.json())
```

```go
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.
package main

import (
	"fmt"
	"io"
	"log"
	"net/http"
)

func main() {
	req, err := http.NewRequest(http.MethodGet, "https://depot.example.com/api/v1/psa/ready-to-invoice?client=<client>&clientId=<clientId>&page=0&pageSize=0", nil)
	if err != nil {
		log.Fatal(err)
	}

	req.Header.Set("X-CSRF-Token", "<csrf-token>")
	req.Header.Set("Accept", "application/json")
	req.AddCookie(&http.Cookie{Name: "outpost_console", Value: "<console-session>"})

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()

	out, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(resp.Status, string(out))
}
```

```ts
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.
// The session cookie is set by signing in. A script cannot set it itself.

const url = new URL("https://depot.example.com/api/v1/psa/ready-to-invoice");
url.searchParams.set("client", "<client>");
url.searchParams.set("clientId", "<clientId>");
url.searchParams.set("page", "0");
url.searchParams.set("pageSize", "0");

const response = await fetch(url, {
	method: "GET",
	headers: {
		"X-CSRF-Token": "<csrf-token>",
		"Accept": "application/json",
	},
	credentials: "include",
});

if (!response.ok) {
	throw new Error(`Request failed with status ${response.status}`);
}

const data = await response.json();
console.log(data);
```

```csharp
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.

using System;
using System.Net.Http;

using var client = new HttpClient();

var request = new HttpRequestMessage(HttpMethod.Get, "https://depot.example.com/api/v1/psa/ready-to-invoice?client=<client>&clientId=<clientId>&page=0&pageSize=0");
request.Headers.TryAddWithoutValidation("X-CSRF-Token", "<csrf-token>");
request.Headers.TryAddWithoutValidation("Accept", "application/json");
request.Headers.TryAddWithoutValidation("Cookie", "outpost_console=<console-session>");

var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
Console.WriteLine(await response.Content.ReadAsStringAsync());
```

### GET `/api/v1/psa/settings` {#depot.psa.getSettings}

**Get settings**

#### Responses

| Status | Body | Description |
| --- | --- | --- |
| `200` | `application/json`, BillingSettingsView | Success. |
| `400` | `application/problem+json`, ProblemDetails | One or more validation errors occurred. |
| `401` | `application/problem+json`, ProblemDetails | No signed-in identity. |
| `403` | `application/problem+json`, ProblemDetails | The identity does not hold psa:read. |
| `404` | `application/problem+json`, ProblemDetails | No resource matches. |
| `500` | `application/problem+json`, ProblemDetails | The request failed. |
| `503` | `application/problem+json`, ProblemDetails | Client billing is not available on this server. |

#### Request samples

```bash
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.
curl -X GET 'https://depot.example.com/api/v1/psa/settings' \
  -H 'X-CSRF-Token: <csrf-token>' \
  -H 'Accept: application/json' \
  -b 'outpost_console=<console-session>'
```

```powershell
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.

$headers = @{
    'X-CSRF-Token' = '<csrf-token>'
    'Accept' = 'application/json'
    'Cookie' = 'outpost_console=<console-session>'
}

$response = Invoke-RestMethod `
    -Method Get `
    -Uri 'https://depot.example.com/api/v1/psa/settings' `
    -Headers $headers

$response | ConvertTo-Json -Depth 10
```

```python
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.

import requests

url = "https://depot.example.com/api/v1/psa/settings"
headers = {
    "X-CSRF-Token": "<csrf-token>",
    "Accept": "application/json",
}
cookies = {
    "outpost_console": "<console-session>",
}

response = requests.get(url, headers=headers, cookies=cookies, timeout=30)
response.raise_for_status()
print(response.json())
```

```go
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.
package main

import (
	"fmt"
	"io"
	"log"
	"net/http"
)

func main() {
	req, err := http.NewRequest(http.MethodGet, "https://depot.example.com/api/v1/psa/settings", nil)
	if err != nil {
		log.Fatal(err)
	}

	req.Header.Set("X-CSRF-Token", "<csrf-token>")
	req.Header.Set("Accept", "application/json")
	req.AddCookie(&http.Cookie{Name: "outpost_console", Value: "<console-session>"})

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()

	out, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(resp.Status, string(out))
}
```

```ts
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.
// The session cookie is set by signing in. A script cannot set it itself.

const url = "https://depot.example.com/api/v1/psa/settings";

const response = await fetch(url, {
	method: "GET",
	headers: {
		"X-CSRF-Token": "<csrf-token>",
		"Accept": "application/json",
	},
	credentials: "include",
});

if (!response.ok) {
	throw new Error(`Request failed with status ${response.status}`);
}

const data = await response.json();
console.log(data);
```

```csharp
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.

using System;
using System.Net.Http;

using var client = new HttpClient();

var request = new HttpRequestMessage(HttpMethod.Get, "https://depot.example.com/api/v1/psa/settings");
request.Headers.TryAddWithoutValidation("X-CSRF-Token", "<csrf-token>");
request.Headers.TryAddWithoutValidation("Accept", "application/json");
request.Headers.TryAddWithoutValidation("Cookie", "outpost_console=<console-session>");

var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
Console.WriteLine(await response.Content.ReadAsStringAsync());
```

### PUT `/api/v1/psa/settings` {#depot.psa.putSettings}

**Put settings**

#### Request body

Required, sent as `application/json`.

#### Responses

| Status | Body | Description |
| --- | --- | --- |
| `200` | `application/json`, BillingSettingsView | Success. |
| `400` | `application/problem+json`, ProblemDetails | The request body is not valid JSON. |
| `401` | `application/problem+json`, ProblemDetails | No signed-in identity. |
| `403` | `application/problem+json`, ProblemDetails | The identity does not hold psa:admin. |
| `404` | `application/problem+json`, ProblemDetails | No resource matches. |
| `500` | `application/problem+json`, ProblemDetails | The request failed. |
| `503` | `application/problem+json`, ProblemDetails | Client billing is not available on this server. |

#### Request samples

```bash
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.
curl -X PUT 'https://depot.example.com/api/v1/psa/settings' \
  -H 'X-CSRF-Token: <csrf-token>' \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -b 'outpost_console=<console-session>' \
  --data '{
  "currencyCode": "string",
  "invoicePrefix": "string",
  "netDays": 0,
  "overdueReminderEveryDays": 0,
  "prepaidLowThresholdMinutes": 0,
  "reminderDaysBefore": 0,
  "remitTo": "string",
  "taxRateBasisPoints": 0,
  "usageMethod": "string"
}'
```

```powershell
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.

$headers = @{
    'X-CSRF-Token' = '<csrf-token>'
    'Accept' = 'application/json'
    'Content-Type' = 'application/json'
    'Cookie' = 'outpost_console=<console-session>'
}

$body = @'
{
  "currencyCode": "string",
  "invoicePrefix": "string",
  "netDays": 0,
  "overdueReminderEveryDays": 0,
  "prepaidLowThresholdMinutes": 0,
  "reminderDaysBefore": 0,
  "remitTo": "string",
  "taxRateBasisPoints": 0,
  "usageMethod": "string"
}
'@

$response = Invoke-RestMethod `
    -Method Put `
    -Uri 'https://depot.example.com/api/v1/psa/settings' `
    -Headers $headers `
    -ContentType 'application/json' `
    -Body $body

$response | ConvertTo-Json -Depth 10
```

```python
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.

import requests

url = "https://depot.example.com/api/v1/psa/settings"
headers = {
    "X-CSRF-Token": "<csrf-token>",
    "Accept": "application/json",
    "Content-Type": "application/json",
}
cookies = {
    "outpost_console": "<console-session>",
}
payload = {
    "currencyCode": "string",
    "invoicePrefix": "string",
    "netDays": 0,
    "overdueReminderEveryDays": 0,
    "prepaidLowThresholdMinutes": 0,
    "reminderDaysBefore": 0,
    "remitTo": "string",
    "taxRateBasisPoints": 0,
    "usageMethod": "string",
}

response = requests.put(url, headers=headers, cookies=cookies, json=payload, timeout=30)
response.raise_for_status()
print(response.json())
```

```go
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.
package main

import (
	"bytes"
	"fmt"
	"io"
	"log"
	"net/http"
)

func main() {
	body := []byte(`{
  "currencyCode": "string",
  "invoicePrefix": "string",
  "netDays": 0,
  "overdueReminderEveryDays": 0,
  "prepaidLowThresholdMinutes": 0,
  "reminderDaysBefore": 0,
  "remitTo": "string",
  "taxRateBasisPoints": 0,
  "usageMethod": "string"
}`)

	req, err := http.NewRequest(http.MethodPut, "https://depot.example.com/api/v1/psa/settings", bytes.NewReader(body))
	if err != nil {
		log.Fatal(err)
	}

	req.Header.Set("X-CSRF-Token", "<csrf-token>")
	req.Header.Set("Accept", "application/json")
	req.Header.Set("Content-Type", "application/json")
	req.AddCookie(&http.Cookie{Name: "outpost_console", Value: "<console-session>"})

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()

	out, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(resp.Status, string(out))
}
```

```ts
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.
// The session cookie is set by signing in. A script cannot set it itself.

const url = "https://depot.example.com/api/v1/psa/settings";

const response = await fetch(url, {
	method: "PUT",
	headers: {
		"X-CSRF-Token": "<csrf-token>",
		"Accept": "application/json",
		"Content-Type": "application/json",
	},
	credentials: "include",
	body: JSON.stringify({
		"currencyCode": "string",
		"invoicePrefix": "string",
		"netDays": 0,
		"overdueReminderEveryDays": 0,
		"prepaidLowThresholdMinutes": 0,
		"reminderDaysBefore": 0,
		"remitTo": "string",
		"taxRateBasisPoints": 0,
		"usageMethod": "string"
	}),
});

if (!response.ok) {
	throw new Error(`Request failed with status ${response.status}`);
}

const data = await response.json();
console.log(data);
```

```csharp
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.

using System;
using System.Net.Http;
using System.Text;

using var client = new HttpClient();

const string body = """
{
  "currencyCode": "string",
  "invoicePrefix": "string",
  "netDays": 0,
  "overdueReminderEveryDays": 0,
  "prepaidLowThresholdMinutes": 0,
  "reminderDaysBefore": 0,
  "remitTo": "string",
  "taxRateBasisPoints": 0,
  "usageMethod": "string"
}
""";

var request = new HttpRequestMessage(HttpMethod.Put, "https://depot.example.com/api/v1/psa/settings");
request.Headers.TryAddWithoutValidation("X-CSRF-Token", "<csrf-token>");
request.Headers.TryAddWithoutValidation("Accept", "application/json");
request.Headers.TryAddWithoutValidation("Cookie", "outpost_console=<console-session>");
request.Content = new StringContent(body, Encoding.UTF8, "application/json");

var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
Console.WriteLine(await response.Content.ReadAsStringAsync());
```

### GET `/api/v1/psa/templates` {#depot.psa.listTemplates}

**List templates**

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `page` | query | integer | No | One-based page number. Defaults to 1. |
| `pageSize` | query | integer | No | Rows per page, 1 to 200. Defaults to 50. |

#### Responses

| Status | Body | Description |
| --- | --- | --- |
| `200` | `application/json`, PagedResultAgreementTemplateView | Success. |
| `400` | `application/problem+json`, ProblemDetails | One or more validation errors occurred. |
| `401` | `application/problem+json`, ProblemDetails | No signed-in identity. |
| `403` | `application/problem+json`, ProblemDetails | The identity does not hold psa:read. |
| `404` | `application/problem+json`, ProblemDetails | No resource matches. |
| `500` | `application/problem+json`, ProblemDetails | The request failed. |
| `503` | `application/problem+json`, ProblemDetails | Client billing is not available on this server. |

#### Request samples

```bash
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.
curl -X GET 'https://depot.example.com/api/v1/psa/templates?page=0&pageSize=0' \
  -H 'X-CSRF-Token: <csrf-token>' \
  -H 'Accept: application/json' \
  -b 'outpost_console=<console-session>'
```

```powershell
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.

$headers = @{
    'X-CSRF-Token' = '<csrf-token>'
    'Accept' = 'application/json'
    'Cookie' = 'outpost_console=<console-session>'
}

$response = Invoke-RestMethod `
    -Method Get `
    -Uri 'https://depot.example.com/api/v1/psa/templates?page=0&pageSize=0' `
    -Headers $headers

$response | ConvertTo-Json -Depth 10
```

```python
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.

import requests

url = "https://depot.example.com/api/v1/psa/templates"
params = {
    "page": "0",
    "pageSize": "0",
}
headers = {
    "X-CSRF-Token": "<csrf-token>",
    "Accept": "application/json",
}
cookies = {
    "outpost_console": "<console-session>",
}

response = requests.get(url, params=params, headers=headers, cookies=cookies, timeout=30)
response.raise_for_status()
print(response.json())
```

```go
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.
package main

import (
	"fmt"
	"io"
	"log"
	"net/http"
)

func main() {
	req, err := http.NewRequest(http.MethodGet, "https://depot.example.com/api/v1/psa/templates?page=0&pageSize=0", nil)
	if err != nil {
		log.Fatal(err)
	}

	req.Header.Set("X-CSRF-Token", "<csrf-token>")
	req.Header.Set("Accept", "application/json")
	req.AddCookie(&http.Cookie{Name: "outpost_console", Value: "<console-session>"})

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()

	out, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(resp.Status, string(out))
}
```

```ts
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.
// The session cookie is set by signing in. A script cannot set it itself.

const url = new URL("https://depot.example.com/api/v1/psa/templates");
url.searchParams.set("page", "0");
url.searchParams.set("pageSize", "0");

const response = await fetch(url, {
	method: "GET",
	headers: {
		"X-CSRF-Token": "<csrf-token>",
		"Accept": "application/json",
	},
	credentials: "include",
});

if (!response.ok) {
	throw new Error(`Request failed with status ${response.status}`);
}

const data = await response.json();
console.log(data);
```

```csharp
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.

using System;
using System.Net.Http;

using var client = new HttpClient();

var request = new HttpRequestMessage(HttpMethod.Get, "https://depot.example.com/api/v1/psa/templates?page=0&pageSize=0");
request.Headers.TryAddWithoutValidation("X-CSRF-Token", "<csrf-token>");
request.Headers.TryAddWithoutValidation("Accept", "application/json");
request.Headers.TryAddWithoutValidation("Cookie", "outpost_console=<console-session>");

var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
Console.WriteLine(await response.Content.ReadAsStringAsync());
```

### POST `/api/v1/psa/templates` {#depot.psa.createTemplate}

**Create template**

#### Request body

Required, sent as `application/json`.

#### Responses

| Status | Body | Description |
| --- | --- | --- |
| `201` | `application/json`, AgreementTemplateView | Created. |
| `400` | `application/problem+json`, ProblemDetails | The request body is not valid JSON. |
| `401` | `application/problem+json`, ProblemDetails | No signed-in identity. |
| `403` | `application/problem+json`, ProblemDetails | The identity does not hold psa:admin. |
| `404` | `application/problem+json`, ProblemDetails | No resource matches. |
| `500` | `application/problem+json`, ProblemDetails | The request failed. |
| `503` | `application/problem+json`, ProblemDetails | Client billing is not available on this server. |

#### Request samples

```bash
# Sign in first. outpost_console is a session cookie, so send it with every request.
curl -X POST 'https://depot.example.com/api/v1/psa/templates' \
  -H 'X-CSRF-Token: <csrf-token>' \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -b 'outpost_console=<console-session>' \
  --data '{
  "billingDay": 0,
  "defaultChargeTypeCode": "string",
  "defaultRateCents": 0,
  "description": "string",
  "lines": [
    {
      "description": "string",
      "exclusive": false,
      "filter": "string",
      "includedDevicesPerSeat": 0,
      "includedMinutes": 0,
      "kind": "string",
      "overageUnitAmountCents": 0,
      "publicId": "string",
      "seatRequiresEndpoint": false,
      "sortOrder": 0,
      "unitAmountCents": 0,
      "usageMethod": "string"
    }
  ],
  "name": "string",
  "rules": [
    {
      "chargeTypeCode": "string",
      "description": "string",
      "outOfHours": false,
      "outcome": "string",
      "position": 0,
      "prepaidTemplateLinePublicId": "string",
      "projectWork": false,
      "publicId": "string",
      "rateCents": 0,
      "ticketTypeCode": "string"
    }
  ]
}'
```

```powershell
# Sign in first. outpost_console is a session cookie, so send it with every request.

$headers = @{
    'X-CSRF-Token' = '<csrf-token>'
    'Accept' = 'application/json'
    'Content-Type' = 'application/json'
    'Cookie' = 'outpost_console=<console-session>'
}

$body = @'
{
  "billingDay": 0,
  "defaultChargeTypeCode": "string",
  "defaultRateCents": 0,
  "description": "string",
  "lines": [
    {
      "description": "string",
      "exclusive": false,
      "filter": "string",
      "includedDevicesPerSeat": 0,
      "includedMinutes": 0,
      "kind": "string",
      "overageUnitAmountCents": 0,
      "publicId": "string",
      "seatRequiresEndpoint": false,
      "sortOrder": 0,
      "unitAmountCents": 0,
      "usageMethod": "string"
    }
  ],
  "name": "string",
  "rules": [
    {
      "chargeTypeCode": "string",
      "description": "string",
      "outOfHours": false,
      "outcome": "string",
      "position": 0,
      "prepaidTemplateLinePublicId": "string",
      "projectWork": false,
      "publicId": "string",
      "rateCents": 0,
      "ticketTypeCode": "string"
    }
  ]
}
'@

$response = Invoke-RestMethod `
    -Method Post `
    -Uri 'https://depot.example.com/api/v1/psa/templates' `
    -Headers $headers `
    -ContentType 'application/json' `
    -Body $body

$response | ConvertTo-Json -Depth 10
```

```python
# Sign in first. outpost_console is a session cookie, so send it with every request.

import requests

url = "https://depot.example.com/api/v1/psa/templates"
headers = {
    "X-CSRF-Token": "<csrf-token>",
    "Accept": "application/json",
    "Content-Type": "application/json",
}
cookies = {
    "outpost_console": "<console-session>",
}
payload = {
    "billingDay": 0,
    "defaultChargeTypeCode": "string",
    "defaultRateCents": 0,
    "description": "string",
    "lines": [
        {
            "description": "string",
            "exclusive": False,
            "filter": "string",
            "includedDevicesPerSeat": 0,
            "includedMinutes": 0,
            "kind": "string",
            "overageUnitAmountCents": 0,
            "publicId": "string",
            "seatRequiresEndpoint": False,
            "sortOrder": 0,
            "unitAmountCents": 0,
            "usageMethod": "string",
        },
    ],
    "name": "string",
    "rules": [
        {
            "chargeTypeCode": "string",
            "description": "string",
            "outOfHours": False,
            "outcome": "string",
            "position": 0,
            "prepaidTemplateLinePublicId": "string",
            "projectWork": False,
            "publicId": "string",
            "rateCents": 0,
            "ticketTypeCode": "string",
        },
    ],
}

response = requests.post(url, headers=headers, cookies=cookies, json=payload, timeout=30)
response.raise_for_status()
print(response.json())
```

```go
// Sign in first. outpost_console is a session cookie, so send it with every request.
package main

import (
	"bytes"
	"fmt"
	"io"
	"log"
	"net/http"
)

func main() {
	body := []byte(`{
  "billingDay": 0,
  "defaultChargeTypeCode": "string",
  "defaultRateCents": 0,
  "description": "string",
  "lines": [
    {
      "description": "string",
      "exclusive": false,
      "filter": "string",
      "includedDevicesPerSeat": 0,
      "includedMinutes": 0,
      "kind": "string",
      "overageUnitAmountCents": 0,
      "publicId": "string",
      "seatRequiresEndpoint": false,
      "sortOrder": 0,
      "unitAmountCents": 0,
      "usageMethod": "string"
    }
  ],
  "name": "string",
  "rules": [
    {
      "chargeTypeCode": "string",
      "description": "string",
      "outOfHours": false,
      "outcome": "string",
      "position": 0,
      "prepaidTemplateLinePublicId": "string",
      "projectWork": false,
      "publicId": "string",
      "rateCents": 0,
      "ticketTypeCode": "string"
    }
  ]
}`)

	req, err := http.NewRequest(http.MethodPost, "https://depot.example.com/api/v1/psa/templates", bytes.NewReader(body))
	if err != nil {
		log.Fatal(err)
	}

	req.Header.Set("X-CSRF-Token", "<csrf-token>")
	req.Header.Set("Accept", "application/json")
	req.Header.Set("Content-Type", "application/json")
	req.AddCookie(&http.Cookie{Name: "outpost_console", Value: "<console-session>"})

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()

	out, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(resp.Status, string(out))
}
```

```ts
// Sign in first. outpost_console is a session cookie, so send it with every request.
// The session cookie is set by signing in. A script cannot set it itself.

const url = "https://depot.example.com/api/v1/psa/templates";

const response = await fetch(url, {
	method: "POST",
	headers: {
		"X-CSRF-Token": "<csrf-token>",
		"Accept": "application/json",
		"Content-Type": "application/json",
	},
	credentials: "include",
	body: JSON.stringify({
		"billingDay": 0,
		"defaultChargeTypeCode": "string",
		"defaultRateCents": 0,
		"description": "string",
		"lines": [
			{
				"description": "string",
				"exclusive": false,
				"filter": "string",
				"includedDevicesPerSeat": 0,
				"includedMinutes": 0,
				"kind": "string",
				"overageUnitAmountCents": 0,
				"publicId": "string",
				"seatRequiresEndpoint": false,
				"sortOrder": 0,
				"unitAmountCents": 0,
				"usageMethod": "string"
			}
		],
		"name": "string",
		"rules": [
			{
				"chargeTypeCode": "string",
				"description": "string",
				"outOfHours": false,
				"outcome": "string",
				"position": 0,
				"prepaidTemplateLinePublicId": "string",
				"projectWork": false,
				"publicId": "string",
				"rateCents": 0,
				"ticketTypeCode": "string"
			}
		]
	}),
});

if (!response.ok) {
	throw new Error(`Request failed with status ${response.status}`);
}

const data = await response.json();
console.log(data);
```

```csharp
// Sign in first. outpost_console is a session cookie, so send it with every request.

using System;
using System.Net.Http;
using System.Text;

using var client = new HttpClient();

const string body = """
{
  "billingDay": 0,
  "defaultChargeTypeCode": "string",
  "defaultRateCents": 0,
  "description": "string",
  "lines": [
    {
      "description": "string",
      "exclusive": false,
      "filter": "string",
      "includedDevicesPerSeat": 0,
      "includedMinutes": 0,
      "kind": "string",
      "overageUnitAmountCents": 0,
      "publicId": "string",
      "seatRequiresEndpoint": false,
      "sortOrder": 0,
      "unitAmountCents": 0,
      "usageMethod": "string"
    }
  ],
  "name": "string",
  "rules": [
    {
      "chargeTypeCode": "string",
      "description": "string",
      "outOfHours": false,
      "outcome": "string",
      "position": 0,
      "prepaidTemplateLinePublicId": "string",
      "projectWork": false,
      "publicId": "string",
      "rateCents": 0,
      "ticketTypeCode": "string"
    }
  ]
}
""";

var request = new HttpRequestMessage(HttpMethod.Post, "https://depot.example.com/api/v1/psa/templates");
request.Headers.TryAddWithoutValidation("X-CSRF-Token", "<csrf-token>");
request.Headers.TryAddWithoutValidation("Accept", "application/json");
request.Headers.TryAddWithoutValidation("Cookie", "outpost_console=<console-session>");
request.Content = new StringContent(body, Encoding.UTF8, "application/json");

var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
Console.WriteLine(await response.Content.ReadAsStringAsync());
```

### GET `/api/v1/psa/templates/{templateID}` {#depot.psa.getTemplate}

**Get template**

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `templateID` | path | string | Yes | A uuid. |

#### Responses

| Status | Body | Description |
| --- | --- | --- |
| `200` | `application/json`, AgreementTemplateView | Success. |
| `400` | `application/problem+json`, ProblemDetails | One or more validation errors occurred. |
| `401` | `application/problem+json`, ProblemDetails | No signed-in identity. |
| `403` | `application/problem+json`, ProblemDetails | The identity does not hold psa:read. |
| `404` | `application/problem+json`, ProblemDetails | No resource matches that id. |
| `500` | `application/problem+json`, ProblemDetails | The request failed. |
| `503` | `application/problem+json`, ProblemDetails | Client billing is not available on this server. |

#### Request samples

```bash
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.
curl -X GET 'https://depot.example.com/api/v1/psa/templates/<templateID>' \
  -H 'X-CSRF-Token: <csrf-token>' \
  -H 'Accept: application/json' \
  -b 'outpost_console=<console-session>'
```

```powershell
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.

$headers = @{
    'X-CSRF-Token' = '<csrf-token>'
    'Accept' = 'application/json'
    'Cookie' = 'outpost_console=<console-session>'
}

$response = Invoke-RestMethod `
    -Method Get `
    -Uri 'https://depot.example.com/api/v1/psa/templates/<templateID>' `
    -Headers $headers

$response | ConvertTo-Json -Depth 10
```

```python
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.

import requests

url = "https://depot.example.com/api/v1/psa/templates/<templateID>"
headers = {
    "X-CSRF-Token": "<csrf-token>",
    "Accept": "application/json",
}
cookies = {
    "outpost_console": "<console-session>",
}

response = requests.get(url, headers=headers, cookies=cookies, timeout=30)
response.raise_for_status()
print(response.json())
```

```go
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.
package main

import (
	"fmt"
	"io"
	"log"
	"net/http"
)

func main() {
	req, err := http.NewRequest(http.MethodGet, "https://depot.example.com/api/v1/psa/templates/<templateID>", nil)
	if err != nil {
		log.Fatal(err)
	}

	req.Header.Set("X-CSRF-Token", "<csrf-token>")
	req.Header.Set("Accept", "application/json")
	req.AddCookie(&http.Cookie{Name: "outpost_console", Value: "<console-session>"})

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()

	out, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(resp.Status, string(out))
}
```

```ts
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.
// The session cookie is set by signing in. A script cannot set it itself.

const url = "https://depot.example.com/api/v1/psa/templates/<templateID>";

const response = await fetch(url, {
	method: "GET",
	headers: {
		"X-CSRF-Token": "<csrf-token>",
		"Accept": "application/json",
	},
	credentials: "include",
});

if (!response.ok) {
	throw new Error(`Request failed with status ${response.status}`);
}

const data = await response.json();
console.log(data);
```

```csharp
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.

using System;
using System.Net.Http;

using var client = new HttpClient();

var request = new HttpRequestMessage(HttpMethod.Get, "https://depot.example.com/api/v1/psa/templates/<templateID>");
request.Headers.TryAddWithoutValidation("X-CSRF-Token", "<csrf-token>");
request.Headers.TryAddWithoutValidation("Accept", "application/json");
request.Headers.TryAddWithoutValidation("Cookie", "outpost_console=<console-session>");

var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
Console.WriteLine(await response.Content.ReadAsStringAsync());
```

### PATCH `/api/v1/psa/templates/{templateID}` {#depot.psa.patchTemplate}

**patchTemplate merges the body onto the stored row and writes the result whole.**

A read-modify-write rather than a partial UPDATE because the store method
replaces the template and its LINE SET together in one transaction, which is
the right shape for the writer and the wrong one for a PATCH that names three
fields. Omitting lines leaves the existing set exactly as it is; sending an
empty array clears it, which is the difference between "I did not touch the
lines" and "this template has none".

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `templateID` | path | string | Yes | A uuid. |

#### Request body

Required, sent as `application/json`.

#### Responses

| Status | Body | Description |
| --- | --- | --- |
| `200` | `application/json`, AgreementTemplateView | Success. |
| `400` | `application/problem+json`, ProblemDetails | The request body is not valid JSON. |
| `401` | `application/problem+json`, ProblemDetails | No signed-in identity. |
| `403` | `application/problem+json`, ProblemDetails | The identity does not hold psa:admin. |
| `404` | `application/problem+json`, ProblemDetails | No resource matches that id. |
| `500` | `application/problem+json`, ProblemDetails | The request failed. |
| `503` | `application/problem+json`, ProblemDetails | Client billing is not available on this server. |

#### Request samples

```bash
# Sign in first. outpost_console is a session cookie, so send it with every request.
curl -X PATCH 'https://depot.example.com/api/v1/psa/templates/<templateID>' \
  -H 'X-CSRF-Token: <csrf-token>' \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -b 'outpost_console=<console-session>' \
  --data '{
  "billingDay": 0,
  "defaultChargeTypeCode": "string",
  "defaultRateCents": 0,
  "description": "string",
  "lines": [
    {
      "description": "string",
      "exclusive": false,
      "filter": "string",
      "includedDevicesPerSeat": 0,
      "includedMinutes": 0,
      "kind": "string",
      "overageUnitAmountCents": 0,
      "publicId": "string",
      "seatRequiresEndpoint": false,
      "sortOrder": 0,
      "unitAmountCents": 0,
      "usageMethod": "string"
    }
  ],
  "name": "string",
  "rules": [
    {
      "chargeTypeCode": "string",
      "description": "string",
      "outOfHours": false,
      "outcome": "string",
      "position": 0,
      "prepaidTemplateLinePublicId": "string",
      "projectWork": false,
      "publicId": "string",
      "rateCents": 0,
      "ticketTypeCode": "string"
    }
  ]
}'
```

```powershell
# Sign in first. outpost_console is a session cookie, so send it with every request.

$headers = @{
    'X-CSRF-Token' = '<csrf-token>'
    'Accept' = 'application/json'
    'Content-Type' = 'application/json'
    'Cookie' = 'outpost_console=<console-session>'
}

$body = @'
{
  "billingDay": 0,
  "defaultChargeTypeCode": "string",
  "defaultRateCents": 0,
  "description": "string",
  "lines": [
    {
      "description": "string",
      "exclusive": false,
      "filter": "string",
      "includedDevicesPerSeat": 0,
      "includedMinutes": 0,
      "kind": "string",
      "overageUnitAmountCents": 0,
      "publicId": "string",
      "seatRequiresEndpoint": false,
      "sortOrder": 0,
      "unitAmountCents": 0,
      "usageMethod": "string"
    }
  ],
  "name": "string",
  "rules": [
    {
      "chargeTypeCode": "string",
      "description": "string",
      "outOfHours": false,
      "outcome": "string",
      "position": 0,
      "prepaidTemplateLinePublicId": "string",
      "projectWork": false,
      "publicId": "string",
      "rateCents": 0,
      "ticketTypeCode": "string"
    }
  ]
}
'@

$response = Invoke-RestMethod `
    -Method Patch `
    -Uri 'https://depot.example.com/api/v1/psa/templates/<templateID>' `
    -Headers $headers `
    -ContentType 'application/json' `
    -Body $body

$response | ConvertTo-Json -Depth 10
```

```python
# Sign in first. outpost_console is a session cookie, so send it with every request.

import requests

url = "https://depot.example.com/api/v1/psa/templates/<templateID>"
headers = {
    "X-CSRF-Token": "<csrf-token>",
    "Accept": "application/json",
    "Content-Type": "application/json",
}
cookies = {
    "outpost_console": "<console-session>",
}
payload = {
    "billingDay": 0,
    "defaultChargeTypeCode": "string",
    "defaultRateCents": 0,
    "description": "string",
    "lines": [
        {
            "description": "string",
            "exclusive": False,
            "filter": "string",
            "includedDevicesPerSeat": 0,
            "includedMinutes": 0,
            "kind": "string",
            "overageUnitAmountCents": 0,
            "publicId": "string",
            "seatRequiresEndpoint": False,
            "sortOrder": 0,
            "unitAmountCents": 0,
            "usageMethod": "string",
        },
    ],
    "name": "string",
    "rules": [
        {
            "chargeTypeCode": "string",
            "description": "string",
            "outOfHours": False,
            "outcome": "string",
            "position": 0,
            "prepaidTemplateLinePublicId": "string",
            "projectWork": False,
            "publicId": "string",
            "rateCents": 0,
            "ticketTypeCode": "string",
        },
    ],
}

response = requests.patch(url, headers=headers, cookies=cookies, json=payload, timeout=30)
response.raise_for_status()
print(response.json())
```

```go
// Sign in first. outpost_console is a session cookie, so send it with every request.
package main

import (
	"bytes"
	"fmt"
	"io"
	"log"
	"net/http"
)

func main() {
	body := []byte(`{
  "billingDay": 0,
  "defaultChargeTypeCode": "string",
  "defaultRateCents": 0,
  "description": "string",
  "lines": [
    {
      "description": "string",
      "exclusive": false,
      "filter": "string",
      "includedDevicesPerSeat": 0,
      "includedMinutes": 0,
      "kind": "string",
      "overageUnitAmountCents": 0,
      "publicId": "string",
      "seatRequiresEndpoint": false,
      "sortOrder": 0,
      "unitAmountCents": 0,
      "usageMethod": "string"
    }
  ],
  "name": "string",
  "rules": [
    {
      "chargeTypeCode": "string",
      "description": "string",
      "outOfHours": false,
      "outcome": "string",
      "position": 0,
      "prepaidTemplateLinePublicId": "string",
      "projectWork": false,
      "publicId": "string",
      "rateCents": 0,
      "ticketTypeCode": "string"
    }
  ]
}`)

	req, err := http.NewRequest(http.MethodPatch, "https://depot.example.com/api/v1/psa/templates/<templateID>", bytes.NewReader(body))
	if err != nil {
		log.Fatal(err)
	}

	req.Header.Set("X-CSRF-Token", "<csrf-token>")
	req.Header.Set("Accept", "application/json")
	req.Header.Set("Content-Type", "application/json")
	req.AddCookie(&http.Cookie{Name: "outpost_console", Value: "<console-session>"})

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()

	out, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(resp.Status, string(out))
}
```

```ts
// Sign in first. outpost_console is a session cookie, so send it with every request.
// The session cookie is set by signing in. A script cannot set it itself.

const url = "https://depot.example.com/api/v1/psa/templates/<templateID>";

const response = await fetch(url, {
	method: "PATCH",
	headers: {
		"X-CSRF-Token": "<csrf-token>",
		"Accept": "application/json",
		"Content-Type": "application/json",
	},
	credentials: "include",
	body: JSON.stringify({
		"billingDay": 0,
		"defaultChargeTypeCode": "string",
		"defaultRateCents": 0,
		"description": "string",
		"lines": [
			{
				"description": "string",
				"exclusive": false,
				"filter": "string",
				"includedDevicesPerSeat": 0,
				"includedMinutes": 0,
				"kind": "string",
				"overageUnitAmountCents": 0,
				"publicId": "string",
				"seatRequiresEndpoint": false,
				"sortOrder": 0,
				"unitAmountCents": 0,
				"usageMethod": "string"
			}
		],
		"name": "string",
		"rules": [
			{
				"chargeTypeCode": "string",
				"description": "string",
				"outOfHours": false,
				"outcome": "string",
				"position": 0,
				"prepaidTemplateLinePublicId": "string",
				"projectWork": false,
				"publicId": "string",
				"rateCents": 0,
				"ticketTypeCode": "string"
			}
		]
	}),
});

if (!response.ok) {
	throw new Error(`Request failed with status ${response.status}`);
}

const data = await response.json();
console.log(data);
```

```csharp
// Sign in first. outpost_console is a session cookie, so send it with every request.

using System;
using System.Net.Http;
using System.Text;

using var client = new HttpClient();

const string body = """
{
  "billingDay": 0,
  "defaultChargeTypeCode": "string",
  "defaultRateCents": 0,
  "description": "string",
  "lines": [
    {
      "description": "string",
      "exclusive": false,
      "filter": "string",
      "includedDevicesPerSeat": 0,
      "includedMinutes": 0,
      "kind": "string",
      "overageUnitAmountCents": 0,
      "publicId": "string",
      "seatRequiresEndpoint": false,
      "sortOrder": 0,
      "unitAmountCents": 0,
      "usageMethod": "string"
    }
  ],
  "name": "string",
  "rules": [
    {
      "chargeTypeCode": "string",
      "description": "string",
      "outOfHours": false,
      "outcome": "string",
      "position": 0,
      "prepaidTemplateLinePublicId": "string",
      "projectWork": false,
      "publicId": "string",
      "rateCents": 0,
      "ticketTypeCode": "string"
    }
  ]
}
""";

var request = new HttpRequestMessage(HttpMethod.Patch, "https://depot.example.com/api/v1/psa/templates/<templateID>");
request.Headers.TryAddWithoutValidation("X-CSRF-Token", "<csrf-token>");
request.Headers.TryAddWithoutValidation("Accept", "application/json");
request.Headers.TryAddWithoutValidation("Cookie", "outpost_console=<console-session>");
request.Content = new StringContent(body, Encoding.UTF8, "application/json");

var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
Console.WriteLine(await response.Content.ReadAsStringAsync());
```

### DELETE `/api/v1/psa/templates/{templateID}` {#depot.psa.deleteTemplate}

**Delete template**

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `templateID` | path | string | Yes | A uuid. |

#### Responses

| Status | Body | Description |
| --- | --- | --- |
| `204` | None | Success, with no body. |
| `400` | `application/problem+json`, ProblemDetails | One or more validation errors occurred. |
| `401` | `application/problem+json`, ProblemDetails | No signed-in identity. |
| `403` | `application/problem+json`, ProblemDetails | The identity does not hold psa:admin. |
| `404` | `application/problem+json`, ProblemDetails | No resource matches that id. |
| `500` | `application/problem+json`, ProblemDetails | The request failed. |
| `503` | `application/problem+json`, ProblemDetails | Client billing is not available on this server. |

#### Request samples

```bash
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.
curl -X DELETE 'https://depot.example.com/api/v1/psa/templates/<templateID>' \
  -H 'X-CSRF-Token: <csrf-token>' \
  -H 'Accept: application/json' \
  -b 'outpost_console=<console-session>'
```

```powershell
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.

$headers = @{
    'X-CSRF-Token' = '<csrf-token>'
    'Accept' = 'application/json'
    'Cookie' = 'outpost_console=<console-session>'
}

$response = Invoke-RestMethod `
    -Method Delete `
    -Uri 'https://depot.example.com/api/v1/psa/templates/<templateID>' `
    -Headers $headers

$response | ConvertTo-Json -Depth 10
```

```python
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.

import requests

url = "https://depot.example.com/api/v1/psa/templates/<templateID>"
headers = {
    "X-CSRF-Token": "<csrf-token>",
    "Accept": "application/json",
}
cookies = {
    "outpost_console": "<console-session>",
}

response = requests.delete(url, headers=headers, cookies=cookies, timeout=30)
response.raise_for_status()
print(response.json())
```

```go
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.
package main

import (
	"fmt"
	"io"
	"log"
	"net/http"
)

func main() {
	req, err := http.NewRequest(http.MethodDelete, "https://depot.example.com/api/v1/psa/templates/<templateID>", nil)
	if err != nil {
		log.Fatal(err)
	}

	req.Header.Set("X-CSRF-Token", "<csrf-token>")
	req.Header.Set("Accept", "application/json")
	req.AddCookie(&http.Cookie{Name: "outpost_console", Value: "<console-session>"})

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()

	out, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(resp.Status, string(out))
}
```

```ts
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.
// The session cookie is set by signing in. A script cannot set it itself.

const url = "https://depot.example.com/api/v1/psa/templates/<templateID>";

const response = await fetch(url, {
	method: "DELETE",
	headers: {
		"X-CSRF-Token": "<csrf-token>",
		"Accept": "application/json",
	},
	credentials: "include",
});

if (!response.ok) {
	throw new Error(`Request failed with status ${response.status}`);
}

const data = await response.json();
console.log(data);
```

```csharp
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.

using System;
using System.Net.Http;

using var client = new HttpClient();

var request = new HttpRequestMessage(HttpMethod.Delete, "https://depot.example.com/api/v1/psa/templates/<templateID>");
request.Headers.TryAddWithoutValidation("X-CSRF-Token", "<csrf-token>");
request.Headers.TryAddWithoutValidation("Accept", "application/json");
request.Headers.TryAddWithoutValidation("Cookie", "outpost_console=<console-session>");

var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
Console.WriteLine(await response.Content.ReadAsStringAsync());
```

### PUT `/api/v1/psa/templates/{templateID}/rules` {#depot.psa.putTemplateRules}

**putTemplateRules replaces the whole ordered rule set on one template.**

NOTHING IS REPRICED AFTERWARDS and nothing can be. That is the one difference
from putAgreementRules, which reclassifies the unbilled work its rules govern:
these rows govern no work at all until somebody builds an agreement from the
template, and the agreement gets a copy.

psa:admin and not psa:write, matching every other template write: the rate
card is a practice-wide document and editing it is not the same permission as
editing one customer's contract.

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `templateID` | path | string | Yes | A uuid. |

#### Request body

Required, sent as `application/json`.

#### Responses

| Status | Body | Description |
| --- | --- | --- |
| `200` | `application/json`, AgreementTemplateRulesRequest | Success. |
| `400` | `application/problem+json`, ProblemDetails | The request body is not valid JSON. |
| `401` | `application/problem+json`, ProblemDetails | No signed-in identity. |
| `403` | `application/problem+json`, ProblemDetails | The identity does not hold psa:admin. |
| `404` | `application/problem+json`, ProblemDetails | No resource matches that id. |
| `500` | `application/problem+json`, ProblemDetails | The request failed. |
| `503` | `application/problem+json`, ProblemDetails | Client billing is not available on this server. |

#### Request samples

```bash
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.
curl -X PUT 'https://depot.example.com/api/v1/psa/templates/<templateID>/rules' \
  -H 'X-CSRF-Token: <csrf-token>' \
  -H 'Accept: application/json' \
  -H 'Content-Type: application/json' \
  -b 'outpost_console=<console-session>' \
  --data '{
  "rules": [
    {
      "chargeTypeCode": "string",
      "description": "string",
      "outOfHours": false,
      "outcome": "string",
      "position": 0,
      "prepaidTemplateLinePublicId": "string",
      "projectWork": false,
      "publicId": "string",
      "rateCents": 0,
      "ticketTypeCode": "string"
    }
  ]
}'
```

```powershell
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.

$headers = @{
    'X-CSRF-Token' = '<csrf-token>'
    'Accept' = 'application/json'
    'Content-Type' = 'application/json'
    'Cookie' = 'outpost_console=<console-session>'
}

$body = @'
{
  "rules": [
    {
      "chargeTypeCode": "string",
      "description": "string",
      "outOfHours": false,
      "outcome": "string",
      "position": 0,
      "prepaidTemplateLinePublicId": "string",
      "projectWork": false,
      "publicId": "string",
      "rateCents": 0,
      "ticketTypeCode": "string"
    }
  ]
}
'@

$response = Invoke-RestMethod `
    -Method Put `
    -Uri 'https://depot.example.com/api/v1/psa/templates/<templateID>/rules' `
    -Headers $headers `
    -ContentType 'application/json' `
    -Body $body

$response | ConvertTo-Json -Depth 10
```

```python
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.

import requests

url = "https://depot.example.com/api/v1/psa/templates/<templateID>/rules"
headers = {
    "X-CSRF-Token": "<csrf-token>",
    "Accept": "application/json",
    "Content-Type": "application/json",
}
cookies = {
    "outpost_console": "<console-session>",
}
payload = {
    "rules": [
        {
            "chargeTypeCode": "string",
            "description": "string",
            "outOfHours": False,
            "outcome": "string",
            "position": 0,
            "prepaidTemplateLinePublicId": "string",
            "projectWork": False,
            "publicId": "string",
            "rateCents": 0,
            "ticketTypeCode": "string",
        },
    ],
}

response = requests.put(url, headers=headers, cookies=cookies, json=payload, timeout=30)
response.raise_for_status()
print(response.json())
```

```go
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.
package main

import (
	"bytes"
	"fmt"
	"io"
	"log"
	"net/http"
)

func main() {
	body := []byte(`{
  "rules": [
    {
      "chargeTypeCode": "string",
      "description": "string",
      "outOfHours": false,
      "outcome": "string",
      "position": 0,
      "prepaidTemplateLinePublicId": "string",
      "projectWork": false,
      "publicId": "string",
      "rateCents": 0,
      "ticketTypeCode": "string"
    }
  ]
}`)

	req, err := http.NewRequest(http.MethodPut, "https://depot.example.com/api/v1/psa/templates/<templateID>/rules", bytes.NewReader(body))
	if err != nil {
		log.Fatal(err)
	}

	req.Header.Set("X-CSRF-Token", "<csrf-token>")
	req.Header.Set("Accept", "application/json")
	req.Header.Set("Content-Type", "application/json")
	req.AddCookie(&http.Cookie{Name: "outpost_console", Value: "<console-session>"})

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()

	out, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(resp.Status, string(out))
}
```

```ts
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.
// The session cookie is set by signing in. A script cannot set it itself.

const url = "https://depot.example.com/api/v1/psa/templates/<templateID>/rules";

const response = await fetch(url, {
	method: "PUT",
	headers: {
		"X-CSRF-Token": "<csrf-token>",
		"Accept": "application/json",
		"Content-Type": "application/json",
	},
	credentials: "include",
	body: JSON.stringify({
		"rules": [
			{
				"chargeTypeCode": "string",
				"description": "string",
				"outOfHours": false,
				"outcome": "string",
				"position": 0,
				"prepaidTemplateLinePublicId": "string",
				"projectWork": false,
				"publicId": "string",
				"rateCents": 0,
				"ticketTypeCode": "string"
			}
		]
	}),
});

if (!response.ok) {
	throw new Error(`Request failed with status ${response.status}`);
}

const data = await response.json();
console.log(data);
```

```csharp
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.

using System;
using System.Net.Http;
using System.Text;

using var client = new HttpClient();

const string body = """
{
  "rules": [
    {
      "chargeTypeCode": "string",
      "description": "string",
      "outOfHours": false,
      "outcome": "string",
      "position": 0,
      "prepaidTemplateLinePublicId": "string",
      "projectWork": false,
      "publicId": "string",
      "rateCents": 0,
      "ticketTypeCode": "string"
    }
  ]
}
""";

var request = new HttpRequestMessage(HttpMethod.Put, "https://depot.example.com/api/v1/psa/templates/<templateID>/rules");
request.Headers.TryAddWithoutValidation("X-CSRF-Token", "<csrf-token>");
request.Headers.TryAddWithoutValidation("Accept", "application/json");
request.Headers.TryAddWithoutValidation("Cookie", "outpost_console=<console-session>");
request.Content = new StringContent(body, Encoding.UTF8, "application/json");

var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
Console.WriteLine(await response.Content.ReadAsStringAsync());
```

### GET `/api/v1/psa/unbilled-time` {#depot.psa.listUnbilledTime}

**List unbilled time**

#### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `client` | query | string | No |  |
| `clientId` | query | string | No |  |
| `outcome` | query | string, one of covered, excluded, fixed, payg, prepaid, unclassified | No | Comma-separated set. An unrecognised value is refused. |
| `page` | query | integer | No | One-based page number. Defaults to 1. |
| `pageSize` | query | integer | No | Rows per page, 1 to 200. Defaults to 50. |
| `project` | query | string | No |  |
| `subject` | query | string | No |  |

#### Responses

| Status | Body | Description |
| --- | --- | --- |
| `200` | `application/json`, PsaFanOutPageUnbilledTimeEntryView | Success. |
| `400` | `application/problem+json`, ProblemDetails | One or more validation errors occurred. |
| `401` | `application/problem+json`, ProblemDetails | No signed-in identity. |
| `403` | `application/problem+json`, ProblemDetails | The identity does not hold psa:read. |
| `500` | `application/problem+json`, ProblemDetails | Could not list clients. |
| `503` | `application/problem+json`, ProblemDetails | Client billing is not available on this server. |

#### Request samples

```bash
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.
curl -X GET 'https://depot.example.com/api/v1/psa/unbilled-time?client=<client>&clientId=<clientId>&outcome=covered&page=0&pageSize=0&project=<project>' \
  -H 'X-CSRF-Token: <csrf-token>' \
  -H 'Accept: application/json' \
  -b 'outpost_console=<console-session>'
```

```powershell
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.

$headers = @{
    'X-CSRF-Token' = '<csrf-token>'
    'Accept' = 'application/json'
    'Cookie' = 'outpost_console=<console-session>'
}

$response = Invoke-RestMethod `
    -Method Get `
    -Uri 'https://depot.example.com/api/v1/psa/unbilled-time?client=<client>&clientId=<clientId>&outcome=covered&page=0&pageSize=0&project=<project>' `
    -Headers $headers

$response | ConvertTo-Json -Depth 10
```

```python
# Sign in first. outpost_console is a session cookie, so send it with every request.
# This operation is idempotent. Repeating it does not repeat its effect.

import requests

url = "https://depot.example.com/api/v1/psa/unbilled-time"
params = {
    "client": "<client>",
    "clientId": "<clientId>",
    "outcome": "covered",
    "page": "0",
    "pageSize": "0",
    "project": "<project>",
}
headers = {
    "X-CSRF-Token": "<csrf-token>",
    "Accept": "application/json",
}
cookies = {
    "outpost_console": "<console-session>",
}

response = requests.get(url, params=params, headers=headers, cookies=cookies, timeout=30)
response.raise_for_status()
print(response.json())
```

```go
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.
package main

import (
	"fmt"
	"io"
	"log"
	"net/http"
)

func main() {
	req, err := http.NewRequest(http.MethodGet, "https://depot.example.com/api/v1/psa/unbilled-time?client=<client>&clientId=<clientId>&outcome=covered&page=0&pageSize=0&project=<project>", nil)
	if err != nil {
		log.Fatal(err)
	}

	req.Header.Set("X-CSRF-Token", "<csrf-token>")
	req.Header.Set("Accept", "application/json")
	req.AddCookie(&http.Cookie{Name: "outpost_console", Value: "<console-session>"})

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()

	out, err := io.ReadAll(resp.Body)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println(resp.Status, string(out))
}
```

```ts
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.
// The session cookie is set by signing in. A script cannot set it itself.

const url = new URL("https://depot.example.com/api/v1/psa/unbilled-time");
url.searchParams.set("client", "<client>");
url.searchParams.set("clientId", "<clientId>");
url.searchParams.set("outcome", "covered");
url.searchParams.set("page", "0");
url.searchParams.set("pageSize", "0");
url.searchParams.set("project", "<project>");

const response = await fetch(url, {
	method: "GET",
	headers: {
		"X-CSRF-Token": "<csrf-token>",
		"Accept": "application/json",
	},
	credentials: "include",
});

if (!response.ok) {
	throw new Error(`Request failed with status ${response.status}`);
}

const data = await response.json();
console.log(data);
```

```csharp
// Sign in first. outpost_console is a session cookie, so send it with every request.
// This operation is idempotent. Repeating it does not repeat its effect.

using System;
using System.Net.Http;

using var client = new HttpClient();

var request = new HttpRequestMessage(HttpMethod.Get, "https://depot.example.com/api/v1/psa/unbilled-time?client=<client>&clientId=<clientId>&outcome=covered&page=0&pageSize=0&project=<project>");
request.Headers.TryAddWithoutValidation("X-CSRF-Token", "<csrf-token>");
request.Headers.TryAddWithoutValidation("Accept", "application/json");
request.Headers.TryAddWithoutValidation("Cookie", "outpost_console=<console-session>");

var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
Console.WriteLine(await response.Content.ReadAsStringAsync());
```
