# Support Tickets API

Open, follow and answer support tickets from a script, a CI job or an AI agent. The API does what the console does: the same fields, the same rules, the same emails.

## Before you start

- **The token needs an ability.** `support:read` lists and reads tickets. `support:write` opens tickets, replies to them and marks them resolved. New tokens do not get either by default, and tokens you created earlier do not have them. Open **API Tokens**, choose **Permissions** on the token and add them, or create a new token. A token with all abilities, such as the one `danube login` creates, can use tickets too.
- **Your email address must be verified.** Until it is, every call answers `403`.
- **Tickets belong to a project.** A call acts on the project its token is bound to. With an account-wide token, send the project's id in the `X-Team-Id` header. See [Project scope](https://docs.danubedata.ro/api-authentication#project-scope).
- **Support is always reachable.** Tickets work when your project has no payment method yet, when a payment failed, and when it is suspended for an unpaid invoice, so you can always tell us. A project we blocked or terminated gets `403` on every call, and a project we suspended can read tickets but not write them. In those cases, email support@danubedata.ro.

## Tickets and replies sent with a token are marked

A token proves that someone holds a credential, not who is asking. Tokens live in CI systems and agents, and they leak. So we treat what a token sends like a request that came by email:

- A ticket you open with a token is marked as opened with an API token, with the token's name as it was when you opened it. Our team reads that name as plain text on a line of its own: it is shortened to 40 characters, and quotation marks, line breaks and other control characters are removed.
- A reply you send with a token is marked the same way, even on a ticket you opened in the console. Our team sees which replies came from a token and which from a person signed in.
- Before we change anything or share account data because of what a token sent, we verify the sender with your [customer number, account email and support PIN](https://docs.danubedata.ro/account-settings#customer-number-and-support-pin). [Set a support PIN](https://docs.danubedata.ro/account-settings#set-a-support-pin) beforehand, or we cannot act on it.
- **We tell you when a token speaks.** The confirmation email for a ticket opened with a token says so and names the token. It does not repeat the subject or the message: read those in the console. A reply sent with a token sends you a short email that names the ticket and the token, and nothing of what was written, at most once an hour for each ticket. That is how you find out about a token you did not expect to be in use. If it is not yours, delete it under **API Tokens** and email support@danubedata.ro.
- Tickets and replies you send from the console are not affected: there we already know it is you.

Give a token `support:write` only when it needs to open or answer tickets. A token with `support:read` alone can follow a ticket without being able to start one.

## Who can see a ticket

A call reaches the tickets its user opened in the current project, and no one else's. Another member's ticket, or one of yours in another project, answers `404`, whatever your role. Internal notes that our team writes are never returned. Files are attached and read in the console only: the API neither takes nor lists them.

## Endpoints

The `{number}` in a URL is the ticket number you see in the console and in emails, such as `TICK-2026-00224`.

| Method | Endpoint | Description | Ability |
|--------|----------|-------------|---------|
| GET | `/api/v1/support-tickets` | List your tickets | `support:read` |
| POST | `/api/v1/support-tickets` | Open a ticket | `support:write` |
| GET | `/api/v1/support-tickets/{number}` | Show a ticket with its replies | `support:read` |
| POST | `/api/v1/support-tickets/{number}/replies` | Reply to a ticket | `support:write` |
| POST | `/api/v1/support-tickets/{number}/resolve` | Mark a ticket resolved | `support:write` |

## Open a ticket

```bash
curl -X POST 'https://danubedata.ro/api/v1/support-tickets' \
  -H 'Authorization: Bearer YOUR_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json' \
  -d '{
    "type": "technical",
    "priority": "high",
    "subject": "Deploys fail with an image pull error",
    "message": "Every deploy since this morning fails with ErrImagePull, on all three of our services.",
    "resource_type": "serverless",
    "resource_id": "9c8b7a6e-5d4c-3b2a-1098-76543210fedc"
  }'
```

| Field | Required | Values |
|-------|----------|--------|
| `type` | Yes | `general`, `technical`, `billing` or `sales` |
| `priority` | Yes | `low`, `normal`, `high` or `urgent` |
| `subject` | Yes | Up to 255 characters |
| `message` | Yes | 10 to 10,000 characters |
| `resource_type` | No | `cache`, `database`, `vps`, `storage`, `queue`, `serverless`, `static_site` or `app` |
| `resource_id` | With `resource_type` | The resource's UUID |

Send JSON. A request that names `attachments` is refused with `422`: add files in the console.

A resource that is not in the project is left off, as it is in the console, and the ticket is still opened. Check `resource` in the response to see what it was linked to.

Text is kept exactly as you send it. Nothing is stripped, and every page and email that shows it escapes it. The one exception is the email we send you about a ticket a token opened, which leaves the subject and the message out.

**Response** (`201 Created`)

```json
{
  "message": "Support ticket created",
  "ticket": {
    "number": "TICK-2026-00224",
    "subject": "Deploys fail with an image pull error",
    "type": "technical",
    "priority": "high",
    "status": "open",
    "resource": {
      "type": "serverless",
      "id": "9c8b7a6e-5d4c-3b2a-1098-76543210fedc",
      "name": "api"
    },
    "created_at": "2026-09-29T08:12:45+00:00",
    "updated_at": "2026-09-29T08:12:45+00:00",
    "message": "Every deploy since this morning fails with ErrImagePull, on all three of our services.",
    "opened_by_support": false,
    "replies": []
  }
}
```

`resource` is `null` for a ticket about nothing in particular. Its `name` is `null` once the resource has been deleted. On a ticket our team opened about your account limits, `type` is `account_limit`, `id` is `null` and `name` is `Your account limits`. The list and a single ticket show `resource` the same way.

## List your tickets

```bash
curl 'https://danubedata.ro/api/v1/support-tickets?status=waiting_customer' \
  -H 'Authorization: Bearer YOUR_TOKEN' \
  -H 'Accept: application/json'
```

Newest first, 15 to a page.

| Query | Values |
|-------|--------|
| `status` | `open`, `in_progress`, `waiting_customer`, `resolved` or `closed`. Anything else is refused with `422`. |
| `per_page` | 1 to 100. Default 15. |
| `page` | The page to read. Default 1. |

`waiting_customer` is a ticket that support has answered and now waits on you for.

```json
{
  "data": [
    {
      "number": "TICK-2026-00224",
      "subject": "Deploys fail with an image pull error",
      "type": "technical",
      "priority": "high",
      "status": "waiting_customer",
      "resource": null,
      "created_at": "2026-09-29T08:12:45+00:00",
      "updated_at": "2026-09-29T09:40:02+00:00"
    }
  ],
  "pagination": {
    "current_page": 1,
    "last_page": 1,
    "per_page": 15,
    "total": 1
  }
}
```

## Show a ticket

```bash
curl 'https://danubedata.ro/api/v1/support-tickets/TICK-2026-00224' \
  -H 'Authorization: Bearer YOUR_TOKEN' \
  -H 'Accept: application/json'
```

You get the ticket as `data` shows it, plus its first message and every reply, oldest first.

```json
{
  "ticket": {
    "number": "TICK-2026-00224",
    "subject": "Deploys fail with an image pull error",
    "type": "technical",
    "priority": "high",
    "status": "waiting_customer",
    "resource": null,
    "created_at": "2026-09-29T08:12:45+00:00",
    "updated_at": "2026-09-29T09:40:02+00:00",
    "message": "Every deploy since this morning fails with ErrImagePull, on all three of our services.",
    "opened_by_support": false,
    "replies": [
      {
        "id": 8121,
        "message": "Which registry do the three services pull from?",
        "from_support": true,
        "author": "Adrian",
        "created_at": "2026-09-29T09:40:02+00:00"
      }
    ]
  }
}
```

- `from_support` says which side wrote a reply. `author` is the agent's name, or `DanubeData support` when none is named, as the console shows it.
- `opened_by_support` is `true` for a ticket our team opened for you, such as the answer to a limit increase request. Its first message is ours.
- Reading a ticket over the API does not mark our replies as read in the console, so the person who uses the console still sees what is new.

## Reply to a ticket

```bash
curl -X POST 'https://danubedata.ro/api/v1/support-tickets/TICK-2026-00224/replies' \
  -H 'Authorization: Bearer YOUR_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json' \
  -d '{"message": "All three pull from cr.danubedata.ro."}'
```

`message` is 1 to 10,000 characters. A reply is always public: it is yours, and never an internal note.

A ticket that was waiting on you, or that was resolved or closed, is open again, and we are told by email. The email and the ticket page in our admin say that the reply came from an API token, and name it. You get a short email about it too, at most once an hour for each ticket: see [above](#tickets-and-replies-sent-with-a-token-are-marked).

**Response** (`201 Created`)

```json
{
  "message": "Reply sent",
  "reply": {
    "id": 8122,
    "message": "All three pull from cr.danubedata.ro.",
    "from_support": false,
    "author": "Ana Pop",
    "created_at": "2026-09-29T09:52:10+00:00"
  },
  "ticket": {
    "number": "TICK-2026-00224",
    "subject": "Deploys fail with an image pull error",
    "type": "technical",
    "priority": "high",
    "status": "open",
    "resource": null,
    "created_at": "2026-09-29T08:12:45+00:00",
    "updated_at": "2026-09-29T09:52:10+00:00"
  }
}
```

## Mark a ticket resolved

```bash
curl -X POST 'https://danubedata.ro/api/v1/support-tickets/TICK-2026-00224/resolve' \
  -H 'Authorization: Bearer YOUR_TOKEN' \
  -H 'Accept: application/json'
```

The response is `{"message": "Ticket marked as resolved", "ticket": {...}}`. A resolved ticket is open again when you reply to it. A ticket that is already resolved or closed answers `409`.

## Retry safely

If the connection drops before you read the answer, you cannot tell whether the ticket was opened or the reply sent. Send an `Idempotency-Key` header with `POST /api/v1/support-tickets` and with `POST /api/v1/support-tickets/{number}/replies`, and retry with the same key and the same body. You get the first answer back, marked `Idempotent-Replay: true`, and nothing is opened or sent twice, so our team is not emailed again.

```bash
curl -X POST 'https://danubedata.ro/api/v1/support-tickets/TICK-2026-00224/replies' \
  -H 'Authorization: Bearer YOUR_TOKEN' \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json' \
  -H 'Idempotency-Key: 6f1c1b0e-0f3b-4f57-9d0a-3f0c9e0a5a11' \
  -d '{"message": "All three pull from cr.danubedata.ro."}'
```

- Use a new key for every request you mean to be different, such as a UUID. A key is up to 255 characters.
- A key is yours alone. Another member of your project who sends the same key makes a request of their own, and does not get your ticket back.
- A key is remembered for the address it was used on (a reply to another ticket is another request), and for at least a day. Never reuse a key for a different request, even much later: it may still be remembered.
- The same key with a different body answers `409` with `error.code` `idempotency.key_reused`. While the first request is still running, the answer is `409` with `idempotency.in_progress` and `"retryable": true`: try again in a moment.
- Only a successful answer is kept. A request that failed, with `422` for instance, does not use up its key: correct it and send it again with the same key.
- A replay counts towards the hourly limits below, because they are counted before we read the key.

```json
{
  "success": false,
  "data": null,
  "error": {
    "code": "idempotency.key_reused",
    "message": "This Idempotency-Key was already used with a different request body. Use a new key for a new request.",
    "retryable": false
  },
  "meta": {}
}
```

## Limits

Each person can open 10 tickets and send 60 replies an hour, counted across all of their tokens and projects: a second token does not add to it. A request that a token is refused for, because it lacks `support:write`, is not counted. Reading is not held to these two limits, but the [API rate limits](https://docs.danubedata.ro/api-rate-limits) of your account apply to every call.

Past a limit the API answers `429`, with the seconds to wait in `retry_after` and in the `Retry-After` header:

```json
{
  "message": "You can open 10 tickets an hour with the API. Try again in 42 minutes, or email support@danubedata.ro.",
  "retry_after": 2520
}
```

## Wait for our answer

Our replies reach you by email as usual. To follow them from a script, ask for the tickets that wait on you and read each one:

```bash
curl 'https://danubedata.ro/api/v1/support-tickets?status=waiting_customer' \
  -H 'Authorization: Bearer YOUR_TOKEN' \
  -H 'Accept: application/json'
```

A few minutes between calls is plenty: a person answers a ticket, and a loop that asks every second only spends your rate limit.

## Errors

| Status | When |
|--------|------|
| `400` | The `Idempotency-Key` is longer than 255 characters. |
| `401` | The token is missing, wrong or revoked. |
| `403` | The token lacks `support:read` or `support:write`, your email address is not verified, the token is bound to another project than the one you named, or the project is blocked, terminated or suspended. |
| `404` | No such ticket among yours in the current project. |
| `409` | Resolving a ticket that is already resolved or closed, or an `Idempotency-Key` that was used with another body or is still running. |
| `422` | A field failed validation, or the request names `attachments`. The body lists each field in `errors`. |
| `429` | You reached the limit for new tickets or replies. |

A validation error looks like this:

```json
{
  "message": "Validation failed.",
  "errors": {
    "type": ["The type must be general, technical, billing or sales."]
  }
}
```

## Next steps

- [API Authentication](https://docs.danubedata.ro/api-authentication): create a token and choose its abilities
- [API Rate Limits](https://docs.danubedata.ro/api-rate-limits): limits and how to back off
- [Customer number and support PIN](https://docs.danubedata.ro/account-settings#customer-number-and-support-pin): what we ask for before we act on a request that did not come through the console
