
This guide is for developers building the endpoint behind Gleap Custom widgets. For a conceptual introduction, see "Custom widgets: bring your own data into the messenger".
The messenger never calls your endpoint directly. Every render request is proxied through Gleap's servers, which attach the signed-in user's context and then call your endpoint. This keeps your URL and credentials private and lets Gleap sanitize the response before it reaches the widget.
The flow is: Messenger, then Gleap server (adds user context, applies limits), then your endpoint.
In Settings, then Channels, then Widget, then Custom widgets (admin only):
API URL: the single endpoint Gleap will POST to.
Custom headers: optional key/value pairs sent with every request, for example Authorization: Bearer <your-secret>. Use these to verify the call came from Gleap.
Gleap sends a POST with a JSON body. There are two request types.
Home widgets request:
{
"type": "widgets",
"context": {
"user": {
"id": "gleap-id-123",
"email": "[email protected]",
"name": "Jane Doe",
"userId": "your-own-user-id",
"customData": { "plan": "pro" }
},
"session": { "id": "gleap-id-123", "gleapId": "gleap-id-123" },
"apiKey": "YOUR_PROJECT_API_KEY"
}
}Detail view request adds viewName and an optional payload (the values you set on an open-view button):
{
"type": "view",
"viewName": "order-details",
"payload": { "orderId": "A-1001" },
"context": { "user": {}, "session": {}, "apiKey": "..." }
}context.user is derived from Gleap's signed session, so you can trust it to identify the end user. Use userId and customData to look the person up in your own systems.
For type: "widgets", return a list of cards. Each widget needs a unique id; title is optional:
{
"widgets": [
{
"id": "orders",
"title": "Your latest order",
"content": [
{
"type": "key-value",
"items": [
{ "key": "Order", "value": "#A-1001" },
{ "key": "Status", "value": "In transit" }
]
},
{
"type": "button",
"label": "View details",
"style": "primary",
"action": {
"type": "open-view",
"viewName": "order-details",
"payload": { "orderId": "A-1001" }
}
}
]
}
]
}For type: "view", return a single content list (or raw HTML, see below):
{
"content": [
{ "type": "text", "text": "Order #A-1001" },
{ "type": "divider" },
{
"type": "button",
"label": "Track package",
"action": {
"type": "open-url",
"url": "https://acme.com/track/A-1001",
"newTab": true
}
}
]
}Each item in a content array is one of:
text: { "type": "text", "text": "..." } renders a paragraph.
image: { "type": "image", "image": "https://...", "alt": "..." } renders an image.
button: { "type": "button", "label": "...", "style": "primary", "action": { } } renders a button. Style can be primary, secondary, or danger.
key-value: { "type": "key-value", "items": [ { "key": "...", "value": "..." } ] } renders a definition list (max 50 rows).
divider: { "type": "divider" } renders a horizontal rule.
html: { "type": "html", "html": "<p>...</p>" } renders an inline, sanitized HTML fragment.
A button's action is one of:
open-url: { "type": "open-url", "url": "https://...", "newTab": true } opens a link. The URL supports {{session.<field>}} placeholders, which are substituted before opening.
open-view: { "type": "open-view", "viewName": "order-details", "payload": { } } opens a detail view. Gleap calls your endpoint again with type: "view" and that viewName and payload.
custom-action: { "type": "custom-action", "actionName": "openCheckout", "actionData": { } } triggers an action on your own website (see next section).
Note: a fourth action type, http-request, is reserved but not yet supported in the messenger. Do not rely on it yet.
Instead of a content array, a widget or view may return an html string. If both are present, html wins. HTML is sanitized, and you can make elements interactive with data attributes:
data-open-detail="viewName" (plus optional data-payload='{"orderId":"A-1001"}') opens a detail view.
data-action="actionName" (plus optional data-action-data='{}') fires a custom action.
data-action="gleap-open-url" with data-action-data='{"url":"https://..."}' opens a URL.
When a user triggers a custom-action, Gleap forwards it to your website through the Gleap SDK's custom action mechanism (the same Gleap.registerCustomAction(...) callback used elsewhere in the SDK). Your handler receives the actionName and actionData and runs whatever logic you want, for example opening your own checkout or navigating your app.
Gleap calls your endpoint server side, so your URL and headers are never exposed in the browser.
Use a custom header (such as a bearer token) to verify requests come from Gleap.
The end user is identified for you in context.user, so you do not need to authenticate them separately.
Your API URL must be a public http or https address. Localhost and private or internal ranges are blocked to prevent SSRF (including the cloud metadata address 169.254.169.254).
Requests time out after 10 seconds.
Responses are capped at 200 KB.
Up to 100 widgets per response, 100 items per content list, and 50 rows per key-value block.
Raw HTML is truncated to 100 KB and sanitized; only safe URL schemes survive (https, http, mailto, tel, #, /).
Widgets without an id are dropped, and unknown content or action types are removed.
// POST https://your-backend.example.com/gleap
app.post("/gleap", (req, res) => {
// Verify the request came from Gleap
if (req.headers["authorization"] !== `Bearer ${process.env.GLEAP_SECRET}`) {
return res.status(401).end();
}
const { type, context, viewName, payload } = req.body;
const user = context.user; // { id, email, name, userId, customData }
if (type === "widgets") {
return res.json({
widgets: [{
id: "orders",
title: "Your latest order",
content: [
{ type: "key-value", items: [
{ key: "Order", value: "#A-1001" },
{ key: "Status", value: "In transit" }
]},
{ type: "button", label: "View details", style: "primary",
action: { type: "open-view", viewName: "order-details", payload: { orderId: "A-1001" } } }
]
}]
});
}
if (type === "view" && viewName === "order-details") {
return res.json({
content: [
{ type: "text", text: `Order ${payload.orderId}` },
{ type: "divider" },
{ type: "button", label: "Track package",
action: { type: "open-url", url: `https://acme.com/track/${payload.orderId}`, newTab: true } }
]
});
}
res.json({ widgets: [] });
});