Set up Kinde Management API access
SDKs and APIs
Kinde environments are isolated — configuration doesn’t automatically carry over between dev, staging, and production. This guide shows you how to treat your Kinde setup like infrastructure: define it once as JSON, then use a JavaScript seeding script with the Kinde Management API to apply it consistently across environments.
Create the seed script directory and file:
mkdir scriptstouch scripts/seed-kinde.mjsEnter the following code into the seed script and save changes. Include only the function calls relevant to your use case.
import fs from "node:fs/promises"
const { KINDE_DOMAIN, KINDE_CLIENT_ID, KINDE_CLIENT_SECRET, KINDE_AUDIENCE, KINDE_SCOPES, CONFIG_PATH,} = process.env
if ( !KINDE_DOMAIN || !KINDE_CLIENT_ID || !KINDE_CLIENT_SECRET || !KINDE_AUDIENCE) { throw new Error("Missing Kinde env vars")}
const cfg = JSON.parse(await fs.readFile(CONFIG_PATH, "utf8"))
async function getToken() { const body = new URLSearchParams({ grant_type: "client_credentials", client_id: KINDE_CLIENT_ID, client_secret: KINDE_CLIENT_SECRET, audience: KINDE_AUDIENCE, scope: KINDE_SCOPES, })
const res = await fetch(`https://${KINDE_DOMAIN}/oauth2/token`, { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded" }, body, })
if (!res.ok) { const text = await res.text() // <-- see why it failed throw new Error(`Token error ${res.status}, ${text}`) } const json = await res.json() return json.access_token}
function client(token) { const base = `https://${process.env.KINDE_DOMAIN}` async function call(method, path, body) { const url = new URL(`/api/v1${path}`, base) const res = await fetch(url, { method, headers: { "content-type": "application/json", authorization: `Bearer ${token}`, }, body: body ? JSON.stringify(body) : undefined, }) if (!res.ok) { throw new Error(`${method} ${base} ${path} -> ${res.status}`) } return res.status === 204 ? null : res.json() } return { call }}
// Helper functions
async function createApplicationWithUrls( api, name, type, redirects = [], logouts = []) { console.log("Creating application...")
const errors = [] let appObj = {}
try { appObj = await api.call("POST", "/applications", { name, type, }) } catch { console.log("Cannot create an application, exiting the function") return }
const { id } = appObj.application
console.log("Creating auth redirect URLs...")
try { await api.call("POST", `/applications/${id}/auth_redirect_urls`, { urls: redirects, }) } catch (error) { errors.push("Cannot create redirect urls") }
console.log("Creating auth logout URLs...")
try { await api.call("POST", `/applications/${id}/auth_logout_urls`, { urls: logouts, }) } catch (error) { errors.push("Cannot create logout urls") }
console.log("Created a Kinde application with id", id) if (errors.length) console.log("with some errors", errors)}
async function createEnvVariable(api, item) { console.log("Creating environment variable...")
try { const { key, value, sensitive = false } = item await api.call("POST", `/environment_variables`, { key, value, is_secret: sensitive, }) console.log("Created environment variable", item.key) } catch { console.log("Could not create env variable", item.key) }}
async function createFeatureFlag(api, flag) { try { await api.call("POST", `/feature_flags`, flag) console.log("Created feature flag:", flag.name) } catch { console.log("Could not create feature flag:", flag.name) }}
async function createApiAndScopes(api, { name, audience, scopes = [] }) { console.log("Creating API and Scopes...")
let apiObj = null
try { apiObj = await api.call("POST", `/apis`, { name, audience }) apiObj.name = name console.log("Created API:", name) } catch { console.log("Error creating API:", name) return }
// You will need a Kinde paid plan // To add scope to your API
// Un-comment this block to use it: // for (const s of scopes) { // try { // await api.call("POST", `/apis/${apiObj.api.id}/scopes`, { // key: s, // description: s, // }) // console.log(`Created scope "${s}" to the API ${apiObj.name}`) // } catch (error) { // console.log("Error creating scope:", s) // } // }}
async function createRoleAndPermissions(api, role) { console.log("Creating roles and permissions...")
let roleObj = null
try { roleObj = await api.call("POST", `/roles`, { key: role.key, name: role.name, }) roleObj.name = role.name console.log("Created role:", role.name) } catch { console.log("Error creating role:", role.name) console.log("Exiting.")
return }
const permObjs = [] let permObj = null
for (const p of role.permissions) { try { permObj = await api.call("POST", `/permissions`, { key: p.key, name: p.name, }) permObj.name = p.name console.log("Permission created:", p.name) permObjs.push(permObj) } catch { console.log("Error creating the permission:", p.name) } }
for (const perm of permObjs) { const { id } = perm.permission try { await api.call("PATCH", `/roles/${roleObj.role.id}/permissions`, { permissions: [{ id }], }) console.log( `Added the permission "${perm.name}" to the role ${roleObj.name}` ) } catch { console.log("Error creating the permission") } }}
// Token and create the clientconst token = await getToken()const api = client(token)
// Destructuring the valuesconst { name, type, redirectUrls, logoutUrls } = cfg.applicationconst { envVars, apis, featureFlags, roles } = cfg
// Execute the scriptawait Promise.all([ createApplicationWithUrls(api, name, type, redirectUrls, logoutUrls), ...(envVars ?? []).map((v) => createEnvVariable(api, v)), ...(apis ?? []).map((a) => createApiAndScopes(api, a)), ...(featureFlags ?? []).map((f) => createFeatureFlag(api, f)), ...(roles ?? []).map((r) => createRoleAndPermissions(api, r)),])
console.log("Kinde Seed Complete")The scope property lets you define a subset of scopes for your script. Use it to keep your script scopes minimal, or omit it entirely to use the scopes you defined for your M2M app.
Create the config files for each environment:
mkdir configtouch config/dev.json config/prod.json config/staging.jsonEnter the following seed data into your config files and save changes. Modify the sample data below according to your needs. See the Kinde Management API documentation for available fields.
{ "application": { "name": "My Kinde App, Created with Script", "type": "reg", "redirectUrls": ["http://localhost:3000/callback"], "logoutUrls": ["http://localhost:3000/"] }, "apis": [ { "key": "orders-api", "name": "Orders API", "audience": "orders", "scopes": ["orders:read", "orders:write"] } ], "roles": [ { "key": "admin", "name": "Admin", "permissions": [ { "key": "orders:read", "name": "Read Orders" }, { "key": "orders:write", "name": "Write Orders" } ] } ], "featureFlags": [ { "name": "New Checkout", "key": "new_checkout", "type": "bool", "allow_override_level": "env", "default_value": true } ], "envVars": [ { "key": "PAYMENTS_GATEWAY", "value": "sandbox", "sensitive": false }, { "key": "SECRET_PAYMENTS_GATEWAY", "value": "sandbox", "sensitive": true } ]}Keep your seed script scopes minimal to avoid security vulnerabilities.
Run the following code in your terminal to set your environment variables:
export KINDE_DOMAIN=<YOUR_KINDE_DOMAIN>.kinde.comexport KINDE_CLIENT_ID=<KINDE_CLIENT_ID>export KINDE_CLIENT_SECRET=<KINDE_CLIENT_SECRET>export KINDE_AUDIENCE=https://<YOUR_KINDE_DOMAIN>.kinde.com/apiexport KINDE_SCOPES="create:applications create:application_redirect_uris create:application_logout_uris create:environment_variables create:feature_flags create:apis create:api_scopes create:roles create:permissions update:role_permissions"export CONFIG_PATH=./config/dev.jsonRun the script with the following terminal command. You should see a success message without any errors.
node scripts/seed-kinde.mjsOutput:
Creating application...Creating environment variable...Creating environment variable...Creating API and Scopes...Creating roles and permissions...Creating auth redirect URLs...Created role: AdminCreated API: Orders APICreated environment variable PAYMENTS_GATEWAYCreated environment variable SECRET_PAYMENTS_GATEWAYCreated feature flag: New CheckoutCreating auth logout URLs...Permission created: Read OrdersCreated a Kinde application with id <created_app_id>Permission created: Write OrdersAdded the permission "Read Orders" to the role AdminAdded the permission "Write Orders" to the role AdminKinde Seed CompleteIf you prefer to use Terraform as Infrastructure as Code, there are two options you can consider:
There is a community Terraform provider for Kinde. It supports configuring the provider with domain and audience, and you can alias providers per environment. Treat it as community software, pin versions, and verify resource coverage in non-prod first.
Enable the following additional scopes to your M2M application:
read:apisupdate:apisdelete:apisCreate the following files with the bash command:
touch provider.tf versions.tf variables.tf main.tfEnter the following code in versions.tf and save changes.
terraform { required_providers { kinde = { source = "axatol/kinde" version = "0.0.1" } }}Enter the following code in provider.tf and save changes.
provider "kinde" { domain = var.kinde_domain audience = var.kinde_audience client_id = var.kinde_client_id client_secret = var.kinde_client_secret}Enter the following code in variables.tf and save changes.
variable kinde_domain {}variable kinde_audience {}variable kinde_client_id {}variable kinde_client_secret {}Enter the following code in main.tf and save changes.
resource "kinde_api" "orders" { name = "Orders API" audience = "orders"}Initiate Terraform.
terraform initCreate environment variables with the following bash command.
export TF_VAR_kinde_domain="https://<your_kinde_domain>.kinde.com"export TF_VAR_kinde_client_id="<your_client_id>"export TF_VAR_kinde_client_secret="<your_client_secret>"export TF_VAR_kinde_audience="https://<your_kinde_domain>.kinde.com/api"Run the Terraform project.
terraform applyYou will see a success message.
Terraform used the selected providers to generate the following execution plan.Resource actions are indicated with the following symbols: + create
Terraform will perform the following actions:
# kinde_api.orders will be created + resource "kinde_api" "orders" { + audience = "orders" + id = (known after apply) + name = "Orders API" }
Plan: 1 to add, 0 to change, 0 to destroy.
Do you want to perform these actions? Terraform will perform the actions described above. Only 'yes' will be accepted to approve.
Enter a value: yes
kinde_api.orders: Creating...kinde_api.orders: Creation complete after 1s [id=the_id]This will create an Orders API in your Kinde environment.
From here, add the resources that the provider currently supports and split values by workspace or tfvars files. Use Terraform workspaces or separate states to keep environments isolated.
If you pass providers down to modules, remember provider alias rules in Terraform.
If the provider’s resource coverage is not yet enough, you can still keep everything in Terraform by invoking the Node seed in local-exec. This preserves a single IaC entry point.
Inside your original scripts directory, initialize Terraform with the command.
terraform initCreate a new workspace for one of your environments (e.g. Staging).
terraform workspace new stagingCreate new Terraform files.
touch main.tf variables.tfOpen variables.tf with your favorite code editor, enter the following code and save changes.
variable kinde_domain {}variable kinde_audience {}variable kinde_client_id {}variable kinde_client_secret {}variable kinde_scopes {}Open main.tf and enter the following code and save changes.
locals { cfg_path = "./config/${terraform.workspace}.json"}
resource "null_resource" "seed_kinde" { triggers = { cfg_sha = filesha256(local.cfg_path) }
provisioner "local-exec" { command = "node ./scripts/seed-kinde.mjs" environment = { KINDE_ENV = terraform.workspace KINDE_DOMAIN = var.kinde_domain KINDE_AUDIENCE = var.kinde_audience KINDE_CLIENT_ID = var.kinde_client_id KINDE_CLIENT_SECRET= var.kinde_client_secret KINDE_SCOPES = var.kinde_scopes CONFIG_PATH = local.cfg_path } }}Create environment variables with the following bash command.
export TF_VAR_kinde_domain="<your_kinde_domain>.kinde.com"export TF_VAR_kinde_client_id="<your_client_id>"export TF_VAR_kinde_client_secret="<your_client_secret>"export TF_VAR_kinde_audience="https://<your_kinde_domain>.kinde.com/api"export TF_VAR_kinde_scopes="create:applications create:application_redirect_uris create:application_logout_uris create:environment_variables create:feature_flags create:apis create:api_scopes create:roles create:permissions update:role_permissions"Run the Terraform project:
terraform applyYou will see the following success output:
null_resource.seed_kinde: Creating...null_resource.seed_kinde: Provisioning with 'local-exec'...null_resource.seed_kinde (local-exec): Executing: ["/bin/sh" "-c" "node ./scripts/seed-kinde.mjs"]null_resource.seed_kinde (local-exec): Creating application...null_resource.seed_kinde (local-exec): Creating environment variable...null_resource.seed_kinde (local-exec): Creating environment variable...null_resource.seed_kinde (local-exec): Creating API and Scopes...null_resource.seed_kinde (local-exec): Creating roles and permissions...null_resource.seed_kinde (local-exec): Creating auth redirect URLs...null_resource.seed_kinde (local-exec): Created API: Orders APInull_resource.seed_kinde (local-exec): Created environment variable SECRET_PAYMENTS_GATEWAYnull_resource.seed_kinde (local-exec): Created environment variable PAYMENTS_GATEWAYnull_resource.seed_kinde (local-exec): Created feature flag: New UInull_resource.seed_kinde (local-exec): Creating auth logout URLs...null_resource.seed_kinde (local-exec): Created role: Adminnull_resource.seed_kinde (local-exec): Created a Kinde application with id ***************null_resource.seed_kinde (local-exec): Permission created: Read Ordersnull_resource.seed_kinde (local-exec): Permission created: Write Ordersnull_resource.seed_kinde (local-exec): Added the permission "Read Orders" to the role Adminnull_resource.seed_kinde (local-exec): Added the permission "Write Orders" to the role Adminnull_resource.seed_kinde (local-exec): Kinde Seed Completenull_resource.seed_kinde: Creation complete after 3s [id=4289077431837605208]Go to your Kinde dashboard to verify the changes.
Create a GitHub workflow directory and a workflow file seed-kinde.yml with the following bash command:
mkdir -p .github/workflowstouch .github/workflows/seed-kinde.ymlOpen seed-kinde.yml in your preferred code editor, add the following code, and save changes.
name: Kinde Seed Automationon: workflow_dispatch: inputs: environment: description: "Which Kinde env to seed" required: true type: choice options: [dev, staging, prod]
jobs: seed: runs-on: ubuntu-latest environment: ${{ inputs.environment }} steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: "20" - run: node ./scripts/seed-kinde.mjs env: KINDE_ENV: ${{ inputs.environment }} KINDE_DOMAIN: ${{ secrets.KINDE_DOMAIN }} # e.g. subdomain.region.kinde.com KINDE_CLIENT_ID: ${{ secrets.KINDE_CLIENT_ID }} KINDE_CLIENT_SECRET: ${{ secrets.KINDE_CLIENT_SECRET }} KINDE_AUDIENCE: ${{ secrets.KINDE_AUDIENCE }} # management API audience # Keep scopes least-privilege. Add only what you need. KINDE_SCOPES: >- create:applications create:application_redirect_uris create:application_logout_uris create:environment_variables create:feature_flags create:apis create:api_scopes create:roles create:permissions update:role_permissions CONFIG_PATH: ./config/${{ inputs.environment }}.jsonPush the changes to GitHub.
git add .git commit -m "Add Kinde Seed Automation workflow"git pushOpen your GitHub repository and go to Settings > Secrets and variables > Actions > Repository secrets > select New repository secret.
Add all the following environment variable values and select Add secret.
KINDE_DOMAINKINDE_CLIENT_IDKINDE_CLIENT_SECRETKINDE_AUDIENCEGo to Actions > Kinde Seed Automation > select Run workflow.
In the drop-down menu:
Select the workflow after it finishes running.
Go to seed > select Run node ./scripts/seed-kinde.mjs to expand the logs. You will see the seed output logs.
Now you can automate the Kinde seed script with your CI pipeline.
A new application in your dashboard with the configured redirect and logout URLs.
Adding APIs and scopes via the API requires a paid plan. See Kinde pricing.
Roles created with permissions assigned.
Run the same script for all of your environments and config presets as required.
configuration as code?Select the environment drop-down in the top left of the Kinde home screen, and select All environments.
Select Add environment. A dialog opens.
dev).Create any additional environments you need (e.g. Staging, Testing).
Kinde’s Free plan allows you to create up to 1 non-production environment. To add more, upgrade to a paid plan. See Kinde pricing.