The Marketplace Management System (MMS) onboarding API lets you create and manage whitelabel marketplaces programmatically — without going through the dashboard. With it you can:
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.
| Environment | Base URL |
|---|---|
| Production | https://mms.bridgify.io |
| Test | https://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.
Every response uses the same envelope:
{ "ok": true, "data": { ... } }{ "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).
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
}
}
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: truefor enrolled users and a follow-upPOST /api/auth/verifystep will return the full-access token. No client changes are needed today —POST /api/auth/loginreturns a usable token as shown above.
Your token carries a role. The role determines which onboarding actions you can perform:
| Role | Create / list / configure template | Create properties | Update properties | Scope |
|---|---|---|---|---|
super-admin | ✅ | ✅ | ✅ | All companies |
company_admin | ✅ | ✅ | ✅ | Own company |
property_admin | ❌ (403) | ❌ (403) | ✅ own property only | One 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.
The recommended flow is template first, then properties:
use_template: true so it inherits those defaults — then override only what's unique to that property (name, logo, landing location). ┌──────────────────────┐
│ 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
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.
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 } }
| Field | Required | Notes |
|---|---|---|
external_property_id | ✅ | Your own reference ID. Must be unique within your company. |
property_name | ✅ | Human-readable name shown in the marketplace. |
property_logo_url | — | Property logo (see Colors & logos). |
primary_color / secondary_color | — | Brand colors, hex #RRGGBB. |
property_address | — | Physical address shown in the marketplace. |
main_page_config | — | Where customers land (see below). |
use_template | — | true 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.
main_page_config)#main_page_config controls where a customer lands when they first open the marketplace. Pick one of four types:
| Type | Shape | Lands on |
|---|---|---|
coordinates | { "type": "coordinates", "radius_km": 20 } + top-level lat & lng | A 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", supplylatandlngas top-level fields of the request body (alongsidemain_page_config), not inside the object.
Look up city and country IDs first via the destinations endpoints:
GET /api/onboarding/destinations/cities?search=londonGET /api/onboarding/destinations/countriesEach 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.
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_nameandis_activecan only be changed bycompany_adminorsuper-admin. Aproperty_adminmay update only their own property's other fields.
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
200with"job_id": null. With more than 10, it runs in the background and returns202with ajob_idyou can track.
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:
branding settings group — the full object, for the complete palette the marketplace renders.Set these directly in a create or patch body:
| Field | Format | Sets | Stored at |
|---|---|---|---|
primary_color | #RRGGBB | Primary brand color | branding.theme_colors.primary_bg |
secondary_color | #RRGGBB | Secondary brand color | branding.theme_colors.secondary_bg |
property_logo_url | URL | Main marketplace logo | branding.logo |
{
"primary_color": "#2120B1",
"secondary_color": "#F19AB4",
"property_logo_url": "https://cdn.example.com/logo.png"
}
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
| Key | Controls |
|---|---|
primary_color | Primary brand color — main buttons, highlights, key UI accents. |
primary_color_text | Text/icon color shown on top of the primary color. |
secondary_color | Secondary brand color — secondary buttons and surfaces. |
secondary_color_text | Text/icon color shown on top of the secondary color. |
tertiary_color | Accent color — tags, badges, star ratings. |
tertiary_color_text | Text color shown on top of the tertiary/accent color. |
Surfaces & sections
| Key | Controls |
|---|---|
background_color | Page/section background (e.g. the footer). |
hero_text_color | Hero banner title and subtitle text color. |
banner_text_color | Promotional banner title and description text color. |
Logos & images
| Key | Controls |
|---|---|
logo | Main marketplace logo (navbar, footer). Set via the property_logo_url shortcut, or directly here. |
fav_icon | Browser tab favicon. |
hero_image_desktop | Hero banner background image, desktop. |
hero_image_mobile | Hero 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.
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.
| Key | Controls |
|---|---|
background_color | Email background color (hex). |
button_color | Call-to-action button background (hex). |
button_text_color | Call-to-action button text (hex). |
logo_url | Logo 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, andcancel_order_linkare derived automatically from the property'swebsite,support, anddomain— don't set them inemail_settings.
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:
| Param | Type | Required | Notes |
|---|---|---|---|
q | string | — | Search query. Empty / omitted returns an empty list (suppliers excepted). |
mode | MER | AFF | — | MER (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.
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" }
}
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" }
}
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
idvalues returned here inmain_page_configwhen you set a property's landing page to acityorcountry(see Step 2c). These MMS-wide search endpoints are an alternative to/api/onboarding/destinations/citiesand/api/onboarding/destinations/countries— they additionally filter by "has attractions" and accept themodeparameter.
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-adminorcompany_admin. Aproperty_adminoremployeetoken gets403.
The {group} path segment is one of:
| Group | Holds |
|---|---|
branding | Logos, favicon, theme colors, typography, shadows, email branding. |
localization | Default language and currency, the enabled language/currency lists. |
billing | Margin, clearing fee, discount, payout details. Validated server-side (must be profitable). |
support | Support address, phone, email. |
navigation | Header/footer link layout. |
content | Editorial copy and CMS content. |
content_filters | Block-lists used to hide attractions from search and listings. See Content filters below. |
seo | Per-page SEO defaults. |
footer | Footer layout and links. |
analytics | Pixel and analytics tag config. |
order_reporting | Reporting & export config. |
templates | Marketplace template overrides. |
category_icons | Category → icon mapping. |
landing_pages | Static landing-page config. |
permissions | Per-feature permission toggles. |
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" }.
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:
| Group | Rule |
|---|---|
billing | (1 + margin) × (1 + clearing_fee) × (1 − discount) > 1.0. Sensitive fields are AES-GCM encrypted on save. |
localization | default_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. |
branding | Nested 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 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.
{
"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"]
}
| Key | Items | Matched against | Effect |
|---|---|---|---|
disabled_attractions | Array of UUID strings | Attraction uuid | Hide 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_languages | Array of language codes ("en", "fr", …) | Property language resolution | Force-disables a language even if it appears in enabled_languages; requests for it fall back to en. |
Only the
id(oruuid) is actually used to filter. Thenamefield exists so the dashboard can render a human-readable label without an extra lookup. Storing it is recommended but not required forcountries,cities, andsegments; forsuppliersthenameis the match field, so it must be present.
Use the search endpoints from Section 4 to populate these lists:
| To block… | Use |
|---|---|
| A specific attraction | GET /api/search/attractions?q=<title-or-uuid> → copy uuid |
| A supplier | GET /api/search/suppliers?q=<name> → copy { id, name } |
| A country | GET /api/search/countries?q=<name> → copy { id, name } |
| A city | GET /api/search/cities?q=<name> → copy { id, name } |
| A segment (main category) | GET /api/onboarding/destinations/segments → copy { id, name } |
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_filtersblob, append to thedisabled_suppliersarray, and PUT the merged object back. Sending only{ "disabled_suppliers": [...] }wipes the other lists.
Filters from all three layers combine — a hit on any of them is enough to hide an attraction:
content_filters (this section) — platform-wide block-list.content_filters (/api/properties/<uuid>/settings/content_filters) — additions for one marketplace.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.
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.
Three layers, each with a different scope:
| Layer | Endpoint | Scope |
|---|---|---|
| Global | /api/global-settings/{group} | Defaults baked into the default MarketplaceClient row — every property inherits these on creation. |
| Template | /api/onboarding/template | Company-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).
| Method & path | Purpose | Min. role |
|---|---|---|
POST /api/auth/login | Get a Bearer token | — |
POST /api/onboarding/template | Create the company template | company_admin |
GET /api/onboarding/template | Get the company template | company_admin |
PATCH /api/onboarding/template | Update the template | company_admin |
POST /api/onboarding/template/propagate | Push template fields to properties | company_admin |
POST /api/onboarding/properties | Create a property | company_admin |
GET /api/onboarding/properties | List properties | any role |
GET /api/onboarding/properties/{pid} | Get one property's full config | any role |
PATCH /api/onboarding/properties/{pid} | Update a property | property_admin (own) |
GET /api/onboarding/destinations/cities | Look up city IDs | any role |
GET /api/onboarding/destinations/countries | Look up country IDs | any role |
GET /api/search/attractions | Search attractions by title or UUID (mode-aware) | any role |
GET /api/search/suppliers | Search active inventory suppliers (mode-aware) | any role |
GET /api/search/countries | Countries that have attractions | any role |
GET /api/search/cities | Cities that have attractions | any role |
GET /api/global-settings/{group} | Read a global settings group blob | company_admin |
PUT /api/global-settings/{group} | Replace a global settings group blob | company_admin |
| Code | Meaning |
|---|---|
200 / 201 | Success (201 on create). |
202 | Propagation queued for background processing (>10 properties). |
400 | Validation error or duplicate external_property_id. |
401 | Missing or invalid Bearer token. |
403 | Your role doesn't permit this action. |
404 | Property or template not found / not accessible. |
409 | Template already exists — use PATCH instead. |
# 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).