` and attaches an **open shadow root** to it. All of the template's markup, styles, and external stylesheets are injected inside that shadow root before the host is placed in the DOM at the configured target position.
The shadow DOM provides full CSS isolation — styles defined in the template don't leak out to the host page, and the host page's styles don't bleed in.
The host element carries a `data-allegro-interaction` attribute set to the Interaction's slug, which you can use for external targeting if needed:
```css
/* from the host page — selects the template host element */
[data-allegro-interaction='my-interaction'] {
margin-top: 2rem;
}
```
#### Alpine.js[](#alpinejs "Direct link to Alpine.js")
[Alpine.js](https://alpinejs.dev) v3 is loaded automatically the first time a template renders — no `
```
#### Local Development[](#local-development "Direct link to Local Development")
By default, Allegro permanently redirects any request on the custom domain that does not match an allowed path to the equivalent path on the tenant's subdomain. This can interfere with local or staging setups where you need to access the admin dashboard through a custom domain.
Set `ALLEGRO_DISABLED_REDIRECT_CANONICAL=true` in your environment to disable this redirect entirely. When set, requests to non-SDK paths on the custom domain are served directly rather than being redirected to the subdomain. Allegro also automatically scopes the session cookie to the custom domain for that tenant so authentication works correctly.
warning
Do not set `ALLEGRO_DISABLED_REDIRECT_CANONICAL=true` in production. It disables a security boundary that prevents audience-facing custom-domain traffic from reaching the operator dashboard.
#### Related[](#related "Direct link to Related")
* [Custom Domain (Administration)](/developer/administration/custom-domain.md) — How a custom domain affects your SDK integration
* [Script Tag](/developer/guides/script-tag.md) — Embedding the SDK on your site
* [Stripe Webhook Setup](/developer/platform/integrations/stripe-webhooks.md) — Configuring the Stripe webhook endpoint
---
### Feature Flags
Allegro has two kinds of feature flags:
* **Tenant flags** belong to a single tenant and are evaluated per organization.
* **Platform-wide (landlord) flags** are global. They live on the landlord rather than on any tenant, and read the same value everywhere — across every tenant, the Allegro Admin, queued jobs, and console commands.
This page covers the platform-wide flags. They are managed by a super admin and are kept completely separate from a tenant's own flags.
#### Managing platform-wide flags[](#managing-platform-wide-flags "Direct link to Managing platform-wide flags")
From the Allegro Admin, open **Global Settings → Settings → Feature Flags**. The page lists every platform-wide flag with its description and a toggle. Switching a flag on or off applies immediately across the whole installation — there's no deploy or cache-clear step.
Only super admins can view or change platform-wide flags.
note
Platform-wide flags are stored on the landlord connection, in a `features` table separate from each tenant's flags. Turning a platform flag on or off has no effect on any tenant-level flag of the same name, and vice versa.
#### Defining a platform-wide flag[](#defining-a-platform-wide-flag "Direct link to Defining a platform-wide flag")
Platform-wide flags are defined in code and then toggled from the settings page. To add one, create a class in the `Allegro\Features\Landlord` namespace (under `src/Features/Landlord/`) that exposes a `resolve()` method returning the flag's default value. Allegro discovers the class automatically and it appears on the **Feature Flags** page.
```php
namespace Allegro\Features\Landlord;
use Allegro\Features\Attributes\FeatureDescription;
use Allegro\Features\LandlordFeature;
#[FeatureDescription(
'Enable the experimental analytics dashboard across every tenant.',
label: 'Experimental analytics',
)]
class ExperimentalAnalytics
{
public function resolve(mixed $scope = null): bool
{
return false;
}
public static function isActive(): bool
{
return LandlordFeature::active(self::class);
}
}
```
The `#[FeatureDescription]` attribute supplies the label and description shown on the settings page. The `resolve()` method returns the value a flag starts with before anyone toggles it — return `false` for a flag that ships off by default.
Read the flag anywhere in the application with `LandlordFeature::active()`, or expose a small helper on the flag class (such as the `isActive()` method above) for readability:
```php
if (ExperimentalAnalytics::isActive()) {
// …
}
```
Because the value is resolved against a single global scope, it reads the same whether you check it inside a tenant request, the Allegro Admin, a queued job, or a scheduled command.
---
### Tenants
A **tenant** is a single, isolated Allegro organization running on your instance. Each tenant has its own database, its own JWT signing keys, and its own audience, templates, and settings. The **Allegro Admin** is the control plane where super administrators manage every tenant on the instance.
Super administrators only
Creating and managing tenants is restricted to super administrators. Regular operators sign in to a single tenant and never see the Allegro Admin. Super admins can jump into any tenant — and back to the Allegro Dashboard — from the tenant switcher.
#### Creating a tenant[](#creating-a-tenant "Direct link to Creating a tenant")
1. Open **Global Settings → Tenants**.
2. Click **New tenant**.
3. Fill in the dialog:
| Field | Rules |
| -------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| **Name** | Required. Up to 255 characters. Letters, numbers, spaces, and dashes only — e.g. `Acme News`. |
| **Slug** | Required. 3–255 characters, URL-safe (letters, numbers, dashes, underscores). Must be unique across the instance — e.g. `acme-news`. |
The slug is generated automatically from the name as you type, until you edit the slug field yourself. Whatever you enter is normalized to a URL-safe value before it is saved, so a name like `Acme News` becomes the slug `acme-news`.
4. Click **Create**. Allegro provisions the tenant and takes you to its detail page.
#### What provisioning does[](#what-provisioning-does "Direct link to What provisioning does")
Tenant creation is synchronous — by the time you land on the new tenant's page, it is ready to use. Provisioning:
* **Creates and migrates the tenant's database**, giving it an isolated schema separate from every other tenant.
* **Generates the tenant's initial JWT signing key** — an RS256 (RSA-2048) key pair used to sign member session tokens. The public half is served from the tenant's [JWKS endpoint](/developer/guides/jwt.md) so you can verify those tokens in your own infrastructure.
* **Loads the compiled `client.js` asset** into the tenant so the browser SDK is immediately available.
Loading the client asset is best-effort: if the compiled asset isn't available when the tenant is created, provisioning still succeeds and the asset is loaded on the next build or asset refresh.
Give the tenant a domain
A new tenant is reachable at its subdomain (for example `acme-news.allegrocdp.com`). To serve the SDK from a domain you control, follow the [Custom Domain](/developer/platform/setup/custom-domain.md) guide.
---
### SDK Reference
The Allegro SDK is a browser JavaScript library that loads via a `
```
`window.allegro.push()` queues callbacks that run as soon as the SDK is ready. If the SDK has already loaded by the time `push()` is called, the callback runs immediately.
#### Namespaces[](#namespaces "Direct link to Namespaces")
| Namespace | Description |
| -------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |
| [`allegro.track()`](/developer/api-reference/interfaces/AllegroSDK.md) | Track named events with automatic page context collection |
| [`allegro.member`](/developer/api-reference/interfaces/MemberNamespace.md) | Authenticate and identify audience members |
| [`allegro.interaction`](/developer/api-reference/interfaces/InteractionNamespace.md) | Trigger and manage on-page interactions |
| `allegro.forms` | Fetch and submit admin-composed forms of user attributes |
| [`allegro.recommendations`](/developer/api-reference/interfaces/RecommendationsNamespace.md) | Fetch ranked article recommendations and report clicks on them |
---
### Webhook Events Reference
This page lists every event you can subscribe to, grouped by the entity that changed, with an example delivery for each payload shape.
For the delivery envelope format, property scoping, and signature verification, see [Webhooks Overview](/developer/webhooks/overview.md).
#### Payload Shape[](#payload-shape "Direct link to Payload Shape")
The envelope is the same for every event. Only `data` changes:
```json
{
"id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"event": "audience_member.created",
"created_at": "2026-08-12T14:32:00+00:00",
"property": "magazine",
"data": { ... }
}
```
For every event except [`ping`](#system), `data` is a single JSON:API resource object:
| Key | Description |
| ------------ | --------------------------------------------------------------------------------------------- |
| `id` | The record's identifier. |
| `type` | The resource type, fixed per event group (for example `audience_members`). |
| `attributes` | The record's fields. This is the part that differs between the shapes documented below. |
| `meta` | Present only where noted below. Carries information about the record rather than its content. |
Events that share a shape share an example. All twenty-three subscribable events resolve to one of eight shapes, plus `ping`.
Two timestamp formats appear in payloads
The envelope's `created_at`, and any field the platform explicitly formats (entitlement `started_at` and `ended_at`, and the ping's `created_at`), use an offset — `2026-08-12T14:32:00+00:00`. Record timestamps inside `attributes` use the microsecond form — `2026-08-12T14:32:00.000000Z`. Both are UTC and both parse as ISO 8601; parse them, don't string-compare them.
Treat unrecognized keys as optional
Payloads are additive. A future release can add attributes, and `data` may carry extra top-level keys such as `relationships` depending on how the record was loaded when the event fired. Read the keys you need and ignore the rest.
#### Audience Member[](#audience-member "Direct link to Audience Member")
These events fire when an audience member record is created, updated, or deleted, and when a member joins a property.
| Event | Trigger |
| --------------------------------- | -------------------------------------------------------------------------- |
| `audience_member.created` | Fired when a new audience member is created. |
| `audience_member.updated` | Fired when an audience member is updated (excludes activity-only changes). |
| `audience_member.deleted` | Fired when an audience member is deleted. |
| `audience_member.property.joined` | Fired the first time an audience member signs in on a property. |
note
`audience_member.updated` is **not** fired for activity-only changes such as `last_active_at` updates. It fires only when meaningful profile data changes.
`meta` inside `attributes` holds the member's raw meta values, keyed by meta key and sorted alphabetically. Every value is a string, including numbers and booleans. For the tenant-defined subset of these values, subscribe to the [user attribute](#user-attribute) events instead.
`properties` lists every property the member has joined, with when they first signed in on it and whether that sign-in created their account. For `audience_member.property.joined`, the joined property is also the envelope's `property`.
```json
{
"id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"event": "audience_member.created",
"created_at": "2026-08-12T14:32:00+00:00",
"property": "magazine",
"data": {
"id": "9d5c1f2a-8b3e-4a17-9f6d-2c4b8e1a7d30",
"type": "audience_members",
"attributes": {
"name": "Jane Doe",
"email": "jane@example.com",
"email_verified": true,
"meta": {
"newsletter_opt_in": "1",
"postal_code": "90210"
},
"properties": [
{
"id": "3b2e9c4d-1f7a-4e8b-9c6d-5a4f3e2d1c0b",
"slug": "magazine",
"name": "Magazine",
"joined_at": "2026-08-12T14:32:00+00:00",
"signed_up": true
}
],
"created_at": "2026-08-12T14:32:00.000000Z",
"updated_at": "2026-08-12T14:32:00.000000Z"
}
}
}
```
#### External Profile[](#external-profile "Direct link to External Profile")
External profiles store data from third-party providers (such as a CRM or identity provider) attached to a member.
| Event | Trigger |
| ------------------------------------------ | ---------------------------------------------------- |
| `audience_member.external_profile.created` | Fired when an external profile is added to a member. |
| `audience_member.external_profile.updated` | Fired when an external profile is updated. |
| `audience_member.external_profile.deleted` | Fired when an external profile is removed. |
`attributes` is the provider's stored payload verbatim, so its keys are whatever that provider supplies — the example below is illustrative, not a schema. Record timestamps are under `meta` rather than `attributes`, because `attributes` is entirely provider-controlled.
For the same reason, neither the member nor the provider name is in the payload: `data.id` identifies the profile row, so key your records on it. If you need the member, subscribe to the [user attribute](#user-attribute) events as well — any user attribute configured to read from this provider is re-synced when the profile changes, and those events do carry `audience_member_id`.
```json
{
"id": "b9f1c7d2-4a8e-4b03-9c15-6d2e8f3a1b47",
"event": "audience_member.external_profile.updated",
"created_at": "2026-08-12T14:35:12+00:00",
"property": "magazine",
"data": {
"id": "3f1b9c04-6d2e-4c8a-b5f7-1a9e3d7c2b48",
"type": "audience_member_external_profiles",
"attributes": {
"contact_id": "C-0031982",
"membership_level": "sustainer",
"lifetime_giving": 240,
"tags": ["donor", "event-attendee"]
},
"meta": {
"created_at": "2026-07-02T09:14:05.000000Z",
"updated_at": "2026-08-12T14:35:12.000000Z"
}
}
}
```
#### Foreign Key[](#foreign-key "Direct link to Foreign Key")
Foreign keys link an audience member to an identifier in an external system (for example, a CRM contact ID or a subscriber ID in your publishing platform).
| Event | Trigger |
| ------------------------------------- | ------------------------------------------------- |
| `audience_member.foreign_key.created` | Fired when a foreign key is attached to a member. |
| `audience_member.foreign_key.updated` | Fired when a foreign key is updated. |
| `audience_member.foreign_key.deleted` | Fired when a foreign key is removed. |
The payload carries the `key` and `value` pair but not the member it belongs to, so key your records on `data.id`.
```json
{
"id": "c1d4e8a2-7b39-4f65-8a02-3e9c5b1d7f84",
"event": "audience_member.foreign_key.created",
"created_at": "2026-08-12T14:36:40+00:00",
"property": "magazine",
"data": {
"id": "7e2a5c81-9d34-4b6f-a017-8c3e2f9b5d60",
"type": "audience_member_foreign_keys",
"attributes": {
"key": "contact_id",
"value": "C-0031982",
"created_at": "2026-08-12T14:36:40.000000Z",
"updated_at": "2026-08-12T14:36:40.000000Z"
}
}
}
```
#### User Attribute[](#user-attribute "Direct link to User Attribute")
User attributes are the tenant-defined profile fields configured in the admin. These events fire only for keys defined in the tenant's user attribute registry — internal and unmapped member meta keys are ignored.
| Event | Trigger |
| ---------------------------------------- | ------------------------------------------------------------------------ |
| `audience_member.user_attribute.created` | Fired when a user attribute value is set on a member for the first time. |
| `audience_member.user_attribute.updated` | Fired when a user attribute value changes. |
| `audience_member.user_attribute.deleted` | Fired when a user attribute value is removed from a member. |
`slug` is the attribute's slug from the registry, and `value` is always a string regardless of the attribute's configured type. Unlike every other shape on this page, `id` is a numeric string rather than a UUID — it identifies the stored value, not the attribute definition, so key your own records on `audience_member_id` plus `slug`.
```json
{
"id": "a48f2d19-6c05-4e73-b8d1-9f27a3e5c064",
"event": "audience_member.user_attribute.updated",
"created_at": "2026-08-12T14:38:02+00:00",
"property": "magazine",
"data": {
"id": "1842",
"type": "audience_member_user_attributes",
"attributes": {
"audience_member_id": "9d5c1f2a-8b3e-4a17-9f6d-2c4b8e1a7d30",
"slug": "newsletter_opt_in",
"value": "1"
}
}
}
```
#### Entitlement[](#entitlement "Direct link to Entitlement")
Entitlement events fire when a member's access to a product changes.
| Event | Trigger |
| --------------------- | ------------------------------------- |
| `entitlement.created` | Fired when an entitlement is granted. |
| `entitlement.updated` | Fired when an entitlement is updated. |
| `entitlement.deleted` | Fired when an entitlement is deleted. |
`is_active` is computed from `started_at` and `ended_at` at the moment the event fired: an entitlement is active once `started_at` has passed and until `ended_at` does. A `null` `ended_at` means indefinite access. Revoking an entitlement sets `ended_at` and fires `entitlement.updated`, not `entitlement.deleted`.
The entitlement's `id` is repeated inside `attributes` as well as at the top of `data`. `audience_member_id` names the member whose access changed, and `product` carries the product it grants — `id`, `name`, and `slug` — so a delivery can be routed without a follow-up API call. That matters most for `entitlement.deleted`, which names a row that no longer exists to read back.
```json
{
"id": "d7c93b15-2e48-4a06-91f5-7b3c8d2e6a19",
"event": "entitlement.created",
"created_at": "2026-08-12T14:40:18+00:00",
"property": "magazine",
"data": {
"id": "c8e40a72-5d16-4f39-8b2c-1e7a9d4f3b05",
"type": "entitlements",
"attributes": {
"id": "c8e40a72-5d16-4f39-8b2c-1e7a9d4f3b05",
"audience_member_id": "9d5c1f2a-8b3e-4a17-9f6d-2c4b8e1a7d30",
"product": {
"id": "3f8b2c91-7d64-4e05-a1b8-5c2f9e4d7a63",
"name": "Magazine Digital",
"slug": "magazine-digital"
},
"started_at": "2026-08-12T00:00:00+00:00",
"ended_at": null,
"note": "Granted by support",
"is_active": true,
"created_at": "2026-08-12T14:40:18.000000Z",
"updated_at": "2026-08-12T14:40:18.000000Z"
}
}
}
```
#### Purchase[](#purchase "Direct link to Purchase")
Purchase events fire when a member's purchase is created or changes (for example, a refund). Purchases are not hard-deleted, so there is no `purchase.deleted` event.
| Event | Trigger |
| ------------------ | --------------------------------------------------------- |
| `purchase.created` | Fired when a purchase is created. |
| `purchase.updated` | Fired when a purchase is updated (for example, refunded). |
`status` is one of `pending`, `completed`, `failed`, or `refunded`. `amount` is an integer in the currency's smallest unit — `999` with a `currency` of `usd` means $9.99. The `provider_*` fields are the payment provider's own identifiers.
A purchase is created as `pending`, and an anonymous checkout is created with a `null` `audience_member_id` and `entitlement_id`; both are filled in when the payment is fulfilled. Expect every `purchase.created` to be followed by a `purchase.updated` that carries the settled record.
```json
{
"id": "e2a6f483-9c71-4d50-b8e3-4f1a7c9d2b60",
"event": "purchase.updated",
"created_at": "2026-08-12T14:42:55+00:00",
"property": "magazine",
"data": {
"id": "7a1e5b39-4c82-4067-9d1f-3b8e2a6c5d40",
"type": "purchase_webhooks",
"attributes": {
"id": "7a1e5b39-4c82-4067-9d1f-3b8e2a6c5d40",
"audience_member_id": "9d5c1f2a-8b3e-4a17-9f6d-2c4b8e1a7d30",
"device_session_id": "5b2f8c14-7a63-4e09-8d25-1f9c3b7a2e58",
"plan_id": "e91d4a07-3b56-4c82-9f14-6d2e8a5c1b73",
"entitlement_id": "c8e40a72-5d16-4f39-8b2c-1e7a9d4f3b05",
"provider": "stripe",
"provider_session_id": "cs_test_a1b2c3d4e5f6",
"provider_payment_id": "pi_3Q1a2b3c4d5e6f7g",
"provider_refund_id": null,
"status": "completed",
"amount": 999,
"currency": "usd",
"metadata": null,
"return_url": "https://acme.com/thank-you",
"created_at": "2026-08-12T14:41:03.000000Z",
"updated_at": "2026-08-12T14:42:55.000000Z"
}
}
}
```
#### Interaction[](#interaction "Direct link to Interaction")
Interaction events fire when an interaction is created, updated, or deleted.
| Event | Trigger |
| --------------------- | ------------------------------------- |
| `interaction.created` | Fired when an interaction is created. |
| `interaction.updated` | Fired when an interaction is updated. |
| `interaction.deleted` | Fired when an interaction is deleted. |
`status` is `draft` or `published`. `trigger_mode` is `manual` or `automatic`. `actions` is the interaction's ordered action list — in automatic mode, the actions of the **Everyone else** default journey; every entry has a `type`, and the remaining keys depend on it. `journeys` is the ordered list of targeting journeys (`null` for interactions that never had any), each with its `id`, `name`, `condition_groups`, and `actions`. Interactions are soft-deleted, so `interaction.deleted` carries the record with `deleted_at` set rather than omitting it.
```json
{
"id": "f5b81e27-6a94-4c13-8d70-2e9f4a1c6b38",
"event": "interaction.updated",
"created_at": "2026-08-12T14:45:20+00:00",
"property": null,
"data": {
"id": "2c9e7f14-8b05-4a63-91d8-5f3a2e7c4b19",
"type": "interaction_webhooks",
"attributes": {
"id": "2c9e7f14-8b05-4a63-91d8-5f3a2e7c4b19",
"title": "Sustainer Upsell",
"slug": "sustainer-upsell",
"status": "published",
"trigger_mode": "manual",
"journeys": null,
"actions": [
{
"type": "template",
"template_id": "8d1f4a92-3c67-4b05-a2e8-7f9c1d5b3a26",
"target_selector": "#article-body",
"field_values": [
{
"slug": "headline",
"value": "Support independent reporting"
}
],
"placement_method": "before",
"truncation_unit": null,
"truncation_count": null,
"truncation_style": null,
"delay_type": null,
"delay_value": null,
"delay_scroll_unit": null,
"delay_scroll_selector": null
}
],
"created_at": "2026-08-01T10:02:44.000000Z",
"updated_at": "2026-08-12T14:45:20.000000Z",
"deleted_at": null
}
}
}
```
#### Template[](#template "Direct link to Template")
Template events fire when a template is created, updated, or deleted.
| Event | Trigger |
| ------------------ | --------------------------------- |
| `template.created` | Fired when a template is created. |
| `template.updated` | Fired when a template is updated. |
| `template.deleted` | Fired when a template is deleted. |
The payload includes the template's full `html`, `js`, and `css` bodies, so template deliveries are the largest ones Allegro sends. `fields` describes the template's field definitions, and the `sync_source` and `github_*` fields are populated only for templates kept in sync with a Git repository. Templates are soft-deleted, so `template.deleted` carries the record with `deleted_at` set.
```json
{
"id": "0a3d9c76-4e18-4b52-8f01-6c7a2e9d5b34",
"event": "template.updated",
"created_at": "2026-08-12T14:48:07+00:00",
"property": null,
"data": {
"id": "8d1f4a92-3c67-4b05-a2e8-7f9c1d5b3a26",
"type": "template_webhooks",
"attributes": {
"id": "8d1f4a92-3c67-4b05-a2e8-7f9c1d5b3a26",
"title": "Sustainer Banner",
"slug": "sustainer-banner",
"status": "published",
"html": "
{{ headline }}
",
"js": "",
"css": ".banner { padding: 1rem; }",
"fields": [
{
"slug": "headline",
"description": "Banner headline",
"type": "text"
}
],
"external_css_urls": [],
"created_at": "2026-07-28T16:20:11.000000Z",
"updated_at": "2026-08-12T14:48:07.000000Z",
"deleted_at": null,
"sync_source": "github",
"github_path": "templates/sustainer-banner.html",
"github_sha": "9f2c1d7e8a3b4c5d6e7f8091a2b3c4d5e6f70819",
"github_commit_sha": "1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d",
"synced_at": "2026-08-12T14:48:07.000000Z"
}
}
}
```
#### System[](#system "Direct link to System")
| Event | Trigger |
| ------ | ------------------------------------------------------------------------------------------------- |
| `ping` | A test event sent when an active webhook is created, or on demand from the webhook's detail page. |
`ping` is not subscribable — it is sent automatically, or on demand from the dashboard, and cannot be selected when configuring event subscriptions.
The ping is the one delivery whose `data` is **not** a JSON:API resource object. It echoes the webhook's own `id`, subscribed `events`, and `created_at`. Pings ignore property scoping and, because webhooks are created from the organization dashboard, normally carry `property: null`.
```json
{
"id": "6d0f8a13-2b47-4e95-8c26-1a3f7d9b5e02",
"event": "ping",
"created_at": "2026-08-12T14:30:00+00:00",
"property": null,
"data": {
"id": "4b7e1c29-8d53-4a06-9f18-2e6c5b3a7d94",
"events": ["audience_member.created", "entitlement.created"],
"created_at": "2026-08-12T14:29:58+00:00"
}
}
```
#### Adding New Events[](#adding-new-events "Direct link to Adding New Events")
The subscribable event list is generated from the event registry in the platform. New event types registered there become immediately available in the webhook subscription UI and in this reference.
#### Related[](#related "Direct link to Related")
* [Webhooks Overview](/developer/webhooks/overview.md) — creating webhooks, property scoping, delivery envelope, signature verification, and retries
* [Properties](/developer/guides/properties.md) — property hosts and the request context that populates the envelope's `property` field
* [Entitlements](/product/entitlements.md) — how entitlements and products work
* [Audience Members](/product/audience.md) — member profiles and data
---
### Webhooks
Webhooks let your systems react to events in Allegro in real time. When something happens — a new audience member joins, an entitlement is granted, a foreign key is attached — Allegro sends an HTTP `POST` request to a URL you control. You don't need to poll the API; Allegro pushes changes to you.
#### Creating a Webhook[](#creating-a-webhook "Direct link to Creating a Webhook")
1. Go to **Organization Settings → Developer → Webhooks**.
2. Click **Add Webhook**.
3. Enter the **Endpoint URL** — the HTTPS URL on your server that will receive deliveries.
4. Enter a **Description** to help you identify the webhook later.
5. Choose which **Events** to subscribe to. You can subscribe to individual events or to all events.
6. Optionally choose which **Properties** the webhook is scoped to, and whether to include **Organization** events that have no property. Leave everything unchecked to receive events from the whole organization — see [Property Scoping](#property-scoping).
7. Click **Create**. Allegro immediately sends a [ping event](#ping) to your endpoint to confirm it is reachable.
Endpoint URLs must be publicly reachable
The endpoint must be a public `https://` URL. Allegro rejects URLs whose host resolves to a private, loopback, link-local, or otherwise reserved address — including cloud metadata endpoints such as `169.254.169.254`. This is checked both when you save the webhook and again before every delivery. Allegro also does not follow redirects, so the URL you register is the exact URL that receives each `POST`.
note
Each organization can have up to **5 webhooks**. If you need more, delete an existing one first.
##### Activating and Deactivating[](#activating-and-deactivating "Direct link to Activating and Deactivating")
Every webhook has an **Active** toggle. Deactivating a webhook suspends deliveries without deleting the webhook or its delivery history. Reactivate it at any time to resume.
#### Property Scoping[](#property-scoping "Direct link to Property Scoping")
If your organization uses [properties](/developer/guides/properties.md), you can limit a webhook to a subset of them. Open the webhook's **Properties** section (**Organization Settings → Developer → Webhooks → your webhook**) and check the properties whose events you want.
Leaving **everything unchecked means organization-wide** — the webhook receives every matching event, with or without a property. That is the default, and it is how all existing webhooks behave, so adding properties to your organization never changes an existing webhook's deliveries.
Every delivery reports the property it came from in the envelope's `property` field, so an organization-wide webhook can still tell properties apart without being scoped to any of them.
##### Events Without a Property[](#events-without-a-property "Direct link to Events Without a Property")
The originating property comes from the **request context the event fired in**, not from the record itself. Audience members, entitlements, purchases, interactions, and templates belong to the organization, not to a property — so an event fired outside a property host has `property: null`.
Events with no property include:
* Changes made in the Allegro dashboard.
* API requests made against the organization host rather than a [property host](/developer/guides/properties.md), which includes most API key traffic.
* Console commands, bulk imports, and queued background work.
Check **Organization** in the webhook's **Properties** section to receive these events:
| Properties checked | Organization checked | The webhook receives |
| ------------------ | -------------------- | ---------------------------------------------------------------- |
| None | No | Every event (organization-wide) |
| None | Yes | Only events with no property |
| Some | No | Only events from the checked properties |
| Some | Yes | Events from the checked properties, plus events with no property |
A scoped webhook skips events with no property unless Organization is checked
A webhook scoped to one or more properties does not receive dashboard changes, organization-host API traffic, or background work unless **Organization** is also checked.
#### Delivery Payload[](#delivery-payload "Direct link to Delivery Payload")
Every delivery is an HTTP `POST` with a JSON body in the following envelope format:
```json
{
"id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"event": "audience_member.created",
"created_at": "2026-08-12T14:32:00+00:00",
"property": "magazine",
"data": { ... }
}
```
| Field | Type | Description |
| ------------ | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `id` | string | Unique identifier for the event (UUID). Matches the `X-Allegro-Event-Id` header. |
| `event` | string | The event type (e.g. `audience_member.created`). |
| `created_at` | string | ISO 8601 timestamp of when the event occurred. |
| `property` | string | null | Slug of the [property](/developer/guides/properties.md) the event fired on. `null` when the event fired without property context — see [Property Scoping](#property-scoping). |
| `data` | object | The resource representation of the entity that changed. Shape varies by event — see [Events Reference](/developer/webhooks/events.md). |
note
`property` is additive
The `property` field was added to every envelope, including on webhooks that are not scoped to any property. Existing consumers keep parsing deliveries unchanged.
##### Request Headers[](#request-headers "Direct link to Request Headers")
Every delivery includes the following HTTP headers:
| Header | Description |
| ------------------------- | ----------------------------------------------------------------------------------------------------------- |
| `Content-Type` | `application/json` |
| `User-Agent` | `Allegro-Webhooks` |
| `X-Allegro-Event` | The event type (e.g. `audience_member.created`). |
| `X-Allegro-Event-Id` | The UUID of the event that triggered this delivery. |
| `X-Allegro-Delivery` | Identifier for the delivery record. Stays the same across retries and manual redeliveries of that delivery. |
| `X-Allegro-Attempt` | Which attempt this request is, starting at `1`. Increments on every automatic retry and manual redelivery. |
| `X-Allegro-Signature-256` | An HMAC-SHA256 signature of the request body (see below). |
#### Signature Verification[](#signature-verification "Direct link to Signature Verification")
Allegro signs every delivery using HMAC-SHA256. The signature is sent in the `X-Allegro-Signature-256` header in the format `sha256=
`.
The signature is computed over the **raw request body** using your webhook's **signing secret** as the key.
Always verify signatures
Verify the signature before processing any webhook payload. Without verification, your endpoint could accept forged requests from anyone on the internet.
##### Finding Your Signing Secret[](#finding-your-signing-secret "Direct link to Finding Your Signing Secret")
Open the webhook detail page (**Organization Settings → Developer → Webhooks → your webhook**). The signing secret is displayed there. Treat it like a password — do not commit it to source control.
##### PHP Example[](#php-example "Direct link to PHP Example")
```php
function verifyAllegroSignature(string $rawBody, string $secret, ?string $signatureHeader): bool
{
// Header format: "sha256=". Reject a missing or malformed header.
if ($signatureHeader === null || !str_starts_with($signatureHeader, 'sha256=')) {
return false;
}
$expected = 'sha256=' . hash_hmac('sha256', $rawBody, $secret);
return hash_equals($expected, $signatureHeader);
}
// Usage (e.g. in a Laravel controller):
$rawBody = $request->getContent();
$signature = $request->header('X-Allegro-Signature-256');
if (!verifyAllegroSignature($rawBody, config('services.allegro.webhook_secret'), $signature)) {
abort(401, 'Invalid signature');
}
```
##### Node.js Example[](#nodejs-example "Direct link to Node.js Example")
```js
import { createHmac, timingSafeEqual } from 'node:crypto';
function verifyAllegroSignature(rawBody, secret, signatureHeader) {
if (!signatureHeader?.startsWith('sha256=')) return false;
const expected =
'sha256=' + createHmac('sha256', secret).update(rawBody).digest('hex');
const actual = Buffer.from(signatureHeader);
const expectedBuf = Buffer.from(expected);
if (actual.length !== expectedBuf.length) return false;
return timingSafeEqual(actual, expectedBuf);
}
// Usage (e.g. in an Express handler):
const rawBody = req.rawBody; // requires bodyParser with verify option
const signature = req.headers['x-allegro-signature-256'];
if (
!verifyAllegroSignature(
rawBody,
process.env.ALLEGRO_WEBHOOK_SECRET,
signature,
)
) {
return res.status(401).send('Invalid signature');
}
```
note
Use a **constant-time comparison** (`hash_equals` in PHP, `timingSafeEqual` in Node.js) to prevent timing attacks.
#### Retries[](#retries "Direct link to Retries")
When a delivery fails — your endpoint returns a non-2xx status code or does not respond within 10 seconds — Allegro retries automatically:
| Attempt | Delay after previous attempt |
| ------- | ---------------------------- |
| Initial | Immediate |
| Retry 1 | 60 seconds |
| Retry 2 | 300 seconds (5 minutes) |
After **3 total attempts** (the initial delivery plus 2 retries), the delivery is marked **Failed** and no further retries occur. The webhook itself remains active and continues to receive future events.
Every attempt is recorded individually. Each one keeps its own response status, response headers, response body, duration, and failure reason, so a delivery that failed twice before succeeding still shows both failures rather than only its final outcome. The full attempt history for a delivery is visible in the dashboard under the webhook's deliveries list.
Use `X-Allegro-Attempt` to tell a retry from a new event. `X-Allegro-Delivery` stays constant for every attempt of the same delivery, so it remains the right header to key idempotency on.
tip
Respond to deliveries as quickly as possible. If processing takes time, accept the delivery immediately (return `200 OK`) and handle it in a background job.
#### Viewing and Managing Deliveries[](#viewing-and-managing-deliveries "Direct link to Viewing and Managing Deliveries")
Open a webhook from **Organization Settings → Developer → Webhooks** to see its recent deliveries. Deliveries are kept for **180 days**.
At the top of the page, a summary shows the **Deliveries**, **Success rate**, and **Failed** counts across every retained delivery — the full 180-day window, not only the sample listed below. Each delivery is counted once, by the outcome of its latest attempt, so a delivery that failed and then succeeded on redelivery counts as a success. The list underneath shows the most recent deliveries; when there are more than it displays, its header notes how many of the total are shown.
Each delivery record shows:
* The event type and delivery timestamp
* The property the event fired on, when it had one
* HTTP response status returned by your endpoint
* The full request payload and response body
The property shown on a delivery is read back out of the delivered payload, so it always matches what your endpoint received. Deliveries cannot be filtered by property.
##### Redelivery[](#redelivery "Direct link to Redelivery")
You can manually redeliver any past delivery from the delivery detail view. This is useful for replaying events after you fix a bug in your endpoint or if your server was temporarily unavailable.
##### Ping[](#ping "Direct link to Ping")
When you create an active webhook, Allegro sends a `ping` event to your endpoint to verify that it is reachable. The ping's `data` payload echoes the webhook's `id`, subscribed `events`, and `created_at` timestamp. Webhooks created inactive are not pinged.
You can also send a ping at any time from the webhook's detail page with **Send ping**, which is the quickest way to test an endpoint after you change it. The button is disabled while the webhook is inactive, and pings are limited to three per webhook per minute.
Pings ignore [property scoping](#property-scoping) — a property-scoped webhook is still pinged, so you can confirm the endpoint is reachable before any real event arrives. Because webhooks are created from the organization dashboard, a ping normally carries `property: null`.
#### Related[](#related "Direct link to Related")
* [Events Reference](/developer/webhooks/events.md) — full list of subscribable events and their payload shapes
* [Settings](/product/administration/settings.md) — how to access Organization Settings
---
## Getting Started
### Allegro Audience
Allegro Audience is an open source, API-driven audience engagement platform. It gives publishers the tools to understand their readers, authenticate them without passwords, and deliver personalized content — all from infrastructure they control.
#### What Allegro Audience does[](#what-allegro-audience-does "Direct link to What Allegro Audience does")
At its core, Allegro connects three things: **who your readers are**, **what they do**, and **what you show them**.
##### Track audience behavior[](#track-audience-behavior "Direct link to Track audience behavior")
Allegro captures what readers do on your site — page views, article reads, clicks, conversions — and builds a profile of each person over time. You see it all in the Audience and Analytics sections of the admin.
##### Authenticate readers[](#authenticate-readers "Direct link to Authenticate readers")
Readers sign in via magic link or social login (Google, Apple) — no passwords. Once signed in, Allegro knows who they are and what they have access to, so you can tailor what each member sees.
##### Deliver dynamic content[](#deliver-dynamic-content "Direct link to Deliver dynamic content")
Templates let you build interactive components in HTML, CSS, and JavaScript — subscription banners, paywalls, email capture forms — and deploy them as Interactions on your site without a code release.
***
#### Two ways to work with Allegro Audience[](#two-ways-to-work-with-allegro-audience "Direct link to Two ways to work with Allegro Audience")
| If you are... | Start here |
| ----------------------------------------------------------- | --------------------------- |
| **An operator** setting up and managing an Allegro instance | You are in the right place! |
| **A developer** embedding Allegro on an external site | [SDK docs →](/developer.md) |
***
#### What you can do in Allegro[](#what-you-can-do-in-allegro "Direct link to What you can do in Allegro")
The Allegro admin is organized around a handful of areas. Here's what each one lets you do.
##### Understand your audience[](#understand-your-audience "Direct link to Understand your audience")
* **[Dashboard](/product/dashboard.md)** — Your home screen. See audience growth and key metrics at a glance the moment you sign in.
* **[Analytics](/product/analytics.md)** — Dig deeper into how members engage with your content and membership programs over time.
* **[Members](/product/audience.md)** — Your contact database. Browse, search, and manage everyone who has signed up or been identified on your site.
##### Sign your readers in[](#sign-your-readers-in "Direct link to Sign your readers in")
* **[Authentication](/product/audience/authentication.md)** — Let readers sign in with a magic link or social login (Google, Apple) — no passwords to manage.
##### Sell access and manage entitlements[](#sell-access-and-manage-entitlements "Direct link to Sell access and manage entitlements")
* **[Entitlements](/product/entitlements.md)** — Control what your audience can access: digital subscriptions, gated content, event access, and more.
* **[Offers and Plans](/product/entitlements/offers.md)** — Package what you sell and set pricing for one-time purchases or recurring plans.
* **[Purchases](/product/entitlements/purchases.md)** — Review what members have bought and the access each purchase grants.
##### Build on-site experiences[](#build-on-site-experiences "Direct link to Build on-site experiences")
* **[Interactions](/product/interactions.md)** — Deploy experiences to your site — paywalls, subscription banners, email capture forms — and trigger them by name, no code release required.
* **[Templates](/product/interactions/templates.md)** — Design the reusable building blocks that Interactions display, and publish them when they're ready.
##### Reach members over email[](#reach-members-over-email "Direct link to Reach members over email")
* **[Email Templates](/product/email-templates.md)** — Create and manage the emails Allegro sends to your members.
##### Manage your team[](#manage-your-team "Direct link to Manage your team")
* **[Users & Permissions](/product/administration/users.md)** — Invite teammates, assign roles, and control who can do what inside your organization.
***
#### Where to start[](#where-to-start "Direct link to Where to start")
New to Allegro? This path walks you through the admin in the order most teams set things up:
1. **[Invite your team](/product/administration/users.md)** so the right people have access.
2. **[Turn on authentication](/product/audience/authentication.md)** so readers can sign in.
3. **[Build your first Interaction](/product/interactions.md)** — start with a [Template](/product/interactions/templates.md), then deploy it to your site.
4. **[Set up entitlements](/product/entitlements.md)** if you sell subscriptions or gate content.
5. **[Watch your Dashboard](/product/dashboard.md)** to see members and engagement grow.
Embedding Allegro on your site
Connecting Allegro to your website — installing the SDK, adding web components, and tracking events — is a developer task. Hand your developers the [SDK documentation](/developer.md) to get the integration in place.
***
#### Open source[](#open-source "Direct link to Open source")
Allegro Audience is developed by [Alley Interactive](https://alley.com) and will be open sourced on GitHub. The platform is built on Laravel and designed to be self-hosted.
---
## Product
### Activity Log
The Activity Log is a running record of the meaningful changes made across your organization — who did what, and when. Use it to answer questions like "who revoked this API key?", "when was this member's entitlement granted?", or "what changed in our settings last week?".
Go to **Activity Log** in the navigation to open it. The Activity Log is available to administrators only.
Who can see the Activity Log
Viewing the Activity Log requires the **Admin** role — the same access that governs organization settings. Members and Viewers do not see it. See [Permissions](/product/administration/permissions.md).
#### Reading the log[](#reading-the-log "Direct link to Reading the log")
Each row is one recorded change. The list is newest-first and shows:
| Column | What it shows |
| ------------ | ------------------------------------------------------------------------------------------------- |
| **When** | How long ago the change happened (for example, "2 hours ago"). |
| **Actor** | Who made the change — a team member's name, an **API key**, or **System** for automated activity. |
| **Category** | The area of the product the change belongs to (see [Categories](#categories)). |
| **Activity** | A one-line description of what happened, along with the specific item it affected. |
| **Details** | A button that opens the full entry. |
When a team member acts through an API token rather than the dashboard, the **Actor** column shows their name with a **· via API** marker.
##### Viewing the details of a change[](#viewing-the-details-of-a-change "Direct link to Viewing the details of a change")
Many rows include an inline summary of the fields that changed. Click **Details** on any row to open the full entry, which shows:
* **When**, **Actor**, **Category**, and the specific **Event**.
* The **Subject** — the item that was changed.
* **Attribute changes** — a before-and-after value for each field that changed.
* **Context** — any extra information recorded with the event.
Sensitive values are never shown
Some changes touch secrets — an API key's value, a signing key, an OAuth client secret. The Activity Log records that the value changed, but the value itself is replaced with a **hidden** marker and is never displayed.
#### Filtering the log[](#filtering-the-log "Direct link to Filtering the log")
The filters at the top of the page narrow the list as you change them. You can combine any of them:
| Filter | What it does |
| -------------------- | ---------------------------------------------------------------------------------- |
| **Search** | Matches text in the activity description or the name of the item that was changed. |
| **Category** | Limits the list to a single [category](#categories). |
| **Actor** | Limits the list to changes made by a **User**, an **API key**, or the **System**. |
| **Event** | Limits the list to a single kind of event. |
| **Start / End date** | Limits the list to a date range. |
The list paginates at 25 entries per page, with a running count of how many entries match your filters.
#### Categories[](#categories "Direct link to Categories")
Every entry is filed under one category so you can narrow the log to the area you care about:
| Category | Covers changes to… |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Settings** | Organization settings and organization-wide branding. |
| **Content** | Templates, email templates, and interactions — including when they are published, unpublished, duplicated, imported, or restored from a revision. |
| **Commerce** | Products, plans, offers, and purchase refunds. |
| **Properties** | Properties and property-level branding overrides. |
| **Security** | Signing keys and OAuth clients. |
| **Authentication** | Staff sign-ins, two-factor and passkey changes, password and profile changes, and connected-app removals (see below). |
| **Developer** | Webhooks and the embed snippet. |
| **GitHub Sync** | Template changes applied by a sync from your connected repository. |
| **Audience** | Audience-member changes made from the dashboard, the API, or an AI assistant (see below). |
| **Exports** | Data exports that were created. |
| **Users** | Team-member invitations sent, resent, and accepted; role changes; and removals. |
##### Account security[](#account-security "Direct link to Account security")
The **Authentication** category records what your team members do with their own accounts, so you can see sign-in activity and security changes across your organization. It captures:
* Successful and failed sign-ins, sign-outs, and sign-ins that were locked out after too many failed attempts.
* Turning two-factor authentication on or off, regenerating recovery codes, a failed two-factor challenge, and signing in with a recovery code.
* Adding or removing a passkey.
* Changing or resetting a password.
* Changing a name or email address, or deleting an account.
* Disconnecting a connected app.
Sign-in and lockout entries record the IP address they came from. Because these changes follow a person wherever they work, each one appears in the log of every organization that person belongs to.
##### Audience activity[](#audience-activity "Direct link to Audience activity")
The **Audience** category records the deliberate changes you or your tools make to an audience member — from the dashboard, through the API, or through an AI assistant. It captures:
* Creating, restoring, or deleting a member.
* Changing a member's name or email address.
* Marking a member as verified or unverified.
* Granting or revoking an entitlement.
* Updating a member's profile fields (which fields changed is recorded; the values are not).
* Linking or unlinking an external profile or an external ID.
* Starting a sync of a member with a connected provider.
* Generating a session to sign in as a member (impersonation).
A member's own everyday activity — signing in, or updating their own profile — is not recorded here, so the log stays focused on changes made on their behalf.
#### Retention[](#retention "Direct link to Retention")
Entries are kept for one year and then removed automatically. The Activity Log is a record of recent change, not a permanent archive.
#### Related Links[](#related-links "Direct link to Related Links")
* [Permissions](/product/administration/permissions.md) — who can see the Activity Log
* [Settings](/product/administration/settings.md) — the organization settings whose changes are logged
* [Users](/product/administration/users.md) — the team members who appear as actors
---
### Branding
Branding is where you give the Allegro components on your site your own visual identity — your logo, colors, fonts, and shape — without writing any CSS. You set a small number of **global design tokens** once, and every embedded component (login forms, email forms, content gates) inherits them automatically. When you need finer control, you can override a single detail on a single component without disturbing the rest.
Go to **Organization Settings → Branding** to open the editor. Branding is an organization-wide setting, so it is available to administrators.

***
#### How the design system works[](#how-the-design-system-works "Direct link to How the design system works")
Allegro's branding is built as a small **design system**: a layered set of variables where broad, brand-wide choices flow down into the specific pieces of each component. There are three levels, from most general to most specific:
| Level | What it is | Example |
| -------------------------------- | ------------------------------------------------------------------------------------- | ------------------------------------------- |
| **Global design tokens** | A handful of brand-wide defaults — your primary color, body font, corner radius, etc. | **Primary** color = your brand teal |
| **Component-specific variables** | Individual settings exposed by each component, grouped by the part they affect. | Login form → **Submit button** → Background |
| **Built-in defaults** | The values a component ships with, used when you haven't set anything. | Black button on a white background |
The key idea is **inheritance**. Most component-specific variables don't have a value of their own until you give them one — instead they *inherit* from a global token. So when you change a global token, every component that inherits it updates at once. You only reach for a component-specific override when you want one spot to differ from the brand-wide default.
note
Everything in branding is optional. Until you change something, components render with their built-in defaults, exactly as they did before you set up branding. There's no need to fill in every field.
##### The inheritance chain[](#the-inheritance-chain "Direct link to The inheritance chain")
When a component renders, each of its settings resolves in this order and uses the first value it finds:
1. **A component-specific override** you set (for example, the login form's submit-button background).
2. Otherwise, the **global token** that setting inherits (for example, your **Primary** color).
3. Otherwise, the component's **built-in default**.
This is why setting a single global token can restyle several components at once, and why an override only ever affects the one place you set it.
***
#### The editor at a glance[](#the-editor-at-a-glance "Direct link to The editor at a glance")
The Branding editor has two columns:
* **Controls** on the left, where you edit tokens and overrides.
* A **live preview** on the right that re-renders the real components with your in-progress changes, so you can see the effect before saving.
At the top of the controls is a **scope switcher** that decides what you're editing:
| Scope | What you edit |
| ----------------------- | ------------------------------------------------------------------- |
| **Global** | Your logo and the global design tokens (colors, fonts, shape). |
| **Login form** | Per-part variables for the `allegro-login-form` component. |
| **Email form** | Per-part variables for the `allegro-email-form` component. |
| **Form** | Per-part variables for the `allegro-form` component. |
| **Content gate** | Per-part variables for the `allegro-content-gate` component. |
| **Inline content gate** | Per-part variables for the `allegro-content-gate-inline` component. |
| **Modal** | Per-part variables for the `allegro-modal` component. |
| **Bottom drawer** | Per-part variables for the `allegro-bottom-drawer` component. |
| **OAuth page** | The full look of your hosted sign-in and error pages. |
A small dot next to a field indicates it has been changed from its default.
***
#### Global design tokens[](#global-design-tokens "Direct link to Global design tokens")
Switch to the **Global** scope to edit the brand-wide tokens. These are the defaults that flow into every component through inheritance. They are grouped into **Colors**, **Typography**, and **Shape & spacing**.
##### Colors[](#colors "Direct link to Colors")
| Token | Default | What it controls |
| ---------------- | --------- | ------------------------------------------------------------------------- |
| **Primary** | `#000` | Your main brand color — buttons, focus states, accents. |
| **Primary text** | `#fff` | Text and icons that sit on top of the primary color (e.g. button labels). |
| **Background** | `#fff` | Component and input backgrounds. |
| **Text** | `#000` | Default body text color. |
| **Muted text** | `#6b7280` | Secondary text — hints, dividers, help copy. |
| **Border** | `#d1d5db` | Input and container borders. |
| **Error** | `#b91c1c` | Validation errors and error messages. |
Each color has both a hex text field and a color picker.
##### Typography[](#typography "Direct link to Typography")
| Token | Default | What it controls |
| ---------------- | ----------------------- | ---------------------------------------------------------- |
| **Body font** | `system-ui, sans-serif` | The default font family for component text. |
| **Heading font** | *(same as body)* | The font family for headings; falls back to the body font. |
| **Base size** | `1rem` | The base font size components scale from. |
Font fields accept any valid CSS `font-family` value (for example, `Georgia, serif`). As you type, a list of common font suggestions appears — the suggestions are a convenience, not a limit, so you can enter any font your site already loads.
note
Branding does not host or load web fonts for you. Enter fonts that are already available on the pages where your components appear.
##### Shape & spacing[](#shape--spacing "Direct link to Shape & spacing")
| Token | Default | What it controls |
| ----------------- | ---------- | ----------------------------------------------------- |
| **Corner radius** | `0.375rem` | How rounded buttons, inputs, and containers are. |
| **Spacing unit** | `1rem` | The base spacing components use for gaps and padding. |
***
#### Component-specific variables[](#component-specific-variables "Direct link to Component-specific variables")
Switch the scope to a component (for example, **Login form**) to fine-tune its individual parts. Variables are organized into collapsible groups by the part of the component they affect — the login form, for instance, groups its variables into **General**, **Email input**, **Submit button**, **One-time code**, **Social buttons**, **Divider**, **Errors**, **State screens**, **Resend link**, and **Loading & disabled**.
Each group header shows either how many variables it contains or, once you've customized some, how many are **overridden**. A **filter** box at the top of the scope lets you search variables by name.
##### Inheriting vs. overriding[](#inheriting-vs-overriding "Direct link to Inheriting vs. overriding")
Every component variable is in one of two states:
* **Inheriting** — the default. The row shows a label like **Inherits Primary**, meaning it takes its value from that global token. You don't have to do anything; if you change the global token later, this variable follows along.
* **Overridden** — you clicked **Override** and gave it a specific value. It now ignores the global token for this one spot. Click **Reset** to return it to inheriting.
When you click **Override**, the field is pre-filled with the value it was already inheriting, so you start from what's currently showing rather than from a blank field.
Not every variable inherits a global token. Structural, one-off settings (such as a button's padding or letter-spacing) show **Component default** instead of an "Inherits" label — there's no brand-wide token that would make sense for them, so they simply use the component's built-in value until you override them.
##### A worked example[](#a-worked-example "Direct link to A worked example")
Suppose you set the global **Primary** color to your brand's teal. Because they all inherit **Primary** by default, the following update at once:
* the login form's **Submit button** background,
* the email form's **Button** background, and
* the content gate's action **Button** background.
Now suppose you want the email form's button to be a slightly darker teal than everything else. Switch to the **Email form** scope, open the **Button** group, click **Override** on **Background**, and enter the darker value. Only the email form button changes — the login form and content gate buttons still follow the global **Primary** token.
This is the whole point of the cascade: broad changes are one edit, and exceptions stay local.
tip
If you want a brand-wide change, edit a **global token** — not each component. Reach for a component override only when one specific spot needs to differ.
***
#### The components you can brand[](#the-components-you-can-brand "Direct link to The components you can brand")
| Scope | Component | Where it appears |
| ----------------------- | ------------------------------- | ------------------------------------------------------------ |
| **Login form** | `` | Member sign-in, including one-time codes and social buttons. |
| **Email form** | `` | Email capture / newsletter sign-up. |
| **Form** | `` | Admin-composed profile forms members fill out. |
| **Content gate** | `` | The modal gate that blocks content until a member acts. |
| **Inline content gate** | `` | The inline (non-modal) variant of the content gate. |
| **Modal** | `` | A centered dialog, like a welcome note, that readers close. |
| **Bottom drawer** | `` | A bottom panel that sits over the page without blocking it. |
For the full list of variables each component exposes, see the developer [Web Components](/developer/components.md) reference.
***
#### The OAuth page[](#the-oauth-page "Direct link to The OAuth page")
The **OAuth page** scope is different from the four above: it brands a page rather than a component. Your hosted sign-in page (and its error page) lives on your Allegro domain, so there is no host site whose CSS your developers can adjust. Everything about how that page looks is therefore controlled here.
The scope is organized into seven groups:
| Group | What it controls |
| ------------------- | ------------------------------------------------------------------------------------------------------------- |
| **Page** | Page background, font family, base font size, and the two text colors. |
| **Panel** | The card the form sits in: background, border color and width, corner radius, padding, max width, and shadow. |
| **Logo** | Maximum height of your logo and the space beneath it. |
| **Continue button** | Background, text color, corner radius, padding, font size, weight, and text transform. |
| **Secondary link** | The "Use a different account" link: color, hover color, and font size. |
| **Header text** | Text color, font family, font size, and link color for your header copy. |
| **Footer text** | The same four controls for your footer copy. |
Header and footer copy is edited elsewhere
The **Header text** and **Footer text** groups control only how that copy *looks*. The words themselves live in **Organization Settings › Sign-in page**, because they are content rather than design. See [Sign-in page](/product/administration/sign-in-page.md).
Every variable falls back to the matching global token, so a page you never touch still follows your brand. Two are worth calling out:
* **Page background** does not inherit the global **Background** token. That token is the panel's surface, so sharing it would flatten the panel into the page. Page background defaults to a light grey instead.
* The sign-in form inside the page is styled by the **Login form** scope, not this one.
***
#### Logo[](#logo "Direct link to Logo")
In the **Global** scope, upload your logo from the **Logo** field.
* Accepted formats: **SVG, PNG, WebP, or JPEG**, up to **2 MB**.
* Add **Alt text** describing the logo for screen readers. Alt text is applied when the logo is uploaded or replaced.
* Once a logo is set, use **Replace** to swap it or **Remove** to clear it.
Your logo appears on your organization's hosted **OAuth sign-in page** (and the matching error page), and is made available to the SDK for use in templates.
note
Uploaded SVG files are sanitized on the server — scripts and remote references are stripped — so an SVG logo is safe to display.
***
#### Live preview[](#live-preview "Direct link to Live preview")
The preview panel on the right renders the actual components with your in-progress values, updating as you type. It follows the scope switcher, so selecting **Login form** previews the login form, and so on. The **Global** scope also previews a representative sign-in card so you can judge your tokens against a realistic layout. Nothing in the preview is saved until you click **Save**.
Two scopes offer extra preview controls:
* **Content gate** lets you switch the gate between its **bottom** and **center** positions.
* **OAuth page** lets you switch between the three states the page can be in: the **sign-in form**, the **continue as** prompt shown to a member who is already signed in, and the **error** page. The preview uses the same stylesheet the real page uses, so what you see is what gets served.
***
#### Saving and revisions[](#saving-and-revisions "Direct link to Saving and revisions")
Click **Save** to publish your branding. You can add an optional **Change note** describing what changed; it's recorded with the revision.
Every save creates a **revision**. Open the **Version History** panel (from the button beside **Save**) to:
* **Browse** past revisions, with the author and time of each.
* **Compare** a revision against the one before it to see exactly what changed.
* **Roll back** to any earlier revision in one click, which immediately restores that version everywhere.
***
#### Where branding appears[](#where-branding-appears "Direct link to Where branding appears")
Your branding is applied to the Allegro components embedded on your own site, and to your hosted OAuth sign-in and error pages. It is delivered as a small stylesheet that is cached for performance, so a change may take a few minutes to appear on live pages.
Crucially, branding only sets **defaults**. Your site's own stylesheet, and any CSS inside an interaction template, always takes precedence over branding — so branding will never fight with, or override, the styles you ship yourself. A tenant that hasn't set any branding is served nothing extra at all.
***
#### Per-property branding[](#per-property-branding "Direct link to Per-property branding")
If your organization runs multiple [properties](/product/administration/properties.md), each property can override this organization-wide branding with its own logo, tokens, and component variables. Overrides are per value: anything a property leaves unset inherits the organization's branding, so a change you make here still flows through to every property automatically unless that property has overridden the specific value. Each property serves its own merged stylesheet on its own web address.
See [Properties → Branding](/product/administration/properties.md#branding) for how to edit a property's branding.
***
#### Related Links[](#related-links "Direct link to Related Links")
* [Settings](/product/administration/settings.md) — other organization-wide configuration
* [Properties](/product/administration/properties.md) — override branding per property
* [Permissions](/product/administration/permissions.md) — who can edit organization settings
* [Web Components](/developer/components.md) — the full CSS variable reference for each component
---
### Captcha
Allegro's sign-in is passwordless, so there is no password for an attacker to guess. The thing worth protecting is different: every sign-in request creates an audience member if one does not exist and sends an email. A script rotating through addresses can fill your audience with junk records and bill you for the mail, and heavy bounce or complaint rates from those sends put email delivery at risk for everyone on the sending domain.
Turning on captcha makes each sign-in attempt prove it came from a person before any of that happens.
**Organization Settings › Access & Identity › Captcha** is where you switch it on. It is off until you configure it, and an organization that never visits this screen behaves exactly as it always has.
#### What readers see[](#what-readers-see "Direct link to What readers see")
Nothing. Google reCAPTCHA v3 scores each attempt in the background rather than showing a puzzle or a checkbox. Google does add a small badge to the corner of pages where the login form appears; its terms require you either to leave that badge in place or to display a short attribution line near your sign-in button instead.
#### Setting it up[](#setting-it-up "Direct link to Setting it up")
You bring your own reCAPTCHA keys rather than using a shared Allegro account. That is deliberate: reCAPTCHA's free tier is capped per Google account across every site using it, so a shared key would run out for everyone at once.
1. In the [reCAPTCHA admin console](https://www.google.com/recaptcha/admin), create a key of type **reCAPTCHA v3**.
2. Register every domain your login form appears on. A domain you forget will fail verification for readers on that domain.
3. Copy the **Site key** and **Secret key** into the Captcha settings screen.
4. Set **Provider** to Google reCAPTCHA v3 and save.
The secret key is encrypted and never sent to a browser. The site key is public by design — it appears in the page source, which is normal and safe.
#### Choosing a minimum score[](#choosing-a-minimum-score "Direct link to Choosing a minimum score")
reCAPTCHA returns a score from 0.0 to 1.0, where lower means more bot-like. The **Minimum score** slider sets the point below which a sign-in attempt is rejected.
| Score | Meaning |
| ----- | -------------------------------------------------------- |
| 0.0 | Almost certainly automated. |
| 0.5 | Google's suggested starting point, and the default here. |
| 1.0 | Almost certainly a person. |
Raise it only after watching real traffic. This is a genuine trade-off rather than a dial that goes from "less safe" to "more safe": every legitimate reader who scores below your threshold is turned away, sees only a generic error, and has no way to prove otherwise. A threshold that is too high looks to you like a quiet drop in sign-ups, not like an error.
#### When Google is unreachable[](#when-google-is-unreachable "Direct link to When Google is unreachable")
If reCAPTCHA cannot be reached, or your credentials are rejected, sign-in requests are allowed through and the failure is logged. A Google outage or a mistyped secret key must never lock readers out of your site, and during one you are back to the protection you had before turning captcha on.
A reader whose browser blocks Google's script — an ad blocker, a privacy extension, or a strict corporate network — is a different case. That attempt is rejected, because a sign-in that can simply decline to be checked would make the whole feature pointless. This is the main cost of switching captcha on, and it is worth weighing against how much automated traffic you are actually seeing.
#### Properties[](#properties "Direct link to Properties")
A property can use the organization's setup, supply its own key pair, or turn captcha off for itself. Credentials are all-or-nothing per property: a property either provides every field or inherits every field, so its site key can never be paired with the organization's secret.
One thing to watch: a property-level selection covers that property's domains only. If the organization itself has no provider selected, sign-in requests to your main Allegro domain stay unprotected even while a property is covered. Select a provider at the organization level to cover everything.
#### Turning it off[](#turning-it-off "Direct link to Turning it off")
Set **Provider** to Off. Your keys stay saved, so switching back on later does not mean fetching them from Google again.
---
### Connected apps
AI clients such as Claude, Cursor, and VS Code can connect to Allegro's MCP server by having you sign in, instead of using an API key. Each one you approve appears under **Settings → Connected apps**, so you can see what can act as you and cut it off at any time.
#### Connect a client[](#connect-a-client "Direct link to Connect a client")
1. In your AI client, add your organization's MCP URL, `https://acme.allegrocdp.com/mcp`, as a remote MCP server.
2. When the client opens Allegro in your browser, sign in to the dashboard.
3. Check the consent screen: the client's name, the organization, and the website you'll be sent back to. Only continue if you recognize them.
4. Leave **Change data** unticked for a read-only connection, or tick it to let the client create and update records. Only admins see this option.
5. Click **Allow**.
#### What the list shows[](#what-the-list-shows "Direct link to What the list shows")
Each connected client shows:
| Detail | Meaning |
| ------------------- | -------------------------------------------------------------------------------------- |
| Name | The name the client gave itself when it connected |
| Organization | The organization the client can reach, or **Allegro platform** for the landlord server |
| **Read**, **Write** | Whether the client can only read data, or also change it |
| Connected | When you first approved it |
| Last used | The last time it made a request |
#### Disconnect a client[](#disconnect-a-client "Direct link to Disconnect a client")
Click **Disconnect** next to the client and confirm. It loses access straight away; you can connect it again later by signing in from the client.
Allegro also disconnects clients for you when you're removed from an organization, when you lose super admin access, or when your account is deleted.
#### Related[](#related "Direct link to Related")
* [OAuth clients](/product/administration/oauth-clients.md) — Partner integrations that sign your *members* in, which are separate from the apps you connect for yourself
---
### Geolocation
Every visitor's country is available for targeting at no cost, along with their continent and the privacy laws decided by country. Anything more precise than the country, such as a visitor's state, city, or ZIP / postal code, comes from an IP geolocation lookup. Allegro doesn't run those lookups for you. You connect an account with one of the supported providers, and lookups are billed to that account.
**Organization Settings › Geolocation** is where you connect a provider. Until you do, your organization targets by country only.
#### What a provider unlocks[](#what-a-provider-unlocks "Direct link to What a provider unlocks")
| Condition | Without a provider | With a provider |
| --------------------- | ---------------------------------------------- | --------------- |
| **Country** | Available | Available |
| **Continent** | Available | Available |
| **Privacy law** | Every law except CCPA, which is shown disabled | Every law |
| **Region** | Shown disabled | Available |
| **City** | Shown disabled | Available |
| **ZIP / postal code** | Shown disabled | Available |
CCPA needs a provider because it applies to visitors in California, so it depends on the visitor's state, not only their country. Disabled conditions and options are marked **Needs geo provider** in the condition picker.
Journeys you built before removing a provider keep their Region, City, ZIP / postal code, and CCPA conditions, and you can still edit or remove them, but they don't match until you connect a provider again.
#### Supported providers[](#supported-providers "Direct link to Supported providers")
Allegro supports the providers below because each one reports a visitor's state or province as a standard code (`CA`, `ON`), which is how Region conditions are stored.
| Provider | Credentials | Get credentials |
| ---------------------------------------------------------------------- | -------------------------- | ----------------------------------------------------------------------- |
| [ipdata](https://ipdata.co/) | API key | [ipdata documentation](https://docs.ipdata.co/) |
| [MaxMind GeoIP2](https://www.maxmind.com/en/geoip2-precision-services) | Account ID and license key | [MaxMind web services](https://dev.maxmind.com/geoip/docs/web-services) |
| [IP-API Pro](https://members.ip-api.com/) | API key | [IP-API Pro documentation](https://members.ip-api.com/docs) |
| [ipapi.co](https://ipapi.co/) | API key | [ipapi.co documentation](https://ipapi.co/api/) |
Check the provider's terms before you choose a plan. Several providers prohibit commercial use on their free tiers.
#### Connecting a provider[](#connecting-a-provider "Direct link to Connecting a provider")
1. Sign up with one of the providers above and copy your credentials.
2. Go to **Organization Settings › Geolocation**.
3. Choose the provider under **Provider** and paste in its credentials.
4. Click **Save changes**.
Allegro makes one test lookup when you save. If the provider rejects the credentials or your plan doesn't include the API, you see an error and nothing is saved.
Credentials are encrypted and never shown again. To keep a saved credential while changing something else, leave its field blank.
To switch providers, choose the new one and enter its credentials. Allegro removes the previous provider's credentials when you save.
#### Disconnecting[](#disconnecting "Direct link to Disconnecting")
Set **Provider** to **None (country only)** and save. Allegro deletes the stored credentials and goes back to country-only targeting.
#### What lookups cost[](#what-lookups-cost "Direct link to What lookups cost")
A lookup runs only when a journey reaches a Region, City, ZIP / postal code, or CCPA condition and the visitor's location isn't already known. Journeys that only check the country never trigger one. Each visitor's result is reused for a day, so a visitor counts as at most one lookup per day no matter how many pages they view or how many conditions use their location.
---
### OAuth clients
An **OAuth client** is a registration for one outside integration that signs your members in through Allegro. A partner's mobile app, a companion site, a community platform, a member portal your team runs separately — each gets its own client, its own credentials, and its own allowlists.
Once a client is registered, the integration can send a member to your hosted sign-in page and, when the member signs in, receive a token that acts as that member. Nothing else on your side needs to change: the member's session, profile, and entitlements are the same ones the rest of Allegro sees.
Manage clients from **Organization Settings → OAuth Clients**.
OAuth needs a custom domain
Members can only sign in through OAuth on a custom domain, such as `members.acme.com`. If neither your organization nor any of your properties has one, the OAuth Clients page says so, and your clients won't work until a domain is set up. Add one from a property's settings, or ask an Allegro admin to set one for your organization.
#### Register a client[](#register-a-client "Direct link to Register a client")
1. Go to **Organization Settings → OAuth Clients** and click **Create client**.
2. Give it a **Name** that identifies the integration, not the person who asked for it. The name appears on your sign-in page, so members will read it.
3. Add each **Redirect URI** the integration will send members back to.
4. Add each **Allowed origin**, if the integration needs to detect existing sessions. Leave this empty otherwise.
5. Leave **Require PKCE** on unless the integration tells you it cannot support it.
6. Click **Create client**.
Allegro then shows the **Client ID** and the **Client secret**. Send both to the integration through a channel you trust.
The secret is shown once
The client secret is displayed only on the screen that follows creation. Allegro stores a hash of it and cannot show it again. If it is lost, use **Regenerate secret** — which invalidates the old one and breaks the integration until they deploy the new value.
#### What each field controls[](#what-each-field-controls "Direct link to What each field controls")
| Field | What it does |
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| **Name** | Identifies the client in this list, in the [Activity Log](/product/administration/activity-log.md), and on the sign-in page. |
| **Redirect URIs** | The only URLs Allegro will send a member back to after they sign in. |
| **Allowed origins** | The only websites allowed to ask Allegro whether a visitor is already signed in. |
| **Require PKCE** | Forces the integration to prove, at the end of the flow, that it started the flow. |
| **Active** | Turns the client off without deleting it. An inactive client cannot start a sign-in. |
##### Redirect URIs[](#redirect-uris "Direct link to Redirect URIs")
A redirect URI is where a member's browser goes once they have signed in, and it carries a one-time code the integration exchanges for a token. Allegro will only use a URI on this list, which is what stops someone from crafting a sign-in link that delivers a member's code somewhere you have not approved.
URIs are matched **exactly**. `https://app.acme.com/callback` and `https://app.acme.com/callback/` are two different values, and a query string that differs by one character will not match. Ask the integration for the precise URL rather than deriving it, and add every environment they need. HTTPS is required.
##### Allowed origins[](#allowed-origins "Direct link to Allowed origins")
Allowed origins are for a different job than redirect URIs. They control which websites may call Allegro to ask *"does this visitor already have a session?"* — so the integration can greet a returning member instead of showing a sign-in prompt they do not need. If the integration is not doing that, leave this list empty; a client with no allowed origins cannot make that request at all.
An origin is a scheme, a host, and an optional port, with nothing after it:
```text
https://community.acme.com
```
Origins are matched **exactly**, and a wildcard cannot be entered here at all. Add each site individually — one entry per community, property, or subdomain that needs it. A parent domain does not cover the hosts beneath it.
Never try to cover a family of subdomains
This request reveals whether a particular person is signed in. If Allegro matched `https://*.acme.com`, then anyone who took over any one unused subdomain could ask that question about your members. Exact matching means a compromised host has to already be on your list.
The same reasoning is why this list is separate from the CORS allowed origins in [Browser Settings](/developer/administration/browser-settings.md), which do accept wildcards.
##### Require PKCE[](#require-pkce "Direct link to Require PKCE")
PKCE is an extra check that ties the end of a sign-in to the beginning of it, so a stolen one-time code cannot be redeemed by whoever stole it. Leave it on.
Turn it off only when an integration tells you it cannot generate the value — typically an older server-side integration that authenticates with its client secret alone. Turning it off weakens that one client and nothing else.
#### Managing an existing client[](#managing-an-existing-client "Direct link to Managing an existing client")
Open a client from the list to rename it, change its allowlists, toggle **Require PKCE** or **Active**, regenerate its secret, or delete it.
| Action | Effect |
| --------------------- | ----------------------------------------------------------------------------------------------- |
| **Save** | Applies immediately. A removed redirect URI or origin stops working on the next request. |
| **Regenerate secret** | Invalidates the current secret at once. The integration is broken until it deploys the new one. |
| **Active** off | The client can no longer start a sign-in. Tokens it already holds are unaffected. |
| **Delete** | Permanent. Use **Active** instead when you may want the integration back. |
Changes to a client are recorded in the [Activity Log](/product/administration/activity-log.md) under the **Security** category, including the before and after values of the redirect URI list. Secret rotations are logged as an event, but the secret itself is never recorded.
#### Related[](#related "Direct link to Related")
* [Sign-in page](/product/administration/sign-in-page.md) — Header and footer copy on the page members see
* [Branding](/product/administration/branding.md) — The look of the sign-in page
* [Activity Log](/product/administration/activity-log.md) — The record of client changes
* [OAuth 2.0](/developer/guides/oauth.md) — The developer guide to send your integration partner
---
### Permissions
Every user in your organization has a role — **Admin**, **Member**, or **Viewer**. Each role determines what that person can see and do across Allegro.
For details on managing users and assigning roles, see [Users](/product/administration/users.md).
#### Role overview[](#role-overview "Direct link to Role overview")
| Role | Best for |
| ---------- | -------------------------------------------------------------------------------- |
| **Admin** | People who manage the organization, invite users, and configure settings. |
| **Member** | Content creators who build templates, interactions, and manage audience members. |
| **Viewer** | Stakeholders who need read-only access to review content and data. |
#### Templates[](#templates "Direct link to Templates")
| Action | Viewer | Member | Admin |
| ---------------------------- | ------ | ------ | ----- |
| View templates | ✅ | ✅ | ✅ |
| Create templates | ❌ | ✅ | ✅ |
| Edit templates | ❌ | ✅ | ✅ |
| Archive templates | ❌ | ✅ | ✅ |
| Restore archived templates | ❌ | ✅ | ✅ |
| Permanently delete templates | ❌ | ❌ | ✅ |
#### Interactions[](#interactions "Direct link to Interactions")
| Action | Viewer | Member | Admin |
| ------------------------------- | ------ | ------ | ----- |
| View interactions | ✅ | ✅ | ✅ |
| Create interactions | ❌ | ✅ | ✅ |
| Edit interactions | ❌ | ✅ | ✅ |
| Publish / unpublish | ❌ | ✅ | ✅ |
| Archive interactions | ❌ | ✅ | ✅ |
| Restore archived interactions | ❌ | ✅ | ✅ |
| Permanently delete interactions | ❌ | ❌ | ✅ |
#### Segments[](#segments "Direct link to Segments")
| Action | Viewer | Member | Admin |
| -------------------------------------------- | ------ | ------ | ----- |
| View segments, counts, and sample members | ✅ | ✅ | ✅ |
| Export segment members | ✅ | ✅ | ✅ |
| Create, edit, duplicate, and delete segments | ❌ | ✅ | ✅ |
#### Email Templates[](#email-templates "Direct link to Email Templates")
| Action | Viewer | Member | Admin |
| ----------------------- | ------ | ------ | ----- |
| View email templates | ✅ | ✅ | ✅ |
| Edit and save templates | ❌ | ✅ | ✅ |
| Send test emails | ❌ | ✅ | ✅ |
| Reset to default | ❌ | ✅ | ✅ |
#### Users[](#users "Direct link to Users")
| Action | Viewer | Member | Admin |
| -------------------- | ------ | ------ | ----- |
| View users | ✅ | ✅ | ✅ |
| Invite and add users | ❌ | ❌ | ✅ |
| Change user roles | ❌ | ❌ | ✅ |
| Remove users | ❌ | ❌ | ✅ |
#### Organization Settings[](#organization-settings "Direct link to Organization Settings")
| Action | Viewer | Member | Admin |
| ---------------------------- | ------ | ------ | ----- |
| View settings | ✅ | ✅ | ✅ |
| Update organization settings | ❌ | ❌ | ✅ |
| Configure login providers | ❌ | ❌ | ✅ |
| Manage packages | ❌ | ❌ | ✅ |
| View the Activity Log | ❌ | ❌ | ✅ |
#### Audience[](#audience "Direct link to Audience")
| Action | Viewer | Member | Admin |
| ----------------------------- | ------ | ------ | ----- |
| View audience members | ✅ | ✅ | ✅ |
| Edit audience members | ❌ | ✅ | ✅ |
| Delete audience members | ❌ | ✅ | ✅ |
| Grant and revoke entitlements | ❌ | ✅ | ✅ |
---
### Properties
Properties let a single organization operate more than one property from one Allegro account. A media company might run two publications as separate properties under one organization; a publisher might separate a flagship publication from a newsletter product. Each property has its own web address and its own browser and email configuration, while sharing the organization's members, entitlements, templates, and reporting.
You do not need properties to use Allegro. If your organization is a single brand, the organization-level settings are all you need. Add properties only when you have distinct properties that must present a different domain or send email under a different identity.
#### How Properties Relate to Your Organization[](#how-properties-relate-to-your-organization "Direct link to How Properties Relate to Your Organization")
A property is scoped to your organization and inherits everything the organization defines. It layers a small number of property-specific overrides on top:
| Area | Behavior with properties |
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Members, entitlements, purchases | Shared across the whole organization — a member is the same person on every property. |
| Templates and interactions | Shared across the whole organization. |
| Reporting and analytics | Shared across the whole organization. |
| Web address | Each property has its own subdomain, and optionally its own custom domain. |
| CORS allowed origins | The property **adds** its origins on top of the organization's — it never removes them. |
| Cookie domain | The property can **replace** the organization's, or inherit it when left blank. |
| Email sender (name & address) | The property can **override** the name and address email is sent from, or inherit the organization's when left blank. |
| Email templates | Resolved per property, then falling back to the organization, then the built-in default. |
| Branding | Each token, component variable, and the logo can be overridden per property, inheriting the organization's value otherwise. |
| Login providers | A property can override any provider with its own OAuth credentials, reorder them, or hide one the organization has configured — inheriting the organization's setup otherwise. |
#### The Properties List[](#the-properties-list "Direct link to The Properties List")
Go to **Organization Settings → Properties** to see every property in your organization. The list shows each property's name, slug, and custom domain (if one is set).
Who can manage properties
Managing properties requires the same permission as managing the organization. Anyone who can edit organization settings can create, configure, and delete properties. See [Permissions](/product/administration/permissions.md).
#### Creating a Property[](#creating-a-property "Direct link to Creating a Property")
Click **New Property** from the Properties list and provide:
| Field | Description |
| ----------------- | -------------------------------------------------------------------------------------------------------------------- |
| **Name** | The display name shown throughout the dashboard. You can change this later. |
| **Slug** | The permanent identifier used in the property's web address. |
| **Custom Domain** | *(Optional)* Choose from the domains already added to your organization. Leave unset to use the default web address. |
Choosing a domain
Custom Domain is a list, not free text — it only offers domains already added to your organization and not already assigned elsewhere. Adding a new domain is not a self-service action from Property Settings; if the domain you need isn't listed, ask your platform administrator to add it.
##### Slug Rules[](#slug-rules "Direct link to Slug Rules")
The slug becomes part of the property's web address, so it has strict formatting rules:
* Lowercase letters, numbers, and single hyphens only.
* **No consecutive hyphens.** A double hyphen (`--`) is reserved as the separator between your organization and the property in the web address.
* Up to 63 characters.
* Must be unique within your organization.
Slugs are permanent
A property's slug **cannot be changed after the property is created**. It is baked into the property's web address and its saved configuration. Choose it carefully. To use a different slug, delete the property and create a new one.
Once created, you are taken to the property's **General** settings, where the rest of its configuration lives.
#### Property Settings[](#property-settings "Direct link to Property Settings")
Each property has its own settings area, reached by clicking a property in the Properties list. The left navigation has six sections: **General**, **Branding**, **Browser Settings**, **Login Providers**, **Email Settings**, and **Email Templates**.
##### General[](#general "Direct link to General")
The General page shows the property's name, its (read-only) slug, and its custom domain.
| Field | Description |
| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Name** | Editable display name. |
| **Slug** | Read-only. Set at creation and permanent. |
| **Custom Domain** | The domain that serves this property's audience-facing traffic, chosen from the domains available to your organization. Leave unset to use the default subdomain. |
A custom domain is provisioned by your platform administrator before it can be selected here — this page only lets you assign an already-available domain to this property, not add a new one. DNS and certificate setup are handled when the domain is added to your organization; this page never asks you to configure DNS. For how custom domains are served and routed, see the developer guide on [Properties](/developer/guides/properties.md).
This field must stay valid to save the page
Every setting on this page saves together. If this property's custom domain is ever no longer available to your organization (for example, it was removed from your organization's inventory), you cannot save **any** change here — not just the domain — until you select a currently available domain or clear the field. Ask your platform administrator to re-add the domain if you need to keep using it.
##### Branding[](#branding "Direct link to Branding")
The Branding page lets a property override the organization's branding — its logo, global design tokens, and per-component variables — so the Allegro components embedded on that property's pages can carry a distinct visual identity. It uses the same editor as organization branding, with one difference: anything a property leaves unset **inherits** the organization's value.
Inheritance works per value:
* Every design token and component variable falls through to the organization's value (and then the built-in default) until you give it a property-specific value. Inherited values appear as placeholders, and a small dot marks the ones you have overridden for this property.
* The **logo** is atomic: a property logo replaces the organization's entirely when set, and removing it falls back to the organization's logo.
* Clearing a value returns it to inheriting, so an organization rebrand still flows through to a property automatically for anything the property hasn't overridden.
The live preview renders the true merged result — the property's overrides layered on top of the organization's branding — and each property's branding keeps its own revision history, so you can browse, compare, and roll back a property's branding independently of the organization's.
For how the branding design system, inheritance, and revisions work in general, see [Branding](/product/administration/branding.md).
##### Browser Settings[](#browser-settings "Direct link to Browser Settings")
The Browser Settings page controls how this property's pages interact with the Allegro SDK in a member's browser. These mirror the organization's browser settings but apply only to this property.
| Setting | Description |
| ------------------------ | --------------------------------------------------------------------- |
| **CORS Allowed Origins** | The web origins allowed to call this property's API from the browser. |
| **Cookie Domain** | The domain the session cookie is scoped to for this property. |
###### CORS Allowed Origins[](#cors-allowed-origins "Direct link to CORS Allowed Origins")
Each origin is a full origin such as `https://acme.com`. Wildcard subdomain origins are supported in the form `https://*.acme.com`.
The property's origins are **added on top of** the organization's allowed origins — configuring them here never removes origins the organization already allows. The property's own web address is always allowed automatically.
###### Cookie Domain[](#cookie-domain "Direct link to Cookie Domain")
Set a cookie domain (for example `.acme.com`) so member sessions are shared across that property's subdomains. If you leave this blank, the property **inherits the organization's cookie domain**. Setting a value here replaces the organization's value for this property only.
##### Login Providers[](#login-providers "Direct link to Login Providers")
The Login Providers page controls how audience members sign in on this property — which OAuth providers appear, in what order, and whether each one uses the organization's OAuth app or the property's own. Everything inherits the organization's [Login Providers](/product/administration/settings.md#login-providers) configuration until you override it here.
Each provider is a card with its own controls:
| Control | What it does |
| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Show on this property** | Turn a provider off to hide it on this property's login form, even when the organization has it configured. |
| **Credentials** | Choose **Organization's** to use the organization's OAuth app (the default), or **This property's** to authenticate with an OAuth app for this property. |
When a provider uses **This property's** credentials, every credential field comes from the property — nothing is inherited — and the page shows a **Redirect URL** to register as the authorized redirect URI on that property's OAuth app. Because a property is served from its own host, this URL differs from the organization's, so a property with its own OAuth app needs its own redirect URI registered with the provider.
A provider's badge reflects its state on this property: **Enabled**, **Not configured**, **Turned off**, or **Own credentials**. To change the order providers appear on the login form, drag them into place in the list at the top of the page.
Inheriting an unconfigured provider
If you leave a provider set to the organization's credentials but the organization has not configured it, the provider stays unavailable on this property until you supply the property's own credentials.
##### Email Settings[](#email-settings "Direct link to Email Settings")
The Email Settings page controls the **sender identity** for email sent through this property — the name and address it comes from. This lets each property send under its own identity while still using the organization's email delivery configuration.
| Field | Description |
| ---------------- | --------------------------------------------------------- |
| **From Name** | The display name recipients see (e.g. `Acme News`). |
| **From Address** | The address email is sent from (e.g. `noreply@acme.com`). |
Leave a field **blank to inherit** the organization's value — when it inherits, the page shows the organization value it is falling back to. The resolution is the familiar three tiers: **property → organization → built-in default**.
Click **Send test email** to send a test message to your own email address using this property's **saved** sender settings, so you can confirm the identity looks right before real email goes out. Save any changes first — the test uses the saved values, and a provider rejection (such as an unverified From Address) is reported to you directly.
Where sender credentials live
Only the sender **name and address** are set per property. The underlying email delivery credentials (AWS SES) remain an organization-wide setting — see [Settings → Email](/product/administration/settings.md#email-aws-ses).
##### Email Templates[](#email-templates "Direct link to Email Templates")
Each property can customize the transactional emails Allegro sends — such as magic sign-in links — so they match that property's brand. The Email Templates page lists every email type and shows where its current content comes from:
| Source | Meaning |
| ---------------- | ------------------------------------------------------------------------------------------------- |
| **Property** | This property has its own customized template. |
| **Organization** | No property override; the organization's template is used. |
| **Default** | Neither the property nor the organization has customized it; Allegro's built-in template is used. |
When you edit a template, the editor shows the inherited content (the organization's version, or the built-in default) alongside your property-specific version so you can see what you are overriding.
* **Send Test** sends a preview of the property's rendered template to your own email address.
* **Reset** removes the property override. The template then falls back to the organization's version, or the built-in default if the organization has not customized it either.
This three-tier resolution — **property → organization → default** — means you only need to override the emails that differ for a given property. Everything else inherits automatically.
For the organization-wide equivalent of this page, see [Email Templates](/product/email-templates.md).
#### Deleting a Property[](#deleting-a-property "Direct link to Deleting a Property")
Delete a property from its settings. Deleting a property removes its property-specific configuration — its browser settings, email template overrides, and custom domain mapping. It does **not** affect members, entitlements, or templates, which belong to the organization.
Deletion is permanent
Deleting a property permanently removes its configuration and frees its slug and custom domain. Any embed code pointing at the deleted property's web address will stop resolving.
#### Related Links[](#related-links "Direct link to Related Links")
* [Properties (Developer)](/developer/guides/properties.md) — host format, custom domains, and how the SDK targets a property
* [Settings](/product/administration/settings.md) — organization-wide configuration
* [Email Templates](/product/email-templates.md) — organization-wide transactional email
* [Permissions](/product/administration/permissions.md) — who can manage properties
---
### Settings
Settings is where you configure how your organization works and customize your own account. There are two areas: **Organization Settings** control tenant-wide behavior, while **Personal Settings** apply only to you.

***
#### Organization Settings[](#organization-settings "Direct link to Organization Settings")
Organization settings are available to administrators. They control how the tenant operates for everyone.
##### General[](#general "Direct link to General")
Go to **Organization Settings → General** to update your organization's basic information.
| Field | Description |
| --------------------- | ----------------------------------------------- |
| **Organization Name** | The name shown throughout the application. |
| **Timezone** | The timezone used for scheduling and reporting. |
Click **Save** to apply changes.
***
##### Email (AWS SES)[](#email-aws-ses "Direct link to Email (AWS SES)")
Go to **Organization Settings → Email** to configure AWS Simple Email Service (SES). Allegro uses SES to send transactional email on behalf of your organization.
| Field | Description |
| --------------------- | ----------------------------------------------------------------------- |
| **Access Key ID** | Your AWS IAM access key ID with SES send permissions. |
| **Secret Access Key** | The corresponding AWS secret access key. |
| **Region** | The AWS region where your SES service is configured (e.g. `us-east-1`). |
If a credential is already saved, the field shows a masked placeholder (`••••••••`). Enter a new value to replace it.
info
Allegro stores your SES credentials encrypted. You can update them at any time by entering new values and saving.
tip
Before saving, make sure your IAM user or role has the `ses:SendEmail` and `ses:SendRawEmail` permissions in the configured region.
###### Sender identity[](#sender-identity "Direct link to Sender identity")
The same page controls the name and address your organization's email is sent **from**:
| Field | Description |
| ---------------- | --------------------------------------------------------------------------------------------- |
| **From Name** | The display name recipients see in their inbox (e.g. `Acme News`). |
| **From Address** | The address email is sent from (e.g. `noreply@acme.com`). It must be verified in SES to send. |
Individual [properties](/product/administration/properties.md#email-settings) can override this sender identity so each property sends under its own name and address.
###### Send a test email[](#send-a-test-email "Direct link to Send a test email")
Click **Send test email** to send a short test message to your own email address using the **saved** settings, so you can confirm delivery works end to end. If you have unsaved changes, save them first — the test always uses the saved values.
If the mail provider rejects the message — for example, because the From Address is not verified in SES — the error is shown to you directly rather than failing silently in the background.
***
##### Login Providers[](#login-providers "Direct link to Login Providers")
Go to **Organization Settings → Login Providers** to set up OAuth login for your audience members. Each provider shows whether it is **Enabled** or **Not configured**.
Supported providers include **Google**, **Apple**, **Facebook**, and **GitHub**.
###### Setting Up a Provider[](#setting-up-a-provider "Direct link to Setting Up a Provider")
Each provider requires a **Client ID** and **Client Secret** (and sometimes additional fields). To configure one:
1. Create an OAuth application in the provider's developer console.
2. Copy the **Redirect URL** shown on the Login Providers page and add it as an authorized redirect URI in your OAuth app.
3. Enter the credentials into the corresponding fields and click **Save**.
Once saved, the provider badge changes to **Enabled** and audience members can sign in with that provider.
Per-property overrides
Organizations that run multiple [properties](/product/administration/properties.md) can override these providers per property — using a property's own OAuth app, reordering them, or hiding one on a specific property. See [Properties → Login Providers](/product/administration/properties.md#login-providers).
note
Encrypted fields such as Client Secret show a masked placeholder once saved. To update a secret, enter a new value. To remove a saved secret, use the **remove it** link below the field.
###### Apple Sign In[](#apple-sign-in "Direct link to Apple Sign In")
Apple Sign In requires a few extra steps compared to other providers:
* **Client ID** — Your Apple Services ID (e.g. `com.example.allegro`). This is the Service ID you create in the Apple Developer portal, not your App ID.
* **Client Secret** — A signed JWT you generate using a private key downloaded from Apple. Apple client secrets have a maximum lifetime of 6 months and must be regenerated before they expire.
When your client secret is approaching expiration, generate a new one in the Apple Developer portal and update it in **Organization Settings → Login Providers → Apple**.
***
##### GitHub Sync[](#github-sync "Direct link to GitHub Sync")
Go to **Organization Settings → GitHub Sync** to connect a GitHub repository as the template source for your organization. When connected, templates sync automatically on every push to the configured branch.
For full setup instructions — including creating the GitHub App, configuring environment variables, and the installation flow — see the [GitHub Sync](/developer/platform/integrations/github-sync.md) guide.
###### Managing an Existing Connection[](#managing-an-existing-connection "Direct link to Managing an Existing Connection")
Once connected, the page shows:
| Detail | Description |
| --------------------- | -------------------------------------------------------------- |
| **Connected Account** | The GitHub account or organization the app is installed under. |
| **Repository** | The full repository name (e.g. `org/repo`) being synced from. |
| **Default Branch** | The branch that triggers syncs on push. |
| **Last Sync** | Timestamp and short commit SHA of the most recent sync. |
From this page you can:
* **Sync Now** — Manually trigger a sync (rate-limited to once every 5 minutes).
* **Change Repository** — Opens the GitHub App settings to adjust which repositories are accessible.
* **Disconnect** — Stops syncing. Existing templates remain but become editable.
warning
Disconnecting does not delete your templates. However, if you later connect a new repository to an organization that already has templates, Allegro will permanently delete all existing templates to replace them with the repository contents. You must type `DELETE` to confirm this action.
###### Latest Sync Status[](#latest-sync-status "Direct link to Latest Sync Status")
Below the connection details, Allegro shows the most recent sync status:
| Status | Meaning |
| --------------------- | --------------------------------------------------------------------------- |
| **Pending / Running** | The sync job is queued or actively processing. |
| **Success** | All templates were synced from the repository. |
| **Failed** | The sync encountered an error. The error message is shown below the status. |
Warnings (e.g. skipped files) are also listed when present.
***
##### Packages[](#packages "Direct link to Packages")
Go to **Organization Settings → Packages** to manage which integration packages are enabled for your organization. Packages are grouped by category.
Check or uncheck a package to enable or disable it, then click **Save**.
note
Packages marked with a **Global** badge are always enabled and cannot be toggled from this page.
***
##### Developer Tools[](#developer-tools "Direct link to Developer Tools")
The **Developer Tools** section groups settings that are relevant to technical integrations and local development — API keys, GitHub template sync, the embed snippet, preview CSS, and webhooks. It appears at the bottom of the Organization Settings sidebar, below the **Access & Identity** section (login providers, OAuth clients, and the sign-in page).
###### API Keys[](#api-keys "Direct link to API Keys")
Go to **Organization Settings → API Keys** to create and manage the API keys your organization uses to authenticate with the Allegro REST API.
API keys belong to the **organization**, not to an individual user. Everyone who manages them sees the same shared list, and a key keeps working as people join or leave the team. Managing keys requires the **Admin** role — members without it cannot view or change this page.
Organization-scoped
An organization API key only works against its own organization. A key from one organization is rejected if it is used against another organization's domain.
###### Creating a key[](#creating-a-key "Direct link to Creating a key")
1. Click **Create Key** and give it a descriptive name (e.g. `data-pipeline`).
2. Choose when the key should **expire** — **30 days** (the default), **90 days**, **1 year**, a **custom date**, or **Never**. A dated key stops working at the end of the chosen day, so prefer a dated expiry over **Never** wherever you can.
3. Choose the [scopes](/developer/api/authentication.md#token-scopes) the key should carry.
4. Confirm. The generated key value is shown once — **copy it now**, because it is never displayed again.
###### Managing existing keys[](#managing-existing-keys "Direct link to Managing existing keys")
Each key in the list shows its **name**, the **scopes** it carries, when it was **created**, when it **expires** (or **Never**), and when it was **last used** (or **Never**). A key approaching its expiry date is highlighted, and one that has already lapsed is marked **Expired**. From the list you can:
* **Edit** a key to rename it, change its expiration, or change its scopes.
* **Revoke** a key to stop it working immediately. Revoked keys cannot be recovered.
Expired keys cannot be extended
An expired key stops working on its own and can no longer be edited. There is no way to extend it — create a new key to replace it.
Creating, updating, and revoking keys is recorded in an **activity log** on the page — showing who made each change and when — so the organization has a clear audit trail.
warning
Treat API keys like passwords. Do not share them or commit them to source control. If a key is compromised, revoke it immediately and create a new one.
###### Embed Snippet[](#embed-snippet "Direct link to Embed Snippet")
Go to **Organization Settings → Embed Snippet** to inject custom JavaScript into every page that loads the Allegro SDK via `client.js`. The snippet is appended to the loader and executes after the SDK initializes.
A typical snippet uses the event queue to run code on every page load:
```js
window.allegro.push((allegro) => {
// Runs after Allegro initializes on every page
});
```
Every save creates a new revision. You can browse the diff history and restore any previous version from the **Version History** panel on the same page.
note
The snippet is cached as part of `client.js` for up to 5 minutes. Changes may not appear on your site immediately after saving.
For more details on how the snippet is served and how to use it, see the [Script Tag](/developer/guides/script-tag.md#embed-snippet) guide.
###### Preview CSS[](#preview-css "Direct link to Preview CSS")
Go to **Organization Settings → Preview CSS** to add custom CSS that is applied when previewing templates with the [Allegro Preview](/developer/guides/local-preview.md) tool. This lets you approximate the look of your site's stylesheet without having to embed Allegro in a live page.
note
This field accepts CSS only. Allegro rejects any value containing a `<` character, so markup such as a `