Set up service-to-service authentication
Set up service-to-service authentication
Authenticate a backend service to the Moveworks Conversations API using a self-signed JWT bearer, letting your service make requests on behalf of any provisioned user.
The Conversations API is in Controlled Availability. Endpoints, behaviors, and configuration surfaces may change during CA. Contact your Moveworks account team to participate.
This guide covers authenticating a backend service to the Moveworks Conversations API, so that service can make requests on behalf of any provisioned user in your Moveworks tenant. Your backend generates and signs the JWT locally, then exchanges it for a Moveworks bearer token.
When to use this
Use this pattern when a trusted backend system (test harness, batch worker, monitoring bot, scripted integration) needs to make Conversations API calls on behalf of an already-known user, and there is no interactive user browser flow available to obtain a token.
Use one of the interactive user guides instead when:
- Real end users sign in through your application and each request represents that specific signed-in user. See Set up authentication with Okta or Set up authentication with Azure AD (Entra ID).
The trade-off:
How it works
Your backend service holds an RSA private key. The matching public key is registered as a credential in your Moveworks tenant. For each API call you want to make on behalf of a user:
- Your backend builds a short-lived JWT that names the user in the
subclaim and signs it with the private key. - Your backend exchanges the signed JWT at
POST /oauth/v1/tokenfor a Moveworks bearer token. - Your backend uses that bearer token on Conversations API calls. The bearer is scoped to the user in
sub.
The private key is a tenant-wide credential. Any process holding it can obtain a bearer for any user in the tenant. Treat it accordingly (see Security).
Prerequisites
- 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-Nameheader on every API call). See Create the Conversations API chatbot. - Admin access to your Moveworks Admin Portal (
https://{org}.moveworks.com). - A Unix-like environment (or WSL / OpenSSL for Windows) to generate keys.
- Ability to run a backend runtime that can sign JWTs (any language with an RS256 JWT library works; the samples in this guide are Python).
- The set of users your backend will act on behalf of are already resolvable against your Moveworks user roster.
Step 1: Generate an RSA keypair
On the machine that will run your backend service, generate a 2048-bit RSA keypair and lock down the private key permissions.
You will register public.pem with Moveworks in step 2 and keep private.pem on your backend.
Step 2: Create the JWT OAuth credential in Moveworks
In the Moveworks Admin Portal, go to https://{org}.moveworks.com/http-connectors/connector_studio/home → Credentials → create a new credential.
Because your backend signs the JWT itself, Issuer and Audience are values you choose. They must match exactly between the credential configuration and the JWT you sign. If either value differs, the token exchange fails with a 401 and INVALID_JWT, because Moveworks locates the credential by that pair.
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.
Alternative: resolving users by an identifier other than email
Use Email when your backend can put the user’s email address in sub, which is the simplest option and needs no identity configuration.
If your backend only has a different identifier for each user, such as an employee number or an IdP object 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 integration ID, 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. Whichever integration you register, the value indexed under it must be the same identifier your backend puts in sub, 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.
A third option, Identifier Type Record ID, matches the sub claim against the Moveworks user record ID directly. Use it only if your backend already stores Moveworks record IDs.
Save and then Publish the credential; the token exchange fails while it is still in draft. Moveworks displays a Key ID (KID) value; copy it. You will include it in every JWT header.
Step 3: Sign the JWT and exchange for a bearer
The examples below 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.
Your backend performs two operations per request (or per short-lived cache window):
- Build a JWT with the user’s identifier in
sub, sign with the private key. - Exchange the signed JWT at Moveworks’ token endpoint for a bearer.
Python example
Install dependencies:
Sign the assertion and exchange it for a bearer:
What each claim does
The kid header identifies which registered public key Moveworks should use to verify the signature.
Caching the bearer
The exchange response includes expires_in (typically ~1 hour). You can cache the returned bearer per user for its lifetime and only re-mint when it expires or nears expiration. Refresh proactively (for example, at 90% of expires_in) so long-running operations do not fail mid-flight.
Step 4: Call the Conversations API
Use the bearer on any Conversations API endpoint. Every request must include the Assistant-Name header (the Bot Name of the chatbot you created) and the Authorization header.
A 201 response with a conversation_id confirms the integration works end to end. To switch to a different user on a subsequent request, sign a new assertion with a different sub and exchange it for a new bearer.
Step 5: Validate
Confirm end to end before wiring it into production:
- Sign an assertion for a known-good test user in your roster and decode it (for example, at jwt.io) to confirm
iss,aud,sub,iat,exp,jti, and thekidheader are all populated as expected. - Exchange the assertion at
/oauth/v1/token. A200response with anaccess_tokenmeans the signature verified and the user resolved. Common failure modes:401INVALID_JWT, “Client credentials not found”: theissandaudpair on your assertion matched no registered credential. Moveworks locates the credential by that pair, so check both against the credential exactly, confirm it is Published, and confirm you are calling your data center’s host.401INVALID_JWT, “JWT signature verification failed”: the credential was found, soissandaudare right. Thekidheader doesn’t match a registered key, or the private key you signed with isn’t the pair of the registered public key.- Generic post-verification failure: the
subvalue doesn’t resolve to a user in the roster. Confirm the identifier your backend sends insubmatches the Identifier Type on the credential: withEmail, it must be the email address on the user’s Moveworks profile; withIdP, it must match the value stored under the integration ID you registered.
- Create a conversation with the returned bearer as above. A
201with aconversation_idcloses the loop.
Security
The private key is a tenant-wide credential. Any process that holds it can obtain a bearer token for any provisioned user in your Moveworks tenant, and every Conversations API call made with that bearer is recorded as an action performed by the impersonated user. Treat the private key with the same rigor as a production API key or a service-account credential.
Recommended handling:
- Store the private key in a secret manager (AWS Secrets Manager, Google Secret Manager, HashiCorp Vault, etc.). Do not commit it to source control, embed it in application bundles, or store it on developer laptops beyond initial setup.
- Restrict which services can read the private key. Only the specific backend process that needs to mint bearers should have access.
- Log every action taken on behalf of each user in your own audit trail. From Moveworks’ perspective, actions taken by your backend on behalf of a user are indistinguishable from actions that user performed themselves. Your own audit trail is the only source of truth for which system initiated the call.
- Rotate the credential if the private key is plausibly exposed. Generate a fresh keypair, register the new public key as a new credential, cut your backend over, then delete the old credential.
- Do not use this credential type for user-facing product integrations where end users obtain tokens directly. Use the interactive user auth guides for those flows.
Summary
For the full API contract and interaction patterns (polling and streaming), see the Conversations API reference.