# Internal Notes and Secrets

https://dev.wisecp.com/es/internal-notes-and-secrets

The nine endpoints for staff-only notes and the secrets kept on a ticket.

## Overview

These nine endpoints deal with the side of a ticket the **client never sees**. Six manage staff-only notes and their files. Three hold labelled secrets, which is where things shared during a conversation, such as server access, belong.

Both live on the ticket's **own row**, encrypted. Neither has a table of its own, so neither has paging or filtering: the list always arrives whole.

Adding a note raises no notice and leaves the ticket status alone. When you want text that stays out of the client's view yet belongs to the **conversation itself**, what you want is a hidden reply rather than a note.

## Reference

### Listing the Notes

get/api/v1/admin/tickets/{id}/notes

`Tickets/GetTicketNotes` admin

Returns the notes staff left on a ticket.

Response fields data[] — 8

idstringThe note id. A sixteen-character string rather than a position.

author_idintWho wrote it.

messagestringThe note text.

pinnedboolWhether it is pinned. Pinned notes sit at the top in the panel.

aiboolWhether an assistant wrote it. The panel shows it under a different author.

legacyboolWhether it came from the older format.

created_atstring | nullWhen it was written.

attachmentsarrayThe attachments: id, shown name, stored name and size.

Errors 2

not_found404No such ticket.

insufficient_scope403The key lacks the required scope.

Request cURL JavaScript PHP (HTTP) PHP (Internal)

```bash
curl 'https://panel.example.com/api/v1/admin/tickets/402/notes' \
  -H "Authorization: Bearer $API_KEY"
```

```javascript
const res  = await fetch(`https://panel.example.com/api/v1/admin/tickets/${id}/notes`, {
  headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();
```

```php
$ch = curl_init('https://panel.example.com/api/v1/admin/tickets/' . $id . '/notes');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . $apiKey],
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
```

```php
// There is NO paging: every note comes at once, decoded from the ticket's own row.
$notes = Api::Tickets()->GetTicketNotes(['id' => $id])['data'];
```

### Adding a Note

post/api/v1/admin/tickets/{id}/notes

`Tickets/AddTicketNote` admin

Leaves a staff-only note on a ticket.

Body 4

messagestringreqThe note text.

pinnedboolPins the note.

aiboolMarks the note as written by an assistant.

attachmentsstring | object | arrayFiles to attach.

Response fields 201 — data

dataobjectThe note added. Same shape as a list item.

Errors 4

message_required422The note text is empty.

blocked_by_gate422A hook refused the note.

not_found404No such ticket.

insufficient_scope403The key lacks the required scope.

Request cURL JavaScript PHP (HTTP) PHP (Internal)

```bash
curl -X POST 'https://panel.example.com/api/v1/admin/tickets/402/notes' \
  -H "Authorization: Bearer $API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"message":"Muhasebeye devredildi.","pinned":true}'
```

```javascript
const res = await fetch(`https://panel.example.com/api/v1/admin/tickets/${id}/notes`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${apiKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    message: 'Escalated to the billing team.',
    pinned: true,
  }),
});

const body = await res.json();
```

```php
$ch = curl_init('https://panel.example.com/api/v1/admin/tickets/' . $id . '/notes');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'message' => 'Escalated to the billing team.',
        'pinned'  => true,
    ]),
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
```

```php
// A note NEVER REACHES THE CLIENT and raises no notice; only staff see it.
$note = Api::Tickets()->AddTicketNote([
    'id'      => $id,
    'message' => 'Escalated to the billing team.',
])['data'];
```

### Updating a Note

patch/api/v1/admin/tickets/{id}/notes/{note_id}

`Tickets/UpdateTicketNote` admin

Changes a note's text or whether it is pinned.

Body 2

messagestringThe new text.

pinnedboolPins it, or unpins it.

Response fields data

dataobjectThe note as it now reads. Same shape as a list item.

Errors 5

no_changes422No field was given. An empty body does not pass quietly.

message_required422The note text is empty.

update_failed422The update could not be written.

not_found404No such ticket or note.

insufficient_scope403The key lacks the required scope.

Request cURL JavaScript PHP (HTTP) PHP (Internal)

```bash
curl -X PATCH 'https://panel.example.com/api/v1/admin/tickets/402/notes/1d9e35094420c723' \
  -H "Authorization: Bearer $API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"pinned":false}'
```

```javascript
const res = await fetch(`https://panel.example.com/api/v1/admin/tickets/${id}/notes/${noteId}`, {
  method: 'PATCH',
  headers: {
    Authorization: `Bearer ${apiKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ pinned: false }),
});

const body = await res.json();
```

```php
$ch = curl_init('https://panel.example.com/api/v1/admin/tickets/' . $id . '/notes/' . $noteId);
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST  => 'PATCH',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode(['pinned' => false]),
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
```

```php
// An empty body is an ERROR rather than a no-op; send at least one field.
Api::Tickets()->UpdateTicketNote([
    'id' => $id, 'note_id' => $noteId, 'pinned' => false,
]);
```

### Deleting a Note

delete/api/v1/admin/tickets/{id}/notes/{note_id}

`Tickets/DeleteTicketNote` admin

Removes a note along with its files.

Response fields data — 2

deletedboolWhether the delete ran.

note_idstringThe id of the note removed.

Errors 3

delete_failed422The delete could not be written.

not_found404No such ticket or note.

insufficient_scope403The key lacks the required scope.

Request cURL JavaScript PHP (HTTP) PHP (Internal)

```bash
curl -X DELETE 'https://panel.example.com/api/v1/admin/tickets/402/notes/1d9e35094420c723' \
  -H "Authorization: Bearer $API_KEY"
```

```javascript
const res = await fetch(`https://panel.example.com/api/v1/admin/tickets/${id}/notes/${noteId}`, {
  method: 'DELETE',
  headers: { Authorization: `Bearer ${apiKey}` },
});

const body = await res.json();
```

```php
$ch = curl_init('https://panel.example.com/api/v1/admin/tickets/' . $id . '/notes/' . $noteId);
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST  => 'DELETE',
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . $apiKey],
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
```

```php
// The note's attachments go too: dropped from the record and removed from disk.
Api::Tickets()->DeleteTicketNote(['id' => $id, 'note_id' => $noteId]);
```

### Downloading a Note Attachment

get/api/v1/admin/tickets/{id}/notes/{note_id}/attachments/{att_id}

`Tickets/GetTicketNoteAttachment` admin

Returns a file attached to a note, with its content.

Response fields data — 6

idstringThe attachment id.

namestringThe name it shows under.

file_namestringThe name it is stored under.

file_sizeintThe file size.

mimestringThe file type.

content_base64stringThe file content. It arrives turned into text.

Errors 2

not_found404No such attachment, or its file is missing.

insufficient_scope403The key lacks the required scope.

Request cURL JavaScript PHP (HTTP) PHP (Internal)

```bash
curl 'https://panel.example.com/api/v1/admin/tickets/402/notes/1d9e35094420c723/attachments/71c918b7ff525444' \
  -H "Authorization: Bearer $API_KEY"
```

```javascript
const res  = await fetch(
  `https://panel.example.com/api/v1/admin/tickets/${id}/notes/${noteId}/attachments/${attId}`,
  { headers: { Authorization: `Bearer ${apiKey}` } },
);
const { data } = await res.json();
```

```php
$url = 'https://panel.example.com/api/v1/admin/tickets/' . $id . '/notes/' . $noteId . '/attachments/' . $attId;

$ch = curl_init($url);
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . $apiKey],
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
```

```php
// Note attachments live on the ticket row and are NOT fetched from the reply endpoint.
$att = Api::Tickets()->GetTicketNoteAttachment([
    'id' => $id, 'note_id' => $noteId, 'att_id' => $attId,
])['data'];
```

### Deleting a Note Attachment

delete/api/v1/admin/tickets/{id}/notes/{note_id}/attachments/{att_id}

`Tickets/DeleteTicketNoteAttachment` admin

Removes a note attachment from its record and from disk.

Response fields data — 2

deletedboolWhether the delete ran.

attachment_idstringThe id of the attachment removed.

Errors 2

not_found404No such ticket, note or attachment.

insufficient_scope403The key lacks the required scope.

Request cURL JavaScript PHP (HTTP) PHP (Internal)

```bash
curl -X DELETE 'https://panel.example.com/api/v1/admin/tickets/402/notes/1d9e35094420c723/attachments/71c918b7ff525444' \
  -H "Authorization: Bearer $API_KEY"
```

```javascript
const res = await fetch(
  `https://panel.example.com/api/v1/admin/tickets/${id}/notes/${noteId}/attachments/${attId}`,
  { method: 'DELETE', headers: { Authorization: `Bearer ${apiKey}` } },
);

const body = await res.json();
```

```php
$url = 'https://panel.example.com/api/v1/admin/tickets/' . $id . '/notes/' . $noteId . '/attachments/' . $attId;

$ch = curl_init($url);
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST  => 'DELETE',
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . $apiKey],
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
```

```php
// The file goes while the note stays; deleting the note takes its files along.
Api::Tickets()->DeleteTicketNoteAttachment([
    'id' => $id, 'note_id' => $noteId, 'att_id' => $attId,
]);
```

### Listing the Secrets

get/api/v1/admin/tickets/{id}/private-data

`Tickets/GetTicketPrivateData` admin content comes back in the clear

Returns the labelled secrets kept on a ticket.

Response fields data[] — 5

idstringThe item id.

labelstringWhat the item is.

contentstringThe item itself. Stored encrypted and handed back decoded.

reply_idintThe reply it belongs to. Zero means it belongs to the ticket as a whole.

created_atstring | nullWhen it was added.

Errors 2

not_found404No such ticket.

insufficient_scope403The key lacks the required scope.

Request cURL JavaScript PHP (HTTP) PHP (Internal)

```bash
curl 'https://panel.example.com/api/v1/admin/tickets/402/private-data' \
  -H "Authorization: Bearer $API_KEY"
```

```javascript
const res  = await fetch(`https://panel.example.com/api/v1/admin/tickets/${id}/private-data`, {
  headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();
```

```php
$ch = curl_init('https://panel.example.com/api/v1/admin/tickets/' . $id . '/private-data');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . $apiKey],
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
```

```php
// The content comes back IN THE CLEAR: a client that logs this call logs the password too.
$items = Api::Tickets()->GetTicketPrivateData(['id' => $id])['data'];
```

### Adding a Secret

post/api/v1/admin/tickets/{id}/private-data

`Tickets/AddTicketPrivateData` admin

Adds a labelled secret to a ticket.

Body 3

labelstringreqWhat the item is.

contentstringreqThe item itself. It gets encrypted on the way in.

reply_idintTies it to a particular reply.

Response fields 201 — data

dataobjectThe item added. Same shape as a list item.

Errors 3

label_content_required422The label or the content is empty.

not_found404No such ticket.

insufficient_scope403The key lacks the required scope.

Request cURL JavaScript PHP (HTTP) PHP (Internal)

```bash
curl -X POST 'https://panel.example.com/api/v1/admin/tickets/402/private-data' \
  -H "Authorization: Bearer $API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"label":"Sunucu erisimi","content":"host: 203.0.113.10"}'
```

```javascript
const res = await fetch(`https://panel.example.com/api/v1/admin/tickets/${id}/private-data`, {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${apiKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    label: 'Server SSH',
    content: 'host: 203.0.113.10\nuser: admin',
  }),
});

const body = await res.json();
```

```php
$ch = curl_init('https://panel.example.com/api/v1/admin/tickets/' . $id . '/private-data');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'label'   => 'Server SSH',
        'content' => "host: 203.0.113.10\nuser: admin",
    ]),
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
```

```php
// Give a reply id to tie it there; zero leaves it on the ticket as a whole.
Api::Tickets()->AddTicketPrivateData([
    'id'       => $id,
    'label'    => 'Server SSH',
    'content'  => $credentials,
    'reply_id' => $replyId,
]);
```

### Deleting a Secret

delete/api/v1/admin/tickets/{id}/private-data/{item_id}

`Tickets/DeleteTicketPrivateData` admin

Removes one secret from a ticket.

Response fields data — 2

deletedboolWhether the delete ran.

item_idstringThe id of the item removed.

Errors 2

not_found404No such ticket or item.

insufficient_scope403The key lacks the required scope.

Request cURL JavaScript PHP (HTTP) PHP (Internal)

```bash
curl -X DELETE 'https://panel.example.com/api/v1/admin/tickets/402/private-data/pd_65a4f0c8a1b23' \
  -H "Authorization: Bearer $API_KEY"
```

```javascript
const res = await fetch(`https://panel.example.com/api/v1/admin/tickets/${id}/private-data/${itemId}`, {
  method: 'DELETE',
  headers: { Authorization: `Bearer ${apiKey}` },
});

const body = await res.json();
```

```php
$ch = curl_init('https://panel.example.com/api/v1/admin/tickets/' . $id . '/private-data/' . $itemId);
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST  => 'DELETE',
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . $apiKey],
]);

$body = json_decode(curl_exec($ch), true);
curl_close($ch);
```

```php
// REMOVING credentials once a ticket closes is a good habit; the record otherwise stays.
Api::Tickets()->DeleteTicketPrivateData(['id' => $id, 'item_id' => $itemId]);
```

## Pitfalls

> **Secrets come back as plain text**
> 
> These items sit encrypted in the database, yet the read endpoint hands the content back **decoded**. A key carrying this scope reads whatever server passwords are stored there. A client that logs its requests and responses logs the password with them, so keep the key narrow and keep this call's body out of your logs.

> **All of it sits in one encrypted block**
> 
> Notes and secrets are not separate rows but **one encrypted field** on the ticket row. Every write saves that whole block again. When two requests add a note at the same moment the second can write over the first, so avoid writing to one ticket in parallel during batch work.

> **The ids are not positions**
> 
> Notes and their attachments are addressed by a **string**, not by where they sit in the list. The older panel used the position, and adding or removing a note shifted it. Keep a position on your side and one day you will update a different note, so store the id that comes back.

> **An empty update is an error**
> 
> Send no field at all on a note update and the call is **refused**. The ticket update does not behave that way, where an unchanged field passes quietly. A sync script that forwards whatever it holds hits an error here when nothing changed. Check you really have a field before you send.

> **A merge does not carry notes and secrets across**
> 
> Merging tickets moves replies and attachments to the main ticket and then **deletes** the other ticket rows. Notes and secrets live on those rows, so they go with them. The same holds when you delete a ticket. If the only copy of a server password sits there, read it and write it onto the main ticket before you merge.

## Related Articles

- [Ticket Replies](https://dev.wisecp.com/en/ticket-replies)
- [Managing Tickets](https://dev.wisecp.com/en/managing-tickets)
- [Ticket Custom Field Definitions](https://dev.wisecp.com/en/ticket-custom-field-definitions)
