> ## Documentation Index
> Fetch the complete documentation index at: https://widgetic.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Security

> Security best practices for Widgetic integrations

This guide covers security best practices when integrating with the Widgetic API.

## API Key Security

### Storage

> [!WARNING]
>
> Never expose API keys in client-side code, version control, or public repositories.

**Do:**

* Store keys in environment variables
* Use secrets management services (AWS Secrets Manager, Vault, etc.)
* Rotate keys periodically

**Don't:**

* Commit keys to git
* Include keys in frontend bundles
* Share keys via unencrypted channels

### Key Types

| Prefix | Security Level | Use Case |
| - | - | - |
| `wgt_usr_` | User | Personal server-side use |
| `wdg_org_` | Organization | Shared server-side use |
| `wgt_prt_` | Partner | White-label integrations |

### Making, Rotating and Deleting Keys

You make and manage your own keys. In the dashboard, open Workspace settings → API keys. With the API, use the key calls below. `{id}` is your own account id, which `GET /tokens/validate` returns as `data.userId`. Your plan must include API access.

* `GET /users/{id}/keys` lists your keys, with what each is limited to (`preset` and `scopes`).
* `POST /users/{id}/keys` makes one (send an `Idempotency-Key`). **Only a person signed in on widgetic.com can make a key**, so make keys in the dashboard: a request made with an API key or an SDK token is refused with `403`. The secret is in the answer once, in `key`, and cannot be shown again. Send a `preset` (`read_only`, `content` or `full_access`) or a list of `scopes` to limit what the key can do, and neither for a key with full access. See [Limit what a key can do](https://widgetic.com/docs/authentication.md#limit-what-a-key-can-do).
* `PUT /users/{id}/keys/{keyId}` renames a key or switches it off with `isActive: false`. Switching a key back on needs a person signed in too. `DELETE /users/{id}/keys/{keyId}` deletes it.
* `GET /users/{id}/api-keys/{keyId}/logs` shows what a key was used for.

To rotate a key, make the new one in the dashboard, switch your servers over, then delete the old one. A key that is switched off or deleted is refused at once by the server that took the request, and within a minute by the others. The limit of a key cannot be changed once it is made: to widen or narrow it, rotate the key the same way.

> [!WARNING]
>
> A key cannot make another key, so a leaked key cannot be used to get back in after you revoke it. If a key leaks, delete it (in the dashboard, or with another key) and make a new one in the dashboard.

## Client-Side Authentication

A browser must never hold an API key (`wgt_usr_`, `wdg_org_`, or `wgt_prt_`). Keep your API key on your server and have your backend make API calls on behalf of your application, or—when mounting Widgetic's editors in your page for one of your customers—mint a short-lived [editor session](https://widgetic.com/docs/partners/editor-sessions.md) on your server:

```typescript theme={null}
import { Configuration, ManagedUsersApi } from '@widgetic/api-sdk-ts';

// Server-side: Mint a short-lived editor session for the signed-in customer
app.post('/api/widgetic-editor-session', async (req, res) => {
  const managedUsers = new ManagedUsersApi(
    new Configuration({ accessToken: process.env.WIDGETIC_PARTNER_KEY })
  );

  const { data: session } = await managedUsers.createManagedUserEditorSession({
    id: req.user.widgeticManagedUserId,
    createManagedUserEditorSessionRequest: {
      surfaces: ['editor', 'files'],
      allowedOrigin: 'https://app.example.com',
      expiresInSeconds: 3600
    }
  });

  res.json({ token: session.token, expiresAt: session.expiresAt });
});
```

> [!WARNING]
>
> Never send a partner key (`wgt_prt_`) or user key (`wgt_usr_`) to a browser: it carries the access of your whole account. Use an [editor session](https://widgetic.com/docs/partners/editor-sessions.md) instead—a session reaches only that customer's content, only the calls the editors need, and can be bound to your page origin and revoked at any time.

## Domain Validation

You can limit the websites an embedded widget may appear on. If you list nothing, the widget works everywhere.

1. Open Allowed Domains in the dashboard (for a team workspace, use that workspace's settings; only its admins and owners can change the list)
2. Add each website: `example.com` also covers `www.example.com`, and `*.example.com` covers the domain and every subdomain
3. Leave `localhost` out: a page on your own machine always works, however short the list is (see below)

You can also keep the list with the API, using your own account id as `{id}`. `GET /users/{id}/workspace/domains` reads it, `POST` adds a website with the body `{ "domain": "example.com" }`, `PUT /users/{id}/workspace/domains/{domainId}` changes one, and `DELETE` removes one. A wildcard such as `*.example.com` is one entry, however many subdomains it covers. The list of an ordinary account holds up to 50 websites, and the 51st answers `409` with the code `BUSINESS_STATE_CONFLICT`. If you administer a partner account, your list has no limit: your customers may publish on their own domains, and you can add each of those by code, one `POST` for each website. A change applies within about 15 seconds.

Once a list exists, the lookup that returns a widget's content answers `403` with the code `DOMAIN_NOT_ALLOWED` to a page on any other website, and the widget's frame page asks the browser not to show it anywhere else. A very long list, which only a partner can have (more than about fifty websites of ordinary length), does not fit in that request to the browser, so the frame page is then shown without it; the lookup, and the entries a visitor sends, still refuse every website that is not on the list. Widgetic's own sites always work, and so does your own machine. If you are a partner, your platform's website always works too ([see below](#your-platform-is-always-allowed)). The lookup never refuses a page on `localhost`, on `127.0.0.1` (or any address from `127.0.0.0` to `127.255.255.255`), on `[::1]` or on a name under `.localhost`, on any port, and the widget's frame page may be shown by `localhost`, `127.0.0.1` and names under `.localhost`, on any port, over `http` or `https`. You can build and test an integration without listing your laptop: only the person who runs such a page can see it, so listing it would protect nothing. A list that cannot be read never blocks a widget.

The frame of a workflow, and its oEmbed card, follow the list of the owner of the workflow's first composition in the same two ways: the frame asks the browser not to show it on another website, and the card answers `403` there. See [Show a workflow where you cannot add a tag](https://widgetic.com/resources/sdks.md#show-a-workflow-where-you-cannot-add-a-tag).

If you use the Partner API, the `allowedDomains` of your partner account is added to the list of the person who administers it, and applies to the widgets made with your API keys.

### Your platform is always allowed

If you are a partner, the website where you integrate Widgetic is always allowed, with all of its subdomains. Tell Widgetic which website that is when your partner account is set up, for example `acme.com`, and Widgetic records it as your **platform domain**. `acme.com` and every subdomain of it (`app.acme.com`, `learn.eu.acme.com`) can then always show your widgets, and the entries their visitors send (such as votes or form answers) are always accepted from them, whatever your list holds. The widgets your customers make through you are covered too, because they belong to your account.

* **You never have to list it, and a list can never lock it out.** It is not an entry of your list, so adding websites to the list, or removing them all, never changes it.
* **It never narrows anything.** If you list nothing, your widgets still work on every website, not only on your platform.
* **Widgetic sets it, you read it.** `GET /partners/mine` gives it as `platformDomain` for each partner you administer, and it is `null` until Widgetic has set one. To change it, write to [partners@widgetic.com](mailto:partners@widgetic.com).
* **It is one website.** Give Widgetic the bare name, such as `acme.com`; an address like `https://www.acme.com/pricing` is shortened to it. A different website that also shows your widgets, such as a separate marketing site, is not covered: put it on your list when you keep one.

> [!NOTE]
>
> A domain list keeps your widgets off other people's pages. It is not access control: the content of a published widget can still be requested by anyone who has its ID, using a plain HTTP request that names no website for the check to read. Do not rely on it to keep content private.

> [!NOTE]
>
> The `<widgetic>` embed script stops at the refusal: it draws nothing, sets `data-widgetic-refused="DOMAIN_NOT_ALLOWED"` on the element, and writes one warning to the browser console for each widget. It does not fall back to a copy of the widget on Widgetic's CDN.

## Webhook Signatures

Partner accounts can have Widgetic call their own server when something happens, for example when an invoice is paid. No other kind of account receives webhooks. Every call is signed, with the [Standard Webhooks](https://www.standardwebhooks.com) headers `webhook-id`, `webhook-timestamp` and `webhook-signature`, under a secret that only you hold.

Before you act on a call:

* Check the signature over the raw body, with a Standard Webhooks library or the short check in [Notifications](https://widgetic.com/docs/partners/notifications.md#verify-it).
* Refuse a call whose `webhook-timestamp` is more than five minutes old.
* Keep the `webhook-id` of every event you handled and drop a repeat: delivery is at least once.

If the secret leaks, make a new one with `POST /partners/{partnerId}/notifications/webhook-secret`. The old one keeps signing for a day, so your receiver can switch without a call being rejected. See [Your Own Webhook](https://widgetic.com/docs/partners/notifications.md#your-own-webhook).

## Rate Limiting

Protect your integration from abuse:

```typescript theme={null}
import rateLimit from 'express-rate-limit';

const limiter = rateLimit({
  windowMs: 60 * 1000, // 1 minute
  max: 100, // Limit each IP to 100 requests per window
  message: 'Too many requests, please try again later'
});

app.use('/api/widgetic', limiter);
```

## Data Protection

### Sensitive Data

Don't store sensitive user data in widget configurations:

```typescript theme={null}
// ❌ Bad
widget.config = {
  apiKey: 'secret',
  password: 'user-password'
};

// ✅ Good
widget.config = {
  userId: 'user-uuid',
  settings: { theme: 'dark' }
};
```

### Audit Logging

Log API interactions for security auditing:

```typescript theme={null}
async function logApiCall(
  action: string,
  resourceId: string,
  userId: string
) {
  await db.auditLog.create({
    action,
    resourceId,
    userId,
    timestamp: new Date(),
    ip: req.ip
  });
}
```

## Your Customers' Data

If you keep [managed users](https://widgetic.com/docs/partners/managed-users.md) for your customers, Widgetic holds a record for each one. You can read a record and delete it:

```bash theme={null}
# Read the record of one customer
curl -sS -H "Authorization: Bearer $WIDGETIC_API_KEY" \
  "https://api.widgetic.com/v1/managed-users/$MANAGED_USER_ID"

# Delete it
curl -sS -X DELETE -H "Authorization: Bearer $WIDGETIC_API_KEY" \
  "https://api.widgetic.com/v1/managed-users/$MANAGED_USER_ID"
```

Deleting the record removes the managed user, not what you filed under it. The compositions, widgets and uploads stay in your account, and lose only their `managedUserId` label. To erase a customer's content as well, list it with the `managedUserId` filter and delete it first, then delete the record (see [Managed Users](https://widgetic.com/docs/partners/managed-users.md#filing-content-under-a-managed-user)).

## Security Checklist

> [accordion] API Key Management
>
> * [ ] Keys stored in environment variables
> * [ ] Keys rotated quarterly
> * [ ] Separate keys for dev/staging/production
> * [ ] Access logged and monitored

> [accordion] Client Security
>
> * [ ] Token exchange for browser apps
> * [ ] Short token expiration (a token lasts at most 1 hour)
> * [ ] Domain validation enabled
> * [ ] Partner accounts: your platform domain is set (`platformDomain` in `GET /partners/mine`)
> * [ ] CSP headers configured

> [accordion] Webhooks (partner accounts)
>
> * [ ] Signature checked on the raw body
> * [ ] Calls older than five minutes refused
> * [ ] Repeats dropped by `webhook-id`
> * [ ] A `2xx` answer within ten seconds, with the slow work done afterwards
> * [ ] Secret kept out of git, and replaced if it leaks

## Reporting Vulnerabilities

Found a security problem in Widgetic? Write to [support@widgetic.com](mailto:support@widgetic.com), say that it is a security report, and give the steps to reproduce it.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.