Kinde Management API Quickstart Guide
SDKs and APIs
Manage your Kinde business as code — applications, APIs, connections, organizations, users, roles, and permissions — with the Kinde Terraform provider (source on GitHub).
The provider authenticates as a machine-to-machine (M2M) application.
In Kinde, go to Settings > Applications, add a Machine to Machine application, and authorize it for the Kinde Management API. See the quick start guide for the full steps.
Grant the following scopes, which cover the permission and role created in this quickstart:
create:permissions read:permissions update:permissions delete:permissionscreate:roles read:roles update:roles delete:rolesread:role_permissions update:role_permissions delete:role_permissionsEach resource needs its own scopes — see Required scopes for the full list.
Select View details on the application and copy the Domain, Client ID, and Client secret.
Add the provider to your configuration file versions.tf, then initialize the working directory.
terraform { required_providers { kinde = { source = "kinde-oss/kinde" version = "~> 0.1.0" } }}terraform initExport your M2M credentials with the following terminal command. Include the https:// scheme in the domain. The audience is your domain followed by /api.
export KINDE_DOMAIN="https://<your_subdomain>.kinde.com"export KINDE_AUDIENCE="https://<your_subdomain>.kinde.com/api"export KINDE_CLIENT_ID="<your_m2m_client_id>"read -rs KINDE_CLIENT_SECRET && export KINDE_CLIENT_SECRETThe read -rs prompt keeps the client secret out of your shell history — paste the secret when prompted. In CI, inject all four values from your secret store.
With the environment variables set, the provider block can be empty.
provider "kinde" {}Add the following code to main.tf to create a permission and a role.
resource "kinde_permission" "read_billing" { name = "Read billing" key = "read:billing" description = "Grants read access to billing"}
resource "kinde_role" "finance" { name = "Finance" key = "finance" description = "Finance team role" permissions = [kinde_permission.read_billing.id]}Apply your configuration with the following terminal command.
terraform applykinde_permission.read_billing: Creating...kinde_permission.read_billing: Creation complete after 1s [id=<permission_id>]kinde_role.finance: Creating...kinde_role.finance: Creation complete after 1s [id=<role_id>]
Apply complete! Resources: 2 added, 0 changed, 0 destroyed.Go to Settings > Roles and Settings > Permissions to see the new role and permission. Make sure you are viewing the environment your M2M application belongs to.
Run terraform plan again at any time to detect drift between your configuration and what is in Kinde.
Management API scopes are resource-specific. Grant only the scopes for the resources in your configuration.
| Resource | Required scopes |
|---|---|
kinde_api | create:apis, read:apis, delete:apis |
kinde_application | create:applications, read:applications, update:applications, delete:applications |
kinde_application_connection | create:application_connections, read:application_connections, delete:application_connections |
kinde_connection | create:connections, read:connections, update:connections, delete:connections |
kinde_organization | create:organizations, read:organizations, update:organizations, delete:organizations |
kinde_organization_user | create:organization_users, read:organization_users, update:organization_users, delete:organization_users |
kinde_permission | create:permissions, read:permissions, update:permissions, delete:permissions |
kinde_role | create:roles, read:roles, update:roles, delete:roles, read:role_permissions, update:role_permissions, delete:role_permissions |
kinde_user | create:users, read:users, update:users, delete:users, create:user_identities, read:user_identities |
kinde_user_role | create:organization_user_roles, read:organization_user_roles, delete:organization_user_roles |
| Data source | Required scopes |
|---|---|
kinde_api | read:apis |
kinde_application | read:applications |
kinde_connections | read:connections |
Setting the roles attribute on kinde_organization_user also requires the organization_user_roles scopes listed for kinde_user_role. kinde_api has no update scope because the Kinde API does not support updating an API — changing one replaces it.
Grant the delete: scopes for everything you manage. Without them Terraform cannot destroy what it creates, and a failed apply may leave resources behind that you need to clean up by hand in Kinde.
Every attribute can be set in the provider block or through the environment. Attributes take precedence over environment variables.
| Attribute | Environment variable |
|---|---|
domain | KINDE_DOMAIN |
audience | KINDE_AUDIENCE |
client_id | KINDE_CLIENT_ID |
client_secret | KINDE_CLIENT_SECRET |
provider "kinde" { domain = var.kinde_domain audience = var.kinde_audience client_id = var.kinde_client_id client_secret = var.kinde_client_secret}Terraform state is not encrypted by default. Values such as an application’s client_secret and a connection’s OAuth credentials are stored in state even when they are marked sensitive. Use an encrypted remote backend, and never commit state or credentials to source control. See Sensitive data in state in the Terraform docs.
Register an application with its callback URLs. Kinde issues the client ID and secret, which you can reference as outputs.
Grant the following scopes on the M2M application that authenticates the provider:
create:applications read:applications update:applications delete:applicationsSee Required scopes for the full list.
Add the following code to applications.tf to create a regular web application.
resource "kinde_application" "web" { name = "Web app" type = "reg" login_uri = "https://example.com/oauth/login" homepage_uri = "https://example.com" logout_uris = ["https://example.com/oauth/logout"] redirect_uris = ["https://example.com/oauth/callback"]}
output "web_app_client_id" { value = kinde_application.web.client_id}Application types:
reg back-end web appspa single-page appm2m machine-to-machineChanging name or type forces a replacement, which issues a new client ID and secret — the URIs are updatable in place.
Memberships and role assignments are separate resources. Because a role can only be assigned to a user who is already a member of the organization, declare the ordering with depends_on.
Grant the following scopes on the M2M application that authenticates the provider:
create:organizations read:organizations update:organizations delete:organizationscreate:users read:users update:users delete:users create:user_identities read:user_identitiescreate:organization_users read:organization_users update:organization_users delete:organization_userscreate:organization_user_roles read:organization_user_roles delete:organization_user_rolesThis example also uses the finance role from the quickstart, so keep the role scopes enabled. See Required scopes for the full list.
Add the following code to organizations.tf to create an organization, add a user, and assign a role.
resource "kinde_organization" "acme" { name = "Acme"}
resource "kinde_user" "jane" { first_name = "Jane" last_name = "Doe"
identities = [ { type = "email" value = "jane@example.com" } ]}
resource "kinde_organization_user" "jane_acme" { organization_code = kinde_organization.acme.code user_id = kinde_user.jane.id
# kinde_user_role manages role assignments for this membership. lifecycle { ignore_changes = [roles] }}
resource "kinde_user_role" "jane_finance" { organization_code = kinde_organization.acme.code user_id = kinde_user.jane.id role_id = kinde_role.finance.id
# A user must be a member of the organization before roles can be assigned. depends_on = [kinde_organization_user.jane_acme]}Choose one way to manage roles for a membership:
kinde_user_role (shown above): one resource per assignment. Add ignore_changes = [roles] to the membership so the two resources do not fight over the same list.roles on kinde_organization_user: pass the full list of role IDs on the membership itself, and do not create kinde_user_role resources for that user.A kinde_user needs a first_name, a last_name, and at least one email identity. You can add username and phone identities alongside it. Social and enterprise identities are created when a user signs in and are not managed by Terraform.
Use the kinde_connections data source to look up built-in connections, or create a social connection with kinde_connection, then enable it on an application with kinde_application_connection.
# Look up a built-in connection by strategydata "kinde_connections" "builtin" { filter = "builtin"}
locals { password_connection_id = one([ for c in data.kinde_connections.builtin.connections : c.id if c.strategy == "username:password" ])}
resource "kinde_application_connection" "web_password" { application_id = kinde_application.web.id connection_id = local.password_connection_id}
# Create a social connection and enable itresource "kinde_connection" "google" { name = "google" display_name = "Google" strategy = "oauth2:google"
options = { client_id = var.google_client_id client_secret = var.google_client_secret }}
resource "kinde_application_connection" "web_google" { application_id = kinde_application.web.id connection_id = kinde_connection.google.id}The filter attribute accepts builtin, custom, or all. Connection strategy values follow the Management API, for example email:otp, username:password, oauth2:google, oauth2:github, oauth2:azure_ad, and saml:custom.
Full schema documentation for every resource and data source is on the Terraform Registry.
| Resource | Description |
|---|---|
kinde_api | APIs registered with your business |
kinde_application | Applications (reg, spa, or m2m), including login, homepage, redirect, and logout URIs |
kinde_application_connection | Enables a connection on an application |
kinde_connection | Social and enterprise identity connections |
kinde_organization | Organizations, including theme colors and handle |
kinde_organization_user | Membership of a user in an organization |
kinde_permission | Permissions |
kinde_role | Roles and their assigned permissions |
kinde_user | Users and their identities |
kinde_user_role | Assignment of a role to a user in an organization |
| Data source | Description |
|---|---|
kinde_api | Look up an existing API by ID |
kinde_application | Look up an existing application by ID |
kinde_connections | List connections in your business |
name or audience of a kinde_api, the name or type of a kinde_application, or the strategy of a kinde_connection destroys and recreates the resource, because the Management API does not support updating those fields. A replaced application gets a new client ID and client secret.kinde_role requires a description. The Management API cannot unset a description once it is set, so the provider requires one to avoid state drift.kinde_user restrictions. is_suspended cannot be true when creating a user — create the user first, then update it. organization_code is sent only on create.kinde_organization_user adds the membership but one of its roles fails — the provider attempts to remove what that resource created. It does not span resources: a failed kinde_user_role does not remove the membership it depends on. After an apply error, run terraform plan to see what exists before resuming or destroying.Every resource supports terraform import, so you can bring existing Kinde configuration under Terraform management.
Add a resource block that matches the existing object.
Import it using its Kinde ID. Find IDs in the Kinde dashboard or through the Management API.
terraform import kinde_role.finance <role_id>On Terraform 1.5 or later you can use an import block instead and generate the configuration with terraform plan -generate-config-out=generated.tf.
import { to = kinde_role.finance id = "<role_id>"}Run terraform plan and adjust the configuration until it shows no changes.
Resources that exist in the context of another resource use a composite ID separated by colons. Organizations are imported by their code.
| Resource | Import ID |
|---|---|
kinde_organization | <org_code> |
kinde_application_connection | <application_id>:<connection_id> |
kinde_organization_user | <org_code>:<user_id> |
kinde_user_role | <org_code>:<user_id>:<role_id> |
Kinde environments are isolated, and each one has its own domain and M2M application keys. Keep one Terraform state per environment and swap the credentials per run.
terraform workspace or one directory per environment, and pass environment-specific values with .tfvars files.provider "kinde" block per environment with an alias, and select it on each resource with provider = kinde.<alias>.KINDE_* environment variables from your secret store for the target environment.name: Kinde Terraformon: workflow_dispatch: inputs: environment: description: "Kinde environment to apply" required: true type: choice options: [dev, staging, prod]
jobs: apply: runs-on: ubuntu-latest environment: ${{ inputs.environment }} env: KINDE_DOMAIN: ${{ secrets.KINDE_DOMAIN }} KINDE_AUDIENCE: ${{ secrets.KINDE_AUDIENCE }} KINDE_CLIENT_ID: ${{ secrets.KINDE_CLIENT_ID }} KINDE_CLIENT_SECRET: ${{ secrets.KINDE_CLIENT_SECRET }} steps: - uses: actions/checkout@v4 - uses: hashicorp/setup-terraform@v3 - run: terraform init - run: terraform plan -out=tfplan -var-file="${{ inputs.environment }}.tfvars" - run: terraform apply tfplanThe environment input selects the matching GitHub environment, so its protection rules — such as required reviewers on prod — gate the job. Saving the plan with -out and applying that file means the apply executes exactly the plan that was produced, not a recomputed one.
For how to create additional Kinde environments, see Manage Kinde environments.
Environment variables, feature flags, API scopes, and business or environment settings are not covered in the current release. Use the Management API seed script for those, or open an issue on the provider repository to request a resource. If you run the seed script alongside Terraform, keep Terraform as the only owner of applications, APIs, connections, organizations, users, roles, and permissions, and limit the seed script to the settings listed above — two owners for the same resource create duplicates and drift.
Terraform 1.0 and later. The provider uses Terraform plugin protocol 6.
On the Terraform Registry. The registry docs are generated from the provider code, so they always match the published version.
Open an issue or pull request on GitHub. The provider is open source under the Mozilla Public License 2.0.
If you need help getting Kinde connected, contact us at support@kinde.com.