# API Credentials

https://dev.wisecp.com/es/api-credentials

The seven endpoints that produce API access keys, limit them and watch their requests.

## Overview

These seven endpoints run **who can reach the API**. They produce keys, set what those keys allow, limit where they work from, and show who called what.

The key itself is **stored as a digest**. The full value appears once, on the creation response, and no endpoint returns it afterwards. A lost key is not recovered, it is replaced.

Permissions are written as scopes: a single operation, or a wildcard for a whole resource. Giving a key only the scopes it truly needs is the one real piece of advice in this article.

Every key belongs to the staff account that made it. A key can never do more than that account may do in the panel. The check runs on **every request**, so narrowing someone's privileges narrows their keys at the same moment.

## Reference

### Listing the Keys

get/api/v1/admin/settings/api-credentials

`Settings/GetApiCredentials` admin returned masked

Returns the keys that grant access to this API.

Query parameters 3

pageintDefaults to 1.

limitintDefaults to 25, maximum 100.

searchstringSearches the keys.

Response fields data[] — 11

idintId of the credential.

namestringThe key's name. For you only, so you remember where it is used.

typestringWho made it: `admin` for staff, `client` for a customer. Customer keys are managed from the client area.

ownerobjectThe account the key belongs to, as `id` and `name`. Its privileges cap what the key can reach.

token_previewstringThe key, masked. The full value **cannot** be recovered; only a digest is stored.

permissionsstring[]The scopes allowed. A single operation, or a wildcard for a whole resource.

ipsstring[]The addresses allowed. Empty means it works from anywhere.

rate_limitintThe per-minute request limit for this key. Zero uses the general default.

created_atstring | nullWhen it was created.

updated_atstring | nullWhen it last changed.

last_accessstring | nullWhen it was last used. Empty means it never has been.

Meta 4

totalintHow many keys there are.

pageintThe page you are on.

limitintThe page size.

next_pageintThe next page. Zero means you are on the last one.

Errors 1

insufficient_scope403The key lacks the required scope.

Request cURL JavaScript PHP (HTTP) PHP (Internal)

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

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

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

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

```php
// Keys never used are candidates for cleanup: an empty last-access means nobody has touched it.
$keys   = Api::Settings()->GetApiCredentials()['data'];
$unused = array_filter($keys, fn (array $k): bool => $k['last_access'] === null);
```

### Creating a Key

post/api/v1/admin/settings/api-credentials

`Settings/CreateApiCredential` admin the key is shown once

Produces a new access key. The full value comes back **on this response only**.

Body 4

namestringrequiredThe key's name.

permissionsstring[]requiredThe scopes to allow. At least one is needed; a wildcard opens every operation on a resource.

ipsstring[] | stringThe addresses to allow. Either an array or text, one per line.

rate_limitintThe per-minute request limit. Zero goes back to the general default.

Response fields data — 10

idintId of the credential.

namestringThe key's name. For you only, so you remember where it is used.

token_previewstringThe key, masked. The full value **cannot** be recovered; only a digest is stored.

permissionsstring[]The scopes allowed. A single operation, or a wildcard for a whole resource.

ipsstring[]The addresses allowed. Empty means it works from anywhere.

rate_limitintThe per-minute request limit for this key. Zero uses the general default.

created_atstring | nullWhen it was created.

updated_atstring | nullWhen it last changed.

last_accessstring | nullWhen it was last used. Empty means it never has been.

api_keystringThe key in full. It appears here and nowhere else; lose it and the only way back is a new key.

Errors 4

name_required422The name was empty.

permissions_required422No permission was given.

permissions_exceed_owner422None of the scopes you asked for are covered by the owning account's privileges.

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/settings/api-credentials' \
  -H "Authorization: Bearer $API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"name":"Integration","permissions":["Clients/*","Services/GetServices"],"ips":[],"rate_limit":300}'
```

```javascript
const res  = await fetch('https://panel.example.com/api/v1/admin/settings/api-credentials', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${apiKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    name: 'Integration',
    permissions: ['Clients/*', 'Services/GetServices'],
    rate_limit: 300,
  }),
});

const body = await res.json();

// Store it now: this is the only time it is shown.
const newKey = body.data.api_key;
```

```php
$ch = curl_init('https://panel.example.com/api/v1/admin/settings/api-credentials');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'name'        => 'Integration',
        'permissions' => ['Clients/*'],
        'rate_limit'  => 300,
    ]),
]);

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

```php
// The full key comes back HERE only; miss it now and you can never read it again.
$cred = Api::Settings()->CreateApiCredential([
    'name'        => 'Integration',
    'permissions' => ['Clients/*'],
])['data'];

$secret = $cred['api_key'] ?? null;   // absent on every later read
```

Response 201 422

```json
{
  "data": {
    "id": 13,
    "name": "Integration",
    "token_preview": "wak_a1b2c3d4e••••••••",
    "permissions": ["Clients/*"],
    "ips": [],
    "rate_limit": 300,
    "api_key": "wak_a1b2c3d4e5f6..."
  }
}
```

```json
{
  "error": {
    "code": "permissions_required",
    "message": "At least one permission is required."
  }
}
```

### Key Detail

get/api/v1/admin/settings/api-credentials/{cid}

`Settings/GetApiCredential` admin

Returns one key. The schema matches a list item and the key is still masked.

Response fields data — 11

idintId of the credential.

namestringThe key's name. For you only, so you remember where it is used.

typestringWho made it: `admin` for staff, `client` for a customer. Customer keys are managed from the client area.

ownerobjectThe account the key belongs to, as `id` and `name`. Its privileges cap what the key can reach.

token_previewstringThe key, masked. The full value **cannot** be recovered; only a digest is stored.

permissionsstring[]The scopes allowed. A single operation, or a wildcard for a whole resource.

ipsstring[]The addresses allowed. Empty means it works from anywhere.

rate_limitintThe per-minute request limit for this key. Zero uses the general default.

created_atstring | nullWhen it was created.

updated_atstring | nullWhen it last changed.

last_accessstring | nullWhen it was last used. Empty means it never has been.

Errors 2

not_found404No such credential.

insufficient_scope403The key lacks the required scope.

Request cURL JavaScript PHP (HTTP) PHP (Internal)

```bash
curl 'https://panel.example.com/api/v1/admin/settings/api-credentials/12' \
  -H "Authorization: Bearer $API_KEY"
```

```javascript
const res  = await fetch('https://panel.example.com/api/v1/admin/settings/api-credentials/12', {
  headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();
```

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

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

```php
// The detail does NOT give the full key either: a lost key is not recovered, it is replaced.
$cred = Api::Settings()->GetApiCredential(['cid' => 12])['data'];
```

### Updating a Key

patch/api/v1/admin/settings/api-credentials/{cid}

`Settings/UpdateApiCredential` admin permissions are written whole

Applies the fields you send and leaves the rest as they were: the name, the permissions, the address list and the request limit. The key itself is untouched.

Body 4

namestringThe key's name. Leave it out and the current name stays.

permissionsstring[]The scopes to allow. Leave them out and the current list stays; sending them replaces the set whole, and an empty list is refused. A wildcard opens every operation on a resource.

ipsstring[] | stringThe addresses to allow. Either an array or text, one per line.

rate_limitintThe per-minute request limit. Zero goes back to the general default.

Response fields data — 11

dataobjectThe credential as it now stands. Same shape as an item in the key list; the key itself is unchanged and still masked.

Errors 7

not_found404No such credential.

not_credential_owner403The key belongs to another staff account. A key owned by the root privilege group reaches every credential.

client_credential422The key belongs to a customer and is managed from the client area.

name_required422The name you sent was empty.

permissions_required422The permission list you sent was empty.

permissions_exceed_owner422None of the scopes you asked for are covered by the owning account's privileges.

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/settings/api-credentials/12' \
  -H "Authorization: Bearer $API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"permissions":["Clients/*","Invoices/*"],"ips":["203.0.113.10"]}'
```

```javascript
const res = await fetch('https://panel.example.com/api/v1/admin/settings/api-credentials/12', {
  method: 'PATCH',
  headers: {
    Authorization: `Bearer ${apiKey}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    permissions: ['Clients/*', 'Invoices/*'],
    ips: ['203.0.113.10'],
  }),
});

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

```php
$ch = curl_init('https://panel.example.com/api/v1/admin/settings/api-credentials/12');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_CUSTOMREQUEST  => 'PATCH',
    CURLOPT_HTTPHEADER     => [
        'Authorization: Bearer ' . $apiKey,
        'Content-Type: application/json',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'permissions' => ['Clients/*', 'Invoices/*'],
        'ips'         => ['203.0.113.10'],
    ]),
]);

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

```php
// To ADD a permission send the existing list too: the set you send replaces the old one.
$cred  = Api::Settings()->GetApiCredential(['cid' => 12])['data'];
$scope = $cred['permissions'];

$scope[] = 'Invoices/*';

Api::Settings()->UpdateApiCredential(['cid' => 12, 'permissions' => $scope]);
```

### Deleting a Key

delete/api/v1/admin/settings/api-credentials/{cid}

`Settings/DeleteApiCredential` admin access stops at once

Revokes the key. Every request using it starts being refused immediately.

Response fields data — 2

deletedboolWhether the delete succeeded.

idintId of the deleted key.

Errors 4

not_found404No such credential.

not_credential_owner403The key belongs to another staff account. A key owned by the root privilege group reaches every credential.

client_credential422The key belongs to a customer and is managed from the client area.

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/settings/api-credentials/12' \
  -H "Authorization: Bearer $API_KEY"
```

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

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

```php
$ch = curl_init('https://panel.example.com/api/v1/admin/settings/api-credentials/12');
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
// Deleting your own key cuts THIS session's access too; take care not to lock yourself out.
Api::Settings()->DeleteApiCredential(['cid' => 12]);
```

### Listing the Request Log

get/api/v1/admin/settings/api-logs

`Settings/GetApiLogs` admin paged

Returns the record of requests that reached the API.

Query parameters 3

pageintDefaults to 1.

limitintDefaults to 25, maximum 100.

searchstringSearches the records.

Response fields data[] — 6

idintId of the record.

credentialobjectThe key that made the request: its id and name.

methodstringThe request method.

actionstringThe operation called.

ipstringThe address the request came from.

created_atstring | nullWhen the request arrived.

Errors 1

insufficient_scope403The key lacks the required scope.

Request cURL JavaScript PHP (HTTP) PHP (Internal)

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

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

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

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

```php
// The record does not keep the DATA sent: you see which key called what, not what it sent.
$logs = Api::Settings()->GetApiLogs()['data'];
```

### Clearing the Request Log

delete/api/v1/admin/settings/api-logs

`Settings/ClearApiLogs` admin takes no date

Deletes every API request record.

Response fields data — 1

clearedboolWhether the clear ran.

Errors 1

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/settings/api-logs' \
  -H "Authorization: Bearer $API_KEY"
```

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

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

```php
$ch = curl_init('https://panel.example.com/api/v1/admin/settings/api-logs');
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
// This endpoint takes NO date: every record goes, there is no selective clear.
Api::Settings()->ClearApiLogs();
```

## Pitfalls

> **A key follows its owner**
> 
> Scopes are intersected with the owner's privileges on every request. Take a permission away from that staff account and the key loses it too, with no edit. If the account is deleted or blocked, the key stops working and returns `owner_inactive`. A key left with nothing its owner may still do returns `owner_scope_revoked`.

> **The key is shown once**
> 
> The full key comes back **on the creation response only**. The list and detail endpoints give it masked. Since a digest is what gets stored, the server does not know the raw value either. Miss that response and the only move left is to delete the key and make a new one.

> **The permission list is written whole**
> 
> The scope list you send on an update **replaces** the old one. Adding a single permission means reading the current list and appending to it. Otherwise the key quietly loses its access and your integration stops working.

> **Deleting your own key cuts you off too**
> 
> The delete takes effect at once, and that includes **the key you are using**. When tidying the keys of an integration, watch which one your requests carry. Lock yourself out and the only way back is producing a new key from the panel.

> **An empty address list opens everywhere**
> 
> A key with an empty address list works **from anywhere in the world**. On a server-to-server integration, naming that one address stops a leaked key being used at all. That is the cheapest protection after narrowing the scope.

> **The request log does not keep what was sent**
> 
> The log shows which key called which operation from which address; it **keeps neither the body nor the response**. Investigating what a request changed needs the affected record's own history, not this. The clear takes no date either: all of it or none.

## Related Articles

- [Security Settings](https://dev.wisecp.com/en/security-settings)
- [Site Settings](https://dev.wisecp.com/en/site-settings)
