> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://help.moveworks.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://help.moveworks.com/_mcp/server.

# Set up service-to-service authentication

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](/api-reference/conversations-api/set-up-authentication-with-okta) or [Set up authentication with Azure AD (Entra ID)](/api-reference/conversations-api/set-up-authentication-with-azure-ad-entra-id).

The trade-off:

| Interactive user auth                                                               | Service-to-service auth                                                          |
| ----------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
| Each user signs in with their IdP; only that user can obtain a token for themselves | Your backend holds a private key; it can obtain a token for any provisioned user |
| Attribution is guaranteed by the IdP                                                | Attribution depends on your backend correctly choosing the `sub` claim           |
| Setup requires an IdP application (Okta, Entra)                                     | Setup requires only a keypair and a Moveworks credential                         |
| Best for user-facing product integrations                                           | Best for automation, testing, monitoring, and back-office integrations           |

## 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:

1. Your backend builds a short-lived JWT that names the user in the `sub` claim and signs it with the private key.
2. Your backend exchanges the signed JWT at `POST /oauth/v1/token` for a Moveworks bearer token.
3. 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](#security)).

## Prerequisites

* 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).
* 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.

```bash
openssl genpkey -algorithm RSA -out private.pem -pkeyopt rsa_keygen_bits:2048
openssl rsa -in private.pem -pubout -out public.pem
chmod 600 private.pem
```

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.

| Field                | Value                                                                                                              | Notes                                                                                                                                       |
| -------------------- | ------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------- |
| **Credential Name**  | A descriptive label, e.g. `Backend-CAPI-Service`                                                                   | Free text.                                                                                                                                  |
| **Credential Type**  | `JWT OAuth`                                                                                                        | Reveals the fields below.                                                                                                                   |
| **Public Key value** | The contents of your `public.pem` file (PEM-encoded, including the `-----BEGIN PUBLIC KEY-----` header and footer) | Paste the raw public key. Unlike the IdP-based flows, there is no discovery URL here because your backend, not an IdP, signs the tokens.    |
| **Audience**         | A unique string of your choice, e.g. `capi-backend.acme.com`                                                       | You control the audience because your backend controls the JWT. Pick a value unique to this credential and use the same value in your JWTs. |
| **Issuer**           | A unique string of your choice, e.g. `capi-backend.acme.com`                                                       | Same reasoning as Audience. Together, Issuer + Audience identify this credential to Moveworks.                                              |
| **Identifier Type**  | `IdP`                                                                                                              | Tells Moveworks to resolve the identifier claim through an integration rather than as a raw email or record ID.                             |
| **Integration ID**   | `conversations_rest_api`                                                                                           | Moveworks matches the `sub` claim against the user's Conversations API channel entry on their profile.                                      |
| **Identifier Claim** | `sub` (default)                                                                                                    | The claim your backend puts the user's identifier in.                                                                                       |

Save the credential. Moveworks displays a **Key ID (KID)** value; copy it. You will include it in every JWT header.

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 `invalid_audience` or `invalid_signature`.

## 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](/api-reference/overview#base-urls) (for example, `https://api.am-eu-central.moveworks.ai` for EU). Requests to the wrong host return 404 or `invalid_audience`.

Your backend performs two operations per request (or per short-lived cache window):

1. Build a JWT with the user's identifier in `sub`, sign with the private key.
2. Exchange the signed JWT at Moveworks' token endpoint for a bearer.

### Python example

Install dependencies:

```bash
pip install pyjwt cryptography httpx
```

Sign the assertion and exchange it for a bearer:

```python
import time
import uuid
import jwt          # PyJWT
import httpx

PRIVATE_KEY = open('private.pem').read()
ISS = 'capi-backend.acme.com'            # matches the credential's Issuer field
AUD = 'capi-backend.acme.com'            # matches the credential's Audience field
KID = '<KEY_ID_FROM_MOVEWORKS>'          # from the credential creation response
TOKEN_URL = 'https://api.moveworks.ai/oauth/v1/token'


def sign_assertion(user_email: str, ttl_seconds: int = 300) -> str:
    now = int(time.time())
    claims = {
        'iss': ISS,
        'sub': user_email,
        'aud': AUD,
        'iat': now,
        'exp': now + ttl_seconds,
        'jti': str(uuid.uuid4()),
    }
    return jwt.encode(
        claims,
        PRIVATE_KEY,
        algorithm='RS256',
        headers={'kid': KID},
    )


def mint_bearer(user_email: str) -> str:
    assertion = sign_assertion(user_email)
    response = httpx.post(
        TOKEN_URL,
        data={
            'grant_type': 'urn:ietf:params:oauth:grant-type:jwt-bearer',
            'assertion': assertion,
        },
        timeout=15.0,
    )
    response.raise_for_status()
    return response.json()['access_token']


bearer = mint_bearer('alice@acme.com')
```

### What each claim does

| Claim | Value                                                      | Purpose                                                                                              |
| ----- | ---------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| `iss` | The Issuer string configured on the Moveworks credential   | Identifies the credential this assertion belongs to.                                                 |
| `aud` | The Audience string configured on the Moveworks credential | Identifies the credential this assertion belongs to, paired with `iss`.                              |
| `sub` | The user's identifier                                      | The user your backend is acting on behalf of. Moveworks resolves this against the user roster.       |
| `iat` | Current Unix timestamp                                     | Issued-at time.                                                                                      |
| `exp` | `iat + ttl_seconds`                                        | Expiration. Keep short (5 minutes or less); this JWT is not the API bearer, it is exchanged for one. |
| `jti` | A random UUID                                              | Unique assertion ID. Prevents replay.                                                                |

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 provided by your Moveworks account team) and the `Authorization` header.

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

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:

1. **Sign an assertion** for a known-good test user in your roster and decode it (for example, at [jwt.io](https://jwt.io/)) to confirm `iss`, `aud`, `sub`, `iat`, `exp`, `jti`, and the `kid` header are all populated as expected.
2. **Exchange the assertion** at `/oauth/v1/token`. A `200` response with an `access_token` means the signature verified and the user resolved. Common failure modes:
   * `invalid_audience`: your JWT's `aud` does not match the credential's Audience field.
   * `invalid_signature`: the `kid` doesn't match a registered key, or the private key doesn't match the registered public key.
   * Generic post-verification failure: the `sub` value doesn't resolve to a user in the roster. Confirm the identifier your backend is sending in `sub` is what your Moveworks tenant expects for that user.
3. **Create a conversation** with the returned bearer as above. A `201` with a `conversation_id` closes 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

| Credential field | Value                                                                                                                                                                        |
| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Public Key value | The full contents of your `public.pem` file                                                                                                                                  |
| Audience         | A unique string you choose, matching the `aud` claim your backend signs                                                                                                      |
| Issuer           | A unique string you choose, matching the `iss` claim your backend signs                                                                                                      |
| Identifier Type  | `IdP`                                                                                                                                                                        |
| Integration ID   | `conversations_rest_api`                                                                                                                                                     |
| Identifier Claim | `sub`                                                                                                                                                                        |
| Your backend     | Holds the private key. Signs an RS256 JWT per user with `iss`, `aud`, `sub`, `iat`, `exp`, `jti`, and the `kid` header, then exchanges it for a bearer at `/oauth/v1/token`. |

For the full API contract and interaction patterns (polling and streaming), see the [Conversations API reference](/api-reference/beta-conversations-api).