> For the complete documentation index, see [llms.txt](https://shoppad.gitbook.io/yedric/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://shoppad.gitbook.io/yedric/developers/custom-apps.md).

# Custom Apps

A custom app renders your own HTML in a panel beside a conversation in the Yedric dashboard. When a teammate opens a conversation, Yedric posts the visitor's context to a URL you host and renders what you return.

Create and scope custom apps in **Organization > Integrations > Custom App**. See [Integrations](/yedric/going-further/integrations.md) for that walkthrough. This page is the contract your endpoint has to satisfy.

***

## The request

Yedric's backend makes the call, not the browser, so your secret is never exposed to the dashboard and your endpoint does not need CORS headers.

```http
POST https://example.com/yedric-sidebar
Content-Type: application/json
Authorization: a1b2c3d4e5f6…

{
  "username": "customer@example.com",
  "context": {
    "sessionId": "sess_…",
    "agentId": "agt_…"
  }
}
```

**`username`** is the end user's identity for the conversation. In [secure mode](/yedric/developers/secure-mode.md) this is the signed username you provided. It is an empty string when the conversation has no identified user.

**`context`** carries the ids for whatever the teammate is looking at, and **the keys depend on the surface**. From the Conversations view you get `sessionId` and `agentId`. From a [live chat](/yedric/going-further/live-chat.md) you get `chatId` and `agentId` instead. Only `agentId` is present in both, so treat the rest as optional and branch on which id you received rather than assuming `sessionId` is always there.

The request times out after **10 seconds**.

***

## Verifying the request

The `Authorization` header is the hex HMAC-SHA256 of the **username**, keyed with the secret you saved on the app:

```
Authorization = HMAC_SHA256(secret, username)
```

Recompute that value on your side and compare it against the header using a constant-time comparison, rather than a plain string equality check. Treat a missing or non-matching header as an unauthenticated request.

{% hint style="warning" %}
This signs the **username only, not the request body**. It tells you the call came from Yedric and which user it concerns. It does not authenticate the rest of the payload, so do not treat `context` as tamper-proof. Look records up by the ids you are given rather than trusting values inside them.
{% endhint %}

This is the same signing scheme used by [Secure Mode](/yedric/developers/secure-mode.md), so if you already sign usernames there, you can reuse that code.

### Signing cannot be turned off

Leaving the **Secret** field blank does not stop Yedric from signing. The `Authorization` header is always sent. A blank secret simply means the HMAC is keyed with an empty string, which is a value anybody can reproduce.

{% hint style="danger" %}
Two things follow, and both matter. Never treat a missing `Authorization` header as a trusted internal call, because Yedric never omits it: a request arriving without one did not come from us. And never leave the secret blank on an endpoint that returns anything private, because the signature is then forgeable by anyone who knows your customer's username. Set a real secret before you point an app at real data.
{% endhint %}

***

## The response

Return an HTML fragment.

* The content type must be `text/html` or `text/plain`. A missing content type is tolerated. Anything else is rejected with a `502` and the panel shows an error.
* The body is capped at **2 MB**. Over the cap, Yedric renders the first 2 MB and shows a note that the rest was not loaded, rather than failing outright. Aim well under it.
* Return a fragment, not a full document. Anything outside the body is discarded.

### How your HTML is rendered

Your markup is sanitized with DOMPurify and injected into a **Shadow DOM** root. That has two consequences worth designing around:

* **Your styles are isolated in both directions.** The dashboard's CSS will not leak into your panel, and yours will not leak out. Inline styles and a `<style>` block inside your fragment work fine.
* **Scripts do not run.** Sanitization strips them. The panel is for displaying information and linking out, not for interactive widgets.

Every link that is not a same-page fragment anchor is rewritten to `target="_blank"` and `rel="noopener noreferrer"`, so clicking one opens a new tab rather than navigating your teammate away mid-conversation.

A small default stylesheet is applied so plain markup looks reasonable without any work from you.

***

## URL requirements

The same rules as [webhook callback URLs](/yedric/developers/webhooks.md#callback-url-requirements) apply. They are checked when you save the app and again on every call, including after each redirect:

* `http` or `https`, with **`https` required in production**.
* Port must be the protocol default, or `80`, `443`, `8080`, or `8443`.
* The hostname must resolve to a public IP address. Private, loopback, link-local, and reserved ranges are refused.
* Redirects are re-validated at each hop. Your `Authorization` header is not carried across a cross-origin redirect, so answer at the URL you configured.

Because loopback and private addresses are refused, your endpoint has to be reachable from the public internet before the panel will load. There is no way to point a custom app at a service running on your own machine. Put it behind a tunnel that gives you a public `https` URL while you are building.

***

## Notes on the secret

Unlike a webhook signing secret, a custom app secret is stored so that it can be reused, and is masked as `***` whenever the app is read back. There is no reveal and no rotate flow. To change it, type a new value into the Secret field and save. Sending `***`, or omitting the field, keeps the value you already have.

***

## Turning an app off

The **Enabled** switch stops the panel appearing for everyone immediately. A disabled app is also refused at the proxy, so an already-open dashboard tab or a saved id cannot keep fetching it.

Scoping matters too: a custom app with **no assistants selected appears nowhere**. This is the opposite of webhooks, where selecting none means all. If your panel is not showing up, check the Assistants boxes first.

{% hint style="info" %}
Assistant scoping controls where the panel is offered, not who may call your endpoint. The dashboard is what filters on your selected assistants, so treat scoping as a display setting and put your own authorization checks in your endpoint. Use **Enabled** when you want an app to stop being served altogether.
{% endhint %}
