Skip to content

Workload identities

A CI job or cloud workload that can get an OIDC token from its platform doesn't need a stored Flint API key. You create a workload identity that says which tokens Flint trusts, and the workload exchanges its platform token for a Flint access token that lasts at most an hour. There is no secret to store, rotate, or leak: the identity's wid_... ID is safe to put in a CI variable or a manifest.

Set up the identity once, then follow the recipe for your platform. For the token exchange request itself, see Workload identities in the Authentication guide.

How an identity works#

Each identity belongs to one environment, live or one sandbox: the one the credential that creates it acts in. It holds:

FieldWhat Flint checks
issuerThe token's iss, compared exactly. Flint fetches the issuer's OpenID Connect discovery document and signing keys from it, so it must be a public HTTPS URL.
audienceThe token's aud. Leave it out to use Flint's, https://api.withflintpay.com. Set it only when the platform can't issue a token for that audience.
subjectThe token's sub, exactly, or as a pattern when subject_match is glob.
claim_conditionsUp to 10 other claims the token must carry, such as the numeric ID of your GitHub organization.
scopesWhat the exchanged token can do. Grant only what the workload calls.

A glob value uses * to match any characters except :, so it stays within one segment of a subject like repo:acme/shop:ref:refs/heads/*. It can't start with *, has at most three, and needs at least one exact claim condition beside it. Shared issuers, which serve every customer of the platform, also need a condition that pins the identity to your account: repository_owner_id on GitHub and namespace_id on GitLab. Flint rejects an identity on those issuers without it.

The Flint token lasts at most one hour, and never longer than the platform token it came from. Flint rejects platform tokens that are valid for more than 24 hours. The CLI and SDKs cache the Flint token and exchange again before it expires. Deactivating, archiving, or narrowing an identity applies to the next request made with its tokens.

Create an identity#

On the Dashboard, open Workload identities and choose your platform. The preset fills in the issuer, asks for the claims your platform needs pinned, and shows what a subject looks like there.

To create one from a script or an agent, call POST /v1/workload-identities or run flint workload-identities create. The credential needs the accounts.workload_identities.write scope; API key management scopes don't include it. The recipes below give the request body for each platform.

cURL
curl -X POST https://api.withflintpay.com/v1/workload-identities \
  -H "Authorization: Bearer $FLINT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Deploy smoke test",
    "issuer": "https://token.actions.githubusercontent.com",
    "subject": "repo:acme/shop:ref:refs/heads/main",
    "claim_conditions": [
      { "claim": "repository_owner_id", "value": "1234567" },
      { "claim": "repository_id", "value": "89012345" }
    ],
    "scopes": ["payments.payment_intents.read"]
  }'

Create a live identity with a live credential, and give the CLI --live on every command that uses it, as with a live API key.

Use it from the CLI and SDKs#

Set FLINT_WORKLOAD_IDENTITY_ID to the identity's ID. The CLI and the Node and PHP SDKs then get a platform token, exchange it, and refresh it, with no other code. They look for the platform token here:

SourceCLISDKs
FLINT_OIDC_TOKEN, a token in the environmentYesYes
FLINT_OIDC_TOKEN_FILE, the path of a token file, read again on each exchangeYesYes
GitHub Actions, with id-token: writeAutomaticAutomatic
GitLab CI, from id_tokens: into FLINT_OIDC_TOKENAutomaticThrough FLINT_OIDC_TOKEN
Kubernetes, a projected token at /var/run/secrets/flint/tokenAutomaticThrough FLINT_OIDC_TOKEN_FILE
Google Cloud, Azure, AWSThrough either variableThrough either variable

The CLI needs FLINT_NO_INPUT=1 in automation. Both check the token's audience before sending it; it must equal FLINT_OIDC_AUDIENCE, which defaults to https://api.withflintpay.com. Set FLINT_OIDC_SCOPE to request fewer scopes than the identity grants. A stored FLINT_API_KEY or FLINT_ACCESS_TOKEN takes precedence over the workload identity, so unset them in jobs that should use it.

GitHub Actions#

Create the identity with the GitHub Actions preset, or this body:

JSON
{
  "name": "acme/shop main",
  "issuer": "https://token.actions.githubusercontent.com",
  "subject": "repo:acme/shop:ref:refs/heads/main",
  "claim_conditions": [
    { "claim": "repository_owner_id", "value": "1234567" },
    { "claim": "repository_id", "value": "89012345" }
  ],
  "scopes": ["payments.payment_intents.read"]
}

gh api repos/acme/shop --jq '.owner.id, .id' prints the two IDs. The IDs stay the same if the repository is renamed or someone recreates its name. A job that uses an environment has a subject ending in :environment:production instead of the :ref: part. Repositories created on or after July 15, 2026, and repositories renamed, transferred, or opted in since, use the immutable subject form repo:acme@1234567/shop@89012345:ref:refs/heads/main.

In the workflow, setup-flint installs the CLI and configures the identity for the CLI and SDKs in later steps:

YAML
permissions:
  contents: read
  id-token: write

jobs:
  smoke-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - uses: flint-pay/setup-flint@v1
        with:
          workload-identity-id: ${{ vars.FLINT_WORKLOAD_IDENTITY_ID }}
      - run: flint payment-intents list --limit 10 --output json
      - run: node scripts/smoke-test.mjs

Without the action, set FLINT_WORKLOAD_IDENTITY_ID in the job's env and keep id-token: write.

GitLab CI#

Create the identity with the GitLab CI preset, or this body:

JSON
{
  "name": "acme/shop main",
  "issuer": "https://gitlab.com",
  "subject": "project_path:acme/shop:ref_type:branch:ref:main",
  "claim_conditions": [
    { "claim": "namespace_id", "value": "4567890" },
    { "claim": "project_id", "value": "12345678" }
  ],
  "scopes": ["payments.payment_intents.read"]
}

namespace_id is the numeric ID of the group that owns the project, and is required. project_id is optional and stops a renamed project from matching by path. On a self-managed GitLab instance, the issuer is your instance's URL.

In .gitlab-ci.yml, issue the token into FLINT_OIDC_TOKEN:

YAML
smoke-test:
  image: node:24
  id_tokens:
    FLINT_OIDC_TOKEN:
      aud: https://api.withflintpay.com
  variables:
    FLINT_WORKLOAD_IDENTITY_ID: wid_01KX3V8Q2M5T7R9B4N6C1D3F5H
    FLINT_NO_INPUT: "1"
  script:
    - npm install -g @flintpay/cli
    - flint payment-intents list --limit 10 --output json
    - node scripts/smoke-test.mjs

Kubernetes#

Create the identity with the Kubernetes preset, or this body:

JSON
{
  "name": "payments deployer",
  "issuer": "https://oidc.eks.us-east-1.amazonaws.com/id/EXAMPLE",
  "subject": "system:serviceaccount:payments:deployer",
  "scopes": ["payments.payment_intents.read"]
}

The issuer is your cluster's service account issuer. kubectl get --raw /.well-known/openid-configuration prints it as issuer; copy it exactly, including a trailing slash if there is one. EKS, GKE, and AKS publish it publicly, which Flint needs. On AKS, turn on the OIDC issuer first. A self-managed cluster has to serve its discovery document and keys from a public HTTPS URL. The subject names the namespace and service account.

In the pod, project a token for Flint's audience. The kubelet refreshes the file before it expires:

YAML
apiVersion: v1
kind: Pod
metadata:
  name: smoke-test
  namespace: payments
spec:
  serviceAccountName: deployer
  containers:
    - name: app
      image: ghcr.io/acme/shop:latest
      env:
        - name: FLINT_WORKLOAD_IDENTITY_ID
          value: wid_01KX3V8Q2M5T7R9B4N6C1D3F5H
        - name: FLINT_OIDC_TOKEN_FILE
          value: /var/run/secrets/flint/token
      volumeMounts:
        - name: flint-token
          mountPath: /var/run/secrets/flint
          readOnly: true
  volumes:
    - name: flint-token
      projected:
        sources:
          - serviceAccountToken:
              path: token
              audience: https://api.withflintpay.com
              expirationSeconds: 3600

The CLI finds the token at that path on its own; the SDKs need FLINT_OIDC_TOKEN_FILE. Neither reads the cluster's default service account token, whose audience is the cluster's API server.

Google Cloud#

Create the identity with the Google Cloud preset, or this body:

JSON
{
  "name": "Cloud Run checkout worker",
  "issuer": "https://accounts.google.com",
  "subject": "107358963010298374651",
  "scopes": ["payments.payment_intents.read"]
}

The subject is the numeric unique ID of the service account your workload runs as. Google subjects must match exactly, with no patterns, and need no claim conditions because the ID is unique across Google. To print it:

Shell
gcloud iam service-accounts describe checkout-worker@acme-shop.iam.gserviceaccount.com \
  --format='value(uniqueId)'

On Cloud Run or Compute Engine, get an ID token for Flint's audience from the metadata server and pass it on:

Shell
export FLINT_WORKLOAD_IDENTITY_ID=wid_01KX3V8Q2M5T7R9B4N6C1D3F5H
export FLINT_OIDC_TOKEN="$(curl -sf -H 'Metadata-Flavor: Google' \
  'http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/identity?audience=https://api.withflintpay.com')"
flint payment-intents list --limit 10 --output json

Google ID tokens last one hour. For a process that runs longer, write the token to a file, set FLINT_OIDC_TOKEN_FILE to its path, and replace the file before the token expires.

Azure#

Azure issues managed identity tokens only for an app registered in your tenant, so the audience is your own app's Application ID URI, not Flint's. Register one app to stand for Flint:

Shell
APP_ID="$(az ad app create --display-name "Flint API" --query appId -o tsv)"
az ad app update --id "$APP_ID" --identifier-uris "api://$APP_ID"
az ad sp create --id "$APP_ID"
az account show --query tenantId -o tsv

$APP_ID holds the new app's ID, and the last command prints your tenant ID. The examples below show them as APP_ID and TENANT_ID.

The subject is the object ID of the managed identity's service principal, its principal ID. For a user-assigned identity, az identity show --resource-group acme --name checkout-worker --query principalId -o tsv prints it.

Create the identity with the CLI or API, which accept the audience:

JSON
{
  "name": "Container Apps checkout worker",
  "issuer": "https://sts.windows.net/TENANT_ID/",
  "audience": "api://APP_ID",
  "subject": "00000000-0000-0000-0000-000000000000",
  "scopes": ["payments.payment_intents.read"]
}

Keep the trailing slash on the issuer, and name your tenant ID, not common or organizations. Managed identity tokens are version 1 unless the app sets requestedAccessTokenVersion to 2, in which case the issuer is https://login.microsoftonline.com/TENANT_ID/v2.0.

On Container Apps or App Service, request the token from the identity endpoint and pass it on with the matching audience:

Shell
export FLINT_WORKLOAD_IDENTITY_ID=wid_01KX3V8Q2M5T7R9B4N6C1D3F5H
export FLINT_OIDC_AUDIENCE=api://APP_ID
export FLINT_OIDC_TOKEN="$(curl -sf -H "X-IDENTITY-HEADER: $IDENTITY_HEADER" \
  "$IDENTITY_ENDPOINT?api-version=2019-08-01&resource=$FLINT_OIDC_AUDIENCE" | jq -r .access_token)"
flint payment-intents list --limit 10 --output json

On a virtual machine, request it from http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=... with the header Metadata: true. For a user-assigned identity, add client_id= with its client ID to either request. For a long-running process, write the token to the file named by FLINT_OIDC_TOKEN_FILE and replace it before it expires.

AWS#

AWS signs tokens for external services through IAM outbound identity federation. Turn it on once per account, then print your account's issuer:

Shell
aws iam enable-outbound-web-identity-federation
aws iam get-outbound-web-identity-federation-info

Allow the role your workload runs as to request tokens for Flint's audience only:

JSON
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": "sts:GetWebIdentityToken",
      "Resource": "*",
      "Condition": {
        "ForAllValues:StringEquals": {
          "sts:IdentityTokenAudience": "https://api.withflintpay.com"
        }
      }
    }
  ]
}

Create the identity with the AWS preset, or this body:

JSON
{
  "name": "Lambda order sync",
  "issuer": "https://UNIQUE_ID.tokens.sts.global.api.aws",
  "subject": "arn:aws:iam::123456789012:role/order-sync",
  "scopes": ["payments.payment_intents.read"]
}

The issuer belongs to your AWS account alone, and the subject is the ARN of the role that requests the token, so no claim condition is needed.

In the workload, request a token and pass it on. GetWebIdentityToken isn't served by the global STS endpoint, so the AWS CLI or SDK needs a region, which Lambda and ECS set:

Shell
export FLINT_WORKLOAD_IDENTITY_ID=wid_01KX3V8Q2M5T7R9B4N6C1D3F5H
export FLINT_OIDC_TOKEN="$(aws sts get-web-identity-token \
  --audience https://api.withflintpay.com \
  --signing-algorithm ES384 \
  --duration-seconds 900 \
  --query WebIdentityToken --output text)"
flint payment-intents list --limit 10 --output json

From application code, call GetWebIdentityToken with your AWS SDK and set FLINT_OIDC_TOKEN, or write the token to the FLINT_OIDC_TOKEN_FILE path, before the Flint client's first request. Tokens last 5 minutes by default and at most an hour, so a long-running process should replace the file before each one expires.

Troubleshooting#

The token exchange returns 400 with error invalid_grant and an error_description naming what failed. The CLI also prints the token's iss, sub, and aud, so you can compare them with the identity.

MessageFix
The subject token audience does not match the workload identity.Request the token for the identity's audience. When the identity has its own audience, as on Azure, set FLINT_OIDC_AUDIENCE to it. The CLI and SDKs check this before sending and fail with an audience mismatch.
The subject token does not meet the workload identity's conditions.The sub or a claim condition differs. Compare the printed sub with the identity's subject, including the branch, environment, or GitHub's immutable subject form.
The subject token issuer does not match the workload identity.Copy the issuer exactly, including any trailing slash, and on Azure the token version's issuer.
The subject token lifetime exceeds the allowed duration.Request a token valid for 24 hours or less.
The workload identity is unavailable.The ID is wrong, the identity is inactive or archived, or it belongs to another environment. A live identity needs --live in the CLI.
GitHub Actions requires permissions: id-token: writeAdd id-token: write to the workflow's or job's permissions. Workflows triggered from forks don't get it.
No source is availableSet FLINT_OIDC_TOKEN or FLINT_OIDC_TOKEN_FILE, or run where the CLI finds a token on its own.

A 503 with temporarily_unavailable means Flint couldn't fetch the issuer's signing keys. Retry, and check that the issuer's discovery document is reachable from the internet.

Next steps#

Run the CLI in CIEnvironment variables, precedence, and handing a token to other tools.SDK default credentialsHow the Node and PHP SDKs find a credential.Workload identities APICreate, update, deactivate, and archive identities.API key securityWhere an API key is still the right credential, and how to keep it safe.

Was this helpful?