Service Accounts
Service accounts are userless machine identities that authenticate to the Third Loop API. Unlike a user account, a service account has no person behind it — no email, no password, no interactive login. It exists so that automated systems (backend services, scheduled jobs, CI pipelines, internal integrations) can call the API on their own behalf.
Use a service account when the caller is a system. Use an API key when the caller is a person acting through a tool.
Overview
| Service Account | API Key | |
|---|---|---|
| Belongs to | The organization | An individual user |
| Identity | Machine (no user) | Acts as the issuing user |
| Credential | Client ID + client secret | Key ID + secret key |
| Auth model | Exchange secret for a short-lived access token | Send the key on every request |
| Managed by | Administrators | The user who owns the key |
| Best for | Backend services, cron jobs, integrations | Coding tools, personal scripts |
Each service account has three things that control what it can do:
- Scope — a coarse permission level:
read,write, oradmin. - Roles — optional named role assignments that grant finer-grained policy-based permissions, the same roles used for users.
- Token lifetime — how long an issued access token stays valid before the account must authenticate again.
Service accounts are scoped to a single organization. A service account can only see and act on resources within the organization that created it.
Adding a Service Account
Service accounts can be created by Administrators or any user with a role allowing the CreateServiceAccount API action. Creation returns the credentials the machine will use from then on.
Required and optional fields
| Field | Required | Description |
|---|---|---|
name | Yes | A descriptive label, e.g. nightly-ingest-job. |
scope | Yes | One of read, write, or admin. |
clientId | No | A custom client identifier. If omitted, one is generated. |
accessTokenExpiration | No | Access token lifetime as a number. Defaults to 60. |
accessTokenExpirationUnit | No | Unit for the lifetime — seconds, minutes, hours, days, etc. Defaults to minutes. |
Creating via the API
curl -X POST https://api.thirdloop.ai/api/user/service-accounts \
-H "Content-Type: application/json" \
-d '{
"name": "nightly-ingest-job",
"scope": "read",
"accessTokenExpiration": 30,
"accessTokenExpirationUnit": "minutes"
}'
The response includes the service account id, its clientId, and the clientSecret:
{
"id": "3f2a1b8c-...",
"organizationId": "9c7e...",
"name": "nightly-ingest-job",
"scope": "read",
"clientId": "...",
"clientSecret": "...",
"accessTokenExpiration": 30,
"accessTokenExpirationUnit": "minutes",
"roles": []
}
Store the id and clientSecret in your secret manager — these are the two values needed to authenticate.
You must store the client secret when you first create a service account. It will not be shown to you again.
Treat the client secret like a password. It grants full access at the account's scope and does not expire on its own. Never commit it to source control or embed it in client-side code.
Choosing a scope
Service account scopes are purely for quick identificaiton and initials role provisioning. You are free to modify service account roles and permissions after creation.
read— Retrieve data only. Correct for reporting, dashboards, and export jobs.write— Create and modify resources. Correct for ingestion pipelines and integrations that push data in.admin— Full administrative access, including managing other service accounts. Reserve for provisioning and platform automation.
Managing Service Accounts
Listing and inspecting
GET /api/user/service-accounts returns every service account in your organization. GET /api/user/service-accounts/{id} returns a single one.
Each record reports operational metadata useful for audit:
createdBy/createdAt— Who provisioned the account and when.lastAccessed— The last time the account successfully authenticated. Anullor stale value is a good signal that the account is unused and can be removed.revokedBy— Who revoked the account, if it has been.roles— Role assignments currently attached.
Updating
PUT /api/user/service-accounts/{id} changes the name or scope:
curl -X PUT https://api.thirdloop.ai/api/user/service-accounts/{id} \
-H "Content-Type: application/json" \
-d '{ "scope": "write" }'
A scope change takes effect on the next access token the account requests. Tokens already issued keep their original scope until they expire, so a downgrade is not fully in force until the current token lifetime has elapsed.
Assigning roles
Roles layer policy-based permissions on top of the account's scope. They use the same role definitions as user accounts — list them with GET /api/user/roles.
# Grant a role
curl -X POST https://api.thirdloop.ai/api/user/service-accounts/{id}/roles/{role_id}
# Revoke a role
curl -X DELETE https://api.thirdloop.ai/api/user/service-accounts/{id}/roles/{role_id}
Rotating the client secret
Rotation issues a new client secret and invalidates the old one. There are two paths, depending on who is rotating:
Administrator rotation — an admin rotates the secret for an account by ID:
curl -X POST https://api.thirdloop.ai/api/user/service-accounts/{id}/secret/rotate
Administrator rotation can be used either when an old client secret is lost or manually for invalidation purposes.
Self-service rotation — the service account rotates its own secret by presenting its current one:
curl -X POST https://api.thirdloop.ai/api/user/service-accounts/secret \
-H "Content-Type: application/json" \
-d '{ "id": "3f2a1b8c-...", "clientSecret": "<current_secret>" }'
Self-service rotation is intended to be used for long running processes or servers that should maintain their own secret rotations.
Rotation invalidates the previous secret immediately. Deploy the new secret to every consumer of the account before rotating, or the integration will start failing authentication.
Deleting
DELETE /api/user/service-accounts/{id} removes the service account. Any system still using its credentials will fail to authenticate on its next token request.
Before deleting, check lastAccessed to confirm the account is genuinely unused. If you are not certain, prefer rotating the secret and observing what breaks — that is reversible in a way deletion is not.
Authenticating a Service Account
Service accounts use a two-step token exchange: trade the long-lived client secret for a short-lived access token, then send that token on API requests. The client secret itself is never sent to regular API endpoints.
Step 1 — Exchange the secret for an access token
curl -X POST https://api.thirdloop.ai/api/user/service-accounts/login \
-H "Content-Type: application/json" \
-d '{
"id": "3f2a1b8c-...",
"clientSecret": "<client_secret>"
}'
Response:
{
"accessToken": "eyJhbGci...",
"expiresIn": 1800,
"tokenType": "Bearer"
}
expiresIn is the token's remaining lifetime in seconds, derived from the account's configured accessTokenExpiration.
Step 2 — Call the API with the access token
curl https://api.thirdloop.ai/api/core/projects \
-H "Authorization: Bearer eyJhbGci..."
Token handling in your integration
Access tokens are short-lived by design — the default is 60 minutes. Build your client to handle that:
- Cache the token in memory for the duration of
expiresIn. Do not call/loginbefore every request. - Refresh proactively, a minute or two before expiry, rather than waiting for a
401. - Retry once on
401by re-authenticating, in case the token expired between your check and the request landing. - Never persist tokens to disk or logs. Keep them in memory only.
- Keep the client secret in a secret manager, injected as an environment variable or mounted secret at runtime.
Choosing a token lifetime
Shorter lifetimes limit the blast radius of a leaked token but mean more /login calls. A useful default:
- 15–60 minutes for most integrations.
- Minutes for high-sensitivity
admin-scoped automation. - Hours only for low-risk
readworkloads where the extra token exchanges are a real cost.
Best Practices
- One service account per integration. Separate accounts mean you can rotate, scope, and revoke each integration independently, and
lastAccessedtells you something meaningful. - Name accounts after the system that uses them, not the person who created them —
billing-sync, notalex-test. - Grant the narrowest scope that works. Start at
readand widen only when a call actually fails. - Rotate secrets on a schedule and immediately whenever someone with access to the secret leaves the team.
- Audit periodically. List all accounts, check
lastAccessed, and delete anything dormant. - Never use a service account as a shared human credential. If a person needs API access, issue them an API key so actions remain attributable.
Reference
Full request and response schemas for every endpoint above are in the API reference under the user-service-accounts tag.