| verifyTokenA | 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: Returns: { status: "success"|"error", message: ... } - BD's standard response envelope. |
| listFormInquiriesA | List Forms Inbox submissions - Paginated Forms Inbox submissions. Read-only. Use when: reading contact-form and lead submissions. Per-row fields: inquiry_id, inquiry_email, inquiry_ip, yourname, phone, inquiry_user_id, inquiry_form, form_title, url_origin, date_submitted, fields. inquiry_form is the form system name and the filter key; form_title is its display name (e.g. Trainer Ebook Lead); url_origin is the page URL the form was submitted on — filter it with contains/starts_with/ends_with (e.g. submissions from one page: property=url_origin property_value=about/contact property_operator=contains; omit the leading /, which the WAF strips); inquiry_user_id appears only for member submissions; inquiry_email/yourname/phone appear only when the form captured them, so read fields (the submission parsed to a {label, value} array, custom fields included) as the source of truth. include_raw=1 returns the raw inquiry_content HTML in place of fields. Search — one form, last 30 days, newest first: property=inquiry_form property_value=contact_form property_operator==, property=date_submitted property_operator=since_days property_value=30, order_column=date_submitted order_type=desc. Dates on date_submitted: a calendar month = month_eq=<n> + year_eq=<yyyy> (two conditions); a relative window = since_days (older bound) with until_days (newer bound). Filters match the UTC-stored value while the response shows a localized string, so a near-midnight row can bucket into the next day/month; between/gt/lt need a 14-digit value (not ISO); starts_with matches the display string, so it returns wrong rows on dates. See Rule: Filter operators for the full date-operator behavior. Pretty name to inquiry_form: for a form named by form_title, call getForm with property=form_title property_value=<title> property_operator== and read form_name — that value is the inquiry_form to filter by. On no match (shorthand or typo), listForms and pick the title. Filter/sort: property+property_value+property_operator; order_column (date_submitted, inquiry_id, inquiry_email, yourname, inquiry_form)+order_type. See Rule: Filter operators. Pagination: limit (max 100)+page. See Rule: Pagination. See also: getFormInquiry (one submission by id). Returns: { status, message: [...rows], total, current_page, total_pages, next_page }. |
| getFormInquiryA | Get a single Forms Inbox submission - One Forms Inbox submission by inquiry_id. Read-only. Use when: you hold an inquiry_id from listFormInquiries. Required: inquiry_id (path). Returns: the listFormInquiries per-row shape — inquiry_email, yourname, phone, inquiry_form, form_title, url_origin, date_submitted, fields ({label, value} array; include_raw=1 for raw HTML). See also: listFormInquiries (enumerate, filter). |
| getUserFieldsA | 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. |
| listUsersA | List members/users - Get a paginated list of all members. Supports filtering by any user field and sorting. Lean-by-default keep-list: rows return only identity + routing + location core: user_id, first_name, last_name, email, company, phone_number, subscription_id, profession_id, active, status, city, state_code, country_code, filename, image_main_file, signup_date, last_login, modtime. Everything else stripped — restore via flags: include_password=1, include_subscription=1 (full subscription_schema), include_clicks=1 (full click array), include_photos=1 (full photos_schema), include_transactions=1, include_profession=1 (profession_schema), include_tags=1, include_services=1 (services_schema), include_seo_hidden=1, include_about=1 (about_me HTML bio), include_legacy_fields=1 (image-import state on photos, requires include_photos), include_extras=1 (everything else — billing/analytics rollups like revenue/card_info/total_clicks/total_photos, duplicate location fields state_ln/country_ln/full_name/user_location/zip_code/lat/lon, plus social URLs, awards, credentials, position, quote, work_experience, rep_matters, cv, gmap, no_geo, user_consent, sign_up_origin, listing_type, profession_name, ref_code, booking_link, bitly, cookie, token, verified, featured, parent_id, clientid, etc.). 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. Do NOT bulk-list users to enumerate cities the site has on file — use listCities (lean, BD-curated, surfaces only cities where members exist). 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 Rule: Filter operators for the verified-working operator set, silent-drop detection, and derived-field unfilterability. Enums: property_operator: word-forms (eq, ne, lt, lte, gt, gte, like, in, contains, date/length/null ops) — see Rule: Filter operators; 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): for one custom field matching any of N values, use property=<meta_key> property_value=v1,v2,v3 property_operator=in (CSV, one field). For a custom field AND another condition, use equal-length parallel arrays — see Rule: Compound filters. BD ANDs array conditions; there is no OR operator. Payment-method field (under include_extras=1): card_info is false when no card is on file (BD's convention), or an object with last4/brand/name when a card IS stored. Check card_info && card_info.last4 (truthy-guard). Authoritative signal for "does this member have a valid payment method on file" — do not infer from subscription_id alone. 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 lean-shaped per the keep-list above. 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. |
| getUserA | Get a single member/user - Fetch a single user record. Read-only. Lean-by-default keep-list: same shape as listUsers — identity + routing + location core (user_id, first_name, last_name, email, company, phone_number, subscription_id, profession_id, active, status, city, state_code, country_code, filename, image_main_file, signup_date, last_login, modtime). Restore extras via flags: include_password=1, include_subscription=1, include_clicks=1, include_photos=1, include_transactions=1, include_profession=1, include_tags=1, include_services=1, include_seo_hidden=1, include_about=1, include_legacy_fields=1, include_extras=1 (billing/analytics rollups revenue/card_info/total_clicks/total_photos, duplicate location fields state_ln/country_ln/full_name/user_location/zip_code/lat/lon, plus social URLs, awards, credentials, position, quote, work_experience, ref_code, booking_link, etc.). 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 lean-shaped record when found. Empty or HTTP 404 when not found. Payment method on file (under include_extras=1): card_info is false when no card is stored (BD convention), or an object with last4/brand/name when one is. Use card_info && card_info.last4 to safely check. Authoritative signal for "does this member have a payment method" — don't infer from subscription_id alone. 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). |
| createUserA | 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. For category assignment via profession_id / profession_name / services, apply Rule: Category taxonomy (auto-create is ON for createUser). 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.
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 — useful for CSV imports / cross-site migrations preserving original tokens; BD auto-generates if omitted): 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: Logo image from their site's header/nav -> logo (businesses). Headshot from Home, About or Team page -> profile_photo (individuals). 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. |
| updateUserA | Update an existing member/user - Update a member. PATCH semantics - omitted fields untouched; send only what changes. Required: user_id. Disambiguation: apply Rule: Resource disambiguation when the user names this member by description (first name only, partial title) rather than by 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 assignment — for profession_id / profession_name / services, apply Rule: Category taxonomy (auto-create is OFF by default on updateUser; pass create_new_categories=1 to enable). 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 — useful for CSV imports / cross-site migrations preserving original tokens; BD auto-generates if omitted): 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: Logo image from their site's header/nav -> logo (businesses). Headshot from Home, About or Team page -> profile_photo (individuals). 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} }. |
| deleteUserA | 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: 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. |
| searchUsersA | Search members/users - Full-text search across members with category, location, and sorting options. Lean-by-default keep-list: same shape as listUsers — identity + routing + location core. Restore extras via the same include flags including include_extras=1 for stripped fields (billing/analytics rollups, duplicate location fields, social URLs, awards, credentials, work_experience, etc.). 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 Rule: Pagination 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
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. |
| loginUserA | 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: Returns: { status: "success"|"error", message: ... } - BD's standard response envelope. |
| getUserTransactionsA | Get member billing transactions (invoices) - Fetch the billing transaction history (invoices) for a specific member. Read-only. Backed by the WHMCS billing integration. Required: exactly one of user_id (standard — the BD member ID) OR client_id (power-user — the WHMCS billing record ID stored on the user as users_data.clientid). Default to user_id; reach for client_id only when you already have one in hand and want to bypass the user lookup. 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 entry is { invoice_details, subscription_details }. invoice_details 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 is the linked subscription, or false when there is none. |
| getUserSubscriptionsA | 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: exactly one of user_id (standard — the BD member ID) OR client_id (power-user — the WHMCS billing record ID stored on the user as users_data.clientid). Default to user_id; reach for client_id only when you already have one in hand and want to bypass the user lookup. 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. |
| listReviewsA | 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 keyword-in-body matching, use property=review_description property_operator=LIKE property_value=<word>. For a single known review use getReview. Pagination: cursor-based (limit, page). See Rule: Pagination for full cursor/cap/stop semantics. Filter/sort: property+property_value+property_operator, order_column+order_type. See Rule: Filter operators for the verified-working operator set, silent-drop detection, and derived-field unfilterability. Enums: property_operator: =, LIKE, >, <, >=, <=. See also: getReview (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 (with review_description truncated by default; see Rule: Lean read responses). |
| getReviewA | 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; supports keyword filter via property=review_description property_operator=LIKE). Returns: { status: "success", message: [{...record}] } - the message array contains 1 record when found. Empty or HTTP 404 when not found. |
| createReviewA | 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). |
| updateReviewA | 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. |
| deleteReviewA | 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. |
| listClicksA | 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 Rule: Pagination for full cursor/cap/stop semantics. Filter/sort: property+property_value+property_operator, order_column+order_type. See Rule: Filter operators for the verified-working operator set, 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. |
| getClickA | 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. |
| createClickA | 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). |
| updateClickA | 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. |
| deleteClickA | 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. |
| listLeadsA | 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 Rule: Pagination for full cursor/cap/stop semantics. Filter/sort: property+property_value+property_operator, order_column+order_type. See Rule: Filter operators for the verified-working operator set, 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. |
| getLeadA | 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. |
| createLeadA | 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. |
| matchLeadA | 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. |
| updateLeadA | Update a lead - Update an existing lead record by ID. Fields omitted are untouched. Required: lead_id. Custom (form-defined) lead fields — e.g. wizard-style hidden match_* fields — live in users_meta with database=leads and database_id=<lead_id>. Use updateUserMeta / createUserMeta for those. Rule: Forms § Form classes → Custom-field storage covers the pattern. See also: createLead, deleteLead, matchLead, updateUserMeta. Returns: { status: "success", message: {...updatedRecord} }. |
| deleteLeadA | 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. |
| listLeadMatchesA | 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: BD returns { status: "error", message: "lead_matches not found", total: 0 } on zero rows (NOT the standard success-shape). The wrapper normalizes this to { status: "success", total: 0, message: [] } before responding — but if a raw BD response leaks through, treat the exact message "lead_matches not found" as an empty result, not as an endpoint failure. Pagination: cursor-based (limit, page). See Rule: Pagination for full cursor/cap/stop semantics. Filter/sort: property+property_value+property_operator, order_column+order_type. See Rule: Filter operators for the verified-working operator set, silent-drop detection, and derived-field unfilterability. Returns: { status: "success", total, current_page, total_pages, next_page, prev_page, message: [...records] }. |
| getLeadMatchA | 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. |
| createLeadMatchA | 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): 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). |
| updateLeadMatchA | 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. |
| deleteLeadMatchA | 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. |
| listSingleImagePostsA | List posts - Paginated enumeration of post records. Read-only. Lean-by-default keep-list: rows return only the core identity + routing + load-bearing fields: post_id, post_title, post_filename, post_status, post_start_date, post_expire_date, post_location, post_venue, post_category, data_id, data_type, system_name, data_name, data_filename, user_id, post_image, original_image_url, revision_timestamp, plus total_clicks (only when > 0) / total_photos rollups. Everything else stripped — restore via flags: include_content=1 - return post_content (HTML body).
include_post_seo=1 - return post_meta_title, post_meta_description, post_meta_keywords.
include_author_full=1 - return the full user nested object. Default omits author detail entirely; call getUser(user_id) for author records.
include_clicks=1 - return the full click array under user_clicks_schema.
include_photos=1 - return the full users_portfolio photo array (multi-image posts).
include_extras=1 - return everything else (lat, lon, country_sn, state_sn, post_org_url, post_date, post_live_date, post_updated, post_token, post_clicks, recurring_type, sticky_post, post_featured, post_tags, post_job, post_video, post_price, image_imported, etc.).
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 Rule: Pagination for full cursor/cap/stop semantics. Filter/sort: property+property_value+property_operator, order_column+order_type. See Rule: Filter operators for the verified-working operator set, silent-drop detection, and derived-field unfilterability. See also: getSingleImagePost (single record by ID). For keyword-in-body matching, use this tool with property=post_title property_operator=LIKE (or post_caption/post_content). Returns: { status: "success", total, current_page, total_pages, next_page, prev_page, message: [...records] }. Each record is the lean-shaped resource object. |
| getSingleImagePostA | Get a single post - Fetch a single post record. Read-only. Lean-by-default keep-list: response returns only the core identity + routing + load-bearing fields: post_id, post_title, post_filename, post_status, post_start_date, post_expire_date, post_location, post_venue, post_category, data_id, data_type, system_name, data_name, data_filename, user_id, post_image, original_image_url, revision_timestamp, plus total_clicks (only when > 0) / total_photos rollups. Same shape as listSingleImagePosts. Restore via flags: include_content=1 (full post_content HTML), include_post_seo=1 (meta_title/description/keywords), include_author_full=1 (full user nested — default omits author detail; call getUser(user_id) otherwise), include_clicks=1 (click array), include_photos=1 (photo array on multi-image), include_extras=1 (everything else: lat, lon, country_sn, state_sn, post_org_url, post_date, post_live_date, post_updated, post_token, post_clicks, recurring_type, sticky_post, post_featured, post_tags, post_job, post_video, post_price, image_imported, etc.). Use when: fetching one post by post_id. For enumeration or keyword search use listSingleImagePosts (with property=post_title property_operator=LIKE for keyword-in-body). Required: post_id. See also: listSingleImagePosts (enumerate many; supports keyword filter via property + property_operator=LIKE). Returns: { status: "success", message: [{...record}] } - the message array contains 1 lean-shaped record when found. Empty or HTTP 404 when not found. |
| getSingleImagePostFieldsA | 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. post_category values do NOT come from this endpoint: on some forms BD fills post_category.choices from platform master defaults (Sport, Business, ...) instead of this site's list. Source post_category from the post type's feature_categories (on your listPostTypes/getPostType result) and pass one value from it. BD does NOT trim whitespace when splitting the CSV - options after the first may have a leading space (e.g. " Category 2"). Pass VERBATIM.
|
| createSingleImagePostA | 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: listSingleImagePosts property=post_title property_value=<proposed> property_operator==. Zero rows = title free; >=1 row = taken. If post_title contains a comma (the = operator trips the CSV validator on commas only - colons and other characters are safe), switch to property_operator=like property_value=<distinctive-prefix>% using a 3-4-word prefix unique to this event. Do NOT paginate unfiltered lists - sites in the wild have thousands of posts; filtered lookup is one tiny response. If taken: compare records, not strings - the same real-world record -> do NOT create (reuse via updateSingleImagePost or skip); a different record that happens to share the name -> retitle to distinguish and re-check. A free title is NOT proof of a new record: retitled duplicates share dates, venues, and employers - for dated or venued post types also probe post_start_date (8-digit day, contains) or post_venue (contains), paired with data_id, before creating. Never create a duplicate under a new name. 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. Read the CSV from your listPostTypes/getPostType result (or getPostTypeCustomFields.post_category.choices where your workflow routes through it) and pass ONE value from it VERBATIM - never from getSingleImagePostFields.post_category.choices, which BD fills from platform master defaults on some forms - 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. |
| updateSingleImagePostA | 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 Rule: URL slug rename 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. |
| deleteSingleImagePostA | 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. |
| listMultiImagePostsA | List album groups - Paginated enumeration of portfoliogroup records. Read-only. Lean-by-default keep-list: rows return only the core identity + routing fields: group_id, group_name, group_filename, group_status, data_id, data_type, system_name, data_name, data_filename, user_id, revision_timestamp, plus total_clicks (only when > 0), total_photos, cover_photo_url, cover_thumbnail_url rollups. Same keep-list as listSingleImagePosts (single-image fields like post_start_date simply won't appear on multi-image rows). Restore via flags: include_content=1 (full group_desc HTML), include_author_full=1 (full user nested — default omits author detail; call getUser(user_id) otherwise), include_clicks=1 (click array), include_photos=1 (full users_portfolio photo array shaped to PHOTO_LEAN_ALWAYS_KEEP), include_extras=1 (everything else: lat, lon, country_sn, state_sn, post_date, post_live_date, post_updated, post_token, etc.). 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 Rule: Pagination for full cursor/cap/stop semantics. Filter/sort: property+property_value+property_operator, order_column+order_type. See Rule: Filter operators for the verified-working operator set, silent-drop detection, and derived-field unfilterability. See also: getMultiImagePost (single record by ID). For keyword-in-body matching, use this tool with property=group_name property_operator=LIKE (or group_desc). Returns: { status: "success", total, current_page, total_pages, next_page, prev_page, message: [...records] }. Each record is lean-shaped per the keep-list above. |
| getMultiImagePostA | Get a single album group - Fetch a single portfoliogroup record. Read-only. Lean-by-default keep-list: same shape as listMultiImagePosts — core identity + routing fields plus total_clicks (only when > 0), total_photos, cover_photo_url, cover_thumbnail_url. Restore via flags: include_content=1 (full group_desc HTML), include_author_full=1 (full user nested — default omits author detail; call getUser(user_id) otherwise), include_clicks=1, include_photos=1 (full users_portfolio photo array), include_extras=1 (everything else — geo, post dates, timestamps, tokens, etc.). 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; supports keyword filter via property + property_operator=LIKE). Returns: { status: "success", message: [{...record}] } - the message array contains 1 lean-shaped record when found. Empty or HTTP 404 when not found. |
| getMultiImagePostFieldsA | 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. |
| createMultiImagePostA | 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 group_name, and the public URL slug is derived from it — duplicate names produce a URL collision (unpredictable which resolves). Do a server-side filter-find: listMultiImagePosts property=group_name property_value=<proposed> property_operator==. Zero rows = name free; >=1 = taken. If taken: compare records, not strings - the same real-world record -> do NOT create (reuse via updateMultiImagePost); a different record sharing the name -> retitle to distinguish and re-check. Never create a duplicate under a new name. 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. |
| updateMultiImagePostA | 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 Rule: URL slug rename 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. |
| deleteMultiImagePostA | 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. |
| listMultiImagePostPhotosA | List album photos - Paginated enumeration of portfoliophoto records. Read-only. Lean-by-default keep-list: rows return photo_id, user_id, group_id, file, original_image_url, title, order, status, image_imported, revision_timestamp. Marketplace fields (price, manufacturer, availability, product_category, product_type, condition, inv_id, link, additional_fields) restore via include_marketplace=1. Use when: fetching all photos within a multi-image post - always pass group_id to filter. For a single photo by ID use getMultiImagePostPhoto. For image-dedup: property=original_image_url property_operator=in property_value=<URL1,URL2,URL3> returns matched rows with original_image_url in the lean response. Pagination: cursor-based (limit, page). See Rule: Pagination for full cursor/cap/stop semantics. Filter/sort: property+property_value+property_operator, order_column+order_type. See Rule: Filter operators for the verified-working operator set, 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 lean-shaped per the keep-list above. |
| getMultiImagePostPhotoA | Get a single album photo - Fetch a single portfoliophoto record. Read-only. Lean-by-default keep-list: same shape as listMultiImagePostPhotos — photo_id, user_id, group_id, file, original_image_url, title, order, status, image_imported, revision_timestamp. Marketplace fields restore via include_marketplace=1. 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. |
| createMultiImagePostPhotoA | 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: See also: createMultiImagePost (the correct path for external URLs), updateMultiImagePostPhoto (modify title/order only — cannot re-import). |
| updateMultiImagePostPhotoA | 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. |
| deleteMultiImagePostPhotoA | 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. |
| listPostTypesA | List post types - Paginated enumeration of posttype records. Read-only. Lean-by-default keep-list: rows return only the core identity + routing fields: data_id, data_type, system_name, data_name, data_filename, form_name, feature_categories, type_of_feature, is_event_feature, is_digital_product, revision_timestamp. Everything else stripped — restore via flags: type_of_feature enum: 1 = events, 2 = real-estate properties, 0 = digital products, null = all other post types. Filter non-event/property/digital-product types by system_name / form_name / data_name — NOT by type_of_feature.
include_code=1 - return the 8 PHP/HTML code-template fields (search_results_div, search_results_layout, profile_results_layout, profile_header, profile_footer, category_header, category_footer, comments_code). Use this when editing post-type templates.
include_post_comment_settings=1 - return the post_comment_settings JSON-string field.
include_review_notifications=1 - return the 5 review-notification email template fields.
include_extras=1 - return everything else (h1, h2, icon, category_tab, profile_tab, per_page, profile_per_page, sidebar configs, always_on, distance_search, display_order, caption_length, data_active, and all per-page/per-tab display toggles).
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. Reserved data_types — default-excluded. 10 (Member Listings — use listUsers / searchUsers; opt-in rows omit data_filename — members live at /<user.filename>, member directory landing is /search_results, never /listing/<id>), 13 (Member Ratings), 21 (Member Categories — use listTopCategories / listSubCategories). To include reserved records, opt in via the property / property_value filter: property=data_type, property_value=10, property_operator== for a single value; property=data_type, property_value=10,4,9, property_operator=in for a comma-list mix of reserved and standard. Pagination: cursor-based (limit, page). See Rule: Pagination for full cursor/cap/stop semantics. Filter/sort: property+property_value+property_operator, order_column+order_type. See Rule: Filter operators for the verified-working operator set, silent-drop detection, and derived-field unfilterability. See also: getPostType (single record by ID). Returns: { status: "success", total, current_page, total_pages, next_page, prev_page, message: [...records] }. Each record is lean-shaped per the keep-list above. |
| getPostTypeA | Get a single post type - Fetch a single posttype record. Read-only. Lean-by-default keep-list: same shape as listPostTypes — returns only the core identity + routing fields: data_id, data_type, system_name, data_name, data_filename, form_name, feature_categories, type_of_feature, is_event_feature, is_digital_product, revision_timestamp. Restore via flags: include_code=1 (the 8 PHP/HTML code-template fields — required when you intend to edit them via updatePostType), include_post_comment_settings=1 (the post_comment_settings JSON), include_review_notifications=1 (the 5 review-notification email fields), include_extras=1 (everything else: h1, h2, icon, category_tab, profile_tab, per_page, profile_per_page, sidebar configs, always_on, distance_search, display_order, caption_length, data_active, and all per-page/per-tab display toggles). 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 — apply Rule: Post-type code fields. 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 (when include_code=1) - 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 Rule: Post-type code fields). Always pass include_code=1 and read current values here BEFORE calling updatePostType for code-field edits. Reserved data_types — not reachable here. If the resolved record's data_type is 10 (Member Listings), 13 (Member Ratings), or 21 (Member Categories), this endpoint returns message: [] (empty). To access these records, use listPostTypes property=data_type property_value=<value> (e.g. property_value=10) which returns the same data. Member Listings rows omit data_filename — members live at /<user.filename>, member directory landing is /search_results, never /listing/<id>. See also: listPostTypes (enumerate many; reserved data_types default-excluded, opt-in via property=data_type, property_value=<value>), updatePostType (write; applies Rule: Post-type code fields and Rule: Member Listings post type), getPostTypeCustomFields (per-type custom field enum). Returns: { status: "success", message: [{...record}] } - the message array contains 1 lean-shaped record when found. Empty or HTTP 404 when not found. |
| getPostTypeCustomFieldsA | 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. Required: exactly one of data_id (numeric post-type ID) OR system_name (string, e.g. website_blog_article). When system_name is given the wrapper resolves it to data_id via listPostTypes before calling BD. Parameter interactions: data_id - the post type to introspect; get via listPostTypes
system_name - friendlier alternative; the wrapper does the lookup
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. |
| updatePostTypeA | Update a post type - Update a post type. PATCH semantics (except per Rule: Post-type code fields). 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. Picking the right post type — disambiguation. Apply Rule: Resource disambiguation before editing. "Edit my classifieds page" is layer-ambiguous (a WebPage vs. the post type's code group vs. Member Listings UI vs. a category landing) — confirm WHICH layer even when only one record string-matches. Resolve to data_id via listPostTypes first; never proceed on semantic similarity alone. 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: Search-results (every post type INCLUDING Member Listings): category_header + search_results_div + category_footer. Send all 3. 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. 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: getPostType(data_id) - returns current values with master fallback.
Identify target group. Build payload: changed field(s) + other group-mates copied verbatim from GET. 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?: "..." }. |
| deletePostTypeA | 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. |
| listUnsubscribesA | 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 Rule: Pagination for full cursor/cap/stop semantics. Filter/sort: property+property_value+property_operator, order_column+order_type. See Rule: Filter operators for the verified-working operator set, 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. |
| getUnsubscribeA | 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. |
| createUnsubscribeA | 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.
|
| updateUnsubscribeA | 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. |
| deleteUnsubscribeA | 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. |
| listWidgetsA | List widgets - Paginated enumeration of widget records. Read-only. Lean-by-default keep-list: rows return only widget_id, widget_name, widget_type, widget_viewport, short_code, date_updated, revision_timestamp, is_default. The code fields (widget_data, widget_style, widget_javascript) are stripped — restore with include_code=1. 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 Rule: Pagination for full cursor/cap/stop semantics. Filter/sort: property+property_value+property_operator, order_column+order_type. See Rule: Filter operators for the verified-working operator set, 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 carries the full widget object (fields enumerated in the table that follows). 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) | is_default
| boolean | false = the site's own record, true = an uncustomised master default (read-only; filter with property=is_default). See Rule: Default-merge models.
|
Site records and platform master defaults are merged in the response — see Rule: Default-merge models to filter to one and to sort. |
| getWidgetA | Get a single widget - Fetch a single widget record by widget_id. Read-only. Lean-by-default keep-list: returns only widget_id, widget_name, widget_type, widget_viewport, short_code, date_updated, revision_timestamp, is_default. The code fields (widget_data, widget_style, widget_javascript) are stripped — restore with include_code=1. 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. |
| createWidgetA | 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. |
| updateWidgetA | 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. |
| deleteWidgetA | 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" }. |
| renderWidgetA | 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). |
| listEmailTemplatesA | 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 Rule: Pagination for full cursor/cap/stop semantics. Filter/sort: property+property_value+property_operator, order_column+order_type. See Rule: Filter operators for the verified-working operator set, 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. |
| getEmailTemplateA | 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. |
| createEmailTemplateA | 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: 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. |
| updateEmailTemplateA | 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. |
| deleteEmailTemplateA | 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. |
| listFormsA | 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 Rule: Pagination for full cursor/cap/stop semantics. Filter/sort: property+property_value+property_operator, order_column+order_type. See Rule: Filter operators for the verified-working operator set, 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. Site records and platform master defaults are merged in the response — see Rule: Default-merge models to filter to one and to sort. |
| getFormA | 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. |
| createFormA | 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, form_url, form_class, table_index, form_action_div, form_email_on, form_success_message. Class selection — see Rule: Forms § Form classes before picking form_table. Follow § Form-level recipe for the canonical creation recipe. Mandatory form_name pre-check per Rule: Pre-check natural keys — BD does NOT enforce uniqueness; duplicate form_name produces ambiguous [form=…] shortcode resolution. Wrapper-enforced refusal: form_action_type=redirect AND empty form_target → call refused (see Rule: Forms § Wrapper-enforced invariants). See also: updateForm, createFormField, listFormFields. Returns: { status: "success", message: {...createdRecord}, _admin_edit_url: "..." }. _admin_edit_url is a centralized-admin deep-link to the Form Builder editor for this form_name — surface it to the user so they can jump straight to the admin edit screen for the form just created. |
| updateFormA | Update a form - Update an existing form record by ID. Fields omitted are untouched. Writes live data. Required: form_id. Cross-refs same as createForm — see Rule: Forms § Form-level recipe / § Lead-match / § Member-dashboard. Before flipping form_action_type to a public-facing value, run listFormFields to confirm the tail pattern exists. Wrapper-enforced refusal: form_action_type=redirect AND empty form_target → call refused. See also: createForm, deleteForm, listFormFields / createFormField. Returns: { status: "success", message: {...updatedRecord}, _admin_edit_url: "..." }. _admin_edit_url is a centralized-admin deep-link to the Form Builder editor for this form's form_name — surface it to the user so they can jump straight to the admin edit screen for the form just updated. |
| deleteFormA | 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. |
| listFormFieldsA | List form fields - Paginated enumeration of formfield records. Read-only. Use when: listing fields on a form. Filter by form_name (text slug — form_fields joins to forms by form_name, not form_id). Pagination: cursor-based (limit, page). See Rule: Pagination for full cursor/cap/stop semantics. Filter/sort: property+property_value+property_operator, order_column+order_type. See Rule: Filter operators for the verified-working operator set, 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. Site records and platform master defaults are merged in the response — see Rule: Default-merge models to filter to one and to sort. |
| getFormFieldA | 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. |
| createFormFieldA | Create a form field - Create a new formfield record. Writes live data. Required: form_name, field_name, field_text, field_type, field_order. Canonical field_name for form_table=website_contacts: yourname / inquiry_email / phone / comments (NOT name / email / message — those persist but don't surface in the admin inbox columns). Anything else = custom field_name. Full table at Rule: Forms § Form classes. See Rule: Forms § Field anatomy for field shape, view-flag defaults, validators, and the canonical json_meta skeleton. § Form-level recipe covers the tail pattern. § Lead-match / § Member-dashboard cover special-case forms. Wrapper-enforced refusals: (1) field_required=1 with field_type ∈ {HoneyPot, HTML, Tip, Button} — Hidden is allowed. (2) field_type not in the canonical enum (strict case match; textarea is the lone lowercase value). (3) field_type=Hidden with empty field_name or empty field_text. (4) Non-binary value on any of field_required / field_input_view / field_display_view / field_email_view / field_search_view / field_grid_view / field_input_view_admin_only (empty / omitted accepted — BD applies per-field defaults). Agent pre-checks (NOT wrapper-enforced): field_name uniqueness within form, single submit element per form. See Rule: Forms § Wrapper-enforced invariants → Agent-side responsibilities. See also: updateFormField, listFormFields. |
| updateFormFieldA | Update a form field - Update an existing formfield record by ID. Fields omitted are untouched. Writes live data. Required: field_id. When renaming field_name on a form_table=website_contacts form, use canonical names (yourname / inquiry_email / phone / comments) — see createFormField and Rule: Forms § Form classes. See Rule: Forms § Field anatomy for field shape, view-flag defaults, validators, and the canonical json_meta skeleton. Wrapper-enforced refusals: (1) field_required=1 with field_type ∈ {HoneyPot, HTML, Tip, Button} — Hidden is allowed. (2) field_type not in the canonical enum (strict case match; textarea is the lone lowercase value). (3) field_type=Hidden with empty field_name or empty field_text. (4) Non-binary value on any of field_required / field_input_view / field_display_view / field_email_view / field_search_view / field_grid_view / field_input_view_admin_only (empty / omitted accepted — BD applies per-field defaults). Agent pre-checks (NOT wrapper-enforced): field_name uniqueness within form, single submit element per form. See Rule: Forms § Wrapper-enforced invariants → Agent-side responsibilities. See also: createFormField, deleteFormField, listFormFields. |
| deleteFormFieldA | 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. |
| listMembershipPlansA | 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 keep-list: rows return only the core fields: subscription_id, subscription_name, subscription_type, profile_type, monthly_amount, yearly_amount, initial_amount, lead_price, searchable, search_membership_permissions, data_settings. data_settings is the comma-separated list of post-type IDs this plan can publish — kept by default to support author-resolution flows (find plans whose members can publish a given post type). A plan's members have a publicly accessible, searchable profile (BD's UI calls this "Listing Searchable") only when searchable=1 AND search_membership_permissions contains visitor. Everything else stripped — restore via 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.
include_extras=1 - returns the full BD plan row, untouched (every column).
Pagination: cursor-based (limit, page). See Rule: Pagination. Filter/sort: property+property_value+property_operator, order_column+order_type. See Rule: Filter operators. See also: getMembershipPlan (single by ID). Returns: { status: "success", total, ..., message: [...records] }. |
| getMembershipPlanA | 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-by-default keep-list: the core fields — subscription_id, subscription_name, subscription_type, profile_type, monthly_amount, yearly_amount, initial_amount, lead_price, searchable, search_membership_permissions, data_settings. A plan's members have a publicly accessible, searchable profile (BD's UI calls this "Listing Searchable") only when searchable=1 AND search_membership_permissions contains visitor. 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.
include_extras=1 - returns the full BD plan row, untouched.
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}] }. |
| listMenusA | List menus - Paginated enumeration of menu records. Read-only. Lean-by-default keep-list: rows return only menu_id, menu_name, menu_title, revision_timestamp. Styling/target/rel/json_meta fields stripped — restore via include_extras=1 when editing menu appearance. 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 Rule: Pagination for full cursor/cap/stop semantics. Filter/sort: property+property_value+property_operator, order_column+order_type. See Rule: Filter operators for the verified-working operator set, 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 lean-shaped per the keep-list above. Site records and platform master defaults are merged in the response — see Rule: Default-merge models to filter to one and to sort. |
| getMenuA | Get a single menu - Fetch a single menu record. Read-only. Lean-by-default keep-list: same shape as listMenus — returns only menu_id, menu_name, menu_title, revision_timestamp. Restore styling/target/rel/json_meta via include_extras=1. 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 lean-shaped record when found. Empty or HTTP 404 when not found. |
| createMenuA | 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
See also: updateMenu (modify existing). Returns: { status: "success", message: {...createdRecord}, _admin_edit_url: "..." }. _admin_edit_url is a centralized-admin deep-link to the Menu Builder editor for this menu_id — surface it to the user so they can jump straight to the admin edit screen for the menu just created. |
| updateMenuA | 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}, _admin_edit_url: "..." } - the full updated record after changes applied. _admin_edit_url is a centralized-admin deep-link to the Menu Builder editor for this menu_id — surface it to the user so they can jump straight to the admin edit screen for the menu just updated. |
| deleteMenuA | 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. |
| listMenuItemsA | List menu items - Paginated enumeration of menuitem records. Read-only. Lean-by-default keep-list: rows return only menu_item_id, menu_name, menu_link, menu_order, menu_id, master_id. Styling/target/rel/json_meta, plus revision_timestamp / menu_title / menu_display / tablesExists (rarely actionable on read), are stripped — restore via include_extras=1. Default empty-link filter: rows where menu_link is empty/null (infrastructure nodes — section headers, placeholders) are excluded by default. They can't be link targets. Opt in with include_empty_links=1 only when auditing the full menu structure. 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 Rule: Pagination for full cursor/cap/stop semantics. Filter/sort: property+property_value+property_operator, order_column+order_type. See Rule: Filter operators for the verified-working operator set, 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 lean-shaped per the keep-list above. total reflects post-filter count when include_empty_links=0. Site records and platform master defaults are merged in the response — see Rule: Default-merge models to filter to one and to sort. |
| getMenuItemA | Get a single menu item - Fetch a single menuitem record. Read-only. Lean-by-default keep-list: same shape as listMenuItems — returns only menu_item_id, menu_name, menu_link, menu_order, menu_id, master_id. Restore styling/target/rel/json_meta + the rarely-actionable revision_timestamp / menu_title / menu_display / tablesExists via include_extras=1. Use when: editing one specific menu entry. Single-record fetch does NOT apply the empty-link filter — caller asked for this specific row by ID and gets it back. Required: menu_item_id. See also: listMenuItems (enumerate many). Returns: { status: "success", message: [{...record}] } - the message array contains 1 lean-shaped record when found. Empty or HTTP 404 when not found. |
| createMenuItemA | 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). |
| updateMenuItemA | 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. |
| deleteMenuItemA | 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. |
| listSubCategoriesA | 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 Rule: Pagination for full cursor/cap/stop semantics. Filter/sort: property+property_value+property_operator, order_column+order_type. See Rule: Filter operators for the verified-working operator set, 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 BD's internal table names. Use createTopCategory / createSubCategory instead (BD's table-name → tool-name mapping is documented in Rule: Table to endpoint). |
| getSubCategoryA | 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 BD's internal table names. Use createTopCategory / createSubCategory instead (BD's table-name → tool-name mapping is documented in Rule: Table to endpoint). |
| createSubCategoryA | Create a service - Create a new SUB-level member category under an existing Top Category. Writes live data. Use createTopCategory or createSubCategory only for a single category that needs desc, keywords, icon, sort_order, lead_price, image, master_id, or a filename other than the default slug set at create time. For every other category create, use createCategoryTree. A Sub Category is level 2 of the 3-tier member classification. It MUST have a parent Top Category (via profession_id). It may optionally sit under another Sub Category (for sub-sub-category nesting, via master_id). Backed by BD's list_services table. Use when: adding one sub-category that needs desc, keywords, sort_order, lead_price, image, master_id, or a filename other than the default slug set at create time. To auto-create sub-categories while writing a member, include the names in services on createUser, or pass create_new_categories=1 on updateUser. Required: name, profession_id. Pre-check before create: BD does NOT enforce uniqueness on filename (URL slug) or name - but uniqueness IS scoped per-parent (two sub-cats with the same filename under different profession_id is fine; same filename under the SAME profession_id is not). Do a server-side filter-find: listSubCategories property=filename property_value=<proposed> property_operator==, then filter results by the intended profession_id. Zero rows under that parent = slug free; >=1 row = taken (URL collision - wrong sub-cat page resolves). Do NOT paginate unfiltered lists - filtered lookup is one tiny response. If taken: reuse via updateSubCategory, OR ask the user, OR pick an alternate filename and re-check. Wrapper safety net: on a missed pre-check, the wrapper auto-suffixes filename on collision (-1...-20) and surfaces the suffix in the response. Pre-checking still preferred — auto-suffix surprises the caller in URL-sensitive workflows. Parameter guidance: name - human-readable (e.g. "Sushi")
profession_id - the parent Top Category's ID (from listTopCategories or createTopCategory)
master_id - for SUB-SUB-CATEGORY nesting, pass the parent Sub Category's ID; default 0 means "directly under the Top Category"
filename - URL-slug form; desc, keywords, sort_order, lead_price, image - all optional. The default slug is the name lower-cased and hyphenated, with any character outside the Latin set percent-encoded.
See also: updateSubCategory (modify), listSubCategories (list), createTopCategory (create parent). Writes live data: changes are immediately visible on the public site. Returns: { status: "success", message: {...createdRecord} } including service_id. Use that to assign members via updateUser.services (CSV) or createMemberSubCategoryLink. 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 BD's internal table names. Use createCategoryTree, or createTopCategory / createSubCategory when a single category needs create-time field control (BD's table-name → tool-name mapping is documented in Rule: Table to endpoint). |
| updateSubCategoryA | Update a service - Update an existing SUB-level member category by service_id. Fields omitted are untouched. Writes live data. Use when: renaming, re-parenting (change profession_id to move under a different Top, or master_id to re-nest as sub-sub), or adjusting lead_price for per-service lead pricing. Required: service_id. Filename rename caveat: if the existing filename has a seo_type=profile_search_results web page bound to it, renaming this category orphans that page. The wrapper rejects renames that would orphan a bound page — rename or delete the bound page first, then rename the category. Parameter notes: See also: createSubCategory (add new), deleteSubCategory (remove). Returns: { status: "success", message: {...updatedRecord} }. 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 BD's internal table names. Use createTopCategory / createSubCategory instead (BD's table-name → tool-name mapping is documented in Rule: Table to endpoint). |
| deleteSubCategoryA | Delete a service - Permanently delete a SUB-level member category by service_id. Destructive - cannot be undone via API. Use when: removing an unused sub-category. Any member with this service_id in their users_data.services CSV or in rel_services rows becomes orphaned - clean those up first. Required: service_id. Destructive: confirm intent. Members whose users_data.services CSV contains this ID will have an orphan reference. Any Member ↔ Sub Category links (rel_services) pointing at this service_id also become orphaned. Bound-page caveat: if this category's filename has a seo_type=profile_search_results web page bound to it, deleting the category orphans that page (it'll render empty — no category to query). The wrapper rejects deletes that would orphan a bound page — delete or repurpose the bound page first. See also: updateSubCategory (modify without removing). Returns: { status: "success", message: "list_services record was deleted" }. 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 BD's internal table names. Use createTopCategory / createSubCategory instead (BD's table-name → tool-name mapping is documented in Rule: Table to endpoint). |
| listMemberSubCategoryLinksA | List user-service relationships - Paginated enumeration of MEMBER ↔ SUB CATEGORY links. Read-only. Each record links a member (user_id) to a Sub Category (service_id) with per-link metadata: avg_price, specialty, num_completed, date. This is level 3 of the member-taxonomy relationship. Backed by BD's rel_services table. Use when: auditing per-service-link metadata (prices, specialty flags, completion counts) across members. Filter by user_id to see one member's links, service_id to see everyone offering that service. For simpler "is this member tagged with this sub-cat" checks, the users_data.services CSV on the member record is cheaper. When to use this vs. the simpler users_data.services CSV field: use this resource when you need PER-LINK metadata (pricing tier, specialty flag, completion counter). If you just want "this member is tagged with these Sub Categories" with no extra data, set updateUser.services (CSV of service IDs) instead. Pagination + filter/sort: standard. See also: getMemberSubCategoryLink, createMemberSubCategoryLink, listSubCategories (available Sub Categories), updateUser (sets the services CSV for simpler cases). Returns: { status: "success", ..., message: [...records] }. Each has rel_id, user_id, service_id, date, avg_price, num_completed, specialty. 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 BD's internal table names. Use createTopCategory / createSubCategory instead (BD's table-name → tool-name mapping is documented in Rule: Table to endpoint). |
| getMemberSubCategoryLinkA | Get a single user-service relationship - Fetch a single Member ↔ Sub Category link by rel_id. Read-only. Use when: you have a rel_id and need the full link row. Rare - most workflows query by user_id or service_id via listMemberSubCategoryLinks. Required: rel_id. See also: listMemberSubCategoryLinks (enumerate, filter by user_id or service_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 BD's internal table names. Use createTopCategory / createSubCategory instead (BD's table-name → tool-name mapping is documented in Rule: Table to endpoint). |
| createMemberSubCategoryLinkA | Link a service to a user - Create a new Member ↔ Sub Category link with optional metadata. Writes live data. Links an existing member (user_id) to an existing Sub Category (service_id) with per-link metadata (pricing, specialty). This is the richer alternative to setting users_data.services CSV via updateUser - use this when you need per-link data. Use when: the simpler users_data.services CSV isn't rich enough - you need per-link avg_price, specialty=1, or num_completed. For plain "tag this member with this sub-cat" use updateUser with services=<service_id> instead. Required: user_id, service_id. Pre-check before create (PAIR uniqueness): BD does NOT enforce uniqueness on the (user_id, service_id) pair in rel_services. Linking the same member to the same Sub Category twice produces two rel_services rows, double-counts the member in that Sub Category's listing widgets, and leaves per-link metadata (specialty/avg_price) ambiguous - which row wins? Filter-find pattern (single-field server filter + client-side intersect): call listMemberSubCategoryLinks property=user_id property_value=<proposed user_id> property_operator== to narrow to all rel_services rows for that member, then CLIENT-SIDE filter to rows where service_id=<proposed service_id>. Zero results after client-side step = link free; >=1 = already linked. If the link already exists: update it via updateMemberSubCategoryLink (e.g. to set specialty=1 or avg_price), OR skip the create (idempotent). Never silently double-link the same member to the same Sub Category. Parameter guidance: user_id - member (from listUsers / searchUsers)
service_id - Sub Category (from listSubCategories)
avg_price - decimal, the member's price for this service
specialty - 0 or 1 flags this Sub Category as a specialty offering on the member's profile
num_completed - counter of jobs/projects completed in this Sub Category
date - YYYYMMDDHHmmss timestamp
See also: updateUser with services="<csv>" (simpler, no per-link metadata), listSubCategories, getMemberSubCategoryLink. Writes live data: appears on the member's public profile immediately. Returns: { status: "success", message: {...createdRecord} } with rel_id. 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 BD's internal table names. Use createTopCategory / createSubCategory instead (BD's table-name → tool-name mapping is documented in Rule: Table to endpoint). |
| updateMemberSubCategoryLinkA | Update a user-service relationship - Update a Member ↔ Sub Category link by rel_id. Fields omitted are untouched. Writes live data. Use when: adjusting per-link metadata - member's price for this service, specialty flag, completion counter. Required: rel_id. Updatable fields: avg_price, specialty, num_completed, date. See also: createMemberSubCategoryLink (add new), deleteMemberSubCategoryLink (remove). Returns: { status: "success", message: {...updatedRecord} }. 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 BD's internal table names. Use createTopCategory / createSubCategory instead (BD's table-name → tool-name mapping is documented in Rule: Table to endpoint). |
| deleteMemberSubCategoryLinkA | Remove a service from a user - Permanently delete a Member ↔ Sub Category link by rel_id. Destructive - cannot be undone via API. Removes the member's link to this Sub Category in the rel_services join table. Does NOT remove the member from users_data.services CSV if the service_id is listed there - update that separately via updateUser if needed. Use when: removing a specific link row. Does NOT update the users_data.services CSV - that's a separate field; update it via updateUser if the service_id is also listed there. Required: rel_id. Returns: { status: "success", message: "rel_services record was deleted" }. 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 BD's internal table names. Use createTopCategory / createSubCategory instead (BD's table-name → tool-name mapping is documented in Rule: Table to endpoint). |
| listUserPhotosA | List user photos - Paginated enumeration of userphoto records. Read-only. Use when: enumerating photos attached to members (profile, logo, cover). Filter by user_id. Pagination: cursor-based (limit, page). See Rule: Pagination for full cursor/cap/stop semantics. Filter/sort: property+property_value+property_operator, order_column+order_type. See Rule: Filter operators for the verified-working operator set, silent-drop detection, and derived-field unfilterability. See also: getUserPhoto (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. |
| getUserPhotoA | Get a single user photo - Fetch a single userphoto record. Read-only. Use when: fetching one photo record by photo_id. Required: photo_id. See also: listUserPhotos (enumerate many). Returns: { status: "success", message: [{...record}] } - the message array contains 1 record when found. Empty or HTTP 404 when not found. |
| createUserPhotoA | Create a user photo - Create a new userphoto record. Writes live data. Use when: attaching a new photo record to a member. The image file must already exist in site storage (upload via admin or auto_image_import). Required: user_id, file, type. Parameter interactions: user_id - the member
type - slot: logo, photo, or cover_photo
file - image filename (must already exist in site storage)
See also: updateUserPhoto (modify existing). |
| updateUserPhotoA | Update a user photo - Update an existing userphoto record by ID. Fields omitted are untouched. Writes live data. Use when: changing a photo's type slot (logo/photo/cover_photo). Required: photo_id. Enums: type: logo, photo, cover_photo. See also: createUserPhoto (add new), deleteUserPhoto (remove permanently). Returns: { status: "success", message: {...updatedRecord} } - the full updated record after changes applied. |
| deleteUserPhotoA | Delete a user photo - Permanently delete a userphoto record by ID. Destructive - cannot be undone via API. Use when: removing a photo attachment. For member image management consider updateUser with images_action=remove_all / remove_cover_image / etc. instead - that covers the member-record side of image cleanup. Required: photo_id. See also: updateUserPhoto (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. |
| listUserMetaA | List user metadata records - Paginated enumeration of users_meta records (EAV key/value table). Read-only. Use when: enumerating metadata rows (key/value pairs) attached to a parent record in any BD table. IDENTITY RULE - (database, database_id) is ONE atomic compound identity, not two independent fields. The same numeric database_id routinely points at UNRELATED rows on different parent tables - any integer ID can exist as a PK on multiple parent tables simultaneously. A database_id=<n>-only query will return a MIX of rows from every parent table where that integer happens to be a PK (even low IDs like 1 return hundreds of rows spanning 2+ parent tables). Always pair database=<parent_table> WITH database_id=<id> whenever reading, writing, updating, or deleting users_meta. Pass database, database_id, and key as first-class query params; the MCP wrapper translates them into BD's multi-condition filter syntax so server-side scoping IS accurate — no client-side post-filter needed. The safety guard requires at least 2 of (database, database_id, key) on every read; single-filter queries are rejected. Do NOT mix first-class filters with the generic property/property_value style in the same call — pick one style. Never act on a partial-identity result - misidentifying a row can silently corrupt or destroy unrelated resource metadata on another table. Commonly-seen database values (BD may accept other table names with users_meta rows - prefer these for known resources; if the user names an unfamiliar table, GET first to verify it actually has meta rows before writing): users_data, deleted_users_data, data_posts, list_seo, subscription_types, list_professions, list_services, rel_services, tags, tag_groups, rel_tags, leads, lead_matches, forms, form_fields, users_reviews, menus, menu_items, data_widgets, email_templates, 301_redirects, data_categories, smart_lists, users_clicks, unsubscribe_list. Pagination: cursor-based (limit, page). See Rule: Pagination for full cursor/cap/stop semantics. Filter: prefer the first-class database / database_id / key params (see Rule: users_meta identity). The generic property / property_value / property_operator + order_column / order_type flow also works as a fallback and counts toward the 2-of-3 guard when property is one of the three target keys — but do not mix the two styles in the same call. See also: getUserMeta (single record by ID), updateUserMeta, deleteUserMeta. Returns: { status: "success", total, current_page, total_pages, next_page, prev_page, message: [...records] }. Each record includes meta_id, database, database_id, key, value, date_added, revision_timestamp. Note on duplicates: BD does NOT enforce a uniqueness constraint on (database, database_id, key) - the same field can have multiple rows (observed live). Read-layer merge uses last-write-wins, but stored data can bloat. When updating, consider patching ALL matching rows, or deleting duplicates first. |
| getUserMetaA | Get a single metadata record - Fetch a single usermeta record. Read-only. Use when: fetching one metadata row by meta_id. Required: meta_id. Identity check before downstream writes: Before using this row's meta_id for any subsequent updateUserMeta/deleteUserMeta call, confirm the response's database and database_id fields BOTH match the parent record you intend to modify. The same database_id can exist across unrelated parent tables (users_data, list_seo, subscription_types, data_posts, etc.) - blindly passing a meta_id forward without verifying its (database, database_id) pair can silently corrupt or destroy data on an unrelated table. Optional database and database_id query params are accepted for documentation/intent — the actual verification is agent-side (compare message[0].database / message[0].database_id to what you expected before acting). See also: listUserMeta (enumerate many). Returns: { status: "success", message: [{...record}] } - the message array contains 1 record when found. Empty or HTTP 404 when not found. |
| updateUserMetaA | Update a metadata record - Update an existing users_meta record's value by meta_id. Fields omitted are untouched. Writes live data. IDENTITY RULE - (database, database_id) is ONE atomic compound identity, not two fields. ALWAYS confirm BOTH match the intended parent BEFORE updating. A users_meta row's identity is (database, database_id, key). The same numeric database_id routinely belongs to UNRELATED rows on different parent tables. Never use a meta_id blindly from an unscoped list - always either (a) getUserMeta(meta_id) first and inspect database+database_id, OR (b) obtain the meta_id from a listUserMeta whose results have been CLIENT-SIDE filtered to the intended database+database_id pair. Misidentifying a row silently overwrites unrelated resource metadata. Use when: Changing a metadata value on any BD table row that was previously created via createUserMeta. For list_seo (web page) EAV fields, use updateWebPage directly — the wrapper auto-routes them through users_meta for you. This updateUserMeta endpoint is for changing existing values on OTHER BD tables (e.g. users_data, subscription_types, data_posts custom meta), or for the rare case where a list_seo EAV field doesn't persist after updateWebPage — file that as a wrapper bug rather than working around it here.
Workflow: find the meta_id by calling listUserMeta with filter database=<table>, database_id=<parent_id>, key=<field>. Then call this endpoint with meta_id, value, and the same database + database_id you used for lookup. If no row exists, the row cannot be created via this endpoint — see Rule: users_meta writes. Never guess meta_id; 404 = stop, not retry. Required: meta_id, value, database, database_id. All four - always. The identity pair (database, database_id) is enforced at the schema level to prevent cross-table corruption. See also: listUserMeta (find the meta_id by filter), deleteUserMeta (remove permanently). Returns: { status: "success", message: {...updatedRecord} } - the full updated record after changes applied. |
| deleteUserMetaA | Delete a metadata record - Permanently delete a users_meta record by meta_id. DESTRUCTIVE - cannot be undone via API. HARD RULE - (database, database_id) is ONE atomic compound identity. Verify BOTH of the row BEFORE deleting. Destructive mistakes on users_meta are unrecoverable. A users_meta row's identity is (database, database_id, key). The same numeric database_id routinely belongs to UNRELATED rows on different parent tables - an integer ID may simultaneously be a WebPage's seo_id, a member's user_id, a post's post_id, and a plan's subscription_id. BEFORE calling this endpoint: (1) call getUserMeta(meta_id) or retain the row's full object from a prior listUserMeta response; (2) confirm the row's database value matches the table you intend to clean up. For batch orphan-cleanup after a parent delete: list by the parent's database_id, then CLIENT-SIDE filter to ONLY rows where database equals the parent table's name BEFORE deleting any meta_id. NEVER loop-delete by database_id alone - you WILL destroy unrelated resource metadata (member data, plan metadata, page settings) that happen to share the same numeric ID on other tables. Use when: removing a specific metadata row, OR cleaning up orphan meta rows after a parent record is deleted (BD does not cascade-delete users_meta when a parent is removed - it's the agent's job to find and delete the orphan rows surgically). Required: meta_id, database, database_id. All three - always. The identity pair (database, database_id) is enforced at the schema level to prevent cross-table destruction. Post-parent-delete cleanup workflow (safe pattern): listUserMeta with filter database_id=<deleted parent's id>
In the returned array, filter CLIENT-SIDE to ONLY rows where database equals the parent table's name (e.g. list_seo for a deleted WebPage) For each filtered meta_id, call deleteUserMeta(meta_id, database=<parent table>, database_id=<parent id>) - all three required Never skip step 2 - the same database_id can belong to unrelated rows on other tables
See also: updateUserMeta (modify without removing), listUserMeta (enumerate with filter). Returns: { status: "success", message: "users_meta record was deleted" }. No body beyond the confirmation string. |
| listTagsA | List tags - Paginated enumeration of tag records. Read-only. Use when: enumerating member tags, fetching tag names for display, or building a tag-management UI. Tags are lightweight labels attached to members, different from categories (which are taxonomy). Pagination: cursor-based (limit, page). See Rule: Pagination for full cursor/cap/stop semantics. Filter/sort: property+property_value+property_operator, order_column+order_type. See Rule: Filter operators for the verified-working operator set, silent-drop detection, and derived-field unfilterability. See also: getTag (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. |
| getTagA | Get a single tag - Fetch a single tag record. Read-only. Use when: fetching one tag by ID. Required: id. See also: listTags (enumerate many). Returns: { status: "success", message: [{...record}] } - the message array contains 1 record when found. Empty or HTTP 404 when not found. |
| createTagA | Create a tag - Create a new tag record. Writes live data. Use when: adding a new tag. Group (group_tag_id) must exist - discover via listTagGroups. Required: tag_name, group_tag_id. added_by is wrapper-managed: the audit-trail added_by field is hardcoded to 0 by the wrapper on every create. Not exposed as an input.
Duplicate tag_name silent-accept: BD does NOT enforce a uniqueness constraint on tag_name within a group_tag_id. Two createTag calls with the same tag_name + group_tag_id both succeed and produce two rows with different tag_ids. Downstream createTagRelationship calls then become ambiguous (which of the two tags?). Recommended pre-check pattern: call listTags with property=tag_name&property_value=<name>&property_operator== (optionally filtered further by group_tag_id) BEFORE create. If a match exists, reuse that tag_id rather than creating a duplicate. Parameter interactions: See also: updateTag (modify existing). Returns: { status: "success", message: {...createdRecord} } - includes the server-assigned ID. Use this ID for follow-up operations. |
| updateTagA | Update a tag - Update an existing tag record by ID. Fields omitted are untouched. Writes live data. Use when: renaming a tag without losing the tag-to-member relationships. Required: id. updated_by is wrapper-managed: the audit-trail updated_by field is hardcoded to 0 by the wrapper on every update. Not exposed as an input.
See also: createTag (add new), deleteTag (remove permanently). Returns: { status: "success", message: {...updatedRecord} } - the full updated record after changes applied. |
| deleteTagA | Delete a tag - Permanently delete a tag record by ID. Destructive - cannot be undone via API. Use when: removing a tag entirely. Tag-relationships (rel_tags) pointing at it may orphan. Required: id. See also: updateTag (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. |
| listTagGroupsA | List tag groups - Paginated enumeration of taggroup records. Read-only. Use when: discovering the tag groupings before creating tags - each tag belongs to a group. Pagination: cursor-based (limit, page). See Rule: Pagination for full cursor/cap/stop semantics. Filter/sort: property+property_value+property_operator, order_column+order_type. See Rule: Filter operators for the verified-working operator set, silent-drop detection, and derived-field unfilterability. See also: getTagGroup (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. |
| getTagGroupA | Get a single tag group - Fetch a single taggroup record. Read-only. Use when: fetching one tag group by ID. Required: id. See also: listTagGroups (enumerate many). Returns: { status: "success", message: [{...record}] } - the message array contains 1 record when found. Empty or HTTP 404 when not found. |
| createTagGroupA | Create a tag group - Create a new taggroup record. Writes live data. Use when: organizing tags into new themes (e.g., "Skill Level", "Service Area"). Rare. Required: group_tag_name, added_by, updated_by. Pre-check before create: BD does NOT enforce uniqueness on group_tag_name. Duplicate group names cause tag-manager ambiguity (admins can't tell which group a tag belongs to) and break filters that select by group name. Do a server-side filter-find: listTagGroups property=group_tag_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 updateTagGroup, OR ask the user, OR pick an alternate group_tag_name and re-check. Never silently create a duplicate. See also: updateTagGroup (modify existing). |
| updateTagGroupA | Update a tag group - Update an existing taggroup record by ID. Fields omitted are untouched. Writes live data. Use when: renaming a tag group. Required: id. See also: createTagGroup (add new), deleteTagGroup (remove permanently). Returns: { status: "success", message: {...updatedRecord} } - the full updated record after changes applied. |
| deleteTagGroupA | Delete a tag group - Permanently delete a taggroup record by ID. Destructive - cannot be undone via API. Use when: removing a group - child tags orphan; delete or re-group them first. Required: id. See also: updateTagGroup (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. |
| listTagTypesA | List tag types - Paginated enumeration of tagtype records. Read-only. Use when: enumerating the tag-type classifiers on this site. Pagination: cursor-based (limit, page). See Rule: Pagination for full cursor/cap/stop semantics. Filter/sort: property+property_value+property_operator, order_column+order_type. See Rule: Filter operators for the verified-working operator set, silent-drop detection, and derived-field unfilterability. See also: getTagType (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. |
| getTagTypeA | Get a single tag type - Fetch a single tagtype record. Read-only. Use when: one tag type by ID. Required: id. See also: listTagTypes (enumerate many). Returns: { status: "success", message: [{...record}] } - the message array contains 1 record when found. Empty or HTTP 404 when not found. |
| listTagRelationshipsA | List tag relationships - Paginated enumeration of tagrelationship records. Read-only. Use when: auditing which tags are attached to which records. Filter by tag or by target record. Pagination: cursor-based (limit, page). See Rule: Pagination for full cursor/cap/stop semantics. Filter/sort: property+property_value+property_operator, order_column+order_type. See Rule: Filter operators for the verified-working operator set, silent-drop detection, and derived-field unfilterability. See also: getTagRelationship (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. |
| getTagRelationshipA | Get a single tag relationship - Fetch a single tagrelationship record. Read-only. Use when: one relationship row by ID. Required: id. See also: listTagRelationships (enumerate many). Returns: { status: "success", message: [{...record}] } - the message array contains 1 record when found. Empty or HTTP 404 when not found. |
| createTagRelationshipA | Create a tag relationship - Create a new tagrelationship record. Writes live data. Use when: attaching an existing tag to a record (member, post, etc.). The alternative for members is setting the member_tags field via updateUser with member_tag_action=1. Required: tag_id, object_id, tag_type_id, added_by. Pre-check before create (TRIPLE uniqueness): BD does NOT enforce uniqueness on the (tag_id, object_id, tag_type_id) triple. Attaching the same tag to the same object twice produces two rel_tag rows, which inflates tag counts on admin reports and can cause some widgets to render the same tag chip twice on the same record. Filter-find pattern (single-field server filter + client-side intersect): call listTagRelationships property=tag_id property_value=<proposed tag_id> property_operator== to narrow to all rows for that tag, then CLIENT-SIDE filter to rows where object_id=<proposed object_id> AND tag_type_id=<proposed tag_type_id>. Zero results after client-side intersect = link free; >=1 = already attached. If the link already exists: skip the create (idempotent - the tag is already on the object). Never silently double-link. Parameter interactions: See also: updateTagRelationship (modify existing). tag_type_id + object_id - how to target the right record (from BD tag_types table):
The tag_type_id determines WHICH resource/table the object_id lives in. Discover mapping via listTagTypes - each tag type row has a table_relation field naming its target table. Example mapping: tag_type_id | type_name | Target table (table_relation) | What object_id references | 1 | Users | users_data
| user_id
| (other rows) | (other types) | e.g. data_widgets, menus, forms | That table's primary key |
Process: call listTagTypes first to see the tag_type_id -> table_relation mapping on your site, then pick the appropriate tag_type_id and supply the matching record's PK as object_id. Widgets, menus, and forms all support tags via the same tag_type_id + object_id lookup pattern. |
| updateTagRelationshipA | Update a tag relationship - Update an existing tagrelationship record by ID. Fields omitted are untouched. Writes live data. Use when: adjusting a tag-relationship record's metadata. Required: id. See also: createTagRelationship (add new), deleteTagRelationship (remove permanently). Returns: { status: "success", message: {...updatedRecord} } - the full updated record after changes applied. |
| deleteTagRelationshipA | Delete a tag relationship - Permanently delete a tagrelationship record by ID. Destructive - cannot be undone via API. Use when: detaching a tag from a record. Note: if the member has member_tags CSV set, update that separately too. Required: id. See also: updateTagRelationship (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. |
| listSmartListsA | List smart lists - Paginated enumeration of smartlist records. Read-only. Use when: enumerating saved dynamic filter configurations the admin has created - these back the BD admin's saved-filter UI for members, leads, reviews, etc. Pagination: cursor-based (limit, page). See Rule: Pagination for full cursor/cap/stop semantics. Filter/sort: property+property_value+property_operator, order_column+order_type. See Rule: Filter operators for the verified-working operator set, silent-drop detection, and derived-field unfilterability. See also: getSmartList (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. |
| getSmartListA | Get a single smart list - Fetch a single smartlist record. Read-only. Use when: fetching one saved filter's config. Required: smart_list_id. See also: listSmartLists (enumerate many). Returns: { status: "success", message: [{...record}] } - the message array contains 1 record when found. Empty or HTTP 404 when not found. |
| createSmartListA | Create a smart list - Create a new smartlist record. Writes live data. Use when: programmatically saving a filter configuration for later reuse. smart_list_type determines the data source (members, leads, reviews, etc.). Required: smart_list_name, smart_list_type, smart_list_created_by. Pre-check before create: BD does NOT enforce uniqueness on smart_list_name. Duplicate list names mean admins and other tools can't tell the lists apart in the Smart Lists manager, and automations that look up a list by name will bind to the wrong record. Do a server-side filter-find: listSmartLists property=smart_list_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 updateSmartList, OR ask the user, OR pick an alternate smart_list_name and re-check. Never silently create a duplicate. Parameter interactions: smart_list_type - data source (see Enums)
smart_list_created_by - admin user ID creating the list
smart_list_query_params - JSON/string of filter criteria specific to the chosen type
schedule - recurrence if the list should auto-refresh
See also: updateSmartList (modify existing). smart_list_query_params format depends on smart_list_type:
For smart_list_type=newsletter: store a URL string (admin view uses it directly as an href link - no filter semantics). For ALL other types (members, leads, reviews, transaction, forms_inbox): pass a JSON string of filter key-value pairs, e.g. {"subscription_id":"1","active":"1"}. The backend encrypts it internally before storing. If empty / no filters: pass "NA" (the controller defaults missing values to this).
The API accepts the value as a plain string; BD handles the internal encryption. Don't pre-encrypt client-side - you'll get double-encrypted garbage. Use the JSON-key-value format for filterable types. |
| updateSmartListA | Update a smart list - Update an existing smartlist record by ID. Fields omitted are untouched. Writes live data. Use when: editing the filter criteria or schedule on a saved list. Required: smart_list_id. See also: createSmartList (add new), deleteSmartList (remove permanently). Returns: { status: "success", message: {...updatedRecord} } - the full updated record after changes applied. |
| deleteSmartListA | Delete a smart list - Permanently delete a smartlist record by ID. Destructive - cannot be undone via API. Use when: removing a saved filter configuration. Required: smart_list_id. See also: updateSmartList (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. |
| listSidebarsA | List custom sidebars - Paginated enumeration of CUSTOM sidebars defined on this site. Read-only in this MCP (create/update/delete deliberately omitted - sidebars are layout infrastructure; changes belong in the BD admin UI). Use when: an agent needs to set form_name on a WebPage (the sidebar assignment) and wants to verify a custom sidebar name exists on this site before using it. Important - this endpoint returns ONLY custom sidebars. It does NOT return the Master Default Sidebars that are seeded in BD's master database and always valid on every site. Those are hardcoded in BD core and are NOT rows in the sidebars table. See Rule: Sidebars for the canonical Master Default list (use those names verbatim in form_name) and the agent workflow for matching a user-named sidebar against masters first, then customs from this endpoint, then asking the user if neither matches. Returns: rows with sidebar_id, name (display name - this is the VALUE to pass to form_name), desc, active (1/0), separator, css, script, short_code, type, div_id, div_class, revision_timestamp. Pagination + filter/sort: standard. Site records and platform master defaults are merged in the response — see Rule: Default-merge models to filter to one and to sort. |
| getSidebarA | Get a single custom sidebar - Fetch a single custom sidebar by sidebar_id. Read-only. Required: sidebar_id (path). Only returns custom sidebars. Master defaults are not rows in this table — see Rule: Sidebars for the canonical Master Default list; use those names directly in form_name without looking them up. Returns: { status: "success", message: [{...record}] }. |
| getSiteInfoA | Get site-level identity, locale, currency, and brand-image URLs - Returns the site's own identity and locale context — what kind of directory this is, who it serves, the formatting conventions to respect, and the URLs of branding assets. Read-only, no params. Call this once on the first BD task of a conversation and cache for the session; the values rarely change mid-conversation. Response shape (wrapper adds current_site_datetime — the site-local datetime at call time, YYYYMMDDHHmmss; use it whenever a date field needs "now" and no more precise clock is available): { status: "success", message: { website_id, website_name, website_phone, full_url, main_directory_url_relative, main_directory_url_absolute, profession, industry, primary_country, language, timezone, date_format, distance_format, website_currency, currency_prefix, currency_suffix, currency_format, currency_decimal_divider, currency_thousand_divider, brand_images_relative: {...}, brand_images_absolute: {...}, default_checkout_url } }. Field semantics to know: website_id = the site's tenant ID (integer). Used for centralized-admin URL composition (e.g. &newsite=<website_id> on ww2.managemydirectory.com/admin/... links). Cache per session.
profession = SITE-LEVEL setting describing the archetype of member this directory lists (e.g. "Doctor", "Personal Trainer"). NOT related to a member's profession_id (that's a foreign key into the per-member list_professions taxonomy).
industry = SITE-LEVEL setting describing the market/vertical the site serves (e.g. "Healthcare", "Fitness"). Site metadata, not a member attribute.
full_url = the canonical site URL, no trailing slash — its scheme matches your connection and is the correct scheme for every site link you compose (http-only sites return http://, https sites https://). Use this when composing public profile URLs (<full_url>/<user.filename>, <full_url>/<seo_id filename>).
main_directory_url_relative / main_directory_url_absolute = the site's main member-search-results page (unfiltered directory landing). The canonical "browse all members" / "see the full directory" internal-link target. Use absolute as-is; relative is path-only with no leading slash (e.g. "search") — compose as <full_url>/<relative>.
timezone / date_format / distance_format / website_currency + the currency_* formatting bits = locale context for how to present data back to the user (dates, distances, money). Respect these when formatting.
brand_images_relative and brand_images_absolute = parallel objects with 8 keys each (website_logo, website_mascot, website_background, favicon, default_profile_image, default_logo_image, verified_member_image, watermark). Relative = path-only (e.g. /images/logo.webp); absolute = full URL (scheme matches your connection). Use absolute URLs when embedding in emails / external content; relative when embedding on the site itself.
default_profile_image on a member read signals "no real photo" — compare image_main_file to this URL to detect placeholder state.
Why agents should call this early: the grounding it provides (site purpose, member archetype, locale) shapes every subsequent decision — what 'add a member' means, what categories are relevant, how to format dates and money, what the profile-placeholder image looks like, which brand assets to use in designs. Auth: X-Api-Key. Rate limit: standard 100 req/60s. Cache for the session. |
| getImageDimensionsA | Probe an image URL and return its dimensions + orientation. - Wrapper-native synthetic tool. Range-GETs the first 64KB of an image URL and parses JPG/PNG header bytes to return width, height, format, aspect_ratio, and orientation (landscape | portrait | square). Does NOT proxy to BD. Used by content-creation skills to verify image orientation before committing to a feature-image field (post_image, cover_photo, hero_image). Batch mode — preferred for 2+ candidates: pass urls (comma-separated, up to 50) instead of url. All URLs probe in parallel; the response is one envelope { status: "success", count, results: [{ url, status, message }, ...] } in input order. A 404/timeout/parse failure is that URL's own status: "error" entry — it never breaks the batch or the other results. Caller contract: filter candidate URLs to .jpg / .jpeg / .png BEFORE calling. WebP / GIF / AVIF are unsupported — the parser returns { status: "error", message: "unsupported image format..." } as a defense-in-depth fallback, but callers must not rely on it; skip non-JPG/PNG extensions outright per Rule: Image dimensions. Any error response (404, timeout, parse fail, unsupported format) means drop the candidate and pick another. See also: Rule: Image dimensions, Rule: Image dedup. |
| getBrandKitA | Get the site's brand kit (colors + fonts) for design decisions - Return a compact, semantically-labeled brand kit for this BD site - colors (body / primary / dark / muted / success / warm / alert accents, card surface) + fonts (body + heading Google Fonts). Call this ONCE at the start of any design-related task (building a widget, WebPage, post template, email, hero banner - anything where colors or fonts are chosen) so the output visually matches the site's brand. Handler is synthetic - makes 20 parallel internal calls to /api/v2/website_design_settings/get?property=setting_name&property_value=custom_N&property_operator== (one per brand-kit slot), then transforms the raw custom_N values into semantic labels. Uses BD's canonical mapping (same mapping BD's admin AI Companion applies). Parallel calls complete in ~1s wall-clock on typical sites; well under the 100 req/60s rate limit even on repeated invocations. No args. Read-only. Safe to call anytime. Response shape: {
body: { background, text, font },
primary: { color, text_on },
dark: { color, text_on },
muted: { color, text_on },
success_accent: { color, text_on },
warm_accent: { color, text_on },
alert_accent: { color, text_on },
card: { background, border, text, title },
heading_font: "<google font family>",
usage_guidance: { primary, dark, muted, success_accent, warm_accent, alert_accent, tint_rule, font_rule }
}
Usage guidance embedded in response - agents should read it every call. Key rules: Primary = brand color - main CTAs, dominant accents. Dark = high-contrast sections or strong backgrounds. Muted = subtle section backgrounds, dividers, badges, pills. Success / Warm / Alert accents = specific semantic states (confirmations / attention / urgency). Use sparingly. Tint rule: derive lighter/darker tints from palette colors for hover states, gradients, low-emphasis backgrounds. Do NOT introduce new unrelated hues. Font rule: the site's body.font and heading_font Google Fonts are already globally loaded by BD. Do NOT redefine them in content_css. To switch to a different font, load it via a <link rel="stylesheet" href="https://fonts.googleapis.com/..."> tag in content_head — never @import inside CSS (Outlook + some BD widget contexts strip or fail on @import).
When a slot is empty on the site, the handler applies BD's documented fallback defaults (same defaults BD's admin AI Companion uses). Response is never missing keys - every field always has a value. Auth: X-Api-Key header. Rate limit: 100 req/60s. Caches well on the agent side - the brand kit rarely changes within a session; call once, reuse. |
| listCitiesA | List cities (location-based search & SEO slugs) - Paginated enumeration of cities enabled on this site for location-based member/post browsing and SEO page URL generation. Read-only source-of-truth for city slugs used in search-result URLs. Backed by BD's location_cities table. Use when: resolving a human city name (e.g. "Beverly Hills") to its city_filename slug (e.g. beverly-hills) before constructing a search-result URL for a static SEO page, or discovering which cities this site has seeded. Pagination: cursor-based (limit, page). See Rule: Pagination for full cursor/cap/stop semantics. Filter/sort: property+property_value+property_operator, order_column+order_type. See Rule: Filter operators for the verified-working operator set, silent-drop detection, and derived-field unfilterability. Useful filters: city_ln (full name, exact match), city_filename (slug, exact), state_sn (scope to one state), country_sn (scope to one country). Returns: { status: "success", message: [...rows] } - each row has: locaiton_id (integer PK, BD schema typo - it is locaiton_id, NOT location_id; pass the typo'd form when looking up a single record)
city_ln (full name)
city_filename (URL slug)
state_sn (2-letter state/province code; references location_states.state_sn)
country_sn (2-letter country code; references list_countries.country_code)
System-critical table - create & delete deliberately omitted from this MCP. Cities are managed by BD automatically (a new city row is added when a member signs up from a new location). Creating cities via API risks slug collisions with auto-created rows, and deleting risks orphaning members whose city references the row. Use updateCity only for corrections (rename, fix typo in filename). For new cities, let the next member signup seed it. Auth: X-Api-Key header. Rate limit: 100 req/60s (on 429, back off 60s). Errors: { "status": "error", "message": "..." } - empty-result responses return {status: "error", message: "location_cities not found", total: 0} (same ambiguous pattern as other list endpoints). |
| getCityA | Get a single city - Fetch one city row. Read-only. Note BD schema typo: PK is locaiton_id (sic), not location_id. Required: locaiton_id (path). See also: listCities (enumerate + filter). Returns: { status: "success", message: [{...record}] }. |
| updateCityA | Update a city (corrections only) - Update an existing city row. Read-mostly - use sparingly. Fields omitted are untouched (PATCH semantics - only send what you want to change). Use when: correcting a typo in city_ln or city_filename, or reassigning a city's state_sn / country_sn if originally miscategorized. For a NEW city, DO NOT create via API - let the next member signup from that location auto-seed the row (BD handles this). Required: locaiton_id (BD schema typo - sic). Warning on city_filename edits: this is the URL slug used in every search-result page for that city. Changing it breaks all inbound links AND any static SEO pages (seo_type=profile_search_results) whose filename includes the old slug. If you must rename, create Redirect (301) records for each affected URL. Returns: { status: "success", message: {...updatedRecord} }. |
| listStatesA | List states / provinces / regions - Paginated enumeration of states/provinces/regions enabled on this site. The location_states table is country-agnostic - it holds US states, Canadian provinces, UK regions, and any other first-admin-level division for any country active on this site, distinguished by country_sn. Read-only source-of-truth for state slugs in search-result URLs. Use when: resolving a state/province name ("California", "Ontario") to its state_filename slug (california, ontario) before constructing a search-result URL. Pagination + filter/sort: standard. Useful filters: state_ln (full name), state_sn (2-letter code), state_filename (slug), country_sn (scope to one country - e.g. US, CA). Returns: rows with location_id (PK - NO typo here, unlike cities), state_sn, state_ln, state_filename, country_sn. System-critical table - create & delete deliberately omitted. States are seeded by BD as needed. Use updateState only for corrections. |
| getStateA | Get a single state/province - Fetch one state row by location_id. Read-only. Required: location_id (path). Note: location_states PK is location_id (correctly spelled, unlike location_cities.locaiton_id). Returns: { status: "success", message: [{...record}] }. |
| updateStateA | Update a state (corrections only) - Update a state row. Read-mostly - use for corrections. Fields omitted are untouched (PATCH semantics - only send what you want to change). Required: location_id. Warning on state_filename: it's the URL slug in every search page using this state. Rename -> broken URLs + orphaned SEO pages. Create redirects if you must. |
| listCountriesA | List countries - Paginated enumeration of countries in the global reference table. Read-only reference. Use when: resolving a country name to its 2-letter country_code (ISO 3166-1 alpha-2) for cross-referencing in location_states.country_sn / location_cities.country_sn, or deriving a country URL slug. Country URL slug derivation: BD does NOT store a country_filename field. To construct the country segment of a search-result URL, derive it from country_name by lowercasing and replacing spaces with hyphens. Example: country_name="United States" -> country-slug="united-states". Pagination + filter/sort: standard. Useful filters: country_code, country_name, active. Returns: rows with country_id, country_code, country_name, active (1=active, 0=inactive). System-critical table - create & delete deliberately omitted. Countries are a global reference list. Use updateCountry only for corrections (e.g. toggling active). |
| getCountryA | Get a single country - Fetch one country row. Read-only. Required: country_id (path). Returns: { status: "success", message: [{...record}] }. |
| updateCountryA | Update a country (corrections / active toggle) - Update a country record. Read-mostly - primary use is toggling active to enable/disable a country on the site. Fields omitted are untouched (PATCH semantics - only send what you want to change). Required: country_id. |
| listWebPagesA | List pages (list_seo) - Paginated enumeration of web pages (list_seo records). Read-only. Returns every static/SEO page on the site - homepage, about, contact, custom landing pages, templates, etc. Filter by seo_type to get pages of a specific type (e.g., only content pages). Use when: listing all site pages. Filter by seo_type to scope. For one page by seo_id use getWebPage. Pagination: cursor-based (limit, page). See Rule: Pagination. Filter/sort: property+property_value+property_operator, order_column+order_type. See Rule: Filter operators. Lean-by-default keep-list: rows return only the core identity + linkage fields: seo_id, seo_type, filename, title, h1, h2, nickname, linked_post_category, linked_post_type, date_updated, revision_timestamp. Everything else is stripped — restore via flags: include_content=1 - return content (body HTML).
include_code=1 - return content_css, content_head, content_footer_html.
include_extras=1 - return everything else (all hero_* fields, meta_desc, meta_keywords, seo_text, facebook_*, content_layout, content_sidebar, menu_layout, all hide_* toggles, master_id, content_active, database, section, custom_html_placement, etc.).
On sites with heavy pages, a single row can be 10-30KB with code assets; opt in only when you actually need the data (e.g. before updateWebPage edits to body/CSS/JS, or when reading hero config to display it). See also: getWebPage (single by ID), createWebPage, updateWebPage. Returns: { status: "success", total, ..., message: [...records] }. |
| getWebPageA | Get a single page - Fetch a single web page by seo_id. Read-only. Use when: fetching one page's metadata (+ optionally its body/CSS/JS) before editing. Common for "let me read the current About page before editing it" workflows. Required: seo_id (path). Lean-by-default keep-list: same shape as listWebPages — returns only the core identity + linkage fields: seo_id, seo_type, filename, title, h1, h2, nickname, linked_post_category, linked_post_type, date_updated, revision_timestamp. Restore via flags: include_content=1 - return content (body HTML).
include_code=1 - return content_css, content_head, content_footer_html.
include_extras=1 - return everything else (all hero_* fields, meta_desc, meta_keywords, seo_text, facebook_*, content_layout, content_sidebar, menu_layout, all hide_* toggles, master_id, content_active, database, section, custom_html_placement, etc.).
Before updateWebPage edits to body, CSS, head, or footer HTML/JS, pass the matching flag so you have the current value to modify. Hero edits require include_extras=1 to read the existing hero_* values first. See also: listWebPages (enumerate), updateWebPage (modify). No seo_id yet (user named the page by title/filename/nickname)? Resolve it via listWebPages first — see Rule: List-first. Returns: { status: "success", message: [{...record}] } - the message array contains 1 lean-shaped record when found. |
| createWebPageA | Create a page - Create a list_seo page record. 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 fields: seo_type. filename is required for every seo_type EXCEPT data_category (the WRAPPER generates a 10-char lowercase alphanumeric placeholder slug for that type and auto-creates a 301 redirect to the canonical post-type URL; the public URL routes via the post type's data_filename, not list_seo.filename). When seo_type=data_category, linked_post_type is also required (auto-validated at runtime). Filename uniqueness — enforced by the wrapper, no exceptions. BD does NOT enforce unique filename server-side, but duplicates break the platform (two pages at the same URL render non-deterministically). The wrapper auto-pre-checks listWebPages for an existing slug before forwarding the create. If a row exists, the create is rejected with the existing seo_id so the agent can updateWebPage instead, or pick a unique slug. There is no agent-facing bypass; for seo_type=data_category the wrapper generates the slug itself (10-char lowercase alphanumeric — statistically unique across 36^10, no pre-check needed). Thin-content warning: if no title, h1, meta_desc, or content is set on a seo_type=content create, a _thin_content_warning field is attached to the response. The page is still created and is publicly live — Google may index it as thin content. Fix: provide at least one of those fields on the create call, or updateWebPage immediately after, or deleteWebPage if the create was premature. Asset field routing (mandatory - Froala strips mismatched content silently): content - body HTML. No <style>/<script> tags. Supports [widget=Name] shortcodes + %%%token%%%.
content_css - raw CSS rules. NO <style> wrapper. Scope to a unique page class; never target .container/.froala-table/.image-placeholder (reserved). Do NOT use @import (causes FOUC/CLS - use content_head <link> tag instead).
content_footer_html - JavaScript, pixels, analytics embeds (<script> tags OK here). IIFE-wrap + scope.
content_head - head-only deps (<link>, <meta>, JSON-LD, external stylesheets, fonts).
content_footer - MISLEADING NAME. NOT footer HTML. Page-access gate enum: "" (public), "members_only", "digital_products".
Hero banner -> enable_hero_section + hero_* + h1_*/h2_* fields.
All asset fields accept raw content verbatim. No CDATA, no <parameter>/<invoke>/<function_calls> scaffolding, no entity-escaped HTML — forbidden anywhere in the value, not just as wrappers. Server strips these as a safety net; do not rely on it. SVG/canvas prohibited in content - Froala strips them. Charts/diagrams go in a custom Widget, embedded via [widget=Name] shortcode. seo_type values: home (system-seeded; cannot CREATE homepage, only updateWebPage), content (generic static page), profile_search_results (member search override — apply Rule: Member search SEO pages), data_category (post search), custom_widget_page, password_retrieval_page, unsubscribed.
Hero section - when enable_hero_section = 1 or 2, apply Rule: Hero readability bundle (atomic — all listed values must be sent together). Notes: All color fields RGB ONLY (rgb(0, 0, 0)) - hex not accepted. Hero h1_*/h2_* fields style ONLY the hero banner; H1/H2 TEXT comes from the record's top-level h1/h2 fields. Hero image: content-relevant Pexels stock photo (free license, no attribution). See Rule: Image URLs (imported field — bare URL, no query string). Never picsum.photos/lorempixel/placekitten. Hero gap-fix CSS (seo_type=content ONLY): add .hero_section_container + div.clearfix-lg {display:none} to content_css to close BD's 40px clearfix gap. Never add this rule on any other seo_type - on profile_search_results / data_category the clearfix provides needed spacing before live search-results; hiding it causes results to butt-join the hero. Hero is cache-gated — but createWebPage/updateWebPage auto-refresh handles it; no separate call needed. Homepage hero is BENIGN: seo_id=1 stores hero fields but the homepage template does NOT render them. Skip hero fields on homepage unless user explicitly asks.
profile_search_results SEO pages - thin-content remedy workflow:
Used to override BD's auto-generated dynamic search URLs (e.g. california/beverly-hills/plumbers) with static custom SEO copy. Creating a list_seo row with a matching filename takes over the public URL. CRITICAL - filename MUST be a real slug BD's dynamic router recognizes. Arbitrary slugs render HTTP 404 publicly even when the record is created successfully. See Rule: Member search SEO pages for the canonical slug hierarchy (country/state/city/top/sub, strict order, any subset valid) and the live-lookup endpoints for each segment. Wrapper validates segments at runtime — country slug is derived from country_name (lowercase + spaces→hyphens). For arbitrary-URL static pages use seo_type=content. Workflow for "add SEO to [category] in [location]": Resolve each human name to its slug via the relevant list* endpoint (exact-match =). For ambiguous inputs (e.g. "Beverly Hills plumbers" - could be beverly-hills/plumbers or california/beverly-hills/plumbers), ask user which variant. Pre-check: listWebPages property=filename property_value=<slug> property_operator==. Exists -> updateWebPage. Missing -> createWebPage with the required defaults listed in step 4. Required defaults on create and every update (unless user overrides): seo_type=profile_search_results
custom_html_placement=4 (Below Body Content - safest for boilerplate intro without disrupting live results)
form_name="Member Search Result" (sidebar - Master Default; do NOT use Member Profile Page, that's for profile pages)
menu_layout=3 (Left Slim sidebar position)
enable_hero_section=1 + content-relevant Pexels hero_image + the readability safe-defaults from Rule: Hero readability bundle (atomic — all listed values must be sent together). Most end-users don't know to ask for a hero; thin-SEO pages underperform without one. User can opt out with enable_hero_section=0. (Cache flush is automatic post-write.)
Auto-generate SEO meta for the specific combo - don't leave blank: title - 50-60 chars ideal, <=70 max. Pattern: "[Category] in [City], [State] | [Site Name]".
meta_desc - 150-160 chars ideal, <=170 max. 1-2 sentence pitch with location + CTA.
meta_keywords - ~200 chars, comma-separated (no spaces).
facebook_title - 55-60 chars, differ from title (more conversational).
facebook_desc - 110-125 chars, punchier than meta_desc.
Do NOT auto-set facebook_image (needs uploaded asset).
H1/H2 double-render trap: if hero enabled AND content contains <h1>/<h2>, both render. Either set h1/h2 fields and omit from content, or put in content and leave fields blank. Never both. No max-width wrappers in content or content_css on profile_search_results pages. BD's layout already provides the outer container; adding max-width: 960px; margin: auto double-constrains to a narrow strip. Let content flow at natural container width. custom_html_placement is only meaningful on profile_search_results (and data_category). Ignored on content pages.
SEO content for categories: route to createWebPage seo_type=profile_search_results (NOT updateTopCategory.desc / updateSubCategory.desc - those are internal labels, not rendered). list_seo EAV fields — auto-routed by the wrapper, no special handling. Pass any field on createWebPage / updateWebPage directly; if it's an EAV-stored field (e.g. hero_*, h1_*, h2_*, linked_post_category, disable_*), the wrapper routes the write through users_meta automatically. Response includes an eav_results array confirming which EAV fields were written. Reads merge automatically via getWebPage/listWebPages. On deleteWebPage: BD does NOT cascade — run orphan cleanup per Rule: users_meta orphans (listUserMeta filtered by database=list_seo+database_id=<deleted seo_id>, then deleteUserMeta each match).
See also: listWebPages, updateWebPage, createRedirect (preserve SEO on slug changes). Returns: { status: "success", message: {...createdRecord}, auto_cache_refreshed: true|false, auto_cache_refresh_error?: "...", _admin_edit_url: "..." } including seo_id. auto_cache_refreshed reports whether the automatic cache flush succeeded; if false, auto_cache_refresh_error explains why and the agent should retry refreshSiteCache manually once. _admin_edit_url is a centralized-admin deep-link to the WebPage editor for this seo_id — surface it to the user so they can jump straight to the admin edit screen for the page just created. |
| updateWebPageA | Update a page - Update an existing list_seo page by seo_id. PATCH semantics - omitted fields untouched. Cache refresh is automatic. Every successful updateWebPage / createWebPage triggers refreshCache(scope=web_pages) server-side; the response includes auto_cache_refreshed: true. Do not call refreshSiteCache manually after — it's already done. If auto_cache_refreshed: false appears in the response, the write succeeded but cache flush failed; check auto_cache_refresh_error and retry refreshSiteCache once. Required fields: seo_id. When changing seo_type to data_category, linked_post_type is also required (auto-validated at runtime). Disambiguation: apply Rule: Resource disambiguation when the user names a page by title or partial filename rather than by seo_id. "Edit my [X] page" is layer-ambiguous — could be a seo_type=content WebPage, a post type's code group, or a category landing. Common edits: Copy/content: content, h1, h2, seo_text, content_footer (access gate, not HTML) SEO meta: title, meta_desc, meta_keywords Social (Open Graph): facebook_title, facebook_desc, facebook_image Layout: content_layout, hide_header, hide_footer, hide_top_right, hide_header_links Hero: enable_hero_section + hero_* + h1_*/h2_*
Misnamed fields (BD repurposed these columns - name is misleading): show_form -> Apply NoIndex,NoFollow. NOT a contact-form toggle. 1=add <meta name="robots" content="noindex,nofollow">.
content_footer -> page access gate enum: "" (public) / "members_only" / "digital_products". NOT footer HTML.
form_name -> sidebar name. NOT a contact-form slug.
seo_text -> Wildcard URL Rewrite (catch-all routing). NOT SEO copy.
Template tokens supported in title, meta_desc, meta_keywords, h1, h2: %%%website_name%%%, %industry%, %profession%. Expanded at render time. Changing filename (URL slug) breaks inbound links - call createRedirect to create a 301 from old slug -> new slug, preserve SEO. Pre-check new slug for duplicate before renaming - listWebPages property=filename property_value=<new-slug> property_operator==. BD does NOT enforce unique filename; renaming to an existing slug silently creates two records at the same URL, render order undefined. Asset field routing (Froala strips mismatched content silently): content - body HTML only. No <style>/<script> tags. Supports [widget=Name] + %%%token%%%.
content_css - raw CSS, no <style> wrapper, scope to a unique page class. Never target .container/.froala-table/.image-placeholder. Never @import (causes FOUC/CLS - use content_head <link> tag instead).
content_footer_html - JavaScript + script embeds (<script> tags OK). IIFE-wrap + scope.
content_head - head-only deps (<link>, <meta>, JSON-LD, external stylesheets, fonts).
content_footer - MISLEADING NAME. Access gate enum only: ""/"members_only"/"digital_products". NOT HTML.
SVG/canvas prohibited in content - Froala strips. Charts/diagrams -> custom Widget via [widget=Name].
All asset fields accept raw content verbatim. No CDATA, no <parameter>/<invoke>/<function_calls> scaffolding, no entity-escaped HTML — forbidden anywhere in the value, not just as wrappers. Server strips these as a safety net; do not rely on it. EAV-stored fields — auto-routed by the wrapper, no special handling. BD's list_seo table mixes direct columns with EAV-stored fields in users_meta. BD's REST API itself silently ignores EAV fields on update, but the wrapper auto-detects fields like hero_*, h1_*, h2_*, linked_post_category, disable_* and routes them through users_meta automatically. Pass any field on updateWebPage directly — the response includes an eav_results array confirming which EAV fields were written. Reads merge automatically via getWebPage / listWebPages. Do NOT call updateUserMeta directly for these. If a field doesn't persist after updateWebPage, file as a wrapper bug (the wrapper's EAV routing table needs the field added) rather than working around with manual updateUserMeta. Hero section — which hero does the user mean? Read the page's current enable_hero_section first. 1/2 = the user means this programmatic hero: change the relevant hero_* field to restyle, or set enable_hero_section=0 to remove it — do NOT rewrite content to remove a programmatic hero. 0 = no programmatic hero is rendering, so a hero the user references lives in content — edit content. If hero markup is also in content, or the user is ADDING a hero, ask which they mean. **Hero section edits - when enable_hero_section toggles from 0/unset to 1 or 2, apply Rule: Hero readability bundle (atomic — all listed values must be sent together unless user overrides). Notes: All color fields RGB ONLY, hex rejected. h1_*/h2_* fields style hero; text comes from record's top-level h1/h2.
Hero image: Pexels stock; see Rule: Image URLs (imported field — bare URL, no query string). Never picsum.photos/lorempixel/placekitten. Hero gap-fix CSS (seo_type=content ONLY): add .hero_section_container + div.clearfix-lg {display:none} to content_css (closes BD's 40px clearfix gap). Never add this rule on any other seo_type - on profile_search_results / data_category the clearfix provides needed spacing before live search-results. Hero is cache-gated but updateWebPage/createWebPage auto-refresh handles it; no separate call needed. Homepage hero is BENIGN: seo_id=1 stores hero fields but homepage template does NOT render them. Skip hero fields on homepage unless user explicitly asks.
Do NOT re-apply hero defaults on updates that don't touch enable_hero_section - respect the user's existing color/overlay/padding values; only change what they asked about. H1/H2 double-render trap: when hero is enabled (enable_hero_section=1 or 2), the record's top-level h1/h2 text renders inside the hero banner. If content ALSO contains <h1>/<h2>, BOTH render -> duplicate headings (bad for SEO). Rule: either put headings in record's h1/h2 fields (leave them out of content), OR put them in content (leave h1/h2 fields empty). Never both. profile_search_results updates - all create-time rules apply:
filename must be a real dynamic slug BD's router recognizes — see Rule: Member search SEO pages for the slug hierarchy (country/state/city/top_cat/sub_cat) and the live-lookup endpoints. Arbitrary slugs render 404. Country-only slug does NOT render for profile_search_results. For arbitrary-URL pages use seo_type=content.
Required defaults on every write: custom_html_placement=4 (Below Body Content), form_name="Member Search Result" (sidebar - Master Default; NOT "Member Profile Page"), menu_layout=3 (Left Slim). custom_html_placement is only meaningful on profile_search_results (and data_category). Ignored on content pages.
Auto-generate SEO meta for the specific combo - don't leave blank: title - 50-60 chars ideal, <=70 max. Pattern: "[Category] in [City], [State] | [Site Name]".
meta_desc - 150-160 chars ideal, <=170 max. 1-2 sentences with location + CTA.
meta_keywords - ~200 chars, comma-separated (no spaces).
facebook_title - 55-60 chars, differ from title (more conversational).
facebook_desc - 110-125 chars, punchier than meta_desc.
Do NOT auto-set facebook_image (needs uploaded asset).
No max-width wrappers in content or content_css - BD's layout already provides the outer container; max-width: 960px; margin: auto double-constrains to a narrow strip. Let content flow at natural width.
SEO content for categories: route to updateWebPage seo_type=profile_search_results (NOT updateTopCategory.desc / updateSubCategory.desc - those are internal labels, not rendered). seo_type enum: content, home, profile_search_results, data_category, custom_widget_page, password_retrieval_page, unsubscribed. OMIT on update unless intentionally changing it — most changes are destructive. home appears in the enum only so existing-record round-trips pass validation; never convert another page TO home (only one homepage per site).
On deleteWebPage: BD does NOT cascade-delete users_meta rows. After deleting, run the orphan cleanup per Rule: users_meta orphans - listUserMeta filtered by database=list_seo + database_id=<deleted seo_id>, then deleteUserMeta each match. Exception: for seo_type=data_category pages the wrapper auto-strips linked_post_type / linked_post_category rows on transition AWAY from data_category, AND auto-cascade-deletes the placeholder-slug 301 redirect on deleteWebPage (annotations: _data_category_orphans_stripped, _data_category_redirect_retired, _data_category_redirect_deleted). Hero / layout EAV rows still need manual cleanup. See also: createWebPage (new page), deleteWebPage (remove + orphan users_meta cleanup), createRedirect (slug-change preservation). Returns: { status: "success", message: {...updatedRecord}, auto_cache_refreshed: true|false, auto_cache_refresh_error?: "...", _admin_edit_url: "..." }. auto_cache_refreshed reports whether the automatic cache flush succeeded; if false, auto_cache_refresh_error explains why and the agent should retry refreshSiteCache manually once. _admin_edit_url is a centralized-admin deep-link to the WebPage editor for this seo_id — surface it to the user so they can jump straight to the admin edit screen for the page just updated. |
| deleteWebPageA | Delete a page - Permanently delete a web page by seo_id. Destructive - cannot be undone via API. Use when: permanently removing a page. Required: seo_id. Destructive: confirm with the user. If inbound links or menus reference the deleted page's URL, consider creating a createRedirect BEFORE deleting so those links don't 404. Menus with items pointing at this page also need cleanup. seo_type=data_category cascade — automatic. When the deleted page is data_category, the wrapper auto-deletes its placeholder-slug 301 redirect (response includes _data_category_redirect_deleted: <redirect_id>). The agent does not need to call deleteRedirect manually.
ORPHAN CLEANUP REQUIRED - BD does NOT cascade-delete users_meta rows. Each WebPage with a hero or custom layout has up to 18 EAV rows in users_meta (database=list_seo, database_id=<seo_id>). These persist after deleteWebPage unless you clean them up. Safe post-delete workflow: Call listUserMeta filtered on database=list_seo AND database_id=<deleted seo_id>. Client-side filter the response - keep only rows whose database field equals list_seo. The same database_id value may exist in users_meta pointing at unrelated parent tables (e.g. a member with user_id=<seo_id> in users_data), and those rows must NOT be deleted. For each remaining row, call deleteUserMeta with meta_id=<row.meta_id>, database=list_seo, database_id=<seo_id> (all three required).
Never loop-delete by database_id alone - you will silently destroy unrelated records on other tables. See also: updateWebPage (modify without removing). Returns: { status: "success", message: "list_seo record was deleted" }. |
| listRedirectsA | List redirects (301) - Paginated list of all 301 redirect rules on the site. Use when: auditing existing 301 rules - useful before bulk URL changes to avoid duplicate rules, or when debugging why a URL unexpectedly redirects. Pagination: cursor-based (limit, page). See Rule: Pagination for full cursor/cap/stop semantics. Filter/sort: property+property_value+property_operator, order_column+order_type. See Rule: Filter operators for the verified-working operator set, silent-drop detection, and derived-field unfilterability. See also: getRedirect (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. |
| getRedirectA | Get a single redirect - Fetch a single redirect record. Read-only. Use when: investigating one specific redirect rule by redirect_id. Required: redirect_id. See also: listRedirects (enumerate many). Returns: { status: "success", message: [{...record}] } - the message array contains 1 record when found. Empty or HTTP 404 when not found. |
| createRedirectA | Create a redirect - Create a new 301 redirect rule. Use when: preserving SEO after any URL change - slug rename on a member profile, post, page, or category. BD auto-creates some redirects on its own (admin-triggered renames), but you must create them manually for API-triggered changes. Avoid duplicate old_filename values. Required: old_filename, new_filename. type is wrapper-managed: the wrapper hardcodes type=custom on every create. The other BD type values (profile, post, category) are reserved for BD's own auto-redirect logic on admin-triggered renames and are not exposed here.
Pre-check before create - TWO checks (redirects are uniquely dangerous: wrong rules cause infinite loops and SEO damage): Check 1 - exact-pair skip (idempotent): Do a server-side filter-find: listRedirects property=old_filename property_value=<proposed old> property_operator==. If a row exists where new_filename also matches the proposed new, skip the create - the rule is already there; creating a duplicate just bloats the redirect table. If a row exists with the same old_filename but a DIFFERENT new_filename, that's a conflict: reuse via updateRedirect with the new target, OR ask the user which destination wins. Never silently create a duplicate or conflicting old_filename. Check 2 - reverse-rule loop prevention (CRITICAL): Do a second filter-find: listRedirects property=old_filename property_value=<proposed NEW> property_operator==. If a row exists where new_filename equals your proposed old_filename, creating this rule would produce an infinite redirect loop (A->B and B->A). STOP. Flag to the user, explain the reverse rule in place, and ask whether to delete the existing reverse rule first or abandon the create. Do NOT paginate unfiltered redirect lists - filtered lookups are two tiny responses. On any site with a large redirect history, dumping the full table wastes rate limit and context. Parameter interactions: See also: updateRedirect (modify existing). |
| updateRedirectA | Update a redirect - Update an existing redirect record by ID. Fields omitted are untouched. Writes live data. Use when: adjusting an existing rule's destination or source path. Rare - most redirects are create-once. Required: redirect_id. type is wrapper-managed: not exposed as an input. All redirects created via this MCP are custom; other BD type values are reserved for system-generated redirects.
See also: createRedirect (add new), deleteRedirect (remove permanently). Returns: { status: "success", message: {...updatedRecord} } - the full updated record after changes applied. |
| deleteRedirectA | Delete a redirect - Permanently delete a redirect record by ID. Destructive - cannot be undone via API. Use when: an old redirect is no longer needed (source content has been offline long enough that the 301 value is gone) or the rule is conflicting with a new page at the same path. Required: redirect_id. See also: updateRedirect (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. |
| refreshSiteCacheA | Refresh the site cache (template/theme/widget/menu/page invalidation) - Clears BD's internal template/theme/widget caches. Useful when recent admin edits to design settings or widgets aren't showing on the public site yet. Use when: the user has just updated a template, theme setting, widget, menu, or page layout and the public site is still serving the old version. Also a safe troubleshooting step if they report a recent admin-edit not appearing after ~1 minute. Optional parameters: scope - target one cache area only (data_widgets, settings, web_pages, css, menus, sidebars). Faster than a full refresh. Omit to refresh all 6.
full=1 - include heavier db_optimization + file_permissions passes in addition to the 6 core areas. Slower; use only when the user reports persistent issues and lighter refreshes didn't help.
Not needed after createWebPage / updateWebPage / createWidget / updateWidget — those tools auto-refresh and return auto_cache_refreshed: true in the response. Only call manually if a write returned auto_cache_refreshed: false (check auto_cache_refresh_error for the cause). Do NOT use for: Returns: { status: "success", message: "Cache refreshed successfully", areas_refreshed: [...], scope: "full", full: false }. The areas_refreshed array lists exactly what was cleared - useful for logging or reporting back to the user. Example default response: {
"status": "success",
"message": "Cache refreshed successfully",
"areas_refreshed": ["data_widgets", "settings", "web_pages", "css", "menus", "sidebars"],
"scope": "full",
"full": false
}
With full=1 the areas_refreshed additionally includes db_optimization and file_permissions. Invalid scope values return an error listing the valid set: { status: "error", message: "Invalid scope value: <x>. Valid values: ..." }. Undocumented by BD publicly; exposed via admin API-permissions UI. |
| listDataTypesA | List data types - List all data types configured on this BD site. Use the data_id values as data_type parameters when creating posts or portfolio groups. Use when: discovering the valid data_type values on the site. Used as a prerequisite lookup when creating posts or portfolio groups that need a data_type foreign-key value. Pagination: cursor-based (limit, page). See Rule: Pagination for full cursor/cap/stop semantics. Filter/sort: property+property_value+property_operator, order_column+order_type. See Rule: Filter operators for the verified-working operator set, silent-drop detection, and derived-field unfilterability. See also: getDataType (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. |
| getDataTypeA | Get a single data type - Fetch a single datatype record. Read-only. Use when: fetching one data type's record by ID. Required: data_id. See also: listDataTypes (enumerate many). Returns: { status: "success", message: [{...record}] } - the message array contains 1 record when found. Empty or HTTP 404 when not found. |
| createDataTypeA | Create a data type - Define a new content-type template. Only do this when the user explicitly wants a new post type - most sites come pre-configured with the types they need. Use when: adding a new data-type classifier. Rare - usually preconfigured. Required: category_name, category_active. Pre-check before create: BD does NOT enforce uniqueness on category_name or the derived system_name. Duplicate data types corrupt the post-type admin UI, break post-listing widgets that bind by name, and risk posts landing under the wrong type. Do a server-side filter-find: listDataTypes property=category_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 updateDataType, OR ask the user, OR pick an alternate category_name and re-check. Never silently create a duplicate. Enums: category_active: 1=active and available for members to use, 0=inactive; limit_available: 0, 1. See also: updateDataType (modify existing). |
| updateDataTypeA | Update a data type - Update an existing datatype record by ID. Fields omitted are untouched. Writes live data. Use when: renaming a data type. Required: data_id. Enums: category_active: 1=active and available for members to use, 0=inactive; limit_available: 0, 1. See also: createDataType (add new), deleteDataType (remove permanently). Returns: { status: "success", message: {...updatedRecord} } - the full updated record after changes applied. |
| deleteDataTypeA | Delete a data type - Deletes a data type definition. Records (posts, portfolio groups) referencing a deleted data type may become orphaned - confirm with the user before deleting. Use when: removing an unused data type. Posts/groups referencing it orphan - clean up first. Required: data_id. See also: updateDataType (modify without removing). Returns: { status: "success", message: "record was deleted" }. No body beyond the confirmation string. |
| listTopCategoriesA | List categories (professions) - Paginated enumeration of TOP-level member categories. Read-only. Lean by default: each row keeps profession_id, name, filename. Strips desc, keywords, image, icon, sort_order, lead_price, revision_timestamp. Pass include_category_schema=1 to restore all category metadata. Top Categories are the highest level of the 3-tier member classification (e.g., "Restaurants", "Dentists"). Each record's profession_id is what populates users_data.profession_id on member records. Backed by BD's list_professions table. Use when: populating a category dropdown, generating a site map, or discovering the profession_id of an existing category before assigning members to it. Returns ALL top-level categories. For sub-categories under a specific top, use listSubCategories with a profession_id filter. Permission note - platform gap: this endpoint (/api/v2/list_professions/*) is NOT in BD's public Swagger spec, so the admin's API key permissions UI does NOT auto-generate a toggle for it. New keys default to DENY on this path even if the admin enables the "Categories (Professions)" toggle - that UI toggle gates the Swagger-documented /api/v2/category/* endpoints (a DIFFERENT legacy table). On a 403 here: the fix is to MANUALLY INSERT a row into bd_api_key_permissions for endpoint_path='/api/v2/list_professions/get' (and the singular /api/v2/list_professions/get/{profession_id} for getTopCategory). This is a platform-level gap worth reporting to BD dev team. Do NOT substitute /api/v2/category/* as a fallback - it reads a different, possibly-empty table and returns inconsistent data. Pagination: cursor-based (limit, page). See Rule: Pagination for full cursor/cap/stop semantics. Filter/sort: property+property_value+property_operator, order_column+order_type. See Rule: Filter operators for the verified-working operator set, silent-drop detection, and derived-field unfilterability. See also: getTopCategory (single by ID), listSubCategories (sub-categories filtered by profession_id), createTopCategory (add new). Returns: { status: "success", total, current_page, total_pages, next_page, prev_page, message: [...records] }. Each record has profession_id, name, desc, filename, keywords, icon, sort_order, lead_price, image, revision_timestamp.
Member Category Hierarchy (3 levels): BD classifies members through a 3-level taxonomy - AI agents MUST understand all three to correctly create, assign, and query member categories: Level | Tool nicknames | Endpoint | BD internal table | Key field | Parent reference | 1. Top Category | listTopCategories, createTopCategory, etc.
| /api/v2/list_professions/*
| list_professions
| profession_id (PK)
| - | 2. Sub Category | listSubCategories, createSubCategory, etc.
| /api/v2/list_services/*
| list_services
| service_id (PK)
| profession_id -> parent Top Category; master_id -> parent Sub Category (for sub-sub nesting; 0 = direct child of Top)
| 3. Member ↔ Sub Category link | listMemberSubCategoryLinks, createMemberSubCategoryLink, etc.
| /api/v2/rel_services/*
| rel_services
| rel_id (PK)
| user_id -> member; service_id -> Sub Category
|
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 BD's internal table names. Use createTopCategory / createSubCategory instead (BD's table-name → tool-name mapping is documented in Rule: Table to endpoint). |
| getTopCategoryA | Get a single category - Fetch a single TOP-level member category by profession_id. Read-only. Lean by default: keeps profession_id, name, filename. Strips SEO metadata. Pass include_category_schema=1 to restore. A Top Category is the highest level of the 3-tier member classification. Backed by BD's list_professions table. Use when: you already have a profession_id and need its full record (name, filename, etc.). For enumeration use listTopCategories. Required: profession_id (path parameter). See also: listTopCategories (enumerate), listSubCategories (sub-categories under this one; filter by profession_id). Returns: { status: "success", message: [{...record}] } - array of 1 record with full fields. 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 BD's internal table names. Use createTopCategory / createSubCategory instead (BD's table-name → tool-name mapping is documented in Rule: Table to endpoint). |
| createTopCategoryA | Create a category - Create a new TOP-level member category. Writes live data. Use createTopCategory or createSubCategory only for a single category that needs desc, keywords, icon, sort_order, lead_price, image, master_id, or a filename other than the default slug set at create time. For every other category create, use createCategoryTree. A Top Category is the highest level of the 3-tier member classification (e.g., "Restaurants"). It populates the profession_id field on user records. Backed by BD's list_professions table. Use when: adding one top category that needs desc, keywords, icon, sort_order, lead_price, image, or a filename other than the default slug set at create time. To auto-create a top category while creating a member, pass profession_name to createUser instead. Required: name, filename. Pre-check before create: BD does NOT enforce uniqueness on filename. Two top categories with the same slug -> which one resolves at /filename is undefined. Do a server-side filter-find: listTopCategories property=filename property_value=<proposed> property_operator==. Zero rows = slug free; >=1 row = taken. Do NOT paginate unfiltered lists - filtered lookup is one tiny response. If taken: reuse via updateTopCategory, OR ask the user, OR pick an alternate filename and re-check. Wrapper safety net: on a missed pre-check, the wrapper auto-suffixes filename on collision (-1...-20) and surfaces the suffix in the response. Pre-checking still preferred — auto-suffix surprises the caller in URL-sensitive workflows. Parameter guidance: name - human-readable (e.g. "Restaurants", "Dentists")
filename - URL-slug form (e.g. "restaurants") used in public member profile URLs. The default slug is the name lower-cased and hyphenated, with any character outside the Latin set percent-encoded.
desc, keywords, icon, sort_order, lead_price, image - all optional
See also: listTopCategories (list all), getTopCategory (by ID), createSubCategory (add a sub-category under this top). Writes live data: changes are immediately visible on the public site. Returns: { status: "success", message: {...createdRecord} } including the new profession_id. Use that value to populate users_data.profession_id on member records. Common workflow - full 3-tier setup ("create Restaurants -> Sushi -> assign Alice"): createCategoryTree with groups=[{ top_category: "Restaurants", sub_categories: ["Sushi"] }] -> returns profession_id (e.g. 42); read the new sub's service_id (e.g. 17) from listSubCategories
Assign Alice: updateUser with user_id=<Alice>, profession_id=42, services="17" (simple), OR createMemberSubCategoryLink with user_id=<Alice>, service_id=17, avg_price=..., specialty=1 (with per-link metadata)
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 BD's internal table names. Use createCategoryTree, or createTopCategory / createSubCategory when a single category needs create-time field control (BD's table-name → tool-name mapping is documented in Rule: Table to endpoint). |
| updateTopCategoryA | Update a category - Update an existing TOP-level member category by profession_id. Fields omitted are untouched. Writes live data. Use when: renaming a category, changing its URL slug (filename), updating SEO keywords, or reordering. Changing filename breaks inbound links - also create a Redirect via createRedirect to preserve SEO. Required: profession_id. Filename rename caveat: if the existing filename has a seo_type=profile_search_results web page bound to it, renaming this category orphans that page. The wrapper rejects renames that would orphan a bound page — rename or delete the bound page first, then rename the category. See also: createTopCategory (add new), deleteTopCategory (remove). Writes live data: changes are immediately visible on the public site. Returns: { status: "success", message: {...updatedRecord} }. 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 BD's internal table names. Use createTopCategory / createSubCategory instead (BD's table-name → tool-name mapping is documented in Rule: Table to endpoint). |
| deleteTopCategoryA | Delete a category - Permanently delete a TOP-level member category by profession_id. Destructive - cannot be undone via API. Use when: removing an unused top-level category. Any members with matching profession_id become orphaned - reassign them first. Any Sub Categories (list_services rows) under this top also orphan - delete or re-parent them. Required: profession_id. Destructive: confirm intent with the user. Members who referenced this profession_id will have orphan references. Sub Categories under this top (with matching profession_id in list_services) also become orphaned - consider reassigning or deleting them first. Bound-page caveat: if this category's filename has a seo_type=profile_search_results web page bound to it, deleting the category orphans that page (it'll render empty — no category to query). The wrapper rejects deletes that would orphan a bound page — delete or repurpose the bound page first. See also: updateTopCategory (modify without removing). Returns: { status: "success", message: "list_professions record was deleted" }. 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 BD's internal table names. Use createTopCategory / createSubCategory instead (BD's table-name → tool-name mapping is documented in Rule: Table to endpoint). |
| createCategoryTreeA | Create a full category taxonomy (tops + subs + sub-subs) in one call. - Wrapper-native synthetic tool. Creates member categories — top categories, their sub-categories, and third-tier sub-sub-categories — in one call. Use when: creating a category at any level, including adding to a structure that already exists. Use createTopCategory or createSubCategory only for a single category that needs desc, keywords, icon, sort_order, lead_price, image, master_id, or a filename other than the default slug set at create time. For every other category create, use createCategoryTree. Required: groups — an array of { top_category, sub_categories }, 25 entries maximum per call. Send the whole taxonomy in one call. A top category named again is matched by name and reused, so a later call adds to it. Never issue two calls concurrently: a top category named in both is created twice, because BD's category list does not reflect a row written by a call still in flight. A second call is safe once the first call's response has returned. The three shapes, all the same input: New top plus its subs: { top_category: "Surf Shops", sub_categories: ["Surfboards", "Wetsuits"] } Subs under a top that already exists: the same shape, naming that top. It is reused; only the listed subs are added, and existing subs are left alone. Top only: omit sub_categories or pass [].
Third tier — Parent=>Child: a sub_categories entry containing => creates Child under Parent, and Parent under the group's top_category. An existing Parent is reused. Exactly one => per entry: "Surf Camps=>Kids Camps". A=>B=>C is rejected — send A=>B in this call, then "B=>C" in a second call naming the same top_category once this call's response has returned. Sub-category names must not contain commas — this tool writes them through BD's comma-separated services field, which would split one name into several. Replace the comma with a hyphen. To create the parts as separate categories instead, send each as its own sub_categories entry. One comma anywhere in sub_categories rejects the call. Top category names may contain commas. Nothing is written until every name validates. Writing itself is not atomic: a failure part-way leaves earlier groups created. groups_completed and each group's top_status / sub_status name exactly what landed — read them before retrying. Re-send a group only when its top_status is error, or its sub_status is error or not attempted; created, existing, and none requested all mean that part landed. Building sub-categories requires a temporary member, which this tool creates and deletes for you. temp_member reports what happened to it; the categories are unaffected either way. See also: createTopCategory, createSubCategory, listTopCategories, listSubCategories. Returns: { status, message: { groups_requested, groups_completed, sub_categories_created, temp_member, groups: [{ top_category, profession_id, top_status, sub_categories, sub_status }] } }. top_status is created, existing, or error. sub_status is created, error, none requested, or not attempted. sub_categories_created is the total across all groups — report it rather than counting names. temp_member is one of: temporary member deleted — cleanup succeeded; not needed (no sub-categories requested); no temporary member was created — no sub-category was written and every sub_status is error; or a user_id — sub-categories were written but cleanup failed, so pass that id to deleteUser. Each group carries the resolved profession_id, ready for updateUser.profession_id. Sub-category ids are not returned — read them with listSubCategories filtered on that profession_id before assigning members via updateUser.services or createMemberSubCategoryLink. |