Set up authentication with Okta

Configure Okta to authenticate against the Moveworks Conversations API using a JWT OAuth credential.
View as Markdown

The Conversations API is in Controlled Availability. Endpoints, behaviors, and configuration surfaces may change during CA. Contact your Moveworks account team to participate.

The examples in this guide use https://api.moveworks.ai, which is only correct for orgs on the US Production data center. Replace it with the base URL for your data center from the Base URLs table (for example, https://api.am-eu-central.moveworks.ai for EU). Requests to the wrong host fail authentication, because credentials are registered per data center.

This guide walks through configuring Okta to authenticate against the Moveworks Conversations API. By the end you will have Okta issuing access tokens for your users, and your application successfully calling the Conversations API on their behalf.

How authentication works

The Conversations API uses a token exchange model. Your application obtains an access token from Okta, exchanges it for a Moveworks bearer token, and uses the Moveworks bearer on Conversations API requests.

1

Get an Okta access token

Your application obtains an access token from Okta the standard way for its client type (Authorization Code + PKCE for a browser SPA, Authorization Code for a server-side web app).

2

Exchange the Okta token for a Moveworks bearer

Your application POSTs the Okta token to https://api.moveworks.ai/oauth/v1/token with grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer. Moveworks validates the signature, claims, and identity, then returns a short-lived opaque bearer token (mwt_...).

3

Call the Conversations API

Your application sends the Moveworks bearer (mwt_...) as Authorization: Bearer on every Conversations API request. The bearer represents the user resolved during exchange, expires in about an hour, and is refreshed by repeating the exchange step with a fresh Okta token.

This guide covers user-facing clients where a real person signs in through Okta. For a backend service that acts on behalf of users without an interactive sign-in, use service-to-service authentication instead.

Prerequisites

Confirm the following before you start:

  • Your account team has enabled the Conversations API for your org, and you have created the chatbot and recorded its Bot Name (used in the Assistant-Name header on every API call). See Create the Conversations API chatbot.
  • You have admin access to the Moveworks Admin Portal (https://{org}.moveworks.com).
  • You have admin access to your Okta org.
  • The identifier your Okta sub claim will carry resolves against your Moveworks user roster. This guide assumes sub carries the user’s email address, which is the default on a Custom Authorization Server and matches the email on the user’s Moveworks profile. If your sub carries a different identifier, see the alternative in step 2.

Step 1: Create the Okta authorization server

Custom vs Org authorization server. This guide uses a Custom Authorization Server. Okta also has a built-in “Org” (default) authorization server, but its audience is nominally fixed and behaves inconsistently in practice, which makes it harder to configure and debug. Use a Custom server unless you have a specific reason not to.

In the Okta Admin Console, go to Security → API → Authorization Servers → Add Authorization Server.

  • Name: any descriptive label (for example Moveworks-CAPI).
  • Audience: enter any placeholder for now if you plan to use a Moveworks-generated audience; you’ll set the real value in Step 3.1. If you intend to keep an audience value you already use, enter it here and register the same value in Moveworks in Step 2.
  • Description: optional.

Click Save. Once created, capture two values from the auth server. You’ll paste both into Moveworks in Step 2.

1

1.1 Copy the Metadata URI

On the auth server’s Settings tab, find the Metadata URI field and copy its value. It looks like https://{yourOktaDomain}/.well-known/oauth-authorization-server/oauth2/{authServerId}. This is your Public Key value for the Moveworks credential.

2

1.2 Get the Issuer URL from the metadata JSON

Okta’s Settings tab shows “Issuer: Dynamic (based on request domain)” rather than a static URL. To get the real value, open the Metadata URI you copied in a browser tab. The response is JSON; find the "issuer" field near the top. It looks like https://{yourOktaDomain}/oauth2/{authServerId} (no .well-known/* in it). That’s your Issuer for the Moveworks credential.

Step 2: Register the credential in Moveworks

In the Moveworks Admin Portal, go to https://{org}.moveworks.com/http-connectors/connector_studio/homeCredentialsCreate Credential. Set the fields as follows.

Two ways to set the Audience. Leave the Audience field blank and Moveworks generates a unique audience value on save, which you will paste into Okta in Step 3. Or, if your Okta authorization server already has an audience value you cannot change, enter that exact value here instead. Whichever path you choose, the Audience on the Moveworks credential and the Audience on the Okta authorization server must match byte-for-byte.

FieldValueNotes
Credential NameAny descriptive label (for example Okta-CAPI).Free text.
Credential TypeJWT OAuthReveals the fields below.
Public Key valueThe Metadata URI you copied in Step 1.1.This field accepts a well-known endpoint URL in place of a raw PEM. Moveworks auto-fetches the JWKS from it and rotates keys automatically.
AudienceLeave blank, or enter your authorization server’s existing audience.Left blank, Moveworks generates a unique value on save (format: api.moveworks.ai/{uuid}). If you enter your own value, it must exactly match the Audience on your Okta authorization server.
IssuerThe Issuer URL you extracted from the metadata JSON in Step 1.2 (looks like https://{yourOktaDomain}/oauth2/{authServerId}, no .well-known/*).Must match the iss claim on every token exactly. No trailing slash.
Identifier TypeEmailMoveworks matches the identifier claim directly against the email address on the user’s Moveworks profile. Because Okta’s sub claim carries the user’s userName, which is an email address in most orgs, this resolves users with no additional identity configuration. If your sub carries something other than an email address, see the alternative below.
Integration IDLeave blankOnly used when Identifier Type is IdP.
Identifier ClaimsubLeave as default.

Credential fields cannot be edited after you save. If the Identifier Type, Identifier Claim, Audience, or Issuer turns out to be wrong, create a new credential with the corrected values. Confirm them before saving.

Use Email when the claim carries an email address. If your Okta sub carries a different identifier, such as an employee number or an internal user ID, set Identifier Type to IdP instead and set Integration ID to the identity integration that stores that same identifier on your users’ Moveworks profiles.

To find the exact name, go to Moveworks Setup > Connectors > Built-in Connectors, find the row whose System matches your identity source, open it, and copy the connection name shown at the top of the connector page. For an Okta-sourced org this is commonly okta, though orgs running more than one instance use names like okta_prod. Whichever integration you register, the value indexed under it must be the same identifier your sub claim carries, since a mismatch causes the token exchange to fail.

Orgs whose profiles already carry a channel entry with the integration ID conversations_rest_api can enter that value here. Existing configurations do not need to change.

Click Save and then Publish. Moveworks displays three values back to you:

  • Subject: Moveworks-internal ID for the credential.
  • Audience: the credential’s audience. If you left the field blank, this is the unique value Moveworks generated (format: api.moveworks.ai/{uuid}); copy it to paste into Okta in Step 3. If you entered your own value, confirm it displays exactly as you entered it.
  • Public Key 1 - ID: Moveworks-side identifier for the key it fetched from your Metadata URI. No action needed.

Step 3: Finish configuring the Okta authorization server

Return to the Okta Admin Console, Security → API → Authorization Servers, and open the authorization server you created in Step 1.

1

3.1 Align the audience

On the Settings tab, set Audience to match the Audience on your Moveworks credential: the generated api.moveworks.ai/{uuid} value from Step 2 if you left the field blank, or the value you entered yourself. If you registered your authorization server’s existing audience in Step 2, no change is needed here; just confirm the two match. Save.

The two values must match byte-for-byte. Any mismatch produces 401 Token validation failed at token exchange.

2

3.2 Confirm the sub claim value

On the Claims tab, find the sub claim on Access Token. The default expression on a Custom Authorization Server is:

(appuser != null) ? appuser.userName : app.clientId

With a signed-in user, that expression sets sub to the user’s userName (typically their email or login), which resolves against the Moveworks user roster.

With Identifier Type set to Email in step 2, sub must carry the same email address as the user’s Moveworks profile. If user.login differs from user.email in your org, override the expression to user.email so the two match. Otherwise, leave the default.

3

3.3 Confirm the openid scope is available

On the Scopes tab, confirm openid is present and enabled. This is the minimum scope required for Moveworks to receive a token containing the user’s identity.

The access policy rule that links this authorization server to your OAuth application comes in Step 4.4, after you create the app.

Step 4: Create the Okta OAuth application

In the Okta Admin Console, go to Applications → Applications → Create App Integration. The dialog asks for two things:

  1. Sign-in method — choose OIDC - OpenID Connect. (The other option, API Services, issues tokens that represent the app itself rather than a signed-in user, which does not fit the Conversations API’s per-user model and is not covered by this guide. For backend integrations, use service-to-service authentication.)
  2. Application type — pick one:
    • Single-Page Application for a browser-based client (React, Vue, Angular, etc.). Uses Authorization Code + PKCE.
    • Web Application for a server-side client with a client secret. Uses Authorization Code.
    • Native Application for a desktop or mobile app.

Click Next.

1

4.1 Configure the application

In the app’s General settings, add all redirect URIs your application will use to receive the auth code (for example https://your-app.example.com/callback).

2

4.2 Assign users or groups

On the app’s Assignments tab, assign the users or groups that should be able to authenticate through this application. Users must be assigned here or Okta will not issue tokens for them (even if the access policy in Step 4.4 allows it).

3

4.3 Add trusted origins (browser clients only)

Under Security → API → Trusted Origins, add your application’s domain with CORS and Redirect both enabled. Skip this step for server-side applications.

4

4.4 Attach the app to the authorization server via access policy

Return to Security → API → Authorization Servers → your Moveworks-CAPI server → Access Policies tab. Add or edit a policy, then add a rule with:

  • Grant type: Authorization Code.
  • User: any user assigned to the application (or a specific group).
  • Scopes: openid (or Any scopes).
  • Assigned to: the OAuth application you created in 4.1.

Without this rule, Okta rejects token requests for your app before Moveworks ever sees them.

Note your app’s Client ID. You need it in the next step. Web Applications also have a Client Secret; SPAs do not.

Step 5: Get an Okta access token

In production, your app’s Okta SDK handles the browser redirect, code exchange, and token capture automatically. For manual testing, use an OIDC playground such as oauth.tools, or run a local PKCE flow with a script and register http://localhost:PORT/callback as a redirect URI on your Okta application.

However you run the flow, the response includes an access_token field. Copy that value; it is your Okta access token. Decode it (for example at jwt.io) and confirm:

  • iss matches the Issuer you registered in Step 2 exactly.
  • aud matches the Audience on your Moveworks credential (the generated api.moveworks.ai/{uuid} value, or the custom value you registered).
  • sub matches the identifier expected by your Moveworks user roster.

If any of these do not match, the exchange in Step 6 will fail. Re-check the corresponding fields in Steps 2 and 3.2 before continuing.

Step 6: Exchange the Okta token for a Moveworks bearer

Your application must complete this step before making any Conversations API call.

curl -X POST "https://api.moveworks.ai/oauth/v1/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer" \
--data-urlencode "assertion=<OKTA_ACCESS_TOKEN>"

A successful response:

{
"access_token": "mwt_...",
"expires_in": 3599,
"token_type": "Bearer"
}

The mwt_... value is your Moveworks access token. It represents the user resolved from the Okta sub claim, expires in about an hour, and is the bearer you use on every Conversations API call.

Cache and refresh: cache this token for the duration in expires_in (typically ~1 hour). When it expires, get a fresh Okta token and repeat this exchange. Do not exchange on every API call; that pattern will hit rate limits.

Step 7: Call the Conversations API

Use the Moveworks bearer from Step 6 on every request. Include the Assistant-Name header (the Bot Name of the chatbot you created).

curl -X POST "https://api.moveworks.ai/assistant/v1/conversations" \
-H "Authorization: Bearer <MW_ACCESS_TOKEN>" \
-H "Assistant-Name: <YOUR_BOT_NAME>" \
-H "Content-Type: application/json" \
-d '{"title": "Test conversation"}'

A 201 response with a conversation_id confirms the whole chain works end to end.

Troubleshooting

401 UNAUTHORIZED with Token validation failed when calling /assistant/v1/*.

Check that your Authorization: Bearer value is the Moveworks bearer from Step 6 (starts with mwt_), not the raw Okta access token (starts with eyJ). If you’re already sending mwt_... and still hitting this error, the bearer has likely expired; repeat Step 6 with a fresh Okta token.

401 UNAUTHORIZED with Token validation failed at the exchange endpoint (/oauth/v1/token).

The Okta token was received but Moveworks rejected it. Decode the Okta token and confirm:

  • iss matches the Issuer field on your Moveworks credential (byte-for-byte, no trailing slash, correct case).
  • aud matches the Audience field on your Moveworks credential exactly (whether Moveworks-generated or the custom value you registered in Step 2).
  • sub matches the identifier your Moveworks user roster resolves. With Identifier Type set to Email, this must be the email address on the user’s Moveworks profile. If sub is an Okta internal user ID starting with 00u, you’re looking at the ID token, not the access token. Decode the access token instead. If sub on the access token is still 00u..., revisit Step 3.2 to override the default expression, or use the IdP alternative in Step 2 to match that ID against an identity integration.
  • The credential in Moveworks Setup is Published (not draft).
  • The token has not expired (exp is in the future).

Okta rejects the token request before Moveworks sees it (error at /authorize or /token).

Okta’s access policy or user assignment blocks the token issuance. Revisit Step 4.2 (assign the user to the app) and Step 4.4 (access policy rule allowing your app + grant type + scopes).

aud claim comes out as api://default in the token.

Your token is being issued by Okta’s pre-built default authorization server rather than the one you created in Step 1. Either point your app’s token requests at the right authorization server, or, since the Moveworks credential accepts any audience value, register api://default as the credential’s Audience. A dedicated authorization server with a distinct audience is easier to debug.

401 INVALID_JWT, “Client credentials not found”, on a call to /oauth/v1/token.

Moveworks locates your credential by the iss and aud pair on the incoming token, so this means that pair matched no registered credential. Compare the decoded aud against the credential’s Audience and the decoded iss against its Issuer, exactly. Also confirm the credential is Published and that you are calling the base URL for your data center.

401 INVALID_JWT, “JWT signature verification failed”, on a call to /oauth/v1/token.

The credential was found, so the issuer and audience are right. Confirm the credential’s metadata URL points at the same Okta authorization server that minted the token, so that Moveworks fetches the matching signing key.

User is authenticated but Conversations API responses look wrong or empty.

The sub claim resolved to a Moveworks user, but potentially the wrong one. Confirm the email address in sub is the one on the intended user’s Moveworks profile, and that the same address is not present on more than one profile. If your Okta emits a different email or login format than what is on the Moveworks user record, adjust the Okta sub claim mapping (Step 3.2).

Summary of what you configured

WhereFieldValue
Okta authorization serverNameAny (for example Moveworks-CAPI)
Okta authorization serverAudienceMatches the Moveworks credential’s Audience (Moveworks-generated api.moveworks.ai/{uuid} or your own value)
Okta authorization serversub claimDefault expression confirmed, or overridden to match your roster identifier
Okta authorization serverAccess policyRule allowing your app + grant type + openid scope
Moveworks credentialCredential TypeJWT OAuth
Moveworks credentialPublic Key valueMetadata URI from your Okta authorization server
Moveworks credentialAudienceMoveworks-generated UUID (left blank on save) or your own value matching the Okta authorization server
Moveworks credentialIssuerIssuer URL from your Okta authorization server
Moveworks credentialIdentifier TypeEmail
Moveworks credentialIntegration IDBlank (only used with Identifier Type IdP)
Moveworks credentialIdentifier Claimsub
Okta applicationSign-in methodOIDC - OpenID Connect
Okta applicationApplication typeSPA, Web Application, or Native
Okta applicationRedirect URIsYour app’s callback URLs (SPA and Web App only)
Okta applicationAssignmentsUsers or groups allowed to authenticate
Your applicationStep 5Get an Okta access token (SDK or manual PKCE flow)
Your applicationStep 6Exchange at POST /oauth/v1/token for mwt_...
Your applicationStep 7Call Conversations API with Authorization: Bearer mwt_... and Assistant-Name: <BotName>

For interaction patterns (polling and streaming) and the full API contract, see the Conversations API reference.