Serverless App `request.invokeTemplate` Returning HTML Instead of JSON (Freshworks CRM)

Hello Freshworks Community,

I’m building a serverless app for our Freshworks CRM instance (specifically in our crm-sandbox environment). The app is designed to react to the onDealCreate event, execute some custom logic, and then update the newly created deal to populate a custom field.

I’ve followed the documentation for setting up external API calls (https://developers-staging.freshworks.com/docs/app-sdk/v2.3/freshsales-suite/advanced-tools/request-method/) and have configured both my manifest.json and requests.json files accordingly.

The app’s activity is visible in the logs, indicating that the onDealCreate event is being triggered. However, when I invoke the external API call using $request.invokeTemplate('updateDeal', requestPayload) and log the response, I receive a 200 OK status, but the Content-Type in the response headers is text/html instead of the expected application/json. The response body itself contains what appears to be HTML for a Freshsales login page or similar, rather than the API’s JSON response.

Here’s the truncated response I’m logging:

{
  "status": 200,
  "headers": {
    "date": "Tue, 17 Jun 2025 15:44:12 GMT",
    "content-type": "text/html; charset=utf-8",
    "transfer-encoding": "chunked",
    "connection": "close",
    "status": "200 OK",
    "vary": "Origin",
    "strict-transport-security": "max-age=31536000; includeSubDomains",
    "x-xss-protection": "1; mode=block",
    "x-request-id": "d5b1f485-bfc3-48b8-b17b-fa4f14e8717b",
    "x-frame-options": "SAMEORIGIN",
    "x-content-type-options": "nosniff",
    "x-envoy-upstream-service-time": "165",
    "server": "fwe",
    "x-trace-id": "00-fec0e01b4220b705f57c791570777559-d912925a05b21e86-00",
    "nel": "{ \"report_to\": \"nel-endpoint-freshworks360\", \"max_age\": 2592000, \"include_subdomains\": true}",
    "report-to": "{ \"group\": \"nel-endpoint-freshworks360\", \"max_age\": 2592000, \"include_subdomains\": true, \"endpoints\": [{\"url\": \"https://edge-admin.us-east-1.freshedge.net/nelreports/freshworks360\"}]}"
  },
  "response": "<!doctype html>\n<html lang=\"en\">\n<head>\n <title>Freshsales</title>\n <meta charset=\"utf-8\" />\n <meta http-equiv=\"content-type\" content=\"text/html; charset=UTF-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <meta name=\"apple-itunes-app\" content=\"app-id=1073125057\">\n <link rel=\"manifest\" href=\"https://assets.freshsales.io/assets/mobile/manifest.json\">\n\n <link href=\"https://assets.freshsales.io/assets/images/favicon_fsa_suite.ico\" rel=\"shortcut icon\" type=\"image/vnd.microsoft.icon\" />\n\n <link rel=\"stylesheet\" media=\"all\" href=\"https://fonts.googleapis.com/css?family=Source+Sans+Pro:200,300,400,700,900\" />\n <link rel=\"stylesheet\" media=\"all\" href=\"https://assets.freshsales.io/stylesheets/public.css\" />\n\n <script type=\"text/javascript\">\n var TRANSLATIONS = {\n \"email\": {\n \"required\": \"Introduzca su dirección de correo electrónico.\",\n \"properEmail\": \"Enter a valid email address\"\n },\n \"password\": {\n \"required\": \"Introduzca su contraseña.\",\n \"minLength\": \"La contraseña debe tener como"
}

I’ve tried different syntaxes for the Authorization header, including:
"Authorization": "Token token=<%= iparam.apiKey %>"
"Authorization": "Bearer <%= iparam.apiKey %>"
I even tried hardcoding the API key directly, but the response format remains the same.

It’s puzzling that the status code is 200, but the response content type is text/html instead of application/json, which the Freshworks CRM API usually returns for deal updates. This suggests the request might be hitting an authentication wall or being redirected to a login page.

Here’s my requests.json file (domain replaced with a placeholder)

{
    "updateDeal": {
        "schema": {
            "method": "PUT",
            "host": "MY_DOMAIN.myfreshworks.com",
            "path": "/crm-sandbox/sales/api/deals/[:deal_id]",
            "headers": {
                "Authorization": "Bearer <%= iparam.apiKey %>",
                "Content-Type": "application/json"
            }
        }
    }
}

And a snippet of how I’m invoking the request in my server.js:

        const requestPayload = {
          context: { deal_id: newlyCreatedDeal.id },
          body: JSON.stringify({
            deal: {
              custom_field: { cf_my_custom_field: newValue}
            }
          })
        };
        const response = await $request.invokeTemplate('updateDeal', requestPayload)
        console.info("response invoke", JSON.stringify(response))

Additional checks I’ve made/considered:

  1. API Key Validity: I’ve confirmed that the API key (iparam.apiKey) is correct and has the necessary permissions (Admin API Token, or a custom token with deal write access).
  2. API Endpoint Correctness: The host and path in requests.json appear to be correct for the Freshworks CRM Deal API (e.g., /crm-sandbox/sales/api/deals/:id).
  3. Content-Type Header: It’s explicitly set to application/json in the request, but the response is text/html.

Could this be an issue with:

  • The API key format or where it’s being read from?
  • A specific setting in my Freshworks CRM instance that’s redirecting unauthenticated API calls to a login page?
  • Any other subtle configuration detail I might be missing for serverless apps making API calls to Freshworks CRM itself?

Any insights or suggestions from the community would be greatly appreciated!

Thank you in advance.

Hi @PabloLV ,

Could you also share the template invocation code explaining how the context variables are being passed?

Regards,
Thakur