All client.request.invoke calls fail with "Invalid feature or feature not enabled"

I am experiencing a critical platform-level issue that is blocking all app development for my account

Every client.request.invoke call from any custom app fails with the error: {message: 'Invalid feature or feature not enabled.'}.

image.png

We have definitively proven that this is a platform issue and not a code or configuration error.

  1. Direct API Calls Work: I can successfully make API calls to my Freshdesk instance using cURL and Postman with my Admin API key and standard Basic authentication. My key is valid and the API is responsive.
  2. App Fails with an Identical Request: We built a minimal test app that does only one thing: makes the exact same add note API call as my working cURL command, using the exact same Basic authentication method. This app still fails with the “Invalid feature” error.
  3. This happens on all app types: We have tried platform-version: 3.0 apps, platform-version: 2.3 apps, plain JavaScript apps, and React apps. We have tried Basic and Bearer authentication methods. Every single attempt to use client.request.invoke results in the same error.

Conclusion: The Freshworks app platform is blocking all server-side requests originating from my custom apps. The “feature” that is “not enabled” appears to be the Request Method (proxy) service itself for my account.

<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Ticket Action</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@freshworks/crayons@v4/css/crayons.css">
    <script async src="{{{appclient}}}"></script>
    <script type="module" src="https://cdn.jsdelivr.net/npm/@freshworks/crayons@v4/dist/crayons/crayons.esm.js"></script>
    <script nomodule src="https://cdn.jsdelivr.net/npm/@freshworks/crayons@v4/dist/crayons/crayons.js"></script>
    <style>
        body {
            font-family: 'Inter', sans-serif;
            margin: 0;
        }
    </style>
</head>
<body>
    <div class="fw-flex fw-flex-column fw-p-16">
        <fw-select
            id="status-select"
            label="Ticket Status"
            value="0"
            placeholder="Your choice"
            disabled
        >
            <fw-select-option value="0" disabled>Please select a status</fw-select-option>
            <fw-select-option value="1">Pending Input</fw-select-option>
            <fw-select-option value="2">Resolved</fw-select-option>
        </fw-select>

        <fw-button color="primary" id="action-btn" disabled>Action Handled</fw-button>
    </div>
    <script src="https://static.freshdev.io/fdk/2.0/assets/fresh_client.js"></script>
    <script src="scripts/tekno.js"></script>
</body>
</html>
[Read me.pdf|attachment](upload://tA1QajFdMset54zO7mMxkDJCjZU.pdf) (83.4 KB)

{
  "getTicketDetails": {
    "schema": {
      "protocol": "https",
      "method": "GET",
      "host": "<%= iparam.freshdesk_domain %>",
      "path": "/api/v2/tickets/<%= context.ticketId %>",
      "headers": {
        "Authorization": "Basic <%= encode(iparam.freshdesk_key) %>",
        "Content-Type": "application/json"
      }
    }
  },
  "updateTicket": {
    "schema": {
      "protocol": "https",
      "method": "PUT",
      "host": "<%= iparam.freshdesk_domain %>",
      "path": "/api/v2/tickets/<%= context.ticketId %>",
      "headers": {
        "Authorization": "Basic <%= encode(iparam.freshdesk_key) %>",
        "Content-Type": "application/json"
      }
    }
  },
  "getConversations": {
    "schema": {
      "protocol": "https",
      "method": "GET",
      "host": "<%= iparam.freshdesk_domain %>",
      "path": "/api/v2/tickets/<%= context.ticketId %>/conversations",
      "headers": {
        "Authorization": "Basic <%= encode(iparam.freshdesk_key) %>",
        "Content-Type": "application/json"
      }
    }
  },
  "addNote": {
    "schema": {
      "protocol": "https",
      "method": "POST",
      "host": "<%= iparam.freshdesk_domain %>",
      "path": "/api/v2/tickets/<%= context.ticketId %>/notes",
      "headers": {
        "Authorization": "Basic <%= encode(iparam.freshdesk_key) %>",
        "Content-Type": "application/json"
      }
    }
  }
}

// scripts/app.js
document.addEventListener("DOMContentLoaded", function() {
    // Initialize the Freshworks client
    app.initialized().then(function(_client) {
        window.client = _client;
        client.events.on("app.activated", getTicketDetails);
    }).catch(function(error) {
        console.error("Failed to initialize the app: ", error);
        client.interface.trigger("showNotify", {
            type: "danger",
            message: "Error initializing app."
        });
    });
});
/**
let  client;

(async function init() {
  client = await app.initialized();
  client.events.on('app.activated', getTicketDetails);
})();

 * Fetches ticket details, checks if it's a child ticket, and updates the UI accordingly.
 */
async function getTicketDetails() {
    const statusSelect = document.getElementById('status-select');
    const actionButton = document.getElementById('action-btn');

    try {
        const data = await client.data.get("ticket");
        const ticket = data.ticket;

        // Save the ticketId to window for later use
        window.currentTicketId = ticket.id;

        // Based on the sample object, 'association_type' is a direct property of the ticket.
        if (ticket.association_type === 2) {
            // Enable UI elements
            statusSelect.disabled = false;
            actionButton.disabled = false;
            client.interface.trigger("showNotify", {
                type: "success",
                message: "This is a child ticket. Please select an action."
            });

        } else {
            // Keep UI disabled
            statusSelect.disabled = true;
            actionButton.disabled = true;
            client.interface.trigger("showNotify", {
                type: "info",
                message: "This app can only be used on child tickets."
            });
        }
    } catch (error) {
        client.interface.trigger("showNotify", {
            type: "danger",
            message: "Could not load ticket details."
        });
        // Ensure UI remains disabled on error
        if(statusSelect) statusSelect.disabled = true;
        if(actionButton) actionButton.disabled = true;
    }
}

// Add event listener for the button
document.getElementById('action-btn').addEventListener('click', async function() {
    const statusSelect = document.getElementById('status-select');
    const selectedValue = statusSelect.value;

    if (selectedValue === "0") {
        client.interface.trigger("showNotify", {
            type: "warning",
            message: "Please select a status from the dropdown before proceeding."
        });
        return;
    }

    if (selectedValue === "1") { // Pending Input
        client.interface.trigger("showNotify", {
            type: "info",
            message: "Team will check your last note and provide you with the input soon."
        });
        setTimeout(() => {
            client.interface.trigger("showNotify", {
                type: "info",
                message: "Ticket will be pending till we provide the input."
            });
        }, 2000);
        await handleAction("pending");
    }

    if (selectedValue === "2") { // Resolved
        client.interface.trigger("showNotify", {
            type: "info",
            message: "Issue will be closed and we will confirm with the customer."
        });
        setTimeout(() => {
            client.interface.trigger("showNotify", {
                type: "info",
                message: "Ticket is closed for now."
            });
        }, 2000);
        await handleAction("closed");
    }
});

/**
 * Main handler for the action. Will:
 * 1. Get ticket details (for parent ID)
 * 2. Get ticket conversations (for last note)
 * 3. Update status
 * 4. Add note to the parent
 */
async function handleAction(targetStatus) {
    try {
        // 1. Get child ticket details
        const ticketDetails = await client.request.invoke("getTicketDetails", {
            context: { ticketId: window.currentTicketId }
        });
        const ticketData = JSON.parse(ticketDetails.response);

        // Find parent ID from associated_tickets_list
        const parentIds = ticketData.associated_tickets_list || [];
        const parentId = parentIds.length > 0 ? parentIds[0] : null;

        if (!parentId) {
            client.interface.trigger("showNotify", {
                type: "danger",
                message: "No parent ticket found for this child ticket."
            });
            return;
        }

        // 2. Get conversations/notes for this ticket
        const convosResp = await client.request.invoke("getConversations", {
            context: { ticketId: window.currentTicketId }
        });
        const conversations = JSON.parse(convosResp.response);

        // Find the latest private note (if any)
        let latestNote = null;
        if (Array.isArray(conversations) && conversations.length > 0) {
            // Sort by created_at descending, just in case
            conversations.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
            latestNote = conversations[0];
        }

        if (!latestNote) {
            client.interface.trigger("showNotify", {
                type: "danger",
                message: "No notes found on this ticket to copy to parent."
            });
            return;
        }

        // 3. Update the ticket status
        let statusCode = 2; // Default "Open"
        if (targetStatus === "pending") statusCode = 3;
        if (targetStatus === "closed") statusCode = 5;

        await client.request.invoke("updateTicket", {
            context: { ticketId: window.currentTicketId },
            body: JSON.stringify({ status: statusCode })
        });

        // 4. Add the latest note to the parent
        await client.request.invoke("addNote", {
            context: { ticketId: parentId },
            body: JSON.stringify({
                body: latestNote.body_text || latestNote.body,
                private: true
            })
        });

        client.interface.trigger("showNotify", {
            type: "success",
            message: "Ticket status updated and note added to parent successfully."
        });

    } catch (err) {
        console.error("Error in handleAction:", err);
        client.interface.trigger("showNotify", {
            type: "danger",
            message: "An error occurred while processing the action. Please try again."
        });
    }
}

This is a hard blocker for any app development.

Thank you.

Hello @Abdallah_Elsayed ,

I beleive you are calling the request method incorrectly. Instead of client.request.invoke() it should be client.request.invokeTemplate() based on the documentation found here

Hope that helps!

Hi @ZacharyKing,

Thank you so much for your reply and suggestion. You were correct to point out the invokeTemplate method, as it is indeed the documented method for platform-version: 2.3.

Platform v2.3 / v3.0 Test:

  • Result: The app failed with the error TypeError: client.request.invokeTemplate is not a function. This was unexpected and contrary to the documentation for that version.

manifest.json

{
  "platform-version": "3.0",
  "modules": {
    "common": {
      "location": {
        "full_page_app": {
          "url": "index.html",
          "icon": "styles/images/icon.svg"
        }
      },
      "requests": {
        "getTicketDetails":{},
        "updateTicket":{},
        "getConversations":{},
        "addNote":{}
      }
    },
    "support_ticket": {
      "location": {
        "ticket_sidebar": {
          "url": "index.html",
          "icon": "styles/images/icon.svg"
        }
      }
    }
  },
  "engines": {
    "node": "18.20.8",
    "fdk": "9.5.0"
  }
}

requests.json

{
  "getTicketDetails": {
    "schema": {
      "method": "GET",
      "host": "<%= iparam.freshdesk_domain %>",
      "path": "/api/v2/tickets/<%= context.ticketId %>",
      "headers": {
        "Authorization": "Basic <%= encode(iparam.freshdesk_key) %>"
      }
    }
  },
  "updateTicket": {
    "schema": {
      "method": "PUT",
      "host": "<%= iparam.freshdesk_domain %>",
      "path": "/api/v2/tickets/<%= context.ticketId %>",
      "headers": {
        "Authorization": "Basic <%= encode(iparam.freshdesk_key) %>",
        "Content-Type": "application/json"
      }
    }
  },
  "getConversations": {
    "schema": {
      "method": "GET",
      "host": "<%= iparam.freshdesk_domain %>",
      "path": "/api/v2/tickets/<%= context.ticketId %>/conversations",
      "headers": {
        "Authorization": "Basic <%= encode(iparam.freshdesk_key) %>"
      }
    }
  },
  "addNote": {
    "schema": {
      "method": "POST",
      "host": "<%= iparam.freshdesk_domain %>",
      "path": "/api/v2/tickets/<%= context.ticketId %>/notes",
      "headers": {
        "Authorization": "Basic <%= encode(iparam.freshdesk_key) %>",
        "Content-Type": "application/json"
      }
    }
  }
}

app.js

document.addEventListener('DOMContentLoaded', init);

function init() {
  app.initialized().then(function(client) {
    // Make the client object available to all functions
    window.client = client;
    client.events.on('app.activated', onAppActivate);
  }).catch(handleError);
}

function onAppActivate() {
  // Check if it's a child ticket when the app loads
  checkIfChildTicket();
  // Set up the button click listener
  document.getElementById('action-btn').addEventListener('click', handleUpdateClick);
}

function handleError(error) {
  console.error("An error occurred: ", error);
  // It's possible client isn't initialized here, so we check first
  if (window.client) {
    client.interface.trigger("showNotify", {
      type: "danger",
      message: "A critical error occurred. Please check the console."
    });
  }
}

function checkIfChildTicket() {
  const statusSelect = document.getElementById('status-select');
  const actionButton = document.getElementById('action-btn');

  client.data.get("ticket").then(function(data) {
    if (data.ticket.association_type === 2) {
      statusSelect.disabled = false;
      actionButton.disabled = false;
      client.interface.trigger("showNotify", { type: "info", message: "Child ticket detected. Select an action." });
    } else {
      statusSelect.disabled = true;
      actionButton.disabled = true;
      client.interface.trigger("showNotify", { type: "info", message: "This app is for child tickets only." });
    }
  }).catch(handleError);
}

function handleUpdateClick() {
  const statusSelect = document.getElementById('status-select');
  const actionButton = document.getElementById('action-btn');
  const selectedValue = statusSelect.value;

  if (selectedValue === "0") {
    return client.interface.trigger("showNotify", { type: "warning", message: "Please select a status." });
  }

  actionButton.loading = true;
  const isResolved = selectedValue === "2";

  processAction(isResolved)
    .then(function() {
      client.interface.trigger("showNotify", { type: "success", message: "Workflow completed successfully." });
    })
    .catch(handleError) // Use a dedicated error handler
    .finally(function() {
      actionButton.loading = false;
    });
}

function processAction(isResolved) {
  let childTicketId, parentId; // Declare variables in the highest scope needed

  return client.data.get("ticket")
    .then(function(data) {
      childTicketId = data.ticket.id;
      return client.request.invokeTemplate("getTicketDetails", { context: { ticketId: childTicketId } });
    })
    .then(function(ticketDetailsResp) {
      const ticketData = JSON.parse(ticketDetailsResp.response);
      parentId = (ticketData.associated_tickets_list || [])[0];
      if (!parentId) {
        throw new Error("Could not find a parent ticket.");
      }
      return client.request.invokeTemplate("getConversations", { context: { ticketId: childTicketId } });
    })
    .then(function(convosResp) {
      const conversations = JSON.parse(convosResp.response);
      const privateNotes = conversations.filter(c => c.private === true).sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
      if (privateNotes.length === 0) {
        throw new Error("No private notes found to copy to parent.");
      }
      const latestNoteBody = privateNotes[0].body;
      const statusCode = isResolved ? 5 : 3; // 5 for Resolved, 3 for Pending
      
      const updatePromise = client.request.invokeTemplate("updateTicket", {
        context: { ticketId: childTicketId },
        body: JSON.stringify({ status: statusCode })
      });
      const addNotePromise = client.request.invokeTemplate("addNote", {
        context: { ticketId: parentId },
        body: JSON.stringify({ body: `Note synced from child ticket #${childTicketId}:<br><hr>${latestNoteBody}` })
      });
      
      return Promise.all([updatePromise, addNotePromise]);
    })
    .then(function() {
      if (isResolved) {
        return client.request.invokeTemplate("addNote", {
          context: { ticketId: parentId },
          body: JSON.stringify({ body: `Child ticket #${childTicketId} has been resolved.` })
        });
      }
    })
    .then(function() {
      client.interface.trigger('click', { id: 'ticket', value: parentId });
    });
}

index.html

<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Ticket Action</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@freshworks/crayons@v4/css/crayons.css">
    <script async src="{{{appclient}}}"></script>
    <script type="module" src="https://cdn.jsdelivr.net/npm/@freshworks/crayons@v4/dist/crayons/crayons.esm.js"></script>
    <script nomodule src="https://cdn.jsdelivr.net/npm/@freshworks/crayons@v4/dist/crayons/crayons.js"></script>
    <style>
        body {
            font-family: 'Inter', sans-serif;
            margin: 0;
        }
    </style>
</head>
<body>
    <div class="fw-flex fw-flex-column fw-p-16">
        <fw-select
            id="status-select"
            label="Ticket Status"
            value="0"
            placeholder="Your choice"
            disabled
        >
            <fw-select-option value="0" disabled>Please select a status</fw-select-option>
            <fw-select-option value="1">Pending Input</fw-select-option>
            <fw-select-option value="2">Resolved</fw-select-option>
        </fw-select>

        <fw-button color="primary" id="action-btn" disabled>Action Handled</fw-button>
    </div>
    <script src="https://static.freshdev.io/fdk/2.0/assets/fresh_client.js"></script>
    <script src="scripts/tekno.js"></script>
</body>
</html>

I would also check your HTML file and make sure that it is using the correct app client. You can find a possible solution in this post

Hopefully that works for you! Good luck!

I have already done this step please check the code attached in my last reply

Hey @Abdallah_Elsayed

I assume your app.js is the tekno.js you are loading in index.html, as I don’t see any app.js in there.
Could you try loading that deferred?

Best
Tom

This topic was automatically closed 6 days after the last reply. New replies are no longer allowed.