API authentication
The admin API is served under /admin_api/v1 and authenticates every request individually — there are
no sessions and no login call. You can authenticate in two ways:
- API secrets — an access key ID and a secret key issued by OSIE, used to sign each request with AWS Signature Version 4. Available on every installation with nothing to configure, and each key can be restricted to the endpoints it needs.
- Bearer tokens — an access token from an external OpenID Connect provider. This requires an OpenID provider to be configured for the admin API first, and the token grants unrestricted access.
Use API secrets unless you already operate an OpenID provider whose lifecycle you want the integration to follow.
Create an API secret
Issue one key per integration. Keys are independent, so you can scope a CI pipeline to project management only, and revoke it later without affecting anything else that calls the API.
In the admin dashboard, go to Settings → Access & Security → API Secrets.

Select Create API Secret, describe what the key is for, and tick the permissions it needs. Ticking
a group name grants the whole group — the wildcard shown beneath it, such as admin:project:*.

OSIE generates the access key ID and the secret key, and displays the secret key once.

The secret key is encrypted at rest and is never displayed again. Copy it before you close the panel. If it is lost, use Rotate secret on the key to issue a new one — this invalidates the previous secret immediately, so update your integration in the same change.
Creating, rotating and deleting API secrets requires the admin:hmac_key:manage permission.
Scope a key to the endpoints it needs
Each admin API endpoint declares the permission it requires. A request is rejected when the signing key does not hold a permission that matches it:
{"error":{"code":"FORBIDDEN","message":"API secret lacks required permission: admin:price_plan:read"}}
Permissions are matched by pattern, so admin:project:* covers admin:project:read,
admin:project:create and every other permission in the project group, and * covers all of them.
A key created with no permissions at all has full access, which is how keys issued before per-key
scoping behave.
Sign a request
Signing is Signature Version 4 with the standard Authorization header, so any AWS SigV4 library
signs OSIE requests without modification. Point it at the region us-east-1 and the service name
osie.
curl 7.75 and later signs the request for you:
curl --aws-sigv4 "aws:amz:us-east-1:osie" \
--user "<access-key-id>:<secret-key>" \
"https://osie.mycompany.com/admin_api/v1/projects?limit=10"
The request body is part of the signature, so it must be passed to the signer as well as to the server:
curl -X POST --aws-sigv4 "aws:amz:us-east-1:osie" \
--user "<access-key-id>:<secret-key>" \
-H "Content-Type: application/json" \
-d '{"name":"Acme Corp"}' \
"https://osie.mycompany.com/admin_api/v1/organizations"
From code
- Python
- Go
Signing with botocore, which ships with boto3:
import json
from urllib.parse import urlencode
import requests
from botocore.auth import SigV4Auth
from botocore.awsrequest import AWSRequest
from botocore.credentials import Credentials
ACCESS_KEY_ID = "<access-key-id>"
SECRET_KEY = "<secret-key>"
BASE_URL = "https://osie.mycompany.com"
SIGNER = SigV4Auth(Credentials(ACCESS_KEY_ID, SECRET_KEY), "osie", "us-east-1")
def call(method, path, params=None, body=None):
"""Signs a request with SigV4 and sends it."""
url = f"{BASE_URL}{path}"
if params:
url = f"{url}?{urlencode(params)}"
headers = {"Content-Type": "application/json"} if body is not None else {}
payload = json.dumps(body) if body is not None else None
request = AWSRequest(method=method, url=url, data=payload, headers=headers)
SIGNER.add_auth(request)
return requests.request(method, url, headers=dict(request.headers), data=payload)
projects = call("GET", "/admin_api/v1/projects", params={"limit": "10"})
print(projects.json()["data"])
call("POST", "/admin_api/v1/organizations", body={"name": "Acme Corp"})
Signing with github.com/aws/aws-sdk-go-v2/aws/signer/v4:
package main
import (
"bytes"
"context"
"crypto/sha256"
"encoding/hex"
"net/http"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4"
)
const (
accessKeyID = "<access-key-id>"
secretKey = "<secret-key>"
baseURL = "https://osie.mycompany.com"
)
// call signs a request with SigV4 and sends it.
func call(method, path string, body []byte) (*http.Response, error) {
req, err := http.NewRequest(method, baseURL+path, bytes.NewReader(body))
if err != nil {
return nil, err
}
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
sum := sha256.Sum256(body)
credentials := aws.Credentials{AccessKeyID: accessKeyID, SecretAccessKey: secretKey}
err = v4.NewSigner().SignHTTP(context.Background(), credentials, req,
hex.EncodeToString(sum[:]), "osie", "us-east-1", time.Now())
if err != nil {
return nil, err
}
return http.DefaultClient.Do(req)
}
func main() {
projects, err := call("GET", "/admin_api/v1/projects?limit=10", nil)
if err != nil {
panic(err)
}
defer projects.Body.Close()
if _, err = call("POST", "/admin_api/v1/organizations", []byte(`{"name":"Acme Corp"}`)); err != nil {
panic(err)
}
}
What OSIE verifies
A signed request carries an Authorization header of this shape, plus the X-Amz-Date header it was
signed with:
Authorization: AWS4-HMAC-SHA256 Credential=<access-key-id>/<yyyymmdd>/us-east-1/osie/aws4_request, SignedHeaders=host;x-amz-date, Signature=<hex>
X-Amz-Date: 20260730T151905Z
OSIE looks up the access key ID, re-signs the request with the stored secret, and compares the result against the signature you sent. Four things decide whether that comparison succeeds:
- The
hostheader must be signed, and must be the hostname the client actually called. OSIE reconstructs the request URL from the signedhostheader rather than from what reached the application, so signing a request for one hostname and sending it to another fails even when a reverse proxy forwards it correctly. X-Amz-Datemust be within 5 minutes of the server's clock. A larger difference is rejected before the signature is checked.- The body is hashed into the signature. Sign the exact bytes you send, including on
POSTandPUT. - The region and service name are part of the signing key. OSIE does not require particular
values — it re-signs with whatever the credential scope declares — so any consistent pair works.
Use
us-east-1andosieso that your requests match these examples.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
401 with Unknown access key | The access key ID is not on this installation | Check the key exists under Settings → Access & Security → API Secrets, and that you are calling the right environment |
401 with Signature mismatch | The secret is wrong, or the signed request differs from the one sent | Confirm the secret, and that the body and query string are signed exactly as sent |
401 with Date skew too large | The client clock is more than 5 minutes from the server's | Synchronise the client clock; do not reuse a signature across requests |
403 with API secret lacks required permission | The key is scoped and does not cover this endpoint | Add the permission named in the message to the key |
401 on every call, no signing error | The Authorization header never reached the API | Check that intermediate proxies forward Authorization and X-Amz-Date unchanged |
Bearer token authentication
An access token issued by an external OpenID Connect provider is accepted as an alternative, for installations that already centralise machine credentials there. Unlike API secrets, a bearer token is not scoped by OSIE — it grants access to the whole admin API.
Point the admin API at your provider in values.yaml:
adminApi:
oauth2:
clientId: "osie-admin-api" # the client_id used to obtain the access token
issuerUri: https://your-openid-provider.com/
See the default values.yaml
of the Helm Chart. issuerUri is empty by default, so this method is unavailable until it is set.
Choosing and connecting a provider is covered in
Identity management.
Obtain a token from your provider with the client credentials grant:
curl --location 'https://your-openid-provider.com/protocol/openid-connect/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'client_id=osie-admin-api' \
--data-urlencode 'client_secret=<client-secret>' \
--data-urlencode 'grant_type=client_credentials'
Then pass it on every call:
curl --location 'https://osie.mycompany.com/admin_api/v1/projects?limit=10' \
--header 'Authorization: Bearer <access-token>'
Access tokens expire. Once a token is past its expires_in, calls return 401 Unauthorized until you
obtain a new one.