> ## Documentation Index
> Fetch the complete documentation index at: https://docs.server.express/llms.txt
> Use this file to discover all available pages before exploring further.

# Billing and tickets

> Read invoices, download PDFs, pay from account credit, merge open invoices, and open or reply to support tickets, with attachments, from your monitoring and accounting jobs.

All amounts on this page are integers in minor units. `16200` is £162.00. That's how money is stored everywhere in this API, because a float is how a penny goes missing.

## Invoices

### Listing

```bash theme={null}
curl https://my.server.express/api/v1/client/invoices \
  -H "Authorization: Bearer $SE_TOKEN"
```

For one invoice, use `GET /v1/client/invoices/{id}`. Both need `invoices:read`. The single-invoice response includes line items and payment status, the same as the client area shows.

### PDFs

```bash theme={null}
curl https://my.server.express/api/v1/client/invoices/$ID/pdf \
  -H "Authorization: Bearer $SE_TOKEN" \
  -o invoice.pdf
```

This returns the PDF itself, not JSON. It also needs `invoices:read`.

If you file invoices for accounting, this one's worth automating. A weekly job that pulls anything new beats remembering to download them.

### Paying

```bash theme={null}
curl -X POST https://my.server.express/api/v1/client/invoices/$ID/pay \
  -H "Authorization: Bearer $SE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"method":"credit"}'
```

Requires `invoices:pay`. It's one of the scopes that can spend money, because **this spends real money**.

`method` is one of:

| Method | What happens |
| - | - |
| `credit` | Pays from your account credit balance. Completes immediately if there's enough. |
| `stripe` | Returns a Stripe Checkout URL for a person to open. This is the default if you leave `method` out. |
| `crypto` | Returns a deposit address and an amount to send. |

Only `credit` finishes inside the API call. The other two hand back something a person has to act on:

```json theme={null}
{
  "success": true,
  "data": { "method": "stripe", "url": "https://checkout.stripe.com/..." }
}
```

Credit is held in GBP. If the invoice is in another currency, we convert at the current rate before checking your balance. If there isn't enough credit, you get a `400` that says how much the invoice needs and how much you have. If we can't get a rate, you get a `503`. Try again shortly.

For crypto, you get the gateway, the deposit address, the amount and a `paymentId`. Poll it with:

```text theme={null}
GET /v1/client/invoices/{id}/pay?paymentId=...
```

That poll is only for showing a status. The gateway's webhook is what actually settles the invoice, so poll to show progress, never to decide whether the money arrived.

> [!TIP]
>
> If you want payments to happen without anyone clicking, use `credit`. Keep the balance topped up and let your automation draw from it.

### Merging invoices

```bash theme={null}
curl -X POST https://my.server.express/api/v1/client/invoices/consolidate \
  -H "Authorization: Bearer $SE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"invoiceIds":["cm4k...","cm4l..."]}'
```

This turns several open invoices into one new invoice you can pay in a single go. Requires `invoices:pay`. An ID that isn't yours isn't found.

### Bank transfer

`GET /v1/client/invoices/{id}/bank-transfer` (`invoices:read`) returns where to send the money, how much, and the reference to quote. `POST` to the same path (`invoices:pay`) tells us you've sent it, so staff know to watch for it.

The `POST` doesn't mark anything paid. A transfer settles when the money actually lands, either when staff match it or when the bank feed picks up the reference. It's limited to 6 calls a minute.

## Tickets

### Reading

```bash theme={null}
curl https://my.server.express/api/v1/client/tickets \
  -H "Authorization: Bearer $SE_TOKEN"
```

`GET /v1/client/tickets/{id}` returns one ticket with the full reply thread. Both need `tickets:read`.

### Opening one

```bash theme={null}
curl -X POST https://my.server.express/api/v1/client/tickets \
  -H "Authorization: Bearer $SE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
        "subject": "Disk latency on web-01",
        "message": "Seeing 200ms+ fsync since 14:00 UTC.",
        "priority": "high"
      }'
```

Requires `tickets:write`. The response carries the new ticket's `id`.

* `subject` and `message` are required and can't be blank.
* `priority` is `low`, `normal`, `high` or `critical`, and defaults to `normal`. An unrecognised value quietly becomes `normal` rather than failing the request.
* `department` is optional. If you leave it out, or send a slug we don't recognise, the ticket goes to `general`.
* `attachmentIds` is optional. See [attachments](#attachments).

### Replying

```bash theme={null}
curl -X POST https://my.server.express/api/v1/client/tickets/$ID/messages \
  -H "Authorization: Bearer $SE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"body":"Resolved after the controller firmware update. Thanks."}'
```

Requires `tickets:write`. Note that the field is `body`, not `message`. It's different from ticket creation, and that inconsistency stays until v2. A reply can be files only, as long as you send `attachmentIds`.

Posting to a closed ticket returns `400`. Open a new one instead.

### Attachments

Attaching a file takes two steps. First, upload the file and get an ID back. Then name that ID in a new ticket or a reply.

```bash theme={null}
curl -X POST "https://my.server.express/api/v1/client/ticket-attachments?name=dmesg.log" \
  -H "Authorization: Bearer $SE_TOKEN" \
  --data-binary @dmesg.log
```

The body is the file's raw bytes, not JSON or multipart. You get `201` with the attachment's `id`, `filename`, `contentType` and `size`. Then post it:

```json theme={null}
{ "body": "Kernel log attached.", "attachmentIds": ["cm4m..."] }
```

The rules:

* Up to 10 MB per file and 5 files per message. You can have up to 20 uploads waiting to be sent.
* Accepted file types: png, jpg, gif, webp, pdf, zip, gz, tgz, txt, log, csv, json, conf, yml and yaml. The file's contents have to match its extension. SVG and HTML are refused, because both can run script.
* Uploads are limited to 20 a minute.
* `DELETE /v1/client/ticket-attachments/{id}` takes back an upload you haven't sent yet.
* `GET /v1/client/tickets/{id}/attachments/{attachmentId}` downloads one (`tickets:read`).

Uploading and deleting need `tickets:write`.

### Alerting into tickets

The obvious use is having your monitoring open a ticket when something breaks:

```bash theme={null}
#!/usr/bin/env bash
# Called by the alerting webhook.
curl -sf -X POST "https://my.server.express/api/v1/client/tickets" \
  -H "Authorization: Bearer $SE_TOKEN" \
  -H 'Content-Type: application/json' \
  -d "$(jq -n --arg s "$ALERT_TITLE" --arg m "$ALERT_BODY" \
        '{subject: $s, message: $m, priority: "high"}')"
```

Two bits of advice, learned from tickets we've received:

1. **Put the service ID and a timestamp in the body.** A ticket that just says "server down" costs a round trip before anyone can help.
2. **De-duplicate on your side.** A flapping check that opens forty tickets overnight gets your alerts muted, not your problem fixed faster.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.