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

Response on ticket:
We truncate the logs in serverless upto 2000 character and that should be the reason you are seeing the response as truncated. You should be getting the proper response in you code and it will not be truncated.