Set up authentication with Okta

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

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 return 404 or invalid_audience.

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 Moveworks tenant is provisioned for the Conversations API and your account team has provided the Bot Name (used in the Assistant-Name header on every API call).
  • 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 (commonly the user’s email or login) resolves against your Moveworks user roster.

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 TypeIdPTells Moveworks to resolve the identifier claim through an integration rather than as a raw email or record ID.
Integration IDconversations_rest_apiRequired when Identifier Type is IdP. Moveworks matches the sub claim against the user’s Conversations API channel entry.
Identifier ClaimsubLeave as default.

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.

If your Moveworks user roster uses a field other than the Okta userName (for example, if user.email differs from user.login in your org), override the sub expression accordingly. 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:

1{
2 "access_token": "mwt_...",
3 "expires_in": 3599,
4 "token_type": "Bearer"
5}

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 (your Bot Name from your Moveworks account team).

$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. 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.
  • 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.

invalid_audience on a call to /oauth/v1/token.

Your Okta token’s aud does not match what’s registered on your Moveworks credential. Compare the decoded aud against the Audience field on your Moveworks credential exactly.

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

The sub claim resolved to a Moveworks user, but potentially the wrong one. Check that the identifier in sub matches the user’s channel entry with integration_id: conversations_rest_api in Moveworks. If your Okta emits a different email or login format than what’s on the Moveworks user record, adjust either the Okta sub claim mapping (Step 3.2) or the user’s channel entry.

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 TypeIdP
Moveworks credentialIntegration IDconversations_rest_api
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.