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
Returns the keys that grant access to this API.
admin for staff, client for a customer. Customer keys are managed from the client area.id and name. Its privileges cap what the key can reach.curl 'https://panel.example.com/api/v1/admin/settings/api-credentials' \
-H "Authorization: Bearer $API_KEY"const res = await fetch('https://panel.example.com/api/v1/admin/settings/api-credentials', {
headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();$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);// 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
Produces a new access key. The full value comes back on this response only.
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}'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;$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);// 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{
"data": {
"id": 13,
"name": "Integration",
"token_preview": "wak_a1b2c3d4e••••••••",
"permissions": ["Clients/*"],
"ips": [],
"rate_limit": 300,
"api_key": "wak_a1b2c3d4e5f6..."
}
}{
"error": {
"code": "permissions_required",
"message": "At least one permission is required."
}
}Key Detail
Returns one key. The schema matches a list item and the key is still masked.
admin for staff, client for a customer. Customer keys are managed from the client area.id and name. Its privileges cap what the key can reach.curl 'https://panel.example.com/api/v1/admin/settings/api-credentials/12' \
-H "Authorization: Bearer $API_KEY"const res = await fetch('https://panel.example.com/api/v1/admin/settings/api-credentials/12', {
headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();$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);// 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
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.
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"]}'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();$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);// 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
Revokes the key. Every request using it starts being refused immediately.
curl -X DELETE 'https://panel.example.com/api/v1/admin/settings/api-credentials/12' \
-H "Authorization: Bearer $API_KEY"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();$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);// 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
Returns the record of requests that reached the API.
curl 'https://panel.example.com/api/v1/admin/settings/api-logs' \
-H "Authorization: Bearer $API_KEY"const res = await fetch('https://panel.example.com/api/v1/admin/settings/api-logs', {
headers: { Authorization: `Bearer ${apiKey}` },
});
const body = await res.json();$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);// 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
Deletes every API request record.
curl -X DELETE 'https://panel.example.com/api/v1/admin/settings/api-logs' \
-H "Authorization: Bearer $API_KEY"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();$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);// This endpoint takes NO date: every record goes, there is no selective clear.
Api::Settings()->ClearApiLogs();Pitfalls
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 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 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.
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.
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 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
Merci pour votre retour !
Notre équipe d'assistance est disponible 24h/24 pour tout ce que vous ne trouvez pas ci-dessus.