Skip to main content
Glama
brilliantdirectories

brilliant-directories-mcp

Official

Server Configuration

Describes the environment variables required to run the server.

NameRequiredDescriptionDefault
BD_API_KEYYesYour Brilliant Directories API key. Generate one in BD Admin → Developer Hub → Generate API Key.
BD_API_URLYesYour BD site URL, e.g. https://yoursite.com

Capabilities

Features and capabilities supported by this server

CapabilityDetails
tools
{}

Tools

Functions exposed to the LLM to take actions

NameDescription
verifyToken

Verify API key - Verify that your API key is valid and check rate limit status.

Use when: at the start of any session or batch job, to confirm the API key is valid and the site is reachable BEFORE burning rate limit on real calls. Also useful for surfacing a clear "bad credentials" error to the user early.

Parameter interactions:

  • Call at the start of a session to confirm the API key is valid BEFORE issuing real calls - saves rate-limit budget on key-config errors

Returns: { status: "success"|"error", message: ... } - BD's standard response envelope.

getUserFields

Get user field definitions - Returns available fields for user records with labels and required flags. Use this to discover custom fields.

Use when: building dynamic forms or importers - you need to discover which fields exist on the User record on THIS specific site (custom fields vary per BD site config). Also useful for validating import-CSV headers before running a batch.

Returns: { status: "success", message: [{...record}] } - the message array contains 1 record when found. Empty or HTTP 404 when not found.

listUsers

List members/users - Get a paginated list of all members. Supports filtering by any user field and sorting.

Lean by default: each row strips subscription_schema, user_clicks_schema.clicks, photos_schema, transactions, profession_schema, tags, services_schema, password, and SEO-hidden meta. Lean row keeps core columns + revenue rollup + summary counts (total_clicks, total_photos) + image_main_file. Opt back in per call with the include_* flags.

Use when: enumerating members for reports, CSV exports, bulk status updates, analytics, or pagination through the full member base. Also used for lookups by field - pass property=email + property_value=<email> to find a single user by email. For keyword/text search use searchUsers; for a single user by known user_id use getUser.

Pagination: cursor-based. Pass limit (default 25, max 100) and page token from the previous response's next_page. Do not assume integer offsets.

Filter/sort: property+property_value+property_operator, order_column+order_type. See top-level filter rule for reliable operators (= only), silent-drop detection, and derived-field unfilterability.

Enums: property_operator: =, LIKE, >, <, >=, <=; order_type: ASC, DESC.

Filter-property rule - use ACTUAL field names: property must reference a real column on users_data or a valid custom user field. If you don't know what's filterable, call getUserFields first - it returns the authoritative list for this site (includes custom fields). BD returns misleading errors like "user not found" when property names a nonexistent field - that is a BAD FILTER, not a 404 on the endpoint. Do not invent properties like user_group (not a real column).

Filtering by TOP CATEGORY (profession): the filter column is profession_id (integer), not a category name string. If the caller gives you a category name, chain: (1) listTopCategories -> find the row whose name matches; (2) grab its profession_id; (3) call listUsers with property=profession_id&property_value=<id>. Same principle for any taxonomy filter - resolve names to IDs first via listSubCategories, listMembershipPlans, etc. For sub-category filtering on users, the authoritative approach is listMemberSubCategoryLinks filtered by service_id -> collect user_ids -> fetch those users. (There is also a service CSV column on user records but exact-match filtering on it requires the complete CSV value and LIKE syntax support is not guaranteed - prefer the link-table route.)

Filtering by users_meta (custom/meta fields): multi-value meta filtering IS supported via the array syntax - e.g. property[]=<meta_key>&property_value[]=foo&property[]=<meta_key>&property_value[]=bar&property_operator[]=OR&property_logic[]==. Use this for OR-across-meta-values lookups (e.g. members whose custom field equals any of N options).

Payment-method field in response: every user record includes card_info. When no card is on file it is the literal boolean false (BD's convention - same as tags: false, transactions: false, etc.). When a card IS stored, it's an object with fields like last4, brand, name. Check card_info && card_info.last4 (truthy-guard) - NOT card_info.last4 directly, since false.last4 throws. This is the authoritative signal for "does this member have a valid payment method on file" - do not infer from subscription_id alone. BD clears card_info back to false when the member removes their card; it does not go stale silently.

See also: getUser (single record by ID), searchUsers (keyword search), getUserFields (list filterable fields).

Returns: { status: "success", total, current_page, total_pages, next_page, prev_page, message: [...records] }. Each record is the full resource object.

Profile URL: every user record has a filename field. To get the full public profile URL, concatenate: <site-domain>/<user.filename>. The filename is the complete relative path (e.g., united-states/monterey-park/doctor/harrison-hasanuddin-d-o) - DO NOT prepend /business/, /profile/, /member/, or any other segment. BD's router resolves filename verbatim.

getUser

Get a single member/user - Fetch a single user record. Read-only.

Lean by default: response strips the same heavy nested buckets as listUsers (see its lean note). Pass include_*=1 flags to restore specific nested fields.

Use when: you already have the user_id (from listUsers, searchUsers, a prior create, or a webhook payload) and need the full member record. Cheaper than listUsers + filter. For lookups by email or other field, use listUsers with property/property_value.

Required: user_id.

See also: listUsers (enumerate many), searchUsers (keyword search).

Returns: { status: "success", message: [{...record}] } - the message array contains 1 record when found. Empty or HTTP 404 when not found.

Payment method on file: the response includes a card_info field. When no card is stored it is the literal boolean false (BD convention). When a card IS stored, it's an object with fields like last4, brand, name. Use card_info && card_info.last4 to safely check - card_info.last4 would throw on the false case. This is the authoritative signal for "does this member have a payment method" - don't infer from subscription_id alone. BD clears card_info back to false when the member removes their card; it does not go stale silently.

Profile URL: every user record has a filename field. To get the full public profile URL, concatenate: <site-domain>/<user.filename>. The filename is the complete relative path (e.g., united-states/monterey-park/doctor/harrison-hasanuddin-d-o) - DO NOT prepend /business/, /profile/, /member/, or any other segment. BD's router resolves filename verbatim. Note: filename is regenerated by BD when member inputs that influence the slug change (category, city, etc.). The value you see NOW is current-as-of-this-read. If you call updateUser afterward, re-fetch before using the filename in URL-referencing content (blog posts, emails, redirects).

createUser

Create a new member/user - Create a member. Writes live data. Welcome email silent by default - set send_email_notifications=1 to trigger.

Required: email, password, subscription_id.

Use when: adding members outside BD signup - CSV imports, scraped listings, Zapier automations, admin test accounts.

Enums: active: 1=Not Active, 2=Active, 3=Canceled, 4=On Hold, 5=Past Due, 6=Incomplete. listing_type: Individual, Company. verified/nationwide: 1/0.

Prerequisites: subscription_id MUST reference an existing plan - discover via listMembershipPlans. Categories auto-create (see taxonomy rule below).

Parameter interactions:

  • auto_image_import=1 - fetch external image URLs into BD storage (for profile_photo, logo, cover_photo holding URLs). Without it, BD stores the URL as-is; images break if source host goes down. Supports JPG/PNG/GIF/WebP/SVG. Processing delay: several minutes. Recommended default for any external image URL.

  • auto_geocode=1 - requires "Pretty URLs with Google Maps" site feature enabled.

  • send_email_notifications=1 - trigger welcome email per plan settings.

Category taxonomy:

  • Top-level: pick ONE of profession_id (numeric, must exist OR will auto-create) or profession_name (string, auto-creates if missing).

  • services (sub-categories) supports 4 formats:

    • Single: "PVC repair" or "1823"

    • Multiple: "PVC repair,Water heater" or "1823,1824"

    • Sub-sub via =>: "Honda=>2022" (creates sub "Honda" + sub-sub "2022" under Honda)

    • Mixed: "Honda=>2022,Honda=>2023,Toyota"

  • profession_id/profession_name is REQUIRED when passing services - otherwise service relations fail silently.

  • Do NOT mix IDs and names in one services call.

  • Right side of => is NAME ONLY (no ID lookup).

  • CSV format: comma-only, NO spaces after commas. BD splits on raw , and does NOT trim whitespace - " b" and "b" store as different values, breaking downstream matches.

  • Auto-create ALWAYS ON for createUser - create_new_categories=true is hardcoded. Unknown profession_name/services names auto-create; no flag needed.

Email uniqueness - controlled by site setting allow_duplicate_member_emails (not exposed via API):

  • OFF (default): BD rejects duplicate email on create (validation error).

  • ON: duplicates accepted and stored with different user_ids. Identical email + password pair is also accepted (no extra uniqueness check on the pair).

  • Safe idempotent pattern: pre-check via listUsers property=email property_value=<email> property_operator==. Match found -> reuse user_id or updateUser instead. Skipping the check on a duplicates-allowed site creates orphan duplicates on retry loops.

Field validation quirks:

  • token (if supplied): exactly 32 alphanumeric chars, unique across members.

  • URL fields (website, booking_link, blog, facebook, twitter, linkedin, instagram, etc.): must start with http:// or https://. Invalid formats silently skipped.

Profile URL: <site-domain>/<user.filename>. filename is the complete relative path. Do NOT prepend /business///profile///member/ - BD resolves filename verbatim.

Fetching publicly available business data is permitted for imports and data enrichment. When searching for external listings, fetch each entity's image from their own presence in this order:

  1. Logo image from their site's header/nav -> logo (businesses).

  2. Headshot from Home, About or Team page -> profile_photo (individuals).

  3. Follow their social links -> profile image there.

Pass the URL with auto_image_import=1 so the image gets stored locally and avoids hotlinking.

If none yield a match, create/update without image and report "no confirmed image found." Never substitute a stock photo or guess. Skip an entire record and find an alternate listing only when the user explicitly requires images.

See also: updateUser (modify existing), deleteUser (prefer active=3 over delete).

Returns: { status: "success", message: {...createdRecord} } including user_id.

updateUser

Update an existing member/user - Update a member. PATCH semantics - omitted fields untouched; send only what changes.

Required: user_id.

Use when: changing any field on an existing member. Prefer active=3 (Canceled) over deleteUser - reversible.

Enums: active: 1=Not Active, 2=Active, 3=Canceled, 4=On Hold, 5=Past Due, 6=Incomplete. listing_type: Individual, Company. verified/nationwide: 1/0.

Parameter interactions:

  • member_tag_action=1 + member_tags - apply tag changes (comma-separated tag IDs from listTags).

  • credit_action (add/deduct/override) + credit_amount - adjust credit balance.

  • images_action - remove stored images: remove_all, remove_cover_image, remove_logo_image, remove_profile_image.

  • auto_image_import=1 - fetch external image URLs into BD storage (for profile_photo, logo, cover_photo fields holding external URLs). Without it, BD stores the URL as-is; images break if source host goes down. Supports JPG/PNG/GIF/WebP/SVG. Processing delay: several minutes.

  • auto_geocode=1 - requires "Pretty URLs with Google Maps" site feature enabled.

  • send_email_notifications=1 - trigger welcome email (per plan settings). Silent by default.

Category taxonomy:

  • Top-level: pick ONE of profession_id (numeric, must exist) or profession_name (string).

  • services (sub-categories) supports 4 formats:

    • Single: "PVC repair" or "1823"

    • Multiple: "PVC repair,Water heater" or "1823,1824"

    • Sub-sub via =>: "Honda=>2022" (creates sub "Honda" + sub-sub "2022")

    • Mixed: "Honda=>2022,Honda=>2023,Toyota"

  • profession_id/profession_name is REQUIRED when passing services - otherwise service relations fail silently.

  • Do NOT mix IDs and names in one services call - pick one format.

  • Right side of => is NAME ONLY (no ID lookup).

  • CSV format: comma-only, NO spaces after commas. BD splits on raw , and does NOT trim whitespace - " b" and "b" store as different values, breaking downstream matches.

  • Auto-create OFF by default on update. Pass create_new_categories=1 to auto-create unknown names.

Destructive side effect: changing profession_id WIPES existing sub-category relations for the member. To move to a new top category without losing sub-cats, re-send complete services list in the same call.

Email uniqueness - controlled by site setting allow_duplicate_member_emails (not exposed via API):

  • OFF (default): BD rejects duplicate email on create.

  • ON: duplicates accepted and stored.

  • Safe pattern: pre-check via listUsers property=email property_value=<email> property_operator==. Reuse match's user_id or updateUser instead. Pre-check beats catch-and-retry - on sites with duplicates ALLOWED, BOTH creates succeed with different user_ids, so a retry loop spawns orphan duplicates instead of failing cleanly.

Field validation quirks:

  • token (if supplied): exactly 32 alphanumeric chars, unique across members.

  • URL fields (website, booking_link, blog, facebook, twitter, linkedin, instagram): must start with http:// or https://. Invalid formats silently skipped.

  • subscription_id must reference an existing plan.

Profile URL: <site-domain>/<user.filename>. filename is the complete relative path (e.g., united-states/monterey-park/doctor/harrison-hasanuddin-d-o). Do NOT prepend /business///profile///member/ - BD resolves filename verbatim.

Filename may regenerate on this update. BD rewrites the slug when category/city/URL-forming inputs change. Do NOT cache pre-update filename - if embedding the profile URL elsewhere (blog posts, emails, redirects), re-fetch via getUser AFTER the update completes.

Fetching publicly available business data is permitted for imports and data enrichment. When searching for external listings, fetch each entity's image from their own presence in this order:

  1. Logo image from their site's header/nav -> logo (businesses).

  2. Headshot from Home, About or Team page -> profile_photo (individuals).

  3. Follow their social links -> profile image there.

Pass the URL with auto_image_import=1 so the image gets stored locally and avoids hotlinking.

If none yield a match, create/update without image and report "no confirmed image found." Never substitute a stock photo or guess. Skip an entire record and find an alternate listing only when the user explicitly requires images.

See also: createUser (new), deleteUser (permanent - prefer active=3 instead).

Returns: { status: "success", message: {...updatedRecord} }.

deleteUser

Delete a member/user - Permanently delete a user record by ID. Destructive - cannot be undone via API.

Use when: the member record truly must be purged (GDPR request, test cleanup, confirmed duplicate). For reversible removal prefer updateUser with active=3 (Canceled) - the record stays queryable and can be reactivated. Use delete_images=1 to also purge stored profile/cover/logo images.

Required: user_id.

Parameter interactions:

  • delete_images=1 (optional) - also deletes the member's stored profile/cover/logo images from site storage

See also: updateUser (modify without removing).

Destructive: confirm intent with the user before bulk use. No soft-delete via API - records removed are not recoverable.

Returns: { status: "success", message: "record was deleted" }. No body beyond the confirmation string.

searchUsers

Search members/users - Full-text search across members with category, location, and sorting options.

Lean by default: rows strip the same heavy nested buckets as listUsers. Pass include_*=1 flags to restore specific nested fields.

Use when: (1) mirroring the public member-search experience - embedding search results in an external app, building a custom search-results page, or letting users search BD from outside the site; (2) verifying what is publicly findable for a given keyword / category / location combo (SEO coverage audits, "who shows up if a visitor searches X?"); (3) keyword / partial-name / location / category lookup in general. For exact-field lookup (by email, by user_id, by phone, by any admin column) use listUsers + property / property_value - faster, more precise, and supports admin-only filters (join date, subscription status, meta fields) that this endpoint does not.

Pagination: cursor-based (limit, page). See top-level pagination rule for full cursor/cap/stop semantics.

Enums: sort: reviews, name ASC, name DESC, last_name_asc, last_name_desc.

Parameter interactions:

  • q - keyword (matches first name, last name, company, about, search_description)

  • pid (category ID), tid (sub-category/service ID), ttid (sub-sub-category) - taxonomy filters, use IDs from listTopCategories / listSubCategories

  • address + dynamic=1 - proximity/geographic filtering

  • output_type=html returns BD's public-site search-results markup (embed-ready for apps that display results as HTML); array (default) returns structured JSON for programmatic use

See also: getUser (single record by ID), listUsers (full enumeration).

Returns: { status: "success", message: [...records] }. Supports pagination fields when result set is large.

Profile URL: every user record has a filename field. To get the full public profile URL, concatenate: <site-domain>/<user.filename>. The filename is the complete relative path (e.g., united-states/monterey-park/doctor/harrison-hasanuddin-d-o) - DO NOT prepend /business/, /profile/, /member/, or any other segment. BD's router resolves filename verbatim.

loginUser

Validate user credentials - Checks if email/password are valid. Does NOT return profile data - use getUser after.

Use when: implementing SSO or a custom login flow against BD - you need to verify a member's email+password is valid WITHOUT starting a web session. Does NOT return profile data; follow with getUser or listUsers to fetch the authenticated member's record.

Required: email, password.

Parameter interactions:

  • Does NOT return profile data on success - follow with getUser using the verified email to retrieve the member record

Returns: { status: "success"|"error", message: ... } - BD's standard response envelope.

getUserTransactions

Get member billing transactions (invoices) - Fetch the billing transaction history (invoices) for a specific member. Read-only. Backed by the WHMCS billing integration.

Required: user_id (POST body).

Use when: you need to see a member's paid/unpaid invoices, payment methods, billing history, or reconcile billing status. Common reasons: answering a member's "what did I pay for?" question, exporting billing history, auditing revenue per member.

See also: getUserSubscriptions (active/past membership plan signups - different resource from invoices), getUser (member profile).

Returns: { status: "success", message: { total: <count>, invoices: [{...invoice records}] } }. Each invoice includes id, invoicenum (may be empty string), date, duedate, datepaid, subtotal, credit, tax, total, status (Paid, Unpaid, etc.), paymentmethod, notes (admin-facing; may contain internal comments - redact before surfacing to end users), and an items array with per-line description, amount, type. NOT a simple list of rows - the message is an object containing invoices as the array. Unpaid invoices have datepaid: "0000-00-00 00:00:00" (MariaDB zero-date sentinel) - do NOT parse as ISO-8601; check status === 'Unpaid' or datepaid.startsWith('0000') first. subscription_details may be false (literal boolean) when absent.

Reference support articles:

getUserSubscriptions

Get member subscriptions (membership plan history) - Fetch the subscription / membership-plan history for a specific member. Read-only. Backed by the WHMCS billing integration.

Required: user_id (POST body).

Use when: checking a member's current membership plan, their billing cycle (Monthly/Yearly), next due date, plan upgrade history, whether auto-renewal is on, or past canceled subscriptions.

See also: getUserTransactions (invoice-level billing records - different resource), getUser (member profile - profile-level subscription references subscription_id), listMembershipPlans (all plan definitions on the site).

Returns: { status: "success", message: { total: <count>, subscriptions: [{...subscription records}] } }. Each subscription includes id, userid, packageid (membership plan ID), regdate, nextduedate, billingcycle (e.g. Monthly, Yearly), paymentmethod, amount, domainstatus (Active, Cancelled, etc.), and related fields. NOT a simple list of rows - the message is an object containing subscriptions as the array.

Reference support articles:

listReviews

List reviews - Paginated enumeration of review records. Read-only.

Lean by default: review_description is truncated to the first 500 chars + when longer. Truncated rows are tagged review_description_truncated: true. Pass include_full_text=1 to restore the full body per call (use sparingly at high limit — review text is unbounded and can dominate payload).

Use when: building moderation queues (filter review_status=0 for Pending), exporting all reviews, running review-velocity reports, or paginating through every review on the site. For searching by keyword in review text use searchReviews; for a single known review use getReview.

Pagination: cursor-based (limit, page). See top-level pagination rule for full cursor/cap/stop semantics.

Filter/sort: property+property_value+property_operator, order_column+order_type. See top-level filter rule for reliable operators (= only), silent-drop detection, and derived-field unfilterability.

Enums: property_operator: =, LIKE, >, <, >=, <=.

See also: getReview (single record by ID), searchReviews (keyword search).

Returns: { status: "success", total, current_page, total_pages, next_page, prev_page, message: [...records] }. Each record is the full resource object (with review_description truncated by default; see Lean note).

getReview

Get a single review - Fetch a single review record. Read-only.

Lean by default: review_description is truncated to the first 500 chars + when longer (tagged review_description_truncated: true). Pass include_full_text=1 to get the complete body. For a single-record inspection this is usually the right call.

Use when: investigating one specific review (usually from a moderation notification or support ticket that includes the review_id). For bulk moderation use listReviews with review_status filter.

Required: review_id.

See also: listReviews (enumerate many), searchReviews (keyword search).

Returns: { status: "success", message: [{...record}] } - the message array contains 1 record when found. Empty or HTTP 404 when not found.

createReview

Create a review - Create a new review record. Writes live data.

Use when: importing legacy reviews from another platform, adding placeholder reviews for test data, or scripting review submissions from an external integration. Real member-submitted reviews come through the BD review form - only use this API when bypassing that form.

Required: user_id, review_email.

Parameter interactions:

  • user_id - the member being reviewed

  • rating_overall: integer 1-5 (higher = better)

  • recommend: 0=No, 1=Yes (shown as a thumbs-up recommendation flag)

  • review_status controls initial visibility - default flow is 0 Pending -> admin review

See also: updateReview (modify existing).

updateReview

Update a review - Update an existing review record by ID. Fields omitted are untouched. Writes live data.

Use when: moderating - change review_status (0=Pending -> 2=Accepted to publish, 3=Declined to reject, 4=Waiting for Admin). Also used for admin corrections of typos in review text.

Required: review_id.

Enums: review_status: 0=Pending, 2=Accepted, 3=Declined, 4=Waiting for Admin.

See also: createReview (add new), deleteReview (remove permanently).

Returns: { status: "success", message: {...updatedRecord} } - the full updated record after changes applied.

deleteReview

Delete a review - Permanently delete a review record by ID. Destructive - cannot be undone via API.

Use when: the review content violates policy and must be purged (spam, abuse, PII). For "hide without removing" use updateReview with review_status=3 (Declined) - preserves the audit trail.

Required: review_id.

See also: updateReview (modify without removing).

Destructive: confirm intent with the user before bulk use. No soft-delete via API - records removed are not recoverable.

Returns: { status: "success", message: "record was deleted" }. No body beyond the confirmation string.

searchReviews

Search reviews - Keyword search across review records. Read-only.

Lean by default: result rows' review_description is truncated to the first 500 chars + when longer (tagged review_description_truncated: true). Pass include_full_text=1 to restore full bodies — needed for keyword-in-body verification and full-content export. Note the search still matches against BD's full text server-side; truncation only affects what's returned.

Use when: the admin wants to find reviews by keyword in the review text, filter by status, or pull all reviews for one member (pass user_id). For structured filtering by any review column (rating, date, property), sorting, or full enumeration use listReviews + property / property_value. For a single known review use getReview.

Pagination: cursor-based (limit, page). See top-level pagination rule for full cursor/cap/stop semantics.

Enums: review_status: 0=Pending, 2=Accepted, 3=Declined, 4=Waiting for Admin.

See also: getReview (single record by ID), listReviews (full enumeration).

Returns: { status: "success", message: [...records] }. Supports pagination fields when result set is large.

listClicks

List click records - Paginated enumeration of click records. Read-only.

Use when: pulling click-tracking analytics for reports - profile views, phone reveals, website clicks, email clicks. Filter by user_id to see clicks for one member.

Pagination: cursor-based (limit, page). See top-level pagination rule for full cursor/cap/stop semantics.

Filter/sort: property+property_value+property_operator, order_column+order_type. See top-level filter rule for reliable operators (= only), silent-drop detection, and derived-field unfilterability.

See also: getClick (single record by ID).

Returns: { status: "success", total, current_page, total_pages, next_page, prev_page, message: [...records] }. Each record is the full resource object.

getClick

Get a single click record - Fetch a single click record. Read-only.

Use when: rare - drilling into a specific click record by click_id. Most click-analytics work happens via listClicks with filters.

Required: click_id.

See also: listClicks (enumerate many).

Returns: { status: "success", message: [{...record}] } - the message array contains 1 record when found. Empty or HTTP 404 when not found.

createClick

Create a click record - Create a new click record. Writes live data.

Use when: replicating a click event from an external source (e.g., tracking clicks on a mirrored profile page on another domain). Usually not needed - BD auto-records clicks on its own surfaces.

Required: user_id, click_type, click_name, click_from, click_url.

Parameter interactions:

  • user_id - the member profile being tracked

  • click_type - link (external), phone (reveal), or email (reveal)

  • click_from - source surface: profile_page or search_results

  • click_url - the URL that was clicked

See also: updateClick (modify existing).

updateClick

Update a click record - Update an existing click record by ID. Fields omitted are untouched. Writes live data.

Use when: correcting click metadata. Rare - click records are typically append-only analytics.

Required: click_id.

See also: createClick (add new), deleteClick (remove permanently).

Returns: { status: "success", message: {...updatedRecord} } - the full updated record after changes applied.

deleteClick

Delete a click record - Permanently delete a click record by ID. Destructive - cannot be undone via API.

Use when: removing test or spam click records from analytics. Does NOT affect the member's click counter if the site displays one.

Required: click_id.

See also: updateClick (modify without removing).

Destructive: confirm intent with the user before bulk use. No soft-delete via API - records removed are not recoverable.

Returns: { status: "success", message: "record was deleted" }. No body beyond the confirmation string.

listLeads

List leads - Paginated enumeration of lead records. Read-only.

Use when: pulling the admin's lead inbox, generating lead reports, or iterating all leads to push into a CRM. For fetching one lead by ID use getLead.

Pagination: cursor-based (limit, page). See top-level pagination rule for full cursor/cap/stop semantics.

Filter/sort: property+property_value+property_operator, order_column+order_type. See top-level filter rule for reliable operators (= only), silent-drop detection, and derived-field unfilterability.

See also: getLead (single record by ID).

Returns: { status: "success", total, current_page, total_pages, next_page, prev_page, message: [...records] }. Each record is the full resource object.

getLead

Get a single lead - Fetch a single lead record. Read-only.

Use when: handling one lead - viewing its details after a lead-notification email, following up in a CRM integration, or confirming the lead exists before calling matchLead.

Required: lead_id.

See also: listLeads (enumerate many).

Returns: { status: "success", message: [{...record}] } - the message array contains 1 record when found. Empty or HTTP 404 when not found.

createLead

Create a lead - Create a new lead record. Writes live data.

Use when: importing leads from an external form, CSV, or web-scrape. Default is SILENT - no notification emails fire unless you pass send_lead_email_notification=1, and no member routing happens unless you pass auto_match=1 (inline) or call matchLead afterward. top_id (category) is required - look it up via listTopCategories.

Required: lead_name, lead_email, lead_phone, lead_message, lead_location, top_id.

Parameter interactions:

  • top_id - category ID; discover via listTopCategories

  • All 6 required fields (lead_name, lead_email, lead_phone, lead_message, lead_location, top_id) must be supplied together

  • Response includes lead_id AND token - token may be needed for customer-facing URLs

  • After creating, call matchLead to trigger member notifications

See also: updateLead (modify existing).

Operational rules (from BD support article 12000091106):

  • send_lead_email_notification=1 - activates lead email notifications to the site admin and/or matched members. Default is off: leads created via API are silent unless this flag is set. For the full auto-matching flow (finds members by category/location and emails them), call matchLead separately after creating the lead (or pass auto_match=1 on this call to run inline).

Targeting specific members (override auto-match): set users_to_match to a comma-separated list of member IDs or emails (e.g. 6099,6100 or user1@example.com,user2@example.com, mixed OK). This BYPASSES the normal category/location/service-area matching and routes the lead to ONLY those members. Typically paired with auto_match=1 (to run the match step inline) and send_lead_email_notification=1 (to fire the matched-member email). Common pattern when an external system already knows who should receive the lead.

matchLead

Auto-match lead to members - Triggers automatic matching - system finds members matching category, location, and service area, then sends notification emails.

Use when: you've just created a lead (or need to re-distribute an existing one) and want BD to automatically email eligible members in matching category + location + service area. SIDE EFFECT: sends real emails to real members. Confirm with the user before calling on production data.

Required: lead_id.

Parameter interactions:

  • Side effect: sends notification emails to ALL members whose category, location, and service area match the lead

  • Not a dry-run - emails go out immediately. Not rate-limited per lead

  • lead_id must reference an existing lead created via createLead

Returns: { status: "success"|"error", message: ... } - BD's standard response envelope.

updateLead

Update a lead - Update an existing lead record by ID. Fields omitted are untouched. Writes live data.

Use when: adding admin notes to a lead, updating lead status, or correcting lead metadata. For deleting use deleteLead; for re-matching members use matchLead.

Required: lead_id.

Enums: lead_status: 1=Pending, 2=Matched, 4=Follow-Up, 5=Sold Out, 6=Closed, 7=Bad Leads, 8=Delete. (Verified against admin UI dropdown 2026-04-19. Value 3 does not exist. BD accepts out-of-range integers silently - stick to this set.)

See also: createLead (add new), deleteLead (remove permanently).

Returns: { status: "success", message: {...updatedRecord} } - the full updated record after changes applied.

deleteLead

Delete a lead - Permanently delete a lead record by ID. Destructive - cannot be undone via API.

Use when: removing a spam or test lead. For preserving the lead but closing it, use updateLead with a status change instead.

Required: lead_id.

See also: updateLead (modify without removing).

Destructive: confirm intent with the user before bulk use. No soft-delete via API - records removed are not recoverable.

Returns: { status: "success", message: "record was deleted" }. No body beyond the confirmation string.

listLeadMatches

List lead matches - Paginated enumeration of lead_matches records. Read-only.

Use when: auditing who got notified about which lead - useful for billing reports (paid-per-lead sites) or explaining to a member why they did/didn't receive a lead notification. Filter by lead_id to see all matches for one lead, or by user_id to see all leads a member received.

Empty-state quirk: when zero rows match the filter (or the lead_matches table is empty on the site), this endpoint returns { status: "error", message: "lead_matches not found", total: 0 } - NOT the standard { status: "success", total: 0, message: [] } that other list endpoints return. Treat the exact message "lead_matches not found" as an empty result, not as an endpoint failure. Once any match exists, normal success-shape responses resume.

Pagination: cursor-based (limit, page). See top-level pagination rule for full cursor/cap/stop semantics.

Filter/sort: property+property_value+property_operator, order_column+order_type. See top-level filter rule for reliable operators (= only), silent-drop detection, and derived-field unfilterability.

Returns: { status: "success", total, current_page, total_pages, next_page, prev_page, message: [...records] } when matches exist. See empty-state quirk above.

getLeadMatch

Get a single lead match - Fetch a single leadmatch record. Read-only.

Use when: you have a specific match_id (from listLeadMatches) and need the full match row - lead points, price, response status, etc.

Required: match_id.

Returns: { status: "success", message: [{...record}] } - the message array contains 1 record when found. Empty or HTTP 404 when not found.

createLeadMatch

Create a lead match - Create a new leadmatch record. Writes live data.

Use when: manually creating a lead↔member match BYPASSING BD's auto-matching. Rarely needed - usually matchLead handles this automatically. Use for data migrations, manual override scenarios, or replaying matches from another system.

Required: lead_id, user_id, lead_status, match_price, lead_token, lead_matched_by.

Pre-check before create (PAIR uniqueness): BD does NOT enforce uniqueness on the (lead_id, user_id) pair. Matching the same lead to the same member twice creates two match rows - the member gets double-billed (if the match charges credits), both rows appear in the member's inbox, and reporting double-counts the match. Filter-find pattern (single-field server filter + client-side intersect - the server does not yet honor array-syntax multi-condition filters): call listLeadMatches property=lead_id property_value=<proposed lead_id> property_operator== to narrow to all matches for that lead, then CLIENT-SIDE filter the returned rows to those where user_id=<proposed user_id>. Zero results after the client-side step = pair free; >=1 = already matched. If the pair already exists: reuse via updateLeadMatch (e.g. to bump lead_status), OR confirm with the user before creating the duplicate match. Never silently double-match.

Parameter interactions:

  • Usually created automatically by matchLead; manual creation bypasses BD's matching logic

  • lead_id and user_id must both exist (use getLead / getUser to verify)

  • lead_status - match lifecycle state (see Enums)

  • match_price, lead_points, lead_rating, lead_distance - scoring fields used in ranking and billing

See also: updateLeadMatch (modify existing).

updateLeadMatch

Update a lead match - Update an existing leadmatch record by ID. Fields omitted are untouched. Writes live data.

Use when: recording a member's response to a lead (lead_response, lead_accepted, lead_chosen) or adjusting lead_points/match_price for billing reconciliation.

Required: match_id.

Enums: lead_status: 1=Pending, 2=Matched, 4=Follow-Up, 5=Sold Out, 6=Closed, 7=Bad Leads, 8=Delete. (Verified against admin UI dropdown 2026-04-19. Value 3 does not exist. BD accepts out-of-range integers silently - stick to this set.)

See also: createLeadMatch (add new), deleteLeadMatch (remove permanently).

Returns: { status: "success", message: {...updatedRecord} } - the full updated record after changes applied.

deleteLeadMatch

Delete a lead match - Permanently delete a leadmatch record by ID. Destructive - cannot be undone via API.

Use when: cleaning up an erroneous match (e.g., test data) or removing a match that was auto-created but shouldn't exist. Does NOT unsend the notification email that may have already fired.

Required: match_id.

See also: updateLeadMatch (modify without removing).

Destructive: confirm intent with the user before bulk use. No soft-delete via API - records removed are not recoverable.

Returns: { status: "success", message: "record was deleted" }. No body beyond the confirmation string.

listSingleImagePosts

List posts - Paginated enumeration of post records. Read-only.

Lean by default: each row strips post_content (HTML body), post_meta_* (SEO bundle), the full nested user author object, the full nested data_category post-type config, user_clicks_schema.clicks array, list_service, and ~25 admin-form residue fields. Post rows always include data_id, data_type, system_name, data_name, data_filename, form_name for post-type routing - full post-type config (sidebars, code fields, search modules, h1/h2, timestamps) is NOT returned on post reads; call getPostType with data_id if you need it. Replaces full user with author: {...} summary (user_id, first_name, last_name, company, email, phone_number, filename, image_main_file, subscription_id, active). Replaces click array with total_clicks: N. Use include_* flags to restore specific nested fields.

Use when: enumerating posts of single-image families - blog articles, events, jobs, coupons, videos, discussions. Filter by user_id for one member's posts, or data_id to scope to one post type. Before using, confirm the target post type has data_type 9 or 20 (single-image); data_type=4 means multi-image and you want listMultiImagePosts instead.

Pagination: cursor-based (limit, page). See top-level pagination rule for full cursor/cap/stop semantics.

Filter/sort: property+property_value+property_operator, order_column+order_type. See top-level filter rule for reliable operators (= only), silent-drop detection, and derived-field unfilterability.

See also: getSingleImagePost (single record by ID), searchSingleImagePosts (keyword search).

Returns: { status: "success", total, current_page, total_pages, next_page, prev_page, message: [...records] }. Each record is the full resource object.

getSingleImagePost

Get a single post - Fetch a single post record. Read-only.

Lean by default: response strips the same heavy nested buckets as listSingleImagePosts (see its lean note). Pass include_*=1 flags to restore specific nested fields.

Use when: fetching one post by post_id. For enumeration use listSingleImagePosts; for keyword search use searchSingleImagePosts.

Required: post_id.

See also: listSingleImagePosts (enumerate many), searchSingleImagePosts (keyword search).

Returns: { status: "success", message: [{...record}] } - the message array contains 1 record when found. Empty or HTTP 404 when not found.

getSingleImagePostFields

Get post field definitions - Returns custom fields for a specific post type form.

Use when: discovering the per-post-type custom fields before building a create/update payload. Pass form_name of the target post type (e.g. blog_article_fields, events_fields, etc.).

Required: form_name.

Returns: a BARE ARRAY of field-definition objects (NOT wrapped in {status, message}). Each entry has key, label, required, type, and optionally choices (for dropdown/radio), default, helpText.

Silent-fallback warning: if form_name does NOT match a real post-type form, BD returns HTTP 200 with a generic SUPER-UNION field list (containing every possible post field including post_location, lat, lon, post_live_date, post_video, post_job, internals like post_type/logged_user/form_security_token) - and post_category.choices will be ABSENT. Always verify form_name exists first via listPostTypes (look for the matching row's form_name column). If your response has post_category WITHOUT a choices array, you hit the fallback.

Discovering post_category dropdown values: post_category is a per-post-type dropdown whose options come from the post type's feature_categories CSV (admin-managed). Read post_category.choices[].key for the exact values to send. BD does NOT trim whitespace when splitting the CSV - options after the first may have a leading space (e.g. " Category 2"). Pass VERBATIM.

createSingleImagePost

Create a post - Create a new post record. Writes live data.

Use when: creating a blog article, event, job listing, coupon, or any other single-image post type. Look up data_id + data_type via listPostTypes first - the post type's data_type field determines which create endpoint is correct. If data_type=4 on the post type, use createMultiImagePost instead. For posts with scraped external image URLs, include auto_image_import=1 to fetch and store them locally.

Required: user_id, data_id, data_type.

Pre-check before create: BD does NOT enforce uniqueness on post_title, and BD auto-generates filename (the URL slug) from the title - so a duplicate title produces a URL collision (two posts fighting for the same public URL, unpredictable which one resolves). Do a server-side filter-find: searchSingleImagePosts property=post_title property_value=<proposed> property_operator== (or listSingleImagePosts with the same filter). Zero rows = title free; >=1 row = taken. Do NOT paginate unfiltered lists - sites in the wild have thousands of posts; filtered lookup is one tiny response. If taken: reuse via updateSingleImagePost, OR ask the user, OR pick an alternate post_title and re-check. Never silently create a duplicate.

Parameter interactions:

  • user_id - owner; must be an existing member (discover via listUsers or searchUsers)

  • data_id - post type category ID; get via listPostTypes

  • data_type - data type classification; usually matches the post type's data type

  • post_status: 0=Draft (not visible), 1=Published (public)

  • Response includes both post_id and post_token - the token is used for sharable URLs

See also: updateSingleImagePost (modify existing).


Which endpoint to use - data_type family decides:

Every post type in data_categories has a data_type field that classifies its family. Call listPostTypes or getPostType to see the data_type of your target post type, then choose:

data_type value

Family

Use endpoint

4

Multi-image (albums, galleries, photo-heavy listings - e.g. Classified, Photo Album, Property, Product)

createMultiImagePost

9

Single-image video

createSingleImagePost

20

Single-image article / event / blog / job / coupon

createSingleImagePost

10, 13, 21, 28, 29 (and similar)

Internal admin types (Member Listings, Reviews, Sub Accounts, Specialties, Favorites) - NOT posts

Use the resource-specific endpoint (e.g. createReview for data_type=13)

If you call the wrong create endpoint for a given post type, BD may accept the row but it won't render on the public site correctly.

For "make a blog post" intent: look up data_categories for data_name matching "blog" (commonly data_id=14 with data_type=20) -> createSingleImagePost with that data_id + data_type.

For "make a photo album" / "gallery" intent: look up the album post type (often data_id=10, data_type=4) -> createMultiImagePost with that data_id + data_type. Photos are added separately via createMultiImagePostPhoto using the returned group_id.

Picking post_category (and other per-type dropdowns): post_category values are configured PER POST TYPE by the site admin in the post type's feature_categories CSV. Before create, call getSingleImagePostFields(form_name=<post type's form_name>) and read post_category.choices[].key. Pass ONLY values from that list, VERBATIM - BD does not trim whitespace when splitting feature_categories, so options after the first may have a leading space (e.g. " Category 2"). If the user names a category that isn't in the list: ask whether to pick the closest existing option or have them add the new option in BD admin first - do NOT invent a new value. WARNING: if form_name does not match a real post type form, getSingleImagePostFields silently returns a generic SUPER-UNION field list (HTTP 200, no error) - verify form_name exists in listPostTypes first.

updateSingleImagePost

Update a post - Update an existing post record by ID. Fields omitted are untouched. Writes live data.

Use when: editing post content, switching from draft to published (post_status=0->1), updating post title/caption, or correcting post metadata. To move a post to a different post type (rare), pass data_id - but validate the new post type is still in the single-image family.

Required: post_id.

Enums: post_status: 0=Draft (saved but not publicly visible), 1=Published (publicly visible on the site).

post_title rename does NOT update post_filename (the URL slug). post_filename is writable — see corpus URL slug rule for when to suggest a slug update + redirect. Report post_filename from getSingleImagePost when giving the user a URL.

See also: createSingleImagePost (add new), deleteSingleImagePost (remove permanently).

Returns: { status: "success", message: {...updatedRecord} } - the full updated record after changes applied.

deleteSingleImagePost

Delete a post - Permanently delete a post record by ID. Destructive - cannot be undone via API.

Use when: removing a post permanently. For "hide without deleting" use updateSingleImagePost with post_status=0 (Draft). Deleting also removes the post_token, breaking any external links to the share URL.

Required: post_id.

See also: updateSingleImagePost (modify without removing).

Destructive: confirm intent with the user before bulk use. No soft-delete via API - records removed are not recoverable.

Returns: { status: "success", message: "record was deleted" }. No body beyond the confirmation string.

searchSingleImagePosts

Search posts - Keyword search across post records. Read-only.

Lean by default: rows strip the same heavy nested buckets as listSingleImagePosts (see its lean note). Pass include_*=1 flags to restore specific nested fields.

Use when: finding a post by keyword in its title / caption / content, or by author (user_id). Pass data_id to scope search to one post type - strongly recommended since results depend on per-post-type configuration. For structured filtering by any post column (status, date, category) or sorting use listSingleImagePosts + property / property_value. For a single known post use getSingleImagePost.

Pagination: cursor-based (limit, page). See top-level pagination rule for full cursor/cap/stop semantics.

Parameter interactions:

  • data_id (post type) is strongly recommended - results vary significantly by post type configuration

  • q - keyword across post title, caption, content

  • Fields exposed in search depend on the post type; use getSingleImagePostFields with form_name to discover them

See also: getSingleImagePost (single record by ID), listSingleImagePosts (full enumeration).

Returns: { status: "success", message: [...records] }. Supports pagination fields when result set is large.

listMultiImagePosts

List album groups - Paginated enumeration of portfoliogroup records. Read-only.

Lean by default: each row strips group_desc (HTML body, field name differs from Single posts), nested user author object, nested data_category, user_clicks_schema.clicks, and nested users_portfolio photo array (keeps cover_photo_url, cover_thumbnail_url, total_photos summary from the first photo). Post rows always include data_id, data_type, system_name, data_name, data_filename, form_name for post-type routing - full post-type config is NOT returned; call getPostType with data_id if you need it. Strips ~25 admin-form residue fields. Replaces user with author: {...} summary. Replaces clicks with total_clicks: N. Use include_* flags to restore anything stripped.

Use when: enumerating photo-album / gallery-style posts (Photo Album, Classified, Property, Product - any post type with data_type=4). For single-image post types use listSingleImagePosts.

Pagination: cursor-based (limit, page). See top-level pagination rule for full cursor/cap/stop semantics.

Filter/sort: property+property_value+property_operator, order_column+order_type. See top-level filter rule for reliable operators (= only), silent-drop detection, and derived-field unfilterability.

See also: getMultiImagePost (single record by ID), searchMultiImagePosts (keyword search).

Returns: { status: "success", total, current_page, total_pages, next_page, prev_page, message: [...records] }. Each record is the full resource object.

getMultiImagePost

Get a single album group - Fetch a single portfoliogroup record. Read-only.

Lean by default: response strips the same heavy nested buckets as listMultiImagePosts (see its lean note). Pass include_*=1 flags to restore specific nested fields.

Use when: fetching one multi-image post by group_id. Photos in this post are loaded separately via listMultiImagePostPhotos with group_id filter.

Required: group_id.

See also: listMultiImagePosts (enumerate many), searchMultiImagePosts (keyword search).

Returns: { status: "success", message: [{...record}] } - the message array contains 1 record when found. Empty or HTTP 404 when not found.

getMultiImagePostFields

Get album group field definitions - Fetch field definitions for a multi-image post type form. Read-only.

Use when: discovering per-post-type custom fields for multi-image posts - same pattern as getSingleImagePostFields but for the users_portfolio_groups resource.

Required: form_name.

Returns: a BARE ARRAY of field-definition objects (NOT wrapped in {status, message}). Each entry has key, label, required, type, and optionally choices, default, helpText. Multi-image post fields seen: user_id, group_status, group_name, group_desc, post_image (CSV of image URLs), auto_image_import, post_tags, auto_geocode. Categorization for multi-image posts is exposed under an internal widget-controller field name, not a clean post_category - not straightforward to write via API.

Silent-fallback warning: if form_name does NOT match a real post-type form, BD may return a generic field list without error. Verify form_name exists in listPostTypes before trusting the response.

createMultiImagePost

Create an album group - Create a new portfoliogroup record. Writes live data.

Use when: creating a photo album, gallery, product listing with multiple photos, or any post type with data_type=4. Confirm the target post type's data_type via listPostTypes first - data_type=4 belongs here; 9/20 belongs in createSingleImagePost. For external image URLs, always use the bulk post_image CSV + auto_image_import=1 here - this is the only path that imports externals into site storage. createMultiImagePostPhoto does NOT import and is only suitable for already-hosted-on-site URLs.

Required: user_id, data_id, data_type.

Pre-check before create: BD does NOT enforce uniqueness on post_title, and the public URL slug is derived from the title - so a duplicate title produces a URL collision (two albums fighting for the same public URL, unpredictable which one resolves). Do a server-side filter-find: searchMultiImagePosts property=post_title property_value=<proposed> property_operator== (or listMultiImagePosts with the same filter). Zero rows = title free; >=1 row = taken. Do NOT paginate unfiltered lists - filtered lookup is one tiny response. If taken: reuse via updateMultiImagePost, OR ask the user, OR pick an alternate post_title and re-check. Never silently create a duplicate.

Parameter interactions:

  • data_id + data_type - specify the post type this album belongs to (from listPostTypes; data_type must be 4)

  • group_status: 0=Hidden, 1=Published

  • post_image - comma-separated image URLs imported as child photos at create time

  • auto_image_import=1 - fetches the post_image URLs into site storage (required for external sources to survive)

Post-create verification (critical): HTTP 200 does NOT mean every photo imported. After create, call listMultiImagePostPhotos property=group_id&property_value=<new_group_id>&property_operator==; row count must equal CSV count and every row needs non-empty file + image_imported=2 (success; 0 = silent-failure row). Fix failed row: deleteMultiImagePostPhoto, then updateMultiImagePost group_id=<same>&post_image=<replacement>&auto_image_import=1 (appends). Do NOT delete and recreate the album.

See also: updateMultiImagePost (modify existing), createMultiImagePostPhoto (already-hosted URLs only).

Returns: { status: "success", message: {...createdRecord} } - includes the server-assigned group_id.


Which endpoint to use - data_type family decides:

Every post type in data_categories has a data_type field that classifies its family. Call listPostTypes or getPostType to see the data_type of your target post type, then choose:

data_type value

Family

Use endpoint

4

Multi-image (albums, galleries, photo-heavy listings - e.g. Classified, Photo Album, Property, Product)

createMultiImagePost

9

Single-image video

createSingleImagePost

20

Single-image article / event / blog / job / coupon

createSingleImagePost

10, 13, 21, 28, 29 (and similar)

Internal admin types (Member Listings, Reviews, Sub Accounts, Specialties, Favorites) - NOT posts

Use the resource-specific endpoint (e.g. createReview for data_type=13)

If you call the wrong create endpoint for a given post type, BD may accept the row but it won't render on the public site correctly.

Category for multi-image posts: multi-image posts do NOT expose post_category like single-image posts do. Album-level categorization is configured differently in BD admin and is not cleanly writable via the create payload. If categorization is needed, add it via a follow-up updateMultiImagePost or BD admin.

updateMultiImagePost

Update an album group - Update an existing portfoliogroup record by ID. Fields omitted are untouched. Writes live data.

Use when: editing metadata (title, description, group_status) OR appending photos via post_image CSV + auto_image_import=1 (APPENDS, does not replace). Existing photos are edited via updateMultiImagePostPhoto (title/order only).

Required: group_id.

Enums: group_status: 0=Draft, 1=Published.

Verify appended photos via listMultiImagePostPhotos, NOT via getMultiImagePost.post_image. The parent's post_image field is a transient write-through, not a mirror of child rows — it does NOT reflect appended photos. Child rows land in users_portfolio. Silent-failure possible (empty file, image_imported=0) — check each child row.

group_name rename does NOT update group_filename (the URL slug). group_filename is writable — see corpus URL slug rule for when to suggest a slug update + redirect. Report group_filename from getMultiImagePost when giving the user a URL.

See also: createMultiImagePost, deleteMultiImagePost, deleteMultiImagePostPhoto.

Returns: { status: "success", message: {...updatedRecord} } - photo rows land asynchronously.

deleteMultiImagePost

Delete an album group - Permanently delete a portfoliogroup record by ID. Destructive - cannot be undone via API.

Use when: removing the entire album. Recommended sequence: delete child photos first via deleteMultiImagePostPhoto (enumerate via listMultiImagePostPhotos property=group_id&property_value=<id>&property_operator==), THEN delete the group. BD does not cascade — skipping this leaves orphan users_portfolio rows.

Required: group_id.

See also: updateMultiImagePost (modify without removing).

Destructive: confirm intent with the user before bulk use. No soft-delete via API - records removed are not recoverable.

Returns: { status: "success", message: "record was deleted" }. No body beyond the confirmation string.

searchMultiImagePosts

Search album groups - Keyword search across portfoliogroup records. Read-only.

Lean by default: rows strip the same heavy nested buckets as listMultiImagePosts (see its lean note). Pass include_*=1 flags to restore specific nested fields.

Use when: finding a multi-image post (photo album, classified, property, product) by keyword in its title / description, or by author (user_id). Pass data_id to scope search to one multi-image post type. For structured filtering by any post column (status, date, category) or sorting use listMultiImagePosts + property / property_value. For a single known post use getMultiImagePost.

Pagination: cursor-based (limit, page). See top-level pagination rule for full cursor/cap/stop semantics.

See also: getMultiImagePost (single record by ID), listMultiImagePosts (full enumeration).

Returns: { status: "success", message: [...records] }. Supports pagination fields when result set is large.

listMultiImagePostPhotos

List album photos - Paginated enumeration of portfoliophoto records. Read-only.

Use when: fetching all photos within a multi-image post - always pass group_id to filter. For a single photo by ID use getMultiImagePostPhoto.

Pagination: cursor-based (limit, page). See top-level pagination rule for full cursor/cap/stop semantics.

Filter/sort: property+property_value+property_operator, order_column+order_type. See top-level filter rule for reliable operators (= only), silent-drop detection, and derived-field unfilterability.

See also: getMultiImagePostPhoto (single record by ID).

Returns: { status: "success", total, current_page, total_pages, next_page, prev_page, message: [...records] }. Each record is the full resource object.

getMultiImagePostPhoto

Get a single album photo - Fetch a single portfoliophoto record. Read-only.

Use when: editing or removing one specific photo within an album. You need the photo_id (from listMultiImagePostPhotos).

Required: photo_id.

See also: listMultiImagePostPhotos (enumerate many).

Returns: { status: "success", message: [{...record}] } - the message array contains 1 record when found. Empty or HTTP 404 when not found.

createMultiImagePostPhoto

Create an album photo - Create a new portfoliophoto record. Writes live data.

Use when: adding ONE image to an existing album where the image is already hosted on the BD site (or hotlinking is acceptable). The parent album must exist (call createMultiImagePost first to get group_id).

Does NOT import external URLs. This endpoint has no auto_image_import field — the URL is recorded as-is. The parent album's auto_image_import=1 applies only to photos passed via the parent's bulk post_image CSV at create time; it does NOT cascade to subsequent createMultiImagePostPhoto calls. For external URLs that must survive source outages (e.g. Pexels, stock sites), do NOT use this endpoint — create a NEW album via createMultiImagePost with a bulk CSV post_image + auto_image_import=1, and delete the old album.

Required: user_id, group_id.

Parameter interactions:

  • group_id - parent album group (from createMultiImagePost or listMultiImagePosts)

  • original_image_url - full URL of the image; must already be publicly accessible; stored verbatim (no fetch)

See also: createMultiImagePost (the correct path for external URLs), updateMultiImagePostPhoto (modify title/order only — cannot re-import).

updateMultiImagePostPhoto

Update an album photo - Update an existing portfoliophoto record by ID. Fields omitted are untouched. Writes live data.

Use when: reordering photos within an album (order field) or renaming (title).

Required: photo_id.

Cannot re-import a failed image. Only writes title and order. To fix an image_imported=0 row: deleteMultiImagePostPhoto, then updateMultiImagePost group_id=<same>&post_image=<new_url>&auto_image_import=1 (appends).

See also: createMultiImagePostPhoto (add new), deleteMultiImagePostPhoto (remove permanently).

Returns: { status: "success", message: {...updatedRecord} } - the full updated record after changes applied.

deleteMultiImagePostPhoto

Delete an album photo - Permanently delete a portfoliophoto record by ID. Destructive - cannot be undone via API.

Use when: permanently removing one photo from an album. For "hide" use updateMultiImagePostPhoto with status=0.

Required: photo_id.

See also: updateMultiImagePostPhoto (modify without removing).

Destructive: confirm intent with the user before bulk use. No soft-delete via API - records removed are not recoverable.

Returns: { status: "success", message: "record was deleted" }. No body beyond the confirmation string.

listPostTypes

List post types - Paginated enumeration of posttype records. Read-only.

Lean by default: rows strip the PHP/HTML code-template fields (search_results_div, search_results_layout, profile_results_layout, profile_header, profile_footer, category_header, category_footer, comments_code), the post_comment_settings JSON-string field, and the 5 review-notification email template fields. Strips ~10 admin-form residue fields (website_id, myid, method, id, save, form, form_fields_name, fromcron, zzz_fake_field, customize). Per-row drops from ~3.5KB (minimal config) / 15-30KB (populated code) to ~1.5KB. Opt back in with include_code=1 (when editing post-type templates), include_post_comment_settings=1, or include_review_notifications=1.

Use when: discovering which post types exist on this site AND their data_type families. The data_type value on each row tells you whether a post type belongs to createSingleImagePost (9/20) or createMultiImagePost (4). Use this BEFORE calling either create endpoint to pick the correct tool. Also the standard discovery step for the Member Listings post type (singleton per site, data_type=10, system_name=member_listings): filter property=data_type&property_value=10&property_operator== to retrieve the single record; the data_id varies per site. Member Listings controls the member search results page UI/UX + profile/detail page - see updatePostType description for editable fields, code-group save rules, and master-fallback behavior.

Pagination: cursor-based (limit, page). See top-level pagination rule for full cursor/cap/stop semantics.

Filter/sort: property+property_value+property_operator, order_column+order_type. See top-level filter rule for reliable operators (= only), silent-drop detection, and derived-field unfilterability.

Payload size - filter, don't enumerate blindly: each row has ~90 fields, and on sites with customized post types, layout fields (search_results_layout, profile_results_layout, search_code_hidden, detail_page_code_hidden) can embed entire PHP templates - responses have been seen at 80k+ chars. BD does not offer a fields= response-shaping parameter. The practical fix is to filter and cap:

  • If you know the target post type name, filter: property=data_name&property_value=Blog&property_operator=LIKE&limit=5

  • If you know the data_id already, skip this entirely - use getPostType (single record)

  • For "discover blog-family post types": filter by data_type (=4 multi-image, =20 single-image article/blog/event) with a small limit to see only the relevant family

  • For full enumeration for site-mapping, set limit=100 and paginate; expect to burn tokens on a site with many customized types

See also: getPostType (single record by ID).

Returns: { status: "success", total, current_page, total_pages, next_page, prev_page, message: [...records] }. Each record is the full resource object.

getPostType

Get a single post type - Fetch a single posttype record. Read-only.

Lean by default: same strip rules as listPostTypes (see its lean note). Pass include_code=1 to restore the 9 PHP/HTML code-template fields — required when you intend to edit them via updatePostType (read current values, modify, write back with all group-mates per the all-or-nothing save rule).

Use when: checking the configuration of one post type (which data_type family, whether active, custom field config, current search-results / profile-page template code). Commonly followed by getPostTypeCustomFields to enumerate per-type fields. Also the canonical read before any updatePostType code-field edit - see master-fallback note below.

Required: data_id.

Code-field master-fallback: the up to eight HTML/PHP code fields on every post type record (category_header, search_results_div, category_footer, profile_header, profile_results_layout, profile_footer, search_results_layout, comments_code) begin life backed by the BD-core master template and only persist locally in the site DB when an admin (or API call) saves them. This endpoint returns the MASTER value for any code field that has no local override - so the agent always sees the real rendered code, not an empty string. This matters because any edit to one of the grouped code fields (search-results group = header+loop+footer, profile group = header+body+footer) MUST include all fields in that group on the write (see updatePostType description for the all-or-nothing save rule). Always read current values here BEFORE calling updatePostType.

See also: listPostTypes (enumerate many; discover Member Listings via data_type=10 filter), updatePostType (write; carries the full code-group save rules and Member Listings workflow), getPostTypeCustomFields (per-type custom field enum).

Returns: { status: "success", message: [{...record}] } - the message array contains 1 record when found. Empty or HTTP 404 when not found.

getPostTypeCustomFields

Get custom fields for a post type - Fetch a single posttypecustomfields record. Read-only.

Use when: building a create/update payload for a post type that has custom fields (most do). Returns the exact per-type schema to send. Pass data_id of the target post type.

Required: data_id.

Parameter interactions:

  • data_id - the post type to introspect; get via listPostTypes

  • Returns custom field definitions specific to this post type - use to build create/update payloads for matching posts

Discovering enumerated field values (e.g. post_category): per-post-type dropdowns like post_category are configured by the site admin and live in this schema. There is NO createPostCategory API tool - if the user needs a new dropdown option, that is admin-side work. Call this before a create/update to see the exact allowed values for select/radio/checkbox fields, and pass only those values verbatim.

Returns: { status: "success", message: [{...record}] } - the message array contains 1 record when found. Empty or HTTP 404 when not found.

updatePostType

Update a post type - Update a post type. PATCH semantics (except code-group save rule below). Writes live data.

Cache refresh is automatic. Response includes auto_cache_refreshed: true after successful writes; no manual refreshSiteCache call needed. If auto_cache_refreshed: false, check auto_cache_refresh_error and retry refreshSiteCache once.

Required: data_id.

Use when: toggling a post type active/inactive, renaming, changing per-page display counts, editing search-results UI or profile-page code. For Member Listings specifically: tuning keyword-search, pagination, sidebar, sort order. Custom field DEFINITIONS live in BD admin UI, not API.

Universal structural safety - NEVER mutate these fields on ANY post type: data_type, system_name, data_name, data_active, data_filename, form_name, software_version, display_order. BD system-seeds them; changes break rendering site-wide.

MEMBER LISTINGS SPECIAL CASE (data_type=10). Every BD site has exactly one post type with data_type=10 (system_name=member_listings) - it controls the Member Search Results page UI/UX. No profile/detail page of its own - members render via the normal profile system. data_id varies per site; discover via listPostTypes property=data_type property_value=10 property_operator==. Cache the data_id for the session - it never changes.

Member Listings cheat-sheet (12 commonly-edited UI/UX settings + 3 search-code fields - NOT a limit, any real column is writable per schema-is-documentation): h1, h2, per_page, keyword_search_filter, enableLazyLoad, category_order_by, category_ignore_search_priority, post_type_cache_system, category_sidebar, sidebar_search_module, sidebar_position_mobile, enable_search_results_map, category_header, search_results_div, category_footer.

Member Listings guardrails (apply ONLY to data_type=10):

  • profile_header / profile_results_layout / profile_footer / search_results_layout have NO effect on Member Listings - skip them.

  • data_active must stay 1; no legitimate reason to disable via API.

  • On other post types (blog, event, coupon, property, product), these ARE legitimate rendering fields - write freely.

CODE FIELDS - master-fallback on GET + all-or-nothing save per group. Up to 8 code-template fields begin life backed by BD's MASTER post-type template; they only persist locally when saved. GET returns master value for un-customized fields (agent sees real rendered code, not empty string). Writing ANY field in a group requires sending ALL fields in that group (unchanged fields copied verbatim from prior GET); omitting group-mates causes them to drift back to master on next render.

Groups:

  1. Search-results (every post type INCLUDING Member Listings): category_header + search_results_div + category_footer. Send all 3.

  2. Profile/detail (post types WITH detail pages - NOT Member Listings): profile_header + profile_results_layout + profile_footer. Send all 3. DO NOT send on Member Listings.

  3. Standalone (post types WITH detail pages - NOT Member Listings): search_results_layout (single.php analogue - misleading name) and comments_code (auxiliary footer, embeds/schema/pixels). Both save independently, no group rule. Master-fallback applies. DO NOT send on Member Listings.

Code-edit workflow:

  1. getPostType(data_id) - returns current values with master fallback.

  2. Identify target group.

  3. Build payload: changed field(s) + other group-mates copied verbatim from GET.

  4. updatePostType with data_id + full group. (Cache flush is automatic post-write.)

Code-field trust level: all 8 code fields are widget-equivalent - accept arbitrary HTML/CSS/JS/iframes/PHP (BD evaluates PHP server-side at render). XSS/SQLi sanitization rules do NOT apply - anyone editing post-type code already has full site code control. Supports PHP variables (<?php echo $user_data['full_name']; ?>) and BD text-label tokens (%%%text_label%%%).

Member Listings code edits affect every member-search page on the site - confirm intent with user before editing Member Listings code fields.

See also: getPostType, listPostTypes (filter by data_type), deletePostType (NOT for Member Listings - system-required).

Returns: { status: "success", message: {...updatedRecord}, auto_cache_refreshed: true|false, auto_cache_refresh_error?: "..." }.

deletePostType

Delete a post type - Permanently delete a posttype record by ID. Destructive - cannot be undone via API.

Use when: removing a post type entirely. Existing posts of this type become orphaned - consider migrating them to another type first via a bulk updateSingleImagePost/updateMultiImagePost.

Required: data_id.

See also: updatePostType (modify without removing).

Destructive: confirm intent with the user before bulk use. No soft-delete via API - records removed are not recoverable.

Returns: { status: "success", message: "record was deleted" }. No body beyond the confirmation string.

listUnsubscribes

List unsubscribe records - Paginated enumeration of unsubscribe records. Read-only.

Use when: auditing the email unsubscribe list - useful for compliance (GDPR, CAN-SPAM) or before launching a new email campaign.

Pagination: cursor-based (limit, page). See top-level pagination rule for full cursor/cap/stop semantics.

Filter/sort: property+property_value+property_operator, order_column+order_type. See top-level filter rule for reliable operators (= only), silent-drop detection, and derived-field unfilterability.

See also: getUnsubscribe (single record by ID).

Returns: { status: "success", total, current_page, total_pages, next_page, prev_page, message: [...records] }. Each record is the full resource object.

getUnsubscribe

Get a single unsubscribe record - Fetch a single unsubscribe record. Read-only.

Use when: checking one unsubscribe record by ID.

Required: id.

See also: listUnsubscribes (enumerate many).

Returns: { status: "success", message: [{...record}] } - the message array contains 1 record when found. Empty or HTTP 404 when not found.

createUnsubscribe

Add email to unsubscribe list - Create a new unsubscribe record. Writes live data.

Use when: programmatically opting a member out of emails (e.g., from an external unsubscribe form or CRM sync). BD adds entries itself when members click email unsubscribe links.

Required: email.

Enums: definitive: 0, 1.

See also: updateUnsubscribe (modify existing).

email is the only meaningful input. Pass the email address to opt out. BD adds unsubscribe records to its global unsubscribe list - this applies across all email campaigns for the site. There is no "unsubscribe from some lists but not others" granularity via this endpoint; it's all-or-nothing.

updateUnsubscribe

Update an unsubscribe record - Update an existing unsubscribe record by ID. Fields omitted are untouched. Writes live data.

Use when: editing an unsubscribe record. Rare.

Required: id.

Enums: definitive: 0, 1.

See also: createUnsubscribe (add new), deleteUnsubscribe (remove permanently).

Returns: { status: "success", message: {...updatedRecord} } - the full updated record after changes applied.

deleteUnsubscribe

Remove email from unsubscribe list - Permanently delete a unsubscribe record by ID. Destructive - cannot be undone via API.

Use when: re-subscribing a member (remove their unsubscribe entry). Confirm the member's consent first - don't use to silently re-enable emails.

Required: id.

See also: updateUnsubscribe (modify without removing).

Destructive: confirm intent with the user before bulk use. No soft-delete via API - records removed are not recoverable.

Returns: { status: "success", message: "record was deleted" }. No body beyond the confirmation string.

listWidgets

List widgets - Paginated enumeration of widget records. Read-only.

Use when: discovering the reusable HTML/CSS/JS components available for embedding in pages (via [widget=Name] shortcode) or email templates. For fetching one specific widget by ID use getWidget.

Pagination: cursor-based (limit, page). See top-level pagination rule for full cursor/cap/stop semantics.

Filter/sort: property+property_value+property_operator, order_column+order_type. See top-level filter rule for reliable operators (= only), silent-drop detection, and derived-field unfilterability. Useful filter: widget_viewport=front to list only public-facing widgets.

See also: getWidget (single by ID), createWidget (add new), updateWidget (modify).

Returns: { status: "success", total, current_page, total_pages, next_page, prev_page, message: [...records] }. Each record has all the widget fields below.

Widget object fields (from BD support article 12000108056):

Field

Type

Description

widget_id

integer

Primary key (read-only)

widget_name

string

Widget name/label - REQUIRED on create; unique per site

widget_type

string

Widget classification (default: Widget)

widget_data

text

Widget HTML content

widget_style

text

Widget CSS styles

widget_javascript

text

Widget JavaScript code

widget_settings

text

Configuration (JSON or serialized)

widget_values

text

Widget variable values

widget_class

string

CSS class names applied to container

widget_viewport

string

Where widget appears: front, admin, both

widget_html_element

string

Container element (default: div)

div_id

string

HTML ID attribute for container

short_code

string

Shortcode reference for this widget

bootstrap_enabled

integer

1 if Bootstrap framework loaded

ssl_enabled

integer

1 if SSL/HTTPS required

mobile_enabled

integer

1 if mobile viewport enabled

file_type

string

File type of the widget

revision_timestamp

timestamp

Last modified (auto-updated)

getWidget

Get a single widget - Fetch a single widget record by widget_id. Returns the raw HTML/CSS/JS source. Read-only.

Use when: you have a widget_id (from listWidgets or admin) and want the widget's SOURCE code to edit or audit. To preview the rendered widget on the front-end, embed it on a page via [widget=Name] shortcode and view the page.

Required: widget_id (path parameter).

See also: listWidgets (enumerate), updateWidget (modify).

Returns: { status: "success", message: [{...record}] } - the message array contains 1 record with all widget fields (widget_data = HTML, widget_style = CSS, widget_javascript = JS, plus metadata).

For the full field list, see listWidgets.

createWidget

Create a widget - Create a new widget (reusable HTML/CSS/JS component). Writes live data.

Cache refresh is automatic. Response includes auto_cache_refreshed: true after successful writes; no manual refreshSiteCache call needed. If auto_cache_refreshed: false, check auto_cache_refresh_error and retry refreshSiteCache once.

Use when: programmatically adding a new reusable block to embed via [widget=Name] shortcode on pages or email templates. Rare in practice - widgets are usually created via BD admin UI where the editor supports live preview. API creation is useful for bulk imports, cross-site migrations, or scripted widget generation.

Required: widget_name (should be unique per site).

widget_name format: alphanumeric + spaces + hyphens + plus + underscores only ([A-Za-z0-9 -+_]+). Special chars (slashes, dots, ampersands, quotes, brackets, etc.) break [widget=Name] shortcode resolution and are runtime-rejected by the wrapper. Examples: Mortgage Calculator, Service-Card, Email_Validator_v2, C++ Course.

Pre-check before create: BD does NOT enforce uniqueness on widget_name. Duplicates break [widget=Name] shortcode resolution - which widget renders at the shortcode is undefined. Do a server-side filter-find: listWidgets property=widget_name property_value=<proposed> property_operator==. Zero rows = name free; >=1 row = taken. Do NOT paginate unfiltered lists looking for the name - on sites with hundreds of custom widgets that burns rate limit for nothing.

On collision (auto-suffix flow): if the proposed name is taken, append -v2 and re-check. Still taken? Try -v3, -v4, ... up through -v10. First free suffix wins. Only if all 10 are taken, ask the user for a different base name. Never silently create a duplicate.

Route by type BEFORE writing values: decide what each piece of code is, then put it in the matching field — HTML → widget_data, CSS → widget_style, JS → widget_javascript. A self-contained block with all three concatenated into widget_data will save successfully but silently break: widget_data strips backslashes on render, mangling regex literals (\d, \s), string escapes (\n, \t), and unicode escapes (\u0022). The other two fields do not strip backslashes. Split by type from the start.

Common fields on create:

  • widget_data - the HTML content

  • widget_style - CSS (scoped to the widget via widget_class or div_id)

  • widget_javascript - JS (runs when widget is rendered on a page)

  • widget_viewport - front (public), admin (admin panel only), or both

  • bootstrap_enabled=1 - ensures Bootstrap framework loaded when this widget is rendered

  • widget_html_element - wrapper element (default div)

See also: updateWidget (modify existing), listWidgets (check if name is taken first), getWidget (verify storage after create).

Writes live data: the widget is available immediately but does nothing until referenced by a [widget=Name] shortcode on a page or email template.

Returns: { status: "success", message: {...createdRecord}, auto_cache_refreshed: true|false, auto_cache_refresh_error?: "..." } including the new widget_id.

Post-create verification (recommended, especially when uncertain about routing): call getWidget once to confirm widget_data contains only HTML, widget_style contains your CSS, and widget_javascript contains your JS wrapped in <script>...</script>. If anything landed in the wrong field, call updateWidget to relocate before the user tests the widget. Proactive relocation here is correct and does NOT violate the "don't relocate without user-reported breakage" rule on updateWidget — that rule applies to subsequent edits, not to self-correcting your own just-created record.

For the full field list, see listWidgets.

updateWidget

Update a widget - Update an existing widget by widget_id. Fields omitted are untouched. Writes live data.

Cache refresh is automatic. Response includes auto_cache_refreshed: true after successful writes; no manual refreshSiteCache call needed. If auto_cache_refreshed: false, check auto_cache_refresh_error and retry refreshSiteCache once.

Use when: editing widget HTML (widget_data), CSS (widget_style), JS (widget_javascript), or metadata. Any page or email referencing this widget via [widget=Name] shortcode will render the updated content on next view.

Required: widget_id.

Common edits:

  • Content: widget_data, widget_style, widget_javascript

  • Visibility: widget_viewport (front/admin/both)

  • Framework: bootstrap_enabled, mobile_enabled, ssl_enabled

Renaming via widget_name: DO NOT pass widget_name unless the user explicitly asks to rename the widget. Renaming a widget breaks every [widget=Name] shortcode reference to its old name on every page/email — silently. If the user does ask: same format rules as create ([A-Za-z0-9 -+_]+, runtime-rejected on bad chars); on collision follow the auto-suffix flow (-v2, -v3, ... up to -v10).

See also: createWidget (add new), deleteWidget (remove).

Writes live data: edits go live immediately for new page loads.

Returns: { status: "success", message: {...updatedRecord}, auto_cache_refreshed: true|false, auto_cache_refresh_error?: "..." }.

For the full field list, see listWidgets.

deleteWidget

Delete a widget - Permanently delete a widget by widget_id. Destructive - cannot be undone via API.

Use when: removing an unused widget. For "disable without deleting" use updateWidget with widget_viewport=admin (hides from public pages) - preserves the source for later use.

Destructive caveat: any page or email using [widget=Name] shortcode referencing the deleted widget will render as empty or broken at that spot. Audit with listWidgets + check page content for shortcodes referencing this widget's widget_name or short_code before deleting.

Required: widget_id.

See also: updateWidget with widget_viewport=admin (reversible hide).

Returns: { status: "success", message: "data_widgets record was deleted" }.

renderWidget

Render a widget to HTML - Diagnostic tool only. Returns BD's rendered HTML output for a widget — useful for confirming render-pipeline symptoms during troubleshoot (backslash strip on widget_data, <style> auto-wrap on widget_style, <script> wrapper presence on widget_javascript). Production widget rendering on a customer's site is always via [widget=Name] shortcode in page or email content — never call this tool to deliver widget HTML to end users.

Use when: the user reports a widget is broken and you need to see what BD's render pipeline actually emits. See Rule: Widget code fields scenario 3 (TROUBLESHOOT).

Required: either widget_id OR widget_name.

Returns (distinct from standard envelope): { status, message, name, output }. The output field contains rendered widget_data HTML with template tokens expanded, plus BD's auto-wrapped <style type='text/css'>-block from widget_style, plus the verbatim widget_javascript content. CSS and JS are NOT in output if their fields are empty.

See also: getWidget (raw source for inspecting field placement), updateWidget (apply fixes after diagnosis).

listEmailTemplates

List email templates - Paginated enumeration of emailtemplate records. Read-only.

Use when: enumerating the site's transactional/marketing email templates before editing. Common audit: before bulk updating "from" addresses or footers.

Lean-by-default: email_body (the HTML body, the heaviest field per row) is stripped. All identity/metadata fields (email_id, email_name, email_subject, email_type, category_id, notemplate, etc.) are always kept. Set include_body=1 to restore.

Pagination: cursor-based (limit, page). See top-level pagination rule for full cursor/cap/stop semantics.

Filter/sort: property+property_value+property_operator, order_column+order_type. See top-level filter rule for reliable operators (= only), silent-drop detection, and derived-field unfilterability.

See also: getEmailTemplate (single record by ID).

Returns: { status: "success", total, current_page, total_pages, next_page, prev_page, message: [...records] }. Each record is the full resource object minus email_body unless include_body=1.

getEmailTemplate

Get a single email template - Fetch a single emailtemplate record. Read-only.

Use when: fetching one template's HTML body and subject for edit.

Required: email_id.

Lean-by-default: email_body is stripped. Set include_body=1 to restore it (always do this when you need to edit the HTML).

See also: listEmailTemplates (enumerate many).

Returns: { status: "success", message: [{...record}] } - the message array contains 1 record when found. Record omits email_body unless include_body=1. Empty or HTTP 404 when not found.

createEmailTemplate

Create an email template - Create a new emailtemplate record. Writes live data.

Use when: adding a new transactional/marketing template. Rare - most BD email templates are built into the admin UI.

Required: email_name.

Pre-check before create: BD does NOT enforce uniqueness on email_name. Duplicates cause the wrong template to fire on transactional triggers. Do a server-side filter-find: listEmailTemplates property=email_name property_value=<proposed> property_operator==. Zero rows = name free; >=1 row = taken. Do NOT paginate unfiltered lists looking for the name - on sites with many templates that burns rate limit for nothing. If taken: reuse via updateEmailTemplate, OR ask the user, OR pick an alternate email_name and re-check. Never silently create a duplicate.

Enums: signature: 0/1; notemplate: default 2 (template + logo center); other values 0 (logo left), 3 (logo right), 4 (template, no logo), 1 (plaintext-only, no wrapper); category_id: default 0 (My Saved Templates) — 1/3/4/15/16 are system-populated, do NOT create under them; unsubscribe_link: 0/1.

Parameter interactions:

  • email_subject and email_body can use template tokens (e.g. %%%website_name%%%, recipient field tokens)

  • email_body supports HTML

See also: updateEmailTemplate (modify existing).

On create: email_name is the only required field. Subject and body are optional at create time - you can create a template stub and fill in email_subject / email_body via updateEmailTemplate later. This lets you programmatically scaffold templates before customizing them via the admin UI.

updateEmailTemplate

Update an email template - Update an existing emailtemplate record by ID. Fields omitted are untouched. Writes live data.

Use when: editing any field on an existing template — subject, body, wrapper mode (notemplate), category, signature, triggers, etc. Mirrors createEmailTemplate field-for-field; only email_id is required.

Required: email_id.

Enums (same as createEmailTemplate): signature: 0/1; notemplate: 0 (logo left), 2 (logo center), 3 (logo right), 4 (template, no logo), 1 (plaintext-only, no wrapper); category_id: 0/1/3/4/15/16 (unrestricted on update); unsubscribe_link: 0/1.

See also: createEmailTemplate (add new), deleteEmailTemplate (remove permanently).

Returns: { status: "success", message: {...updatedRecord} } — the full updated record after changes applied.

deleteEmailTemplate

Delete an email template - Permanently delete a emailtemplate record by ID. Destructive - cannot be undone via API.

Use when: removing a deprecated template. BD may fall back to defaults if a required system template is deleted - confirm before purging.

Required: email_id.

See also: updateEmailTemplate (modify without removing).

Destructive: confirm intent with the user before bulk use. No soft-delete via API - records removed are not recoverable.

Returns: { status: "success", message: "record was deleted" }. No body beyond the confirmation string.

listForms

List forms - Paginated enumeration of form records. Read-only.

Use when: enumerating the site's forms (signup, contact, quote request, custom forms). Child fields are fetched separately via listFormFields.

Pagination: cursor-based (limit, page). See top-level pagination rule for full cursor/cap/stop semantics.

Filter/sort: property+property_value+property_operator, order_column+order_type. See top-level filter rule for reliable operators (= only), silent-drop detection, and derived-field unfilterability.

See also: getForm (single record by ID).

Returns: { status: "success", total, current_page, total_pages, next_page, prev_page, message: [...records] }. Each record is the full resource object.

getForm

Get a single form - Fetch a single form record. Read-only.

Use when: fetching one form's metadata.

Required: form_id.

See also: listForms (enumerate many).

Returns: { status: "success", message: [{...record}] } - the message array contains 1 record when found. Empty or HTTP 404 when not found.

createForm

Create a form - Create a new form record. Writes live data. Add fields afterward via createFormField.

Required: form_name, form_title, form_action, form_layout, form_table.

ALWAYS pre-check for duplicate form_name BEFORE calling createForm. BD does NOT enforce unique form_name values - two createForm calls with the same form_name both succeed and produce two separate form records with different form_id values. Downstream createFormField calls that target form_name then become ambiguous (which form gets the field?), and anywhere the site renders a form by name - [form=my_form_name] shortcodes, sidebar assignments, template tokens - the behavior is undefined. Mandatory pre-check: before every createForm, call listForms filtered property=form_name&property_value=<slug>&property_operator==. If a row exists: DO NOT create a new one - either updateForm the existing record, ask the user whether to overwrite or pick a different slug, OR pick an alternate slug (append -v2, -2026, etc.) and confirm with the user. Same as the duplicate-filename rule on createWebPage - identical AI prompts in two sessions produce identical-slug duplicates.

Required form recipe (must follow for the form to submit without errors):

  1. form_url - set to the exact value /api/widget/json/post/Bootstrap%20Theme%20-%20Function%20-%20Save%20Form. This is the BD Save Form widget endpoint; without it the form's action= attribute is wrong and the submit won't wire up. URL-encoded spaces (%20) must stay encoded.

  2. table_index - set to the exact value ID. Tells BD which column is the primary key on the submissions table.

  3. form_action_type - set to widget (Display Success Pop-Up Message - safe default) unless the user specifies notification or redirect. Never leave empty for a public-facing form.

  4. If form_action_type=widget (the default) - set form_action_div=#main-content. This is the DOM target element that gets swapped out when the form submits. Required when action type is widget; ignored otherwise. Must include the leading #.

  5. form_email_on - set to 0 (OFF) unless the user explicitly asks for admin notification emails. Admin UI defaults this to ON; agent default is OFF to avoid spammy-form-floods-admin-inbox.

  6. If form_action_type=redirect - set form_target=<destination URL>. Not schema-enforced, but BD silently accepts the create without it and the form's submit goes nowhere on the live site. Always pair the two.

  7. When form_action_type is widget / notification / redirect, the form's fields list MUST end with these three fields in this order, as the three HIGHEST-ORDERED fields on the form (add via createFormField):

    • field_type=ReCaptcha - Google reCAPTCHA challenge

    • field_type=HoneyPot - invisible spam trap

    • field_type=Button - the actual Submit button (exactly ONE Button per form; always last). REQUIRED input_class: btn btn-lg btn-block <variant> - variant is btn-primary / btn-secondary / btn-danger / btn-success / btn-warning / btn-info / btn-dark, OR a custom class the site targets with its own CSS. Example value: btn btn-lg btn-block btn-secondary. Without input_class, the button renders unstyled.

    Picking field_order: call listFormFields first, find the current max field_order, then use max+1, max+2, max+3. On a brand-new form, 1/2/3 is fine. Never add any field AFTER Button.

Without every rule above, BD errors on submit and the form won't function. ReCaptcha/HoneyPot need no configuration beyond field_type - OMIT field_required, field_placeholder, and view-flags; BD handles these fields specially server-side.

Field defaults and valid values:

  • form_action - post (default) or get (use get only for bookmarkable search/filter forms).

  • form_layout - bootstrapvertical (default; Labels Above Inputs) or bootstrap (Labels Left of Inputs).

  • form_table - submissions table, default website_contacts. Override only for custom workflows.

  • form_action_type - widget (default) / notification / redirect / "" (empty, internal-only).

  • form_target - required when form_action_type=redirect, otherwise ignored.

  • form_email_on - 0 (default) / 1 (enable notification emails to admin).

See also: updateForm (modify existing), createFormField (add fields), listFormFields (inspect existing forms).

Returns: { status: "success", message: {...createdRecord} } - includes the server-assigned form identifier.

updateForm

Update a form - Update an existing form record by ID. Fields omitted are untouched. Writes live data.

Required: form_id.

If changing form_action_type to widget / notification / redirect (or if it was already set to any of those), the form's fields list MUST end with ReCaptcha -> HoneyPot -> Button (in that order). Verify via listFormFields before changing form_action_type - if the tail pattern is missing, the form will error on submit. Add missing fields via createFormField.

If changing to redirect, also set form_target = destination URL. Not schema-enforced - BD silently accepts the update without it and the form's submits go nowhere on the live site. Always pair the two.

See also: createForm (add new), deleteForm (remove permanently), listFormFields / createFormField (inspect / add the ReCaptcha+HoneyPot+Button tail).

Returns: { status: "success", message: {...updatedRecord} }.

deleteForm

Delete a form - Permanently delete a form record by ID. Destructive - cannot be undone via API.

Use when: removing a form - child fields orphan.

Required: form_id.

See also: updateForm (modify without removing).

Destructive: confirm intent with the user before bulk use. No soft-delete via API - records removed are not recoverable.

Returns: { status: "success", message: "record was deleted" }. No body beyond the confirmation string.

listFormFields

List form fields - Paginated enumeration of formfield records. Read-only.

Use when: listing fields on a form. Filter by form_id or form_name.

Pagination: cursor-based (limit, page). See top-level pagination rule for full cursor/cap/stop semantics.

Filter/sort: property+property_value+property_operator, order_column+order_type. See top-level filter rule for reliable operators (= only), silent-drop detection, and derived-field unfilterability.

See also: getFormField (single record by ID).

Returns: { status: "success", total, current_page, total_pages, next_page, prev_page, message: [...records] }. Each record is the full resource object.

getFormField

Get a single form field - Fetch a single formfield record. Read-only.

Use when: one field by ID.

Required: field_id.

See also: listFormFields (enumerate many).

Returns: { status: "success", message: [{...record}] } - the message array contains 1 record when found. Empty or HTTP 404 when not found.

createFormField

Create a form field - Create a new formfield record. Writes live data.

Use when: adding an input to an existing form.

Required: form_name, field_name, field_text, field_type, field_order.

Pre-check before create: BD does NOT enforce uniqueness on field_name WITHIN a form. A duplicate field_name on the same form breaks submission - the form POST ships two inputs with the same HTML name attribute, and BD's backend stores only one (unpredictable which) while dropping the other. Uniqueness is scoped PER form_name (same field_name on different forms is fine and common - e.g. first_name appears on dozens of forms). Do a server-side filter-find scoped to the form: listFormFields property[]=form_name property_value[]=<proposed form_name> property_operator[]== property[]=field_name property_value[]=<proposed field_name> property_operator[]==. Zero rows = field_name free on that form; >=1 row = taken. Do NOT paginate the full unfiltered field list - sites with hundreds of forms produce thousands of fields; filtered lookup is one tiny response. If taken: reuse via updateFormField, OR ask the user, OR pick an alternate field_name and re-check. Never silently create a duplicate.

Enums: field_required: 0, 1.

Parameter interactions:

  • form_name - the parent form slug (from listForms / createForm). Note: only form_name is accepted; there is no form_id parameter on this endpoint.

  • Field type and options depend on the form type; validate against the form's field schema

Limitation - dropdown option lists: Checkbox, Select, Radio, and YesNo field types accept their type value on create but there is NO API parameter to supply their option list (no field_options/choices/enum field on this endpoint). Create the field with field_type set, then populate its options in BD admin's form editor. Until options are populated, the rendered field will be empty.

See also: updateFormField (modify existing).

field_type valid values (BD admin form-builder UI, full list):

Select-type fields: Checkbox (checkboxes), Select (dropdown list), Radio (radio buttons), YesNo (yes/no toggle)

Text inputs: Custom (custom HTML), Email, HTML (section title / heading), Button (submit button), Textbox (single-line text), textarea (paragraph), Url (website URL)

Fancy fields: Date (date picker), DateTimeLocal (date + time picker), File (file upload), FroalaEditor (rich text - basic), FroalaEditorUserUpload (+ image upload), FroalaEditorUserUploadPreMadeElem (+ pre-made elements), FroalaEditorAdmin (+ media manager), Tip (help alert box), Hidden, Country (country list), State (state list), Number (integer), Password, Phone, CountryCodePhone, Pricebox, ReCaptcha (Google reCAPTCHA), HoneyPot (spam protection), Category (top-level category list), Years (year picker)

Field naming rules:

  • field_name - internal system key. Underscores only, no spaces. E.g. first_name, company_email. Used as HTML name attribute.

  • field_text - public-facing display label shown to form users. Free-form, e.g. "First Name", "Your Email Address".

Required tail-end pattern for submittable forms. When the parent form's form_action_type is widget, notification, or redirect (see createForm), the fields list MUST end with these three fields in this exact order, and they MUST remain the three highest-ordered fields on the form - no other field can have a field_order value equal to or greater than theirs:

  1. field_type=ReCaptcha

  2. field_type=HoneyPot

  3. field_type=Button (the submit button - exactly ONE Button per form, and it must be last)

Picking field_order values: call listFormFields first, find the current max field_order on the form, then use max+1, max+2, max+3 for ReCaptcha / HoneyPot / Button respectively. Never add fields AFTER Button - this pattern is the tail; nothing comes later. On a brand-new form with no other fields, 1 / 2 / 3 is fine.

Without all three at the end in order, BD errors on submit and the form won't function.

ReCaptcha / HoneyPot configuration: pass ONLY field_type + required scaffolding (form_name, field_name, field_text, field_order). OMIT field_required, field_placeholder, and the view-flags (field_display_view / field_input_view / field_email_view) - BD server-side handles these fields specially and ignores those settings. Pick any internal field_name (e.g. recaptcha_field, hp_field) and any field_text (empty string works for ReCaptcha; for HoneyPot, Leave this blank is the convention). The Button field's field_text IS the visible submit-button label (e.g. Send Message, Submit). Button input_class is REQUIRED - without it, the submit button renders unstyled. Canonical pattern: btn btn-lg btn-block <variant> where <variant> is a Bootstrap button class (btn-primary / btn-secondary / btn-danger / btn-success / btn-warning / btn-info / btn-dark) OR a custom class targeted by site CSS. Example value: btn btn-lg btn-block btn-secondary.

updateFormField

Update a form field - Update an existing formfield record by ID. Fields omitted are untouched. Writes live data.

Use when: renaming, changing field type, or reordering.

Required: field_id.

Editing ReCaptcha or HoneyPot fields: OMIT field_required, field_placeholder, and view-flags (field_display_view / field_input_view / field_email_view). BD handles these fields specially server-side and sets those attributes itself; overriding can break submit behavior. Only change field_text (the label) or field_order (position - must remain in the tail). See createFormField for the full tail-pattern rule.

Reordering: if moving a field via field_order, make sure the ReCaptcha -> HoneyPot -> Button tail pattern remains the three highest-ordered fields on the form. Run listFormFields first to see current order.

See also: createFormField (add new), deleteFormField (remove permanently), listFormFields (inspect order before reordering).

Returns: { status: "success", message: {...updatedRecord} } - the full updated record after changes applied.

deleteFormField

Delete a form field - Permanently delete a formfield record by ID. Destructive - cannot be undone via API.

Use when: removing a field. Existing submission records may reference the old field name - data persists but becomes orphan metadata.

Required: field_id.

See also: updateFormField (modify without removing).

Destructive: confirm intent with the user before bulk use. No soft-delete via API - records removed are not recoverable.

Returns: { status: "success", message: "record was deleted" }. No body beyond the confirmation string.

listMembershipPlans

List membership plans - Paginated enumeration of membership-plan records. Read-only.

Use when: discovering subscription_id values to use when creating members. Essential prerequisite for createUser - every member needs a valid subscription_id.

Lean-by-default: always-kept = subscription_id, subscription_name, subscription_type, profile_type, monthly_amount, yearly_amount, initial_amount, lead_price, searchable. Heavy config (~40 fields) + display-visibility toggles (~13 fields) are stripped. Most 'pick a plan' workflows don't need them. Flags:

  • include_plan_config=1 - restores config bundle (active/searchable toggles, limits, forms, sidebars, email templates, upgrade chain, payment defaults, etc.).

  • include_plan_display_flags=1 - restores show_* profile-visibility toggles.

Pagination: cursor-based (limit, page). See top-level pagination rule.

Filter/sort: property+property_value+property_operator, order_column+order_type. See top-level filter rule.

See also: getMembershipPlan (single by ID).

Returns: { status: "success", total, ..., message: [...records] }.

getMembershipPlan

Get a single membership plan - Fetch a single membership-plan record. Read-only.

Use when: fetching one plan's config. Same lean-by-default as listMembershipPlans.

Required: subscription_id.

Lean shape: always-kept = subscription_id, subscription_name, subscription_type, profile_type, monthly_amount, yearly_amount, initial_amount, lead_price, searchable. Opt in to restore:

  • include_plan_config=1 - config bundle (limits, sidebars, forms, email templates, upgrade chain, payment defaults).

  • include_plan_display_flags=1 - show_* profile-visibility toggles.

EAV-routed fields not merged: custom_checkout_url (and any future EAV-routed plan fields) are stored in users_meta and NOT returned by this endpoint even with include_plan_config=1. Read via listUserMeta database=subscription_types database_id=<subscription_id> to fetch them.

See also: listMembershipPlans (enumerate).

Returns: { status: "success", message: [{...record}] }.

createMembershipPlan

Create a membership plan - Create a new membershipplan record. Writes live data.

Use when: launching a new plan tier. Rare - usually configured in BD admin UI. Check profile_type carefully: paid, free, or claim - changing later affects billing and visibility.

Required: subscription_name, profile_type.

Pre-check before create: BD does NOT enforce uniqueness on subscription_name. Duplicate plan names confuse admins at signup-form configuration, billing reports, and member migration. Do a server-side filter-find: listMembershipPlans property=subscription_name property_value=<proposed> property_operator==. Zero rows = name free; >=1 row = taken. Do NOT paginate unfiltered lists - filtered lookup is one tiny response. If taken: reuse via updateMembershipPlan, OR ask the user, OR pick an alternate subscription_name and re-check. Never silently create a duplicate.

Enums: payment_default: yearly, monthly.

Parameter interactions:

  • subscription_type - typically member for standard member plans

  • profile_type: paid, free, or claim - controls how profile visibility and billing work

  • monthly_amount / yearly_amount - price in the site's currency

  • sub_active=1 makes the plan available for new signups; sub_active=0 grandfathers existing members only

  • searchable=1 makes members on this plan findable in public search

See also: updateMembershipPlan (modify existing).

updateMembershipPlan

Update a membership plan - Update an existing membershipplan record by ID. Fields omitted are untouched. Writes live data.

Use when: adjusting pricing (monthly_amount/yearly_amount), toggling sub_active (new-signup availability), or changing feature flags like searchable, photo_limit. Changes apply to NEW signups; existing members on this plan keep their original terms unless manually migrated.

Required: subscription_id.

See also: createMembershipPlan (add new), deleteMembershipPlan (remove permanently).

Returns: { status: "success", message: {...updatedRecord} } - the full updated record after changes applied.

deleteMembershipPlan

Delete a membership plan - Permanently delete a membershipplan record by ID. Destructive - cannot be undone via API.

Use when: retiring a plan that has no members (or all its members have been migrated). Members with matching subscription_id become orphaned - migrate them first via updateUser.

Required: subscription_id.

See also: updateMembershipPlan (modify without removing).

Destructive: confirm intent with the user before bulk use. No soft-delete via API - records removed are not recoverable.

Returns: { status: "success", message: "record was deleted" }. No body beyond the confirmation string.

listMenus

List menus - Paginated enumeration of menu records. Read-only.

Use when: enumerating navigation menus on the site (main menu, footer menu, sidebar, etc.). For items within a menu use listMenuItems with menu_id filter.

Pagination: cursor-based (limit, page). See top-level pagination rule for full cursor/cap/stop semantics.

Filter/sort: property+property_value+property_operator, order_column+order_type. See top-level filter rule for reliable operators (= only), silent-drop detection, and derived-field unfilterability.

See also: getMenu (single record by ID).

Returns: { status: "success", total, current_page, total_pages, next_page, prev_page, message: [...records] }. Each record is the full resource object.

getMenu

Get a single menu - Fetch a single menu record. Read-only.

Use when: fetching one menu's metadata. Child items are fetched separately.

Required: menu_id.

See also: listMenus (enumerate many).

Returns: { status: "success", message: [{...record}] } - the message array contains 1 record when found. Empty or HTTP 404 when not found.

createMenu

Create a menu - Create a new menu record. Writes live data.

Use when: adding a new navigation container. After creating the container, add entries via createMenuItem using the returned menu_id.

Required: menu_name, menu_title.

Pre-check before create: BD does NOT enforce uniqueness on menu_name. Duplicates cause the wrong menu to render wherever the menu is referenced. Do a server-side filter-find: listMenus property=menu_name property_value=<proposed> property_operator==. Zero rows = name free; >=1 row = taken. Do NOT paginate unfiltered lists - filtered lookup is one tiny response. If taken: reuse via updateMenu, OR ask the user, OR pick an alternate menu_name and re-check. Never silently create a duplicate.

Parameter interactions:

  • menu_name (max 35 chars) - the internal identifier

  • menu_title - the visible heading

  • menu_active: 0=Inactive, 1=Active

  • After creating the container, add entries via createMenuItem using the returned menu_id

See also: updateMenu (modify existing).

updateMenu

Update a menu - Update an existing menu record by ID. Fields omitted are untouched. Writes live data.

Use when: renaming a menu, toggling menu_active, or adjusting its CSS/HTML wrapper attributes.

Required: menu_id.

See also: createMenu (add new), deleteMenu (remove permanently).

Returns: { status: "success", message: {...updatedRecord} } - the full updated record after changes applied.

deleteMenu

Delete a menu - Permanently delete a menu record by ID. Destructive - cannot be undone via API.

Use when: removing a menu container. Child items (menu_items rows with matching menu_id) become orphaned - delete them first.

Required: menu_id.

See also: updateMenu (modify without removing).

Destructive: confirm intent with the user before bulk use. No soft-delete via API - records removed are not recoverable.

Returns: { status: "success", message: "record was deleted" }. No body beyond the confirmation string.

listMenuItems

List menu items - Paginated enumeration of menuitem records. Read-only.

Use when: enumerating items in a menu - always filter by menu_id. Use master_id filter for sub-menu items.

Pagination: cursor-based (limit, page). See top-level pagination rule for full cursor/cap/stop semantics.

Filter/sort: property+property_value+property_operator, order_column+order_type. See top-level filter rule for reliable operators (= only), silent-drop detection, and derived-field unfilterability.

See also: getMenuItem (single record by ID).

Returns: { status: "success", total, current_page, total_pages, next_page, prev_page, message: [...records] }. Each record is the full resource object.

getMenuItem

Get a single menu item - Fetch a single menuitem record. Read-only.

Use when: editing one specific menu entry.

Required: menu_item_id.

See also: listMenuItems (enumerate many).

Returns: { status: "success", message: [{...record}] } - the message array contains 1 record when found. Empty or HTTP 404 when not found.

createMenuItem

Create a menu item - Create a new menuitem record. Writes live data.

Use when: adding a nav link to an existing menu. Parent menu_id must exist. For nested items pass master_id=<parent menu_item_id>; for top-level pass 0. menu_order determines display position (lower = earlier).

Required: menu_id, menu_name, menu_link, master_id, menu_order.

Enums: menu_active: 0=Inactive, 1=Active.

Parameter interactions:

  • menu_id - parent menu container (from createMenu or listMenus)

  • master_id - 0 for top-level items; for nested items, the ID of the parent menu item

  • menu_order - display position within the parent menu (integer, lower = earlier)

  • menu_target: _blank (new tab) or _self (same window)

See also: updateMenuItem (modify existing).

updateMenuItem

Update a menu item - Update an existing menuitem record by ID. Fields omitted are untouched. Writes live data.

Use when: renaming, re-linking (change menu_link), reordering (change menu_order), or hiding (change menu_active=0).

Required: menu_item_id.

See also: createMenuItem (add new), deleteMenuItem (remove permanently).

Returns: { status: "success", message: {...updatedRecord} } - the full updated record after changes applied.

deleteMenuItem

Delete a menu item - Permanently delete a menuitem record by ID. Destructive - cannot be undone via API.

Use when: removing a single menu entry.

Required: menu_item_id.

See also: updateMenuItem (modify without removing).

Destructive: confirm intent with the user before bulk use. No soft-delete via API - records removed are not recoverable.

Returns: { status: "success", message: "record was deleted" }. No body beyond the confirmation string.

listSubCategories

List services (sub-categories) - Paginated enumeration of SUB-level member categories (services). Read-only.

Lean by default: each row keeps service_id, profession_id (parent Top Category link), master_id (parent Sub Category for sub-sub), name, filename. Strips desc, keywords, image, icon, sort_order, lead_price, revision_timestamp. Pass include_category_schema=1 to restore all category metadata. Hierarchy is always visible so agents can traverse top -> sub -> sub-sub without opt-in.

Sub Categories are level 2 of the 3-tier member classification (e.g., "Sushi" under "Restaurants"). Each has a profession_id pointing at its parent Top Category. master_id points at a parent Sub Category for sub-sub-category nesting (master_id=0 = directly under a Top Category). Backed by BD's list_services table.

Use when: enumerating sub-categories (services) - always filter by profession_id to scope to one Top Category, otherwise you get all sub-cats across all tops (noisy). For sub-sub nesting, master_id filter narrows further.

Permission note - platform gap: this endpoint (/api/v2/list_services/*) is NOT in BD's public Swagger spec, so the admin's API key permissions UI does NOT auto-generate a toggle for it. The admin's "Services" toggle gates the Swagger-documented /api/v2/service/* endpoints (a DIFFERENT legacy table) - enabling that toggle does NOT grant access here. On 403: admin must manually INSERT a row into bd_api_key_permissions for endpoint_path='/api/v2/list_services/get' (and the singular /api/v2/list_services/get/{service_id} for getSubCategory). Do NOT substitute /api/v2/service/* - different table, inconsistent data.

Pagination: cursor-based (limit, page). See top-level pagination rule for full cursor/cap/stop semantics.

Filter/sort: property+property_value+property_operator, order_column+order_type. See top-level filter rule for reliable operators (= only), silent-drop detection, and derived-field unfilterability.

See also: getSubCategory (single by ID), listTopCategories (parents), createSubCategory (add new).

Returns: { status: "success", total, ..., message: [...records] }. Each record has service_id, name, desc, profession_id, master_id, filename, keywords, sort_order, lead_price, image.

How a member gets classified on their public profile:

  • users_data.profession_id -> points at a single Top Category (the member's primary classification; shown in URL slug)

  • users_data.services -> CSV of Sub Category IDs the member is tagged with (multiple allowed; simpler than the join table)

  • rel_services rows (Member ↔ Sub Category links) -> used when you need per-link metadata like avg_price, specialty, num_completed. Optional; most sites use just the CSV field.

Sub-sub-categories: createSubCategory with master_id=<parent service_id> creates a Sub Category nested under another Sub Category (a "sub-sub"). master_id=0 (default) means the Sub Category sits directly under a Top Category (the profession_id).

There is NO createProfession or createService tool in this MCP - those are the BD internal resource names. Use the tools named above.

getSubCategory

Get a single service - Fetch a single SUB-level member category (service) by service_id. Read-only.

Lean by default: keeps service_id, profession_id, master_id, name, filename. Strips SEO metadata (desc, keywords, image, icon, sort_order, lead_price, revision_timestamp). Pass include_category_schema=1 to restore.

Use when: fetching one sub-category by service_id - usually after discovering it via listSubCategories.

Required: service_id (path).

See also: listSubCategories (enumerate; filter by profession_id for scope), getTopCategory (fetch parent Top by profession_id).

Returns: { status: "success", message: [{...record}] }.

How a member gets classified on their public profile:

  • users_data.profession_id -> points at a single Top Category (the member's primary classification; shown in URL slug)

  • users_data.services -> CSV of Sub Category IDs the member is tagged with (multiple allowed; simpler than the join table)

  • rel_services rows (Member ↔ Sub Category links) -> used when you need per-link metadata like avg_price, specialty, num_completed. Optional; most sites use just the CSV field.

Sub-sub-categories: createSubCategory with master_id=<parent service_id> creates a Sub Category nested under another Sub Category (a "sub-sub"). master_id=0 (default) means the Sub Category sits directly under a Top Category (the profession_id).

There is NO createProfession or createService tool in this MCP - those are the BD internal resource names. Use the tools named above.

Prompts

Interactive templates invoked by user choice

NameDescription

No prompts

Resources

Contextual data attached and managed by the client

NameDescription

No resources

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/brilliantdirectories/brilliant-directories-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server