Service Accounts
Service Accounts
Section titled “Service Accounts”A service account is a dedicated, non-human identity for machine-to-machine access to the Credal API. Instead of sharing a static organization-wide API key, create a service account for each integration—such as an ingestion pipeline or internal tool—grant only the permissions it needs, and mint scoped, expiring, and individually revocable tokens.
Service accounts always act as themselves. Unlike legacy API keys, a service account token never impersonates a human user. Every API call runs with the service account's own permissions, and every action is attributed to the service account in your audit logs.
How it works
Section titled “How it works”- An organization admin creates a service account with a name and an email address on your organization's domain, such as
ingestion-bot@yourcompany.com. - The admin grants the service account access like any other user, including collection permissions, agent deployments, and IdP group membership. The service account can only access what it has explicitly been granted.
- The admin mints one or more tokens for the service account. Each token has a name, a set of scopes, and an expiration date.
- Your integration calls the v0 API with the token in the
Authorizationheader.
Create a service account and token
Section titled “Create a service account and token”Organization admins manage service accounts from Service Accounts in the Credal web app.
Create a service account
Section titled “Create a service account”Provide a display name and an email address. The email domain must match your organization's domain. If you want integration permissions to sync to the service account, use an address that corresponds to its identity in your IdP or source systems.
Create a token
Section titled “Create a token”Select the service account, then provide:
| Field | Description |
|---|---|
| Name | A label for the token, such as prod-ingestion. |
| Description | Optional free-form notes. |
| Scopes | The parts of the API the token may call. |
| Expiration date | The date the token stops working. Defaults to one year. |
The token is displayed exactly once at creation time and is never shown again. Copy it immediately and store it in a secrets manager. Tokens look like:
cred_sa_<your_token>If a token is compromised or no longer needed, revoke it from the same page. Revocation takes effect immediately. Deleting a service account revokes all of its tokens.
Scopes
Section titled “Scopes”Scopes control which API surfaces a token may call. They are layered on top of the service account's own permissions: a token can never read data its service account has not been granted, regardless of scope. A token with no scopes cannot call any scoped endpoint.
| Scope | Grants |
|---|---|
agent:message |
Send messages to deployed agents and read their responses. |
catalog:write |
Upload and update documents through the catalog API. |
catalog:read |
Read documents and collections through the catalog API. |
Grant only the scopes each integration needs.
Authentication
Section titled “Authentication”Store the token in an environment variable instead of hard-coding it:
export CREDAL_SERVICE_ACCOUNT_TOKEN="cred_sa_<your_token>"Pass the token as a Bearer token in the Authorization header of every request:
Authorization: Bearer cred_sa_<your_token>Send a message to an agent
Section titled “Send a message to an agent”This endpoint requires the agent:message scope. The agent must have API access enabled and be deployed to the service account, just as it would be for a human user. agentId is required, and userEmail must be omitted.
curl -X POST https://app.credal.ai/api/v0/copilots/sendMessage \
-H "Authorization: Bearer $CREDAL_SERVICE_ACCOUNT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"agentId": "3a1b8c2d-0e4f-4a6b-9c8d-7e5f3a1b8c2d",
"message": "Summarize open support tickets from this week."
}'import os
import requests
response = requests.post(
"https://app.credal.ai/api/v0/copilots/sendMessage",
headers={
"Authorization": f"Bearer {os.environ['CREDAL_SERVICE_ACCOUNT_TOKEN']}"
},
json={
"agentId": "3a1b8c2d-0e4f-4a6b-9c8d-7e5f3a1b8c2d",
"message": "Summarize open support tickets from this week.",
},
)
response.raise_for_status()
result = response.json()
print(result["sendChatResult"]["response"]["message"])const response = await fetch("https://app.credal.ai/api/v0/copilots/sendMessage", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.CREDAL_SERVICE_ACCOUNT_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
agentId: "3a1b8c2d-0e4f-4a6b-9c8d-7e5f3a1b8c2d",
message: "Summarize open support tickets from this week.",
}),
});
if (!response.ok) {
throw new Error(`Credal API request failed: ${response.status}`);
}
const result = await response.json();
console.log(result.sendChatResult.response.message);A successful response looks like:
{
"sendChatResult": {
"type": "ai_response_result",
"conversationId": "9f2e7d61-4c3b-4a5e-8f1d-2b6c9a0e7d61",
"response": {
"message": "Here is a summary of this week's open support tickets: ..."
}
}
}To continue a conversation, pass the conversationId from a previous response in your next request:
{
"agentId": "3a1b8c2d-0e4f-4a6b-9c8d-7e5f3a1b8c2d",
"conversationId": "9f2e7d61-4c3b-4a5e-8f1d-2b6c9a0e7d61",
"message": "Which of those tickets are highest priority?"
}Upload a file to the catalog
Section titled “Upload a file to the catalog”This endpoint requires the catalog:write scope. Send the file as multipart/form-data. documentExternalId is required and identifies the document in your system; uploading again with the same ID updates the document. Omit uploadAsUserEmail because the document is uploaded as the service account.
curl -X POST https://app.credal.ai/api/v0/catalog/uploadFile \
-H "Authorization: Bearer $CREDAL_SERVICE_ACCOUNT_TOKEN" \
-F "file=@quarterly-report.pdf" \
-F "documentExternalId=quarterly-report-2026-q2" \
-F "documentName=Quarterly Report Q2 2026" \
-F "collectionId=5b2c9d3e-1f4a-4b6c-8d9e-0a7f5b2c9d3e"import os
import requests
with open("quarterly-report.pdf", "rb") as report:
response = requests.post(
"https://app.credal.ai/api/v0/catalog/uploadFile",
headers={
"Authorization": f"Bearer {os.environ['CREDAL_SERVICE_ACCOUNT_TOKEN']}"
},
files={"file": report},
data={
"documentExternalId": "quarterly-report-2026-q2",
"documentName": "Quarterly Report Q2 2026",
"collectionId": "5b2c9d3e-1f4a-4b6c-8d9e-0a7f5b2c9d3e",
},
)
response.raise_for_status()
print(response.json()) # {"documentId": "..."}import { openAsBlob } from "node:fs";
const form = new FormData();
form.append("file", await openAsBlob("quarterly-report.pdf"));
form.append("documentExternalId", "quarterly-report-2026-q2");
form.append("documentName", "Quarterly Report Q2 2026");
form.append("collectionId", "5b2c9d3e-1f4a-4b6c-8d9e-0a7f5b2c9d3e");
const response = await fetch(
"https://app.credal.ai/api/v0/catalog/uploadFile",
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.CREDAL_SERVICE_ACCOUNT_TOKEN}`,
},
body: form,
},
);
if (!response.ok) {
throw new Error(`Credal API request failed: ${response.status}`);
}
console.log(await response.json()); // {"documentId": "..."}A successful response returns the Credal document ID:
{
"documentId": "7c4d1e8f-2a5b-4c7d-9e0f-3b8a7c4d1e8f"
}Errors
Section titled “Errors”| Status | Meaning |
|---|---|
401 |
The token is missing, invalid, expired, or revoked. |
403 |
Service accounts are not enabled for your organization, or the token lacks the scope required by the endpoint. |
400 |
The request is invalid—for example, it contains userEmail or uploadAsUserEmail, or omits agentId when messaging an agent. |
Error responses have this shape:
{
"error": {
"message": "This service account token is missing the required 'agent:message' scope."
}
}Best practices
Section titled “Best practices”- Use one service account per integration. This keeps permissions minimal and audit trails clear.
- Grant the fewest scopes that work. A token for an ingestion job needs
catalog:write, notagent:message. - Set short expirations and rotate tokens. Mint a new token before the old one expires, deploy it, then revoke the old token.
- Store tokens in a secrets manager. The plaintext token is shown only once and cannot be recovered; it can only be revoked and reissued.
- Never send impersonation fields. Service accounts act as themselves, so
userEmailanduploadAsUserEmailare rejected.