MMS Onboarding Guide

Bridgify - Go to main page

Bridgify MMS Onboarding API#

Implementation Guide#

Overview#

The Marketplace Management System (MMS) onboarding API lets you create and manage whitelabel marketplaces programmatically — without going through the dashboard. With it you can:

  • Create a reusable company template that holds the settings every marketplace should share.
  • Create as many properties (individual whitelabel marketplaces) as you need, each inheriting the template.
  • Brand each property with its own colors and logo, and control where customers land when they open it.
  • Push template changes out to existing properties in one call.
  • Search the catalog for attractions, suppliers, countries, and cities (Section 4).
  • Manage the global settings that every property inherits — branding, localization, billing, etc. (Section 5).

A property is one whitelabel marketplace — its own branding, domain, landing page, and content rules. A company can own many properties. The template is the shared blueprint they're built from.

Base URLs#

EnvironmentBase URL
Productionhttps://mms.bridgify.io
Testhttps://mms.test.bridgify.io

All onboarding endpoints live under /api/onboarding/... and require a Bearer token. Examples below use the production base URL — swap in the test URL if you're integrating against test.

Response envelope#

Every response uses the same envelope:

  • Success: { "ok": true, "data": { ... } }
  • Failure: { "ok": false, "error": { "code": "...", "message": "..." } }

Error code is one of: VALIDATION_ERROR, AUTH_FAILED, TOKEN_EXPIRED, TOKEN_INVALID, PERMISSION_DENIED, NOT_FOUND, UPSTREAM_ERROR, INTERNAL_ERROR (plus TWO_FACTOR_* reserved for upcoming 2FA).


1. How to connect#

Authenticate#

Obtain a token with your email and password:

POST /api/auth/login
Content-Type: application/json

{
  "email": "user@bridgify.io",
  "password": "your-password"
}
curl -X POST https://mms.bridgify.io/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email": "user@bridgify.io", "password": "your-password"}'

Successful response:

{
  "ok": true,
  "data": {
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
    "user_id": 42
  }
}

Use the token#

Send the access_token as a Bearer token on every /api/onboarding/... request:

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

A missing or invalid token returns 401; a valid token whose role lacks permission for the action returns 403.

Planned: two-factor authentication. TOTP-based 2FA is on the roadmap. Once it ships, the login response will include twoFactorRequired: true for enrolled users and a follow-up POST /api/auth/verify step will return the full-access token. No client changes are needed todayPOST /api/auth/login returns a usable token as shown above.

Roles and what they can do#

Your token carries a role. The role determines which onboarding actions you can perform:

RoleCreate / list / configure templateCreate propertiesUpdate propertiesScope
super-adminAll companies
company_adminOwn company
property_admin❌ (403)❌ (403)✅ own property onlyOne property
employee❌ (403)❌ (403)❌ (403)Read-only across company

To create a company template and properties you need a super-admin or company_admin token. The rest of this guide assumes you're authenticated as one of those.


2. Create a company with many properties#

The recommended flow is template first, then properties:

  1. Create the company template once, with everything your marketplaces should share (auth behaviour, support details, branding defaults).
  2. Create each property with use_template: true so it inherits those defaults — then override only what's unique to that property (name, logo, landing location).
  3. Later, when a shared setting changes, propagate it to all existing properties in one call.
                ┌──────────────────────┐
                │   Company template    │   shared defaults
                │  (one per company)    │
                └──────────┬───────────┘
            use_template:true │  (copied on create)
        ┌──────────────┬──────┴───────┬──────────────┐
        ▼              ▼              ▼              ▼
   Property 1042   Property 1043   Property 1044   …  (whitelabel marketplaces)
   own logo +      own logo +      own logo +
   colors +        colors +        colors +
   landing page    landing page    landing page

Step 2a — Create the company template#

The template has the same shape as a property config. Set the groups you want every property to share. A group left out (or set to null) simply means "not defined in the template" — properties keep their own value for it.

POST /api/onboarding/template
Authorization: Bearer <token>
Content-Type: application/json

{
  "auth_required": false,
  "session_ttl": 3600,
  "support": { "address": "1 King St, London, UK" },
  "primary_color": "#2120B1",
  "secondary_color": "#F19AB4"
}

Returns 201 with the created template. If a template already exists you'll get 409 — update it with PATCH /api/onboarding/template instead (all fields optional, merged into the existing template). Fetch the current template any time with GET /api/onboarding/template.

Step 2b — Create each property#

Only external_property_id and property_name are required. Set use_template: true to copy every settings group and auth default from the template into the new property.

POST /api/onboarding/properties
Authorization: Bearer <token>
Content-Type: application/json

{
  "external_property_id": "VENUE-123",
  "property_name": "Grand Central",
  "property_logo_url": "https://cdn.example.com/grand-central-logo.png",
  "primary_color": "#2120B1",
  "secondary_color": "#F19AB4",
  "use_template": true
}
curl -X POST https://mms.bridgify.io/api/onboarding/properties \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "external_property_id": "VENUE-123",
    "property_name": "Grand Central",
    "use_template": true
  }'

Response — note the auto-assigned numeric pid, which you'll use for all later calls:

{ "ok": true, "data": { "pid": 1042 } }
FieldRequiredNotes
external_property_idYour own reference ID. Must be unique within your company.
property_nameHuman-readable name shown in the marketplace.
property_logo_urlProperty logo (see Colors & logos).
primary_color / secondary_colorBrand colors, hex #RRGGBB.
property_addressPhysical address shown in the marketplace.
main_page_configWhere customers land (see below).
use_templatetrue copies all template settings; default false starts blank.

Repeat this call for each marketplace you want to launch. Because every property inherits the template, the only fields you typically vary per property are external_property_id, property_name, the logo, and the landing location.

Step 2c — Set the landing location (main_page_config)#

main_page_config controls where a customer lands when they first open the marketplace. Pick one of four types:

TypeShapeLands on
coordinates{ "type": "coordinates", "radius_km": 20 } + top-level lat & lngA geo-radius search around a point
city{ "type": "city", "id": 2643743 }A specific city's results
country{ "type": "country", "id": 2635167 }A specific country's results
url{ "type": "url", "url": "/en/search/1/cities/london-c2643743" }A custom path or URL

For type: "coordinates", supply lat and lng as top-level fields of the request body (alongside main_page_config), not inside the object.

Look up city and country IDs first via the destinations endpoints:

  • GET /api/onboarding/destinations/cities?search=london
  • GET /api/onboarding/destinations/countries

Each returns { "ok": true, "data": { "results": [ { "id": 2643743, "name": "London (United Kingdom)" } ] } }.

Example — a property that lands on London:

{
  "external_property_id": "VENUE-124",
  "property_name": "London Hub",
  "use_template": true,
  "main_page_config": { "type": "city", "id": 2643743 }
}

If you omit main_page_config, the property opens to its own website URL with no redirect.

Step 2d — List and update properties#

List your company's properties (paginated, default 50, max 200):

GET /api/onboarding/properties?limit=50&offset=0
{
  "ok": true,
  "data": {
    "count": 142, "limit": 50, "offset": 0,
    "results": [
      { "pid": 1042, "name": "Grand Central", "is_active": true,
        "external_property_id": "VENUE-123", "created_at": "2026-05-13T10:00:00Z" }
    ]
  }
}

To page through results, request offset + limit; you've reached the end when offset + len(results) >= count.

Fetch one property's full config with GET /api/onboarding/properties/{pid}, and update it with PATCH /api/onboarding/properties/{pid}. PATCH is partial — only the fields you send change; everything else is left untouched.

PATCH /api/onboarding/properties/1042
Authorization: Bearer <token>
Content-Type: application/json

{ "primary_color": "#0A7C3F" }

property_name and is_active can only be changed by company_admin or super-admin. A property_admin may update only their own property's other fields.

Step 2e — Propagate template changes to existing properties#

When you change a shared setting, push it to properties that already exist:

POST /api/onboarding/template/propagate
Authorization: Bearer <token>
Content-Type: application/json

{
  "fields": ["branding.theme_colors", "support.address"],
  "target": "all"
}
  • fields — dot-notation paths to copy from the template. "branding" copies the whole branding group; "branding.theme_colors" copies only the theme colors sub-object.
  • target"all" for every property, or a list of pids, e.g. [1042, 1043].

The response reports how many properties were updated:

{ "ok": true, "data": { "updated": 5, "job_id": null } }

Sync vs async: with 10 or fewer properties the operation runs immediately and returns 200 with "job_id": null. With more than 10, it runs in the background and returns 202 with a job_id you can track.


3. Colors & logos#

A property's look is defined by its branding palette — the colors, logo, and images the marketplace renders. The MMS API gives you two ways to set it:

  1. Shortcut fields on property/template create & patch — the quickest way to set the core brand colors and logo. These are validated (hex format, valid URL).
  2. The branding settings group — the full object, for the complete palette the marketplace renders.

Set these directly in a create or patch body:

FieldFormatSetsStored at
primary_color#RRGGBBPrimary brand colorbranding.theme_colors.primary_bg
secondary_color#RRGGBBSecondary brand colorbranding.theme_colors.secondary_bg
property_logo_urlURLMain marketplace logobranding.logo
{
  "primary_color": "#2120B1",
  "secondary_color": "#F19AB4",
  "property_logo_url": "https://cdn.example.com/logo.png"
}

The full branding palette#

These are every color, logo, and image the marketplace renders. Set them on a property (or in the template) and they apply across the storefront. Colors are hex strings (#RRGGBB); logos and images are URLs.

Brand colors

KeyControls
primary_colorPrimary brand color — main buttons, highlights, key UI accents.
primary_color_textText/icon color shown on top of the primary color.
secondary_colorSecondary brand color — secondary buttons and surfaces.
secondary_color_textText/icon color shown on top of the secondary color.
tertiary_colorAccent color — tags, badges, star ratings.
tertiary_color_textText color shown on top of the tertiary/accent color.

Surfaces & sections

KeyControls
background_colorPage/section background (e.g. the footer).
hero_text_colorHero banner title and subtitle text color.
banner_text_colorPromotional banner title and description text color.

Logos & images

KeyControls
logoMain marketplace logo (navbar, footer). Set via the property_logo_url shortcut, or directly here.
fav_iconBrowser tab favicon.
hero_image_desktopHero banner background image, desktop.
hero_image_mobileHero banner background image, mobile.

Hero images can be overridden per language via the marketplace's language packs, falling back to these defaults when no per-language image is set.

Email branding#

Transactional emails (booking confirmations, etc.) have their own branding overrides via email_settings on a property or template. Omit any key to inherit the platform default.

KeyControls
background_colorEmail background color (hex).
button_colorCall-to-action button background (hex).
button_text_colorCall-to-action button text (hex).
logo_urlLogo shown in the email header.
{
  "email_settings": {
    "background_color": "#ffffff",
    "button_color": "#3b82f6",
    "button_text_color": "#ffffff",
    "logo_url": "https://cdn.example.com/logo.png"
  }
}

button_url, support_email, and cancel_order_link are derived automatically from the property's website, support, and domain — don't set them in email_settings.

Rolling branding out to every property#

Set the palette once in the template, then propagate it (see Step 2e):

{
  "fields": ["branding.theme_colors", "branding.logo"],
  "target": "all"
}

Four small, read-only endpoints power the lookups you'll need when wiring up properties — finding the attraction UUIDs to feature, the suppliers your marketplace is allowed to source from, and the countries/cities you want to land customers on.

All four share the same query string:

ParamTypeRequiredNotes
qstringSearch query. Empty / omitted returns an empty list (suppliers excepted).
modeMER | AFFMER (default) → merchant suppliers; anything else → affiliate suppliers. Has no effect on countries and cities — they accept it for parity only.

Required role: any authenticated role, including employee. These endpoints use the same Bearer token as the rest of the API.

Search attractions#

GET /api/search/attractions?q=eiffel&mode=MER
Authorization: Bearer <token>

If q is a full UUID, it's matched as an exact term on uuid. Otherwise it's a match_phrase_prefix on title. Results are restricted to the supplier role selected by mode.

{
  "ok": true,
  "data": {
    "attractions": [
      {
        "uuid": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
        "title": "Eiffel Tower Skip-the-Line",
        "inventory_supplier": "GYG",
        "external_city_name": "Paris"
      }
    ]
  },
  "meta": { "request_id": "f0c7e3d1-2a44-4f1e-9b7e-bb9a3a8a91e5" }
}

Search suppliers#

GET /api/search/suppliers?q=gyg&mode=MER

Case-insensitive substring match on supplier name; q is optional. With mode=MER (default) you get merchant suppliers; with mode=AFF you get affiliates; any other value applies no role filter.

{
  "ok": true,
  "data": {
    "suppliers": [
      { "id": 3, "name": "GYG" },
      { "id": 4, "name": "Viator" }
    ]
  },
  "meta": { "request_id": "29e5a8c1-9c2c-4a1e-bd11-0f7a8f7d6a40" }
}

Search countries & cities#

GET /api/search/countries?q=france
GET /api/search/cities?q=paris

Both return only destinations that actually have attractions. mode is accepted for parity with the other endpoints but currently has no effect on either query.

{
  "ok": true,
  "data": {
    "cities": [
      { "id": 2988507, "name": "Paris (France)" },
      { "id": 3169070, "name": "Rome (Italy)" }
    ]
  },
  "meta": { "request_id": "61a44b80-1d6b-4a7d-8f6a-92a4c2d4b9e1" }
}

Use the id values returned here in main_page_config when you set a property's landing page to a city or country (see Step 2c). These MMS-wide search endpoints are an alternative to /api/onboarding/destinations/cities and /api/onboarding/destinations/countries — they additionally filter by "has attractions" and accept the mode parameter.


5. Global settings#

The platform keeps a default MarketplaceClient row (is_default=True) that holds the company-wide settings every whitelabel property inherits — branding palette, supported currencies and languages, billing defaults, navigation, etc. The global-settings endpoints let you read or replace any one of those groups in isolation.

Required role: super-admin or company_admin. A property_admin or employee token gets 403.

Settings groups#

The {group} path segment is one of:

GroupHolds
brandingLogos, favicon, theme colors, typography, shadows, email branding.
localizationDefault language and currency, the enabled language/currency lists.
billingMargin, clearing fee, discount, payout details. Validated server-side (must be profitable).
supportSupport address, phone, email.
navigationHeader/footer link layout.
contentEditorial copy and CMS content.
content_filtersBlock-lists used to hide attractions from search and listings. See Content filters below.
seoPer-page SEO defaults.
footerFooter layout and links.
analyticsPixel and analytics tag config.
order_reportingReporting & export config.
templatesMarketplace template overrides.
category_iconsCategory → icon mapping.
landing_pagesStatic landing-page config.
permissionsPer-feature permission toggles.

Read a group#

GET /api/global-settings/{group}
Authorization: Bearer <token>
curl https://mms.bridgify.io/api/global-settings/branding \
  -H "Authorization: Bearer $TOKEN"

Returns the stored blob, or null if the group has never been configured.

{
  "ok": true,
  "data": {
    "logo": {
      "light": "https://cdn.example.com/logo-light.png",
      "dark":  "https://cdn.example.com/logo-dark.png"
    },
    "theme_colors": {
      "primary_bg": "#6e40ff", "primary_text": "#ffffff",
      "secondary_bg": "#e2e8f0", "secondary_text": "#1a1a2e"
    }
  },
  "meta": { "request_id": "0a2c45dd-7e91-4b3a-9b6e-22f7c45f7891" }
}

An unknown group name returns 400 with the raw shape { "error": "Unknown settings group: foo" }.

Replace a group#

PUT /api/global-settings/{group}
Authorization: Bearer <token>
Content-Type: application/json

PUT replaces the whole blob. Send the complete object you want stored — keys you omit are dropped. There is no PATCH for global-settings; if you want a delta, GET the current blob, mutate it, then PUT it back.

curl -X PUT https://mms.bridgify.io/api/global-settings/localization \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{
    "default_language": "en",
    "default_currency": "USD",
    "enabled_languages": ["en", "he", "fr"],
    "currencies": ["USD", "EUR", "ILS"]
  }'

Validation runs server-side for three groups:

GroupRule
billing(1 + margin) × (1 + clearing_fee) × (1 − discount) > 1.0. Sensitive fields are AES-GCM encrypted on save.
localizationdefault_language and every entry in enabled_languages must be a valid language code; default_currency and every entry in currencies must be a valid currency code.
brandingNested logo, favicon, and theme_colors shapes are validated (hex colors, URL format).

A failed check returns 400 with the raw { "error": "..." } shape — for example Billing settings are not profitable: ....

Content filters#

content_filters is the block-list group — it tells search and listing endpoints which attractions to hide. Anything listed here is removed from the marketplace's catalog: search results, geo-radius queries, category pages, the home page, deep links — everywhere. Nothing in the blob is additive; every key is an exclusion list, and a missing or empty key means "nothing blocked of this kind."

Internally each list is fed straight into OpenSearch as a must_not clause, so blocking is effectively instant once the blob is saved.

Shape#

{
  "disabled_attractions": [
    "a1b2c3d4-5678-90ab-cdef-1234567890ab",
    "f0e9d8c7-b6a5-4321-fedc-ba9876543210"
  ],
  "disabled_suppliers":   [{ "id": 3, "name": "GYG" }],
  "disabled_countries":   [{ "id": 3017382, "name": "France" }],
  "disabled_cities":      [{ "id": 2988507, "name": "Paris (France)" }],
  "disabled_segments":    [{ "id": 5,  "name": "Day Tours" }],
  "disabled_languages":   ["ru", "fr"]
}
KeyItemsMatched againstEffect
disabled_attractionsArray of UUID stringsAttraction uuidHide these specific attractions, regardless of supplier or location.
disabled_suppliers{ "id": number, "name": string }Supplier name (server reads name only)Hide every attraction from these inventory suppliers.
disabled_countries{ "id": number, "name"?: string }Country id (geoname)Hide every attraction whose city's country matches.
disabled_cities{ "id": number, "name"?: string }City id (geoname)Hide every attraction in these cities.
disabled_segments{ "id": number, "name"?: string }Main category id (categories_main.id)Hide every attraction in these top-level segments.
disabled_languagesArray of language codes ("en", "fr", …)Property language resolutionForce-disables a language even if it appears in enabled_languages; requests for it fall back to en.

Only the id (or uuid) is actually used to filter. The name field exists so the dashboard can render a human-readable label without an extra lookup. Storing it is recommended but not required for countries, cities, and segments; for suppliers the name is the match field, so it must be present.

Looking up IDs and UUIDs#

Use the search endpoints from Section 4 to populate these lists:

To block…Use
A specific attractionGET /api/search/attractions?q=<title-or-uuid> → copy uuid
A supplierGET /api/search/suppliers?q=<name> → copy { id, name }
A countryGET /api/search/countries?q=<name> → copy { id, name }
A cityGET /api/search/cities?q=<name> → copy { id, name }
A segment (main category)GET /api/onboarding/destinations/segments → copy { id, name }

Replacing the block-list#

curl -X PUT https://mms.bridgify.io/api/global-settings/content_filters \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{
    "disabled_attractions": [
      "a1b2c3d4-5678-90ab-cdef-1234567890ab"
    ],
    "disabled_suppliers": [{ "id": 3, "name": "GYG" }],
    "disabled_countries": [{ "id": 3017382, "name": "France" }],
    "disabled_cities":    [],
    "disabled_segments":  [],
    "disabled_languages": []
  }'

Remember: PUT replaces the whole blob. To add one supplier to the existing block-list, GET the current content_filters blob, append to the disabled_suppliers array, and PUT the merged object back. Sending only { "disabled_suppliers": [...] } wipes the other lists.

How exclusions stack#

Filters from all three layers combine — a hit on any of them is enough to hide an attraction:

  1. Global content_filters (this section) — platform-wide block-list.
  2. Property content_filters (/api/properties/<uuid>/settings/content_filters) — additions for one marketplace.
  3. Supplier mode — the property's marketplace_mode (MER / AFF) already drops suppliers that don't fit the role; disabled_suppliers removes more on top.

There is no allow-list — every attraction is visible unless something in the block-list catches it. If you need a curated catalog instead, use featured blocks rather than content filters.

A worked example#

You want to hide everything from one risky supplier across every property and block a single problem attraction:

# 1. Find the supplier
curl "https://mms.bridgify.io/api/search/suppliers?q=acmetours" \
  -H "Authorization: Bearer $TOKEN"
# → { "ok": true, "data": { "suppliers": [{ "id": 17, "name": "AcmeTours" }] } }

# 2. Read the current block-list so we don't clobber it
curl "https://mms.bridgify.io/api/global-settings/content_filters" \
  -H "Authorization: Bearer $TOKEN"

# 3. PUT the merged blob
curl -X PUT https://mms.bridgify.io/api/global-settings/content_filters \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{
    "disabled_attractions": [
      "a1b2c3d4-5678-90ab-cdef-1234567890ab"
    ],
    "disabled_suppliers": [{ "id": 17, "name": "AcmeTours" }],
    "disabled_countries": [],
    "disabled_cities":    [],
    "disabled_segments":  [],
    "disabled_languages": []
  }'

Both items disappear from every property's search results on the next request.

Global settings vs property settings vs template#

Three layers, each with a different scope:

LayerEndpointScope
Global/api/global-settings/{group}Defaults baked into the default MarketplaceClient row — every property inherits these on creation.
Template/api/onboarding/templateCompany-level overrides for new and existing properties. Use POST /api/onboarding/template/propagate to push template fields out.
Property/api/properties/<uuid>/settings/<group>Per-property overrides — the most specific layer.

Use global settings when you're seeding the platform-wide defaults; use the template + propagate flow when rolling a company-level change out to existing properties (see Step 2e).


Quick reference#

Endpoints#

Method & pathPurposeMin. role
POST /api/auth/loginGet a Bearer token
POST /api/onboarding/templateCreate the company templatecompany_admin
GET /api/onboarding/templateGet the company templatecompany_admin
PATCH /api/onboarding/templateUpdate the templatecompany_admin
POST /api/onboarding/template/propagatePush template fields to propertiescompany_admin
POST /api/onboarding/propertiesCreate a propertycompany_admin
GET /api/onboarding/propertiesList propertiesany role
GET /api/onboarding/properties/{pid}Get one property's full configany role
PATCH /api/onboarding/properties/{pid}Update a propertyproperty_admin (own)
GET /api/onboarding/destinations/citiesLook up city IDsany role
GET /api/onboarding/destinations/countriesLook up country IDsany role
GET /api/search/attractionsSearch attractions by title or UUID (mode-aware)any role
GET /api/search/suppliersSearch active inventory suppliers (mode-aware)any role
GET /api/search/countriesCountries that have attractionsany role
GET /api/search/citiesCities that have attractionsany role
GET /api/global-settings/{group}Read a global settings group blobcompany_admin
PUT /api/global-settings/{group}Replace a global settings group blobcompany_admin

Status codes#

CodeMeaning
200 / 201Success (201 on create).
202Propagation queued for background processing (>10 properties).
400Validation error or duplicate external_property_id.
401Missing or invalid Bearer token.
403Your role doesn't permit this action.
404Property or template not found / not accessible.
409Template already exists — use PATCH instead.

A complete walkthrough#

# 1. Connect
TOKEN=$(curl -s -X POST https://mms.bridgify.io/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{"email":"user@bridgify.io","password":"your-password"}' \
  | python3 -c "import sys,json; print(json.load(sys.stdin)['data']['access_token'])")

# 2. Create the shared company template
curl -X POST https://mms.bridgify.io/api/onboarding/template \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"primary_color":"#2120B1","secondary_color":"#F19AB4",
       "support":{"address":"1 King St, London, UK"}}'

# 3. Create three properties that inherit it
for v in VENUE-1 VENUE-2 VENUE-3; do
  curl -X POST https://mms.bridgify.io/api/onboarding/properties \
    -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
    -d "{\"external_property_id\":\"$v\",\"property_name\":\"$v Store\",\"use_template\":true}"
done

# 4. Later: roll a branding change out to all of them
curl -X POST https://mms.bridgify.io/api/onboarding/template/propagate \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"fields":["branding.theme_colors"],"target":"all"}'

For the complete field-by-field schema of every request and response, see the MMS API Reference (/mms).