Authentication
Requests to the Atomic API are authenticated using the client credentials OAuth 2 flow, via a client ID and secret created in the Atomic Workbench.
API credentials can be created in the Atomic Workbench under Configuration > API Keys. Read more about this in the API section of the Workbench guide.
The basic flow is:
- Base64-encode your client id and secret i.e.
echo -n "$CLIENT_ID:$CLIENT_SECRET" | base64 - Pass this value as the
Authorizationheader in a request to our oauth server - Use the returned token to authenticate requests to Atomic
Atomic's OAuth URL is:
https://master-atomic-io.auth.us-east-1.amazoncognito.com/oauth2/token
Tokens have a limited lifespan, so should be renewed when appropriate. Access Tokens are valid for 1 hour, Refresh Tokens are valid for 30 days.
Note: requests to the Atomic API must include a User-Agent header.
When accessing the Atomic API programmatically, each API request should not request a new token. Instead, tokens should be cached and used until expiry and then a new token requested. The oauth endpoint used to retrieve tokens is rate limited and will return HTTP 429 status codes when used at too high a rate.
Examples
These examples fetch an API token, cache it based on the expiry with a small amount of clock skew, and perform a request to list all environments for the organization.
Each example requires three environment variables which can be managed in the Configuration > API > API keys section of the Workbench..
ATOMIC_API_ORG_ID: Your Atomic organisation IDATOMIC_API_CLIENT_ID: The client ID of an API key you've createdATOMIC_API_CLIENT_SECRET: The client secret of an API key you've created
These examples use a mix of in-memory or file-based token caching. Which one fits depends on how your code runs:
- Long-running process (e.g. a server or worker): an in-memory cache is simplest, as a variable persists across many requests within the same process. The Javascript example uses this approach.
- Script or scheduled job that runs as a fresh process each time: an in-memory cache wouldn't survive between runs, so the token must be persisted somewhere it outlives a single execution, such as a file. The Bash and Python examples use this approach.
or a real-world implementation, persist the token somewhere appropriate to your setup and shared across your services, such as a database, secrets manager, or a shared cache like Redis, and restrict access to it so the token can't be read by others.
Bash
An example using Bash with file caching implemented on the token. Requires curl, base64, and jq.
#!/bin/bash
# These can be found in Configuration > API > API keys
ORG_ID=$ATOMIC_API_ORG_ID
CLIENT_ID=$ATOMIC_API_CLIENT_ID
CLIENT_SECRET=$ATOMIC_API_CLIENT_SECRET
OAUTH_URL=https://master-atomic-io.auth.us-east-1.amazoncognito.com/oauth2/token
ATOMIC_API=https://$ORG_ID.customer-api.atomic.io
# Cache the token on disk so repeated runs of the script reuse the token
# until it expires, rather than requesting a new token every time.
# In a real-world implementation, you should persist the token using
# other more secure means (e.g a database, keychain, or in-memory cache).
TOKEN_CACHE="$HOME/.cache/atomic-api-token.json"
mkdir -p "$(dirname "$TOKEN_CACHE")"
# Refresh a little before the 1 hour expiry to allow for clock skew
EXPIRY_MARGIN=60
# Fetch a fresh token and cache it along with its expiry timestamp
fetch_token() {
# Base64 encode your credentials
CREDENTIALS=$(echo -n "$CLIENT_ID:$CLIENT_SECRET" | base64)
# Retrieve access token details as JSON
TOKEN_DETAILS=$(curl -s -X POST "$OAUTH_URL?grant_type=client_credentials&client_id=$CLIENT_ID" \
--header "Content-Type:application/x-www-form-urlencoded" \
--header "Authorization: Basic $CREDENTIALS")
# Store the token value alongside an absolute expiry time (now + expires_in).
# The umask ensures the file is created readable only by the current user
( umask 077; echo "$TOKEN_DETAILS" | jq -c \
--argjson now "$(date +%s)" \
'{access_token, expires_at: ($now + .expires_in)}' > "$TOKEN_CACHE" )
}
# Return a cached token if it is still valid, otherwise fetch a new one
get_token() {
if [ -f "$TOKEN_CACHE" ]; then
EXPIRES_AT=$(jq -r '.expires_at' "$TOKEN_CACHE")
if [ "$(date +%s)" -lt "$((EXPIRES_AT - EXPIRY_MARGIN))" ]; then
jq -r '.access_token' "$TOKEN_CACHE"
return
fi
fi
fetch_token
jq -r '.access_token' "$TOKEN_CACHE"
}
# Example API call using the token
# Retrieve environment details. Call get_token for each request so it reuses
# the cached token, or fetches a fresh one if it has expired between requests.
ENVIRONMENTS=$(curl -X GET "$ATOMIC_API/v1/environments" \
--header "Content-Type:application/json" \
--header "Authorization: Bearer $(get_token)")
# Store first environment ID for use in other examples
ENVIRONMENT_ID=$(echo $ENVIRONMENTS | jq -r ".environments[0].id")
# Output environment details, formatted with jq
echo $ENVIRONMENTS | jq
The output should be a list of Atomic environments you have access to, similar to:
[
{
"id": "example",
"name": "production",
"created": "2019-07-04T04:18:08.153Z",
"organisationName": "Your organisation"
}
]
Javascript
An example in Javascript with in-memory caching implemented on the token.
// Cache the token in memory. This suits a long-running process (e.g. a server)
// where the variable persists across many requests. If this were a script or
// scheduled job that runs as a fresh process each time, an in-memory cache
// wouldn't survive between runs, so persist the token elsewhere (a file,
// database, or shared cache) instead.
let cachedToken = {
token: null,
expires: null
}
const getAPIToken = async () => {
if (cachedToken.token && cachedToken.expires > new Date()) {
return cachedToken.token
}
// These can be found in Configuration > API > API keys
const clientId = process.env.ATOMIC_API_CLIENT_ID
const clientSecret = process.env.ATOMIC_API_CLIENT_SECRET
const oauthURL = 'https://master-atomic-io.auth.us-east-1.amazoncognito.com/oauth2/token'
// Base64 encode your credentials
const credentials = Buffer.from(`${clientId}:${clientSecret}`).toString('base64')
// Retrieve access token details as JSON
const tokenDetails = await fetch(`${oauthURL}?grant_type=client_credentials&client_id=${clientId}`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Authorization': `Basic ${credentials}`
}
})
// Parse the token value
const tokenResp = (await tokenDetails.json())
// Cache the token until it expires. Refresh 60 seconds early to allow for
// clock skew and in-flight requests, so we never send an expired token.
const expiryMarginMs = 60 * 1000
cachedToken = {
token: tokenResp.access_token,
expires: new Date(Date.now() + (tokenResp.expires_in * 1000) - expiryMarginMs)
}
return cachedToken.token
}
// Example API call using the token
const orgId = process.env.ATOMIC_API_ORG_ID
const atomicAPI = `https://${orgId}.customer-api.atomic.io`
// Retrieve environment details. Call getAPIToken for each request so it reuses
// the cached token, or fetches a fresh one if it has expired between requests.
const response = await fetch(`${atomicAPI}/v1/environments`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${await getAPIToken()}`
}
})
const environments = await response.json()
console.log(environments)
The output should be a list of Atomic environments you have access to, similar to:
{
environments: [
{
id: 'example',
name: 'production',
created: '2019-07-04T04:18:08.153Z',
organisationName: 'Your organisation'
}
]
}
Python
An example in Python with file caching implemented on the token. Requires the requests library.
import base64
import json
import os
import time
from pathlib import Path
import requests
# These can be found in Configuration > API > API keys
CLIENT_ID = os.environ["ATOMIC_API_CLIENT_ID"]
CLIENT_SECRET = os.environ["ATOMIC_API_CLIENT_SECRET"]
OAUTH_URL = "https://master-atomic-io.auth.us-east-1.amazoncognito.com/oauth2/token"
# Refresh a little before the 1 hour expiry to allow for clock skew
EXPIRY_MARGIN = 60
# Cache the token on disk so repeated runs of the script reuse the token
# until it expires, rather than requesting a new token every time. In a
# real-world implementation, you may persist the token using other means
# (e.g. a database or shared cache)
TOKEN_CACHE = Path.home() / ".cache" / "atomic-api-token.json"
def get_api_token():
# Reuse the cached token from disk until it is close to expiring
try:
cached = json.loads(TOKEN_CACHE.read_text())
if time.time() < cached["expires_at"] - EXPIRY_MARGIN:
return cached["access_token"]
except (FileNotFoundError, ValueError):
pass
# Base64 encode your credentials
credentials = base64.b64encode(f"{CLIENT_ID}:{CLIENT_SECRET}".encode()).decode()
# Retrieve access token details as JSON
response = requests.post(
OAUTH_URL,
params={"grant_type": "client_credentials", "client_id": CLIENT_ID},
headers={
"Content-Type": "application/x-www-form-urlencoded",
"Authorization": f"Basic {credentials}",
},
)
response.raise_for_status()
token_details = response.json()
# Cache the token on disk alongside an absolute expiry time (now + expires_in).
# The file is created readable only by the current user (0600)
TOKEN_CACHE.parent.mkdir(parents=True, exist_ok=True)
cached = {
"access_token": token_details["access_token"],
"expires_at": time.time() + token_details["expires_in"],
}
fd = os.open(TOKEN_CACHE, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
with os.fdopen(fd, "w") as f:
json.dump(cached, f)
return cached["access_token"]
# Example API call using the token
ORG_ID = os.environ["ATOMIC_API_ORG_ID"]
ATOMIC_API = f"https://{ORG_ID}.customer-api.atomic.io"
# Retrieve environment details, reusing the cached token
environments = requests.get(
f"{ATOMIC_API}/v1/environments",
headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {get_api_token()}",
},
)
environments.raise_for_status()
print(environments.json())
The output should be a list of Atomic environments you have access to, similar to:
[{'id': 'example', 'name': 'production', 'created': '2019-07-04T04:18:08.153Z', 'organisationName': 'Your organisation'}]
API key permissions
Fine-grained API keys
A fine-grained API key is granted a set of permissions that control which operations it can perform. The Atomic API spec shows for each endpoint which permission is required.
For example, retrieving cards requires the cards:read permission:
paths:
/v1/{environmentId}/cards:
get:
summary: "Retrieve cards"
security:
- oAuth2: ['cards:read']
When creating a fine-grained key, grant only the permissions needed for your integration.
Legacy API keys
Each legacy API credential pair is scoped to the environment and assigned one of the following roles:
auth- used only to create and rotate other credentialsworkbench- used to manage workbench resources such as cards and SNS platformsevents- used to create cards, and manage customers
Roles are associated with legacy API keys. We recommend using fine-grained API keys which use permissions instead that allow you to set what actions the API key is allowed to take.
The Atomic API spec shows for each request which role should be used.
For example, retrieving cards requires the events role:
paths:
/v1/{environmentId}/cards:
get:
summary: "Retrieve cards"
security:
- oAuth2: [events]
If you haven't added any API credentials yet, follow the steps as described in the API keys section to add new ones.
Troubleshooting
401 Unauthorized
Your access token is missing, malformed, or expired. Check that you're passing the token correctly as a Bearer token in the Authorization header, and that it hasn't exceeded its 1-hour lifespan. If it has expired, request a new token.
403 Forbidden
Your token is valid but the credential doesn't have permission to perform the operation. For fine-grained keys, check that the API key has the permission required by the endpoint. Refer to the API spec to confirm which permission is needed. For legacy keys, check that the key's role matches what the endpoint requires.
A 403 can also occur if the request comes from an IP address not on the API key's allowlist. Check whether the key has IP allowlisting configured under Authentication controls.
429 Too Many Requests
Your code is requesting a new token on every API call. Tokens are valid for 1 hour. Cache the token and reuse it until it expires rather than fetching a new one each time.
Key not working after rotation
After rotating an API key, the previous Client Secret is immediately invalidated. Make sure all services using the key have been updated to use the new secret.
Key not working after the expiry date
API keys with an expiry date stop working once that date passes. Create a new API key or rotate the existing one before it expires to avoid downtime.