> ## Documentation Index
> Fetch the complete documentation index at: https://docs.reelevant.com/llms.txt
> Use this file to discover all available pages before exploring further.

# OAuth Client Debugging

> Diagnose and troubleshoot OAuth client configurations used by Datasources — status checks, dry-run token tests, cache management, and request previews

## Overview

Datasources that connect to external platforms (Shopify, Salesforce, Google, Instagram, etc.) authenticate using OAuth client configurations. When a Datasource fails to fetch data, the root cause is often an OAuth issue — expired credentials, a misconfigured token URL, or a broken chain of authentication steps.

The OAuth debugging endpoints let you inspect the health of an OAuth client, test token acquisition without affecting live data, clear stale cached tokens, and preview the outgoing authentication request. Each endpoint is designed for safe, non-destructive use in production.

All calls require an access token — see [Authentication](/developer-docs/api-reference/authentication) for how to obtain one.

<Info>
  These diagnostic endpoints never store tokens. Every test is strictly a dry run — it verifies the flow and discards the result. Your live Datasource configuration is never modified.
</Info>

***

## OAuth client status

The status check gives you a snapshot of an OAuth client's current health. Use it as the first diagnostic step when a Datasource reports authentication errors.

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -XGET https://api.reelevant.com/v2/datasources/oauth/<auth_id>/status \
  -H "Authorization: Bearer <access_token>"
```

### Example response

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "status": "success",
  "data": {
    "id": "665f1c0b2a9d4e0008b3a1f2",
    "mode": "refresh_token",
    "hasRefreshToken": true,
    "hasAccessToken": false,
    "hasPassword": false,
    "tokenUrl": "https://accounts.google.com/o/oauth2/token",
    "scopes": ["offline_access", "https://www.googleapis.com/auth/analytics.readonly"],
    "cachedToken": {
      "exists": true,
      "ttlSeconds": 1842
    },
    "chain": {
      "hasNext": false
    },
    "configuration": {
      "type": "company-scoped",
      "clientId": "123456789.apps.googleusercontent.com"
    }
  }
}
```

### Response fields

| Field                      | Description                                                                                                                     |
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------- |
| **id**                     | The unique identifier of the OAuth client.                                                                                      |
| **mode**                   | The authentication mode — `password`, `client_credentials`, `refresh_token`, `access_token`, `custom_payload`, or `jwt_bearer`. |
| **hasRefreshToken**        | Whether a refresh token is stored (boolean).                                                                                    |
| **hasAccessToken**         | Whether an access token is stored (boolean).                                                                                    |
| **hasPassword**            | Whether a password is stored (boolean).                                                                                         |
| **tokenUrl**               | The URL used to request tokens from the external platform.                                                                      |
| **scopes**                 | The list of permission scopes requested during authorisation.                                                                   |
| **cachedToken**            | Whether a cached token exists and its remaining time-to-live in seconds.                                                        |
| **chain**                  | Whether this client is chained to another OAuth client, and if so, the chained client's identifier and mode.                    |
| **configuration.type**     | Whether the configuration is `company-scoped` or `global`.                                                                      |
| **configuration.clientId** | The OAuth client identifier registered with the external platform.                                                              |

### Interpreting the results

Start by checking the credential booleans. The expected values depend on the authentication mode:

| Mode                 | Expected credentials                                                      |
| -------------------- | ------------------------------------------------------------------------- |
| `refresh_token`      | **hasRefreshToken** = true                                                |
| `access_token`       | **hasAccessToken** = true                                                 |
| `password`           | **hasPassword** = true                                                    |
| `client_credentials` | Credentials are in the configuration itself (clientId + clientSecret).    |
| `custom_payload`     | Credentials are embedded in the custom payload template.                  |
| `jwt_bearer`         | The signing key is stored in the clientSecret field of the configuration. |

If the expected credential boolean is `false`, the OAuth callback likely did not complete successfully. Re-run the authorisation flow for that Datasource.

<Info>
  The **hasRefreshToken**, **hasAccessToken**, and **hasPassword** booleans also appear in the standard OAuth client listing. You do not need the status endpoint to see them — any response that includes an OAuth client will show these fields.
</Info>

***

## Test token (dry-run)

The test token endpoint executes the full token acquisition flow and reports each step with its outcome, duration, and diagnostic detail. It is the most powerful debugging tool — it tells you exactly which step failed and why.

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -XPOST https://api.reelevant.com/v2/datasources/oauth/<auth_id>/test-token \
  -H "Authorization: Bearer <access_token>"
```

### How it works

The endpoint runs through up to seven steps, stopping at the first failure:

| Step                   | What it checks                                                                                                                                     |
| ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| **resolve\_auth**      | Verifies the OAuth client configuration is populated. Reports the mode, configuration type, and whether a chain exists.                            |
| **check\_credentials** | Validates that the required credentials are present for the authentication mode (see the table above).                                             |
| **build\_request**     | Constructs the token request. Reports the target URL, the method, and the body fields that will be sent.                                           |
| **send\_request**      | Sends the token request to the external platform. Reports whether a token was received.                                                            |
| **parse\_response**    | Validates that the response contains a usable access token.                                                                                        |
| **resolve\_chain**     | *(Only for chained configurations.)* Looks up the next OAuth client in the chain and verifies it exists.                                           |
| **chain\_request**     | *(Only for chained configurations.)* Runs the full test token flow recursively on the chained client, nesting its own step trace inside this step. |

### Example response — successful client\_credentials flow

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "status": "success",
  "data": {
    "success": true,
    "steps": [
      {
        "name": "resolve_auth",
        "status": "success",
        "durationMs": 1,
        "detail": {
          "mode": "client_credentials",
          "configType": "company-scoped",
          "hasChain": false
        }
      },
      {
        "name": "check_credentials",
        "status": "success",
        "durationMs": 0,
        "detail": {
          "hasClientId": true,
          "hasClientSecret": true
        }
      },
      {
        "name": "build_request",
        "status": "success",
        "durationMs": 0,
        "detail": {
          "method": "GET",
          "url": "https://api.partner.com/oauth/token",
          "bodyFields": ["grant_type", "client_id", "client_secret"]
        }
      },
      {
        "name": "send_request",
        "status": "success",
        "durationMs": 245,
        "detail": {
          "hasTokenAcquired": true
        }
      },
      {
        "name": "parse_response",
        "status": "success",
        "durationMs": 0,
        "detail": {
          "tokenFound": true
        }
      }
    ]
  }
}
```

### Example response — expired refresh token

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "status": "success",
  "data": {
    "success": false,
    "steps": [
      {
        "name": "resolve_auth",
        "status": "success",
        "durationMs": 1,
        "detail": {
          "mode": "refresh_token",
          "configType": "company-scoped",
          "hasChain": false
        }
      },
      {
        "name": "check_credentials",
        "status": "success",
        "durationMs": 0,
        "detail": {
          "hasRefreshToken": true
        }
      },
      {
        "name": "build_request",
        "status": "success",
        "durationMs": 0,
        "detail": {
          "method": "GET",
          "url": "https://accounts.google.com/o/oauth2/token",
          "bodyFields": ["grant_type", "refresh_token", "client_id", "client_secret"]
        }
      },
      {
        "name": "send_request",
        "status": "failed",
        "durationMs": 312,
        "detail": {
          "httpStatus": 401,
          "responseBody": "{\"error\": \"invalid_grant\"}"
        },
        "error": "Token request failed with status 401"
      }
    ]
  }
}
```

The refresh token has been revoked or has expired. Re-authorise the Datasource to obtain a new refresh token.

### Example response — chained configuration failure

Some OAuth setups use **chained authentication**, where the first OAuth client's token is fed into a second client. The test token endpoint handles this automatically — the chained client's step trace is nested inside the `chain_request` step.

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "status": "success",
  "data": {
    "success": false,
    "steps": [
      {
        "name": "resolve_auth",
        "status": "success",
        "durationMs": 1,
        "detail": {
          "mode": "client_credentials",
          "hasChain": true
        }
      },
      {
        "name": "check_credentials",
        "status": "success",
        "durationMs": 0,
        "detail": { "hasClientId": true, "hasClientSecret": true }
      },
      {
        "name": "build_request",
        "status": "success",
        "durationMs": 0,
        "detail": { "method": "GET", "url": "https://auth.partner.com/token" }
      },
      {
        "name": "send_request",
        "status": "success",
        "durationMs": 198,
        "detail": { "hasTokenAcquired": true }
      },
      {
        "name": "parse_response",
        "status": "success",
        "durationMs": 0,
        "detail": { "tokenFound": true }
      },
      {
        "name": "resolve_chain",
        "status": "success",
        "durationMs": 2,
        "detail": {
          "chainedAuthId": "665f1c0b2a9d4e0008b3a1f3",
          "chainedMode": "custom_payload"
        }
      },
      {
        "name": "chain_request",
        "status": "failed",
        "durationMs": 450,
        "detail": {
          "chainedSteps": [
            {
              "name": "resolve_auth",
              "status": "success",
              "durationMs": 1,
              "detail": { "mode": "custom_payload", "hasChain": false }
            },
            {
              "name": "build_request",
              "status": "failed",
              "durationMs": 0,
              "error": "Missing required variable in custom payload template"
            }
          ]
        },
        "error": "Chained token acquisition failed at build_request"
      }
    ]
  }
}
```

The primary client authenticated correctly, but the chained client failed at the `build_request` step. Inspect the nested `chainedSteps` to identify the exact failure in the second client.

<Info>
  Chained configurations are common with platforms that require a two-stage authentication process — for example, obtaining a platform token first, then exchanging it for a data-access token with a different provider.
</Info>

***

## Invalidate cache

Reelevant caches access tokens to avoid requesting a new token on every Datasource refresh. The cache uses a time-to-live based on the token's expiration. In some cases, the cached token may become invalid before its TTL expires — for example, when the external platform revokes it.

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -XPOST https://api.reelevant.com/v2/datasources/oauth/<auth_id>/invalidate-cache \
  -H "Authorization: Bearer <access_token>"
```

### Example response — cache existed

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "status": "success",
  "data": {
    "cacheKey": "test-client-id-a1b2c3d4-665f1c0b2a9d4e0008b3a1f2-access-token",
    "deleted": true
  }
}
```

### Example response — no cache

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "status": "success",
  "data": {
    "cacheKey": "test-client-id-a1b2c3d4-665f1c0b2a9d4e0008b3a1f2-access-token",
    "deleted": false
  }
}
```

| Field        | Description                                                                         |
| ------------ | ----------------------------------------------------------------------------------- |
| **cacheKey** | The internal cache key that was targeted.                                           |
| **deleted**  | `true` if a cached token was found and removed; `false` if no cached token existed. |

After invalidation, the next Datasource refresh will request a fresh token from the external platform.

<Info>
  This endpoint requires **update** permission on the OAuth client (not just read). If you receive a 404, verify that your access token has write access to the Datasource.
</Info>

***

## Preview request

For OAuth clients using `custom_payload` or `jwt_bearer` mode, the authentication request is constructed dynamically from a template with variable substitution. The preview request endpoint shows you the fully interpolated request — exactly what would be sent to the external platform — without actually sending it.

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
curl -XPOST https://api.reelevant.com/v2/datasources/oauth/<auth_id>/preview-request \
  -H "Authorization: Bearer <access_token>"
```

### Example response

```json theme={"theme":{"light":"github-light","dark":"github-dark"}}
{
  "status": "success",
  "data": {
    "method": "POST",
    "url": "https://auth.partner.com/oauth2/token",
    "headers": {
      "Content-Type": "application/x-www-form-urlencoded",
      "Authorization": "Basic ****"
    },
    "body": {
      "grant_type": "urn:ietf:params:oauth:grant-type:jwt-bearer",
      "assertion": "****"
    },
    "mode": "jwt_bearer"
  }
}
```

| Field       | Description                                                                           |
| ----------- | ------------------------------------------------------------------------------------- |
| **method**  | The request method (`GET` or `POST`).                                                 |
| **url**     | The fully constructed token URL.                                                      |
| **headers** | The request headers after variable substitution, with secret values masked as `****`. |
| **body**    | The request body after variable substitution, with secret values masked as `****`.    |
| **mode**    | The authentication mode (`custom_payload` or `jwt_bearer`).                           |

Use this to verify that variable placeholders (such as `{{clientId}}` or `{{jwt}}`) are being replaced correctly and that the request structure matches the external platform's requirements.

<Info>
  The preview endpoint only works for `custom_payload` and `jwt_bearer` modes. For other modes it returns a 400 error, since those modes use standard OAuth flows that do not construct a custom request.
</Info>

***

## Callback diagnostics

When a user completes the OAuth authorisation flow (clicking "Authorise" in the Datasource wizard), the callback page now shows diagnostic information if something goes wrong.

### Success

On a successful callback, the page displays a confirmation message and can be closed.

### Failure

On failure, the callback page displays:

| Field     | Description                                                                                        |
| --------- | -------------------------------------------------------------------------------------------------- |
| **Error** | The type of failure — for example, `token_mismatch` when the expected token type was not returned. |
| **Mode**  | The authentication mode of the OAuth client.                                                       |
| **Hint**  | A human-readable suggestion for resolving the issue.                                               |

A `token_mismatch` error typically means the external platform returned an access token when a refresh token was expected (or vice versa). Check that the OAuth scopes include `offline_access` or the platform-specific equivalent for obtaining refresh tokens.

***

## Debugging workflow

A recommended sequence for diagnosing a failing Datasource authentication:

<Steps>
  <Step title="Check the status">
    Call `GET /oauth/<auth_id>/status` to verify credentials are present and check whether a cached token exists.

    If `hasRefreshToken`, `hasAccessToken`, or `hasPassword` is unexpectedly `false`, re-authorise the Datasource.
  </Step>

  <Step title="Run a dry-run test">
    Call `POST /oauth/<auth_id>/test-token` to execute the full token acquisition flow. Read the step trace to identify the first failing step.

    Common failures:

    * `send_request` with status 401 → expired or revoked credentials
    * `build_request` failure → misconfigured request template
    * `chain_request` failure → issue in the chained OAuth client
  </Step>

  <Step title="Invalidate the cache if needed">
    If the test token succeeds but the Datasource still fails, the issue may be a stale cached token. Call `POST /oauth/<auth_id>/invalidate-cache` to clear it.
  </Step>

  <Step title="Preview the request (custom modes)">
    For `custom_payload` or `jwt_bearer` modes, call `POST /oauth/<auth_id>/preview-request` to inspect the fully interpolated request and compare it with the external platform's documentation.
  </Step>
</Steps>

***

## Common issues and solutions

| Symptom                                          | Likely cause                                                   | Recommended action                                                                                                                    |
| ------------------------------------------------ | -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| Datasource fails with "authentication error"     | Expired or revoked token                                       | Run **test-token** to identify the failing step. If `send_request` fails with a 401, re-authorise the Datasource.                     |
| **hasRefreshToken** is false after authorisation | OAuth callback did not receive a refresh token                 | Check the scopes — ensure `offline_access` (or the platform equivalent) is included. Re-authorise.                                    |
| Test token succeeds but Datasource still fails   | Stale cached token                                             | **Invalidate the cache**, then trigger a Datasource refresh.                                                                          |
| `custom_payload` mode returns unexpected errors  | Incorrect variable substitution in the request template        | Use **preview-request** to inspect the interpolated request and compare it with the platform's documentation.                         |
| Chained configuration fails at `chain_request`   | The second OAuth client in the chain has a configuration issue | Expand the nested `chainedSteps` in the `chain_request` step to find the failure. Fix the chained client, then re-test.               |
| Callback page shows `token_mismatch`             | The platform returned a different token type than expected     | Verify the OAuth scopes and the platform's token exchange behaviour. Some platforms require specific scopes to return refresh tokens. |
| 404 on `invalidate-cache`                        | Insufficient permissions                                       | This endpoint requires **update** permission. Verify your access token has write access to the Datasource.                            |

***

## Related pages

* [Authentication](/developer-docs/api-reference/authentication) — obtaining an access token to call the Datasources API.
* [Special Configurations](/advanced-guide/datahub/special-configurations) — OAuth integration setup, API keys, and other advanced Datasource configurations.
* [Logs](/advanced-guide/datahub/logs) — monitoring Datasource execution history.
* [Source Types](/advanced-guide/datahub/source-types) — configuring different Datasource source types.
