Service accounts
UpdatedService accounts let you authenticate with the Customer.io CLI and other programmatic tools without using your personal credentials. They provide long-lived tokens with fine-grained access controls—ideal for AI coding assistants, CI/CD pipelines, and automation scripts.
You can setup multiple service accounts and multiple tokens per account. You might have different service accounts for different projects or departments in your account; and different tokens within each account to represent the different tools or users.
Create a service account
- Go to Account Settings > API Credentials and click the Service Accounts tab.
- Click Create Service Account.
- Enter a name and optional description.
- (Optional) check Read-only to permanently restrict the service account to GET requests.
- Click Create.
After creating the service account, you’re prompted to create your first token.
Create a token
A service account can have multiple tokens. Use separate tokens for different integrations so you can rotate or revoke them independently.
- In the Service Accounts tab, expand the service account.
- Click Create Token.
- Enter a name for the token.
- Set Expiration: No expiration, 30, 60, 90 days, or 1 year.
- (Optional) check Read-only to permanently restrict the token to GET requests.
- Click Create.
Save the token value starting with sa_live_. You can’t retrieve it later. You should store it in a secure location or use it with cio auth login so you don’t lose it.
Revoke a token
Revoking a token immediately invalidates it and any active sessions. Other tokens on the same service account aren’t affected.
- Expand the service account in the Service Accounts tab.
- Find the token and click Revoke.
Delete a service account
Deleting a service account revokes all of its tokens. This can’t be undone.
Store tokens securely
Service account tokens are long-lived and can grant full write access to your account. Treat them like passwords, and pick a storage method that matches how you’ll use the token.
| Use case | Recommendation | Why |
|---|---|---|
| Local interactive development | Run cio auth login to store your token in ~/.cio/config.json | Built in; scoped to your user account |
| Local scripts or non-interactive runs | A project-scoped .env file (git ignored), loaded with a tool like direnv or dotenv | Scoped to one project; rotate independently per project |
| CI/CD pipelines | Your CI provider’s secrets store (for example, GitHub Actions secrets), exposed as CIO_TOKEN | Encrypted at rest, masked in logs, auditable |
| Shared machines | Your OS keychain (macOS Keychain, secret-tool on Linux) | Encrypted, per-user, no plaintext on disk |
Best practices:
- Don’t put
sa_live_tokens in shell config files like~/.zshrcor~/.bashrc. You don’t want to leak the token to these files, that might be synced somewhere or backed up to cloud storage. - Don’t commit
.envfiles. If you use one, add it to your.gitignoreand ship a.env.examplewith placeholder values for teammates. - Use one token per integration. Separate tokens let you rotate or revoke access for one tool without disrupting the others.
Token permissions
When you create a token, it inherits your permissions. The token also stays in sync with your permissions: if your permissions change, the token’s permissions change with it.
If you sign up for a new account through the CLI using the cio auth signup verify command, the CLI generates an “admin” token with full permissions for the account to help you get set up. This is the only time you’ll create a token through the CLI. You’ll create subsequent tokens in the UI.
Read-only tokens
You can narrow what a token can do in two places: when you create it, or when you log into the CLI.
Read-only token (permanent): Check Read-only when creating the token. Every session minted from this token only permits GET requests, regardless of the creator’s role.
Read-only session (per-exchange): Pass
scope=read_onlyto the token exchange. The resulting JWT only permits GET requests for its lifetime, even if the underlying token has write access. Useful for one-off scripts or pipeline branches that should never write, without rotating the underlying token.The CLI exposes this as
--read-only:cio --read-only api /v1/environments/{environment_id}/campaigns \ --params '{"environment_id":"123"}'
How service account token authentication works
Your sa_live_ token isn’t sent on every API call. The CLI exchanges it for a short-lived (1 hour) JSON Web Token (JWT) and sends the JWT as the bearer credential instead. The sa_live_ token only ever appears on the call to the token endpoint.
The exchange exists so the long-lived credential touches as few systems as possible. A JWT that leaks expires on its own; a sa_live_ token that leaks has to be rotated by hand. Exchanging also lets the CLI request a scoped session (for example, scope=read_only) without altering the underlying token.
JWT exchange without the CLI
If you want to integrate directly with the APIs exposed by our CLI, you’ll need to exchange your service account token for a JWT yourself.
curl -X POST https://us.fly.customer.io/v1/service_accounts/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials" \
-d "client_secret=sa_live_xxxxx"
For accounts in our EU region, use https://eu.fly.customer.io.
| Form field | Required | Description |
|---|---|---|
grant_type | yes | Must be client_credentials. |
client_secret | yes | Your sa_live_ service account token. |
scope | no | Set to read_only for a session that only permits GET requests. |
The exchange follows the OAuth 2.0 client credentials grant.
Response
{
"access_token": "eyJhbGciOi...",
"token_type": "Bearer",
"expires_in": 3600
}
Send access_token as Authorization: Bearer <access_token> on subsequent API calls. When it’s close to expiry, repeat the exchange to get a new one. Cache the JWT between calls—the token endpoint is rate-limited per IP, so don’t exchange on every request.
