{"id":99,"date":"2026-10-05T20:09:10","date_gmt":"2026-10-06T00:09:10","guid":{"rendered":"https:\/\/demo.doctorx.ca\/realtor\/for-developers\/"},"modified":"2026-10-05T20:09:10","modified_gmt":"2026-10-06T00:09:10","slug":"for-developers","status":"publish","type":"page","link":"https:\/\/demo.doctorx.ca\/realtor\/for-developers\/","title":{"rendered":"For developers"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">What developers can build on: hooks, shortcodes, blocks, REST routes and WP-CLI commands, taken from each plugin&#8217;s own readme and from what this site has registered.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">DoctorX Listing Data Model<\/h2>\n\n\n\n<p>Use these functions rather than the custom fields directly. They are available once plugins have loaded; save listings on or after the init action. Check for the plugin with <code>defined( &#039;DOCTORX_LISTINGS_API&#039; )<\/code> (version 1 of the API) or <code>function_exists( &#039;doctorx_listings_upsert&#039; )<\/code>.<\/p>\n<h3>doctorx_listings_upsert( $source, $source_id, array $data, array $args = array() )<\/h3>\n<p>Creates the listing a source knows by $source_id, or updates it. Safe to call for every record of every sync: when nothing changed, nothing is written.<\/p>\n<ul>\n<li>$source: your source name, lowercase letters, digits, dashes and underscores, up to 40 characters (for example &#039;crea-ddf&#039;). &#039;manual&#039; is used for listings entered by hand.<\/li>\n<li>$source_id: the source&#039;s own ID for the listing, up to 150 characters, compared exactly (for RESO feeds, use ListingKey).<\/li>\n<li>$data: field =&gt; value, using the names above (any letter case) or their meta keys. Also: &#039;title&#039; (post title; default the address, or &quot;Semi-Detached in Glebe, Ottawa&quot; when the street address may not be shown), &#039;PublicRemarks&#039; (the description, plain text) and &#039;Media&#039; (accepted for &#039;photos&#039;). null or &#039;&#039; clears a field.<\/li>\n<li>Photos accept: a list of web addresses; a list of attachment IDs; or a list of arrays with url or MediaURL, id, caption or ShortDescription, and order or Order. Entries whose MediaCategory is not a photo (a tour, a document) are left out. At most 100 photos (filter doctorx_listings_max_photos).<\/li>\n<li>When &#039;StandardStatus&#039; is given and &#039;status&#039; is not, the status is worked out from it (Active \u2192 active, Active Under Contract or Pending \u2192 pending, Closed or Sold \u2192 sold, or leased for a lease, Expired, Withdrawn, Canceled and the like \u2192 off_market). An unknown status keeps the listing off the site and is reported in &#039;skipped&#039;.<\/li>\n<li>$args: &#039;post_status&#039; (publish, draft, pending or private; default publish for a new listing and unchanged for an existing one; a listing in the bin is restored), &#039;replace&#039; (true clears every field not in $data; default false, a partial update), &#039;author&#039; (user ID for a new listing), &#039;keep_unknown&#039; (true keeps fields this plugin does not know in &#039;extra&#039;), &#039;force&#039; (write even when nothing changed).<\/li>\n<li>Returns array( &#039;post_id&#039; =&gt; int, &#039;created&#039; =&gt; bool, &#039;changed&#039; =&gt; bool, &#039;skipped&#039; =&gt; array( field =&gt; reason ) ), or WP_Error (bad source or ID, called too early, busy, or WordPress could not save).<\/li>\n<li>A listing the feed says may not be shown on the internet at all (RESO InternetEntireListingDisplayYN false) should not be published: skip it, remove it, or upsert it with &#039;post_status&#039; =&gt; &#039;draft&#039;. InternetAddressDisplayYN false is handled here (the street address is hidden).<\/li>\n<\/ul>\n<pre class=\"wp-block-code\"><code>$r = doctorx_listings_upsert( &#039;crea-ddf&#039;, $row&#91;&#039;ListingKey&#039;&#93;, array(\n    &#039;ListingId&#039;       =&gt; $row&#91;&#039;ListingId&#039;&#93;,\n    &#039;StandardStatus&#039;  =&gt; $row&#91;&#039;StandardStatus&#039;&#93;,\n    &#039;ListPrice&#039;       =&gt; $row&#91;&#039;ListPrice&#039;&#93;,\n    &#039;UnparsedAddress&#039; =&gt; $row&#91;&#039;UnparsedAddress&#039;&#93;,\n    &#039;City&#039;            =&gt; $row&#91;&#039;City&#039;&#93;,\n    &#039;CityRegion&#039;      =&gt; $row&#91;&#039;CityRegion&#039;&#93;,\n    &#039;StateOrProvince&#039; =&gt; $row&#91;&#039;StateOrProvince&#039;&#93;,\n    &#039;InternetAddressDisplayYN&#039; =&gt; $row&#91;&#039;InternetAddressDisplayYN&#039;&#93;,\n    &#039;ListOfficeName&#039;  =&gt; $office_name,\n    &#039;PublicRemarks&#039;   =&gt; $row&#91;&#039;PublicRemarks&#039;&#93;,\n    &#039;Media&#039;           =&gt; $row&#91;&#039;Media&#039;&#93;,\n) );<\/code><\/pre>\n<h3>doctorx_listings_remove( $source, $source_id, $mode = &#039;&#039; )<\/h3>\n<p>Takes a source&#039;s listing off the site. $mode: &#039;trash&#039; (the default: it leaves the site at once and WordPress empties the bin after 30 days; an upsert of the same ID restores it), &#039;delete&#039; (gone at once, with the media uploaded to it) or &#039;off_market&#039; (kept and marked off the market). Returns true when removed, false when the site had no such listing, or WP_Error for an unknown mode.<\/p>\n<h3>doctorx_listings_source_ids( $source )<\/h3>\n<p>Every listing one source has on the site (not counting the bin), as source ID =&gt; post ID. Compare it with your feed to find the listings it no longer sends, then remove them.<\/p>\n<h3>doctorx_listings_find( $source, $source_id )<\/h3>\n<p>The post ID of a source&#039;s listing in any status, the bin included, or 0.<\/p>\n<h3>doctorx_listings_get( $post_id, $context = &#039;view&#039; )<\/h3>\n<p>A listing&#039;s values in their types (numbers as int or float, yes\/no as bool, empty as null), plus post_id, title, url, PublicRemarks, post_status, address_line (the street address on one line, &#039;&#039; when hidden), days_on_market, photos as array( url, id, caption, alt, remote ), Rooms as array( level, type, dimensions, description ), and owner_media (kind =&gt; value, see below). &#039;view&#039; gives what visitors may see: private fields and source keys are left out, the street fields are null when the address is hidden, and ClosePrice and CloseDate are null unless sold prices are allowed. &#039;edit&#039; gives everything, with source, source_id and synced_at. Returns null when the post is not a listing.<\/p>\n<h3>doctorx_listings_set_owner_media( $post_id, $kind, $value )<\/h3>\n<p>Keeps media the site owner added to a listing, such as a tour video, next to the listing&#039;s data. Feed updates never touch it, not even doctorx_listings_upsert() with &#039;replace&#039; =&gt; true; it goes when the listing is deleted. doctorx_listings_get() returns it under &#039;owner_media&#039;, as kind =&gt; value, in both contexts, so store only what may be public.<\/p>\n<ul>\n<li>$kind: a short name of lowercase letters, digits, dashes and underscores (DoctorX Listing Video Uploader uses &#039;tour_video&#039;).<\/li>\n<li>$value: text, a number, yes\/no, or an array of those up to three levels deep and under 20 KB; text is stored as plain text. null removes that kind.<\/li>\n<li>Returns true, or WP_Error when the post is not a listing, the kind is not usable, or the value is not storable.<\/li>\n<\/ul>\n<pre class=\"wp-block-code\"><code>if ( function_exists( &#039;doctorx_listings_set_owner_media&#039; ) ) {\n    doctorx_listings_set_owner_media( $post_id, &#039;tour_video&#039;, array( &#039;provider&#039; =&gt; &#039;youtube&#039;, &#039;id&#039; =&gt; $video_id ) );\n}<\/code><\/pre>\n<p>doctorx_listings_get_owner_media( $post_id, $kind = &#039;&#039; ) returns one kind&#039;s value (or null), or every kind when $kind is &#039;&#039;.<\/p>\n<h3>doctorx_listings_walk( array $fields, callable $callback, array $args = array() )<\/h3>\n<p>Visits every published listing without loading whole posts, a chunk at a time: $callback( $post_id, $values ) receives the fields asked for, plus status and transaction. By default only listings visitors may see, with the visitor rules applied (&#039;public_only&#039; =&gt; false for all of them, unfiltered; &#039;chunk&#039; =&gt; 500). Returns the number visited.<\/p>\n<h3>Other functions<\/h3>\n<ul>\n<li>doctorx_listings_post_type(): &#039;drx_listing&#039;.<\/li>\n<li>doctorx_listings_fields(): every field with its meta key, type, label, group and flags.<\/li>\n<li>doctorx_listings_meta_key( $field ): the meta key of a field (&#039;ListPrice&#039; \u2192 &#039;_drx_list_price&#039;).<\/li>\n<li>doctorx_listings_is_public( $post_id ): whether visitors may see the listing.<\/li>\n<li>doctorx_listings_visible_statuses(): the statuses visitors see.<\/li>\n<li>doctorx_listings_normalize_status( $standard_status, $transaction = &#039;sale&#039; ): a source status word as a listing status, or &#039;&#039;.<\/li>\n<li>doctorx_listings_sold_prices_allowed(): whether sold prices may be shown publicly.<\/li>\n<li>doctorx_listings_currency(): the three-letter currency of prices (Listings \u2192 Settings, default CAD).<\/li>\n<li>doctorx_listings_format_price( $amount, $frequency = &#039;&#039; ): &quot;$1,250,000&quot;, &quot;$2,400\/month&quot;.<\/li>\n<li>doctorx_listings_photo_classes( $post_id ): CSS classes for a listing&#039;s photos: doctorx-listing-source-&lt;source&gt;, plus drx-listing-uncropped when the photos must be shown whole (use object-fit: contain for it). The same classes are on the listing page&#039;s body and on the listing in post lists, and the featured image of such a listing gets object-fit: contain.<\/li>\n<\/ul>\n<h3>Actions<\/h3>\n<ul>\n<li>doctorx_listings_saved( $post_id, $created, $source, $source_id ): after doctorx_listings_upsert() wrote a listing (not when nothing changed).<\/li>\n<li>doctorx_listings_removed( $post_id, $source, $source_id, $mode ): after doctorx_listings_remove().<\/li>\n<li>doctorx_listings_after_facts( $post_id, $values ): print more under a listing page&#039;s description (DoctorX Neighbourhood Guides adds its link here).<\/li>\n<li>doctorx_listings_owner_media_set( $post_id, $kind, $value ): after doctorx_listings_set_owner_media() ($value null when removed).<\/li>\n<\/ul>\n<h3>Filters<\/h3>\n<ul>\n<li>doctorx_listings_fields( $fields ): add, relabel or hide fields. A new field needs a unique &#039;meta&#039; key; removing a built-in field breaks plugins that use it.<\/li>\n<li>doctorx_listings_upsert_data( $data, $source, $source_id, $args ): change incoming data before it is saved.<\/li>\n<li>doctorx_listings_status_map( $map ): add source status words (lowercase letters only, e.g. &#039;activeundercontract&#039;) =&gt; listing status.<\/li>\n<li>doctorx_listings_visible_statuses( $statuses ): the statuses visitors see.<\/li>\n<li>doctorx_listings_sold_prices_allowed( $allowed ): force false when a feed licence forbids sold prices.<\/li>\n<li>doctorx_listings_remove_mode( $mode, $source, $source_id, $post_id ): what doctorx_listings_remove() does when no mode is given.<\/li>\n<li>doctorx_listings_delete_attachments( $delete, $post_id ): whether deleting a listing deletes the media uploaded to it (default true).<\/li>\n<li>doctorx_listings_max_photos( $max ): most photos kept per listing (default 100).<\/li>\n<li>doctorx_listings_data( $values, $post_id, $context ): the values doctorx_listings_get() returns.<\/li>\n<li>doctorx_listings_rest_fields( $names ): the fields the REST API shows.<\/li>\n<li>doctorx_listings_facts_rows( $rows, $values, $post_id ): the rows of the facts table on a listing page.<\/li>\n<li>doctorx_listings_field_label( $label, $field, $source, $post_id ): a label in the facts and rooms tables, per source (for example, call the listing number &quot;Ref.&quot; for your own listings).<\/li>\n<li>doctorx_listings_mls_sources( $sources ): sources whose listing number is labelled &quot;MLS\u00ae number&quot;. Default array( &#039;crea-ddf&#039; ), the source of DoctorX Listing Sync for CREA DDF\u00ae. Add yours only if its listings come from an MLS\u00ae System.<\/li>\n<li>doctorx_listings_uncropped_photos( $uncropped, $source, $post_id ): whether a listing&#039;s photos are shown whole. Default true for every source except listings entered by hand.<\/li>\n<li>doctorx_listings_post_type_args( $args ): register_post_type() arguments.<\/li>\n<li>doctorx_listings_use_block_editor( $use ): edit listings in the block editor (default false, the classic screen with the field boxes).<\/li>\n<\/ul>\n<h3>REST API<\/h3>\n<p>Listings are at \/wp-json\/wp\/v2\/listings with a read-only &quot;listing&quot; object holding the REST-safe fields by RESO name, address_line and days_on_market. Visitors get the same view as doctorx_listings_get( $id, &#039;view&#039; ); hidden listings answer &quot;not found&quot;. People who can edit a listing get every field with ?context=edit. The custom fields themselves are not exposed. To write listings, use doctorx_listings_upsert() in PHP.<\/p>\n<h3>WP-CLI<\/h3>\n<ul>\n<li>wp doctorx-listings fields: the field list with meta keys.<\/li>\n<li>wp doctorx-listings get &lt;id&gt; &#91;&#8211;all&#93;: a listing as JSON.<\/li>\n<li>wp doctorx-listings upsert &lt;source&gt; &lt;source_id&gt; &lt;file.json|-&gt; &#91;&#8211;replace&#93;<\/li>\n<li>wp doctorx-listings remove &lt;source&gt; &lt;source_id&gt; &#91;&#8211;mode=trash|delete|off_market&#93;<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">DoctorX Listing Schema<\/h2>\n\n\n\n<h3>Filters<\/h3>\n<ul>\n<li>doctorx_listing_schema_print( $print, $what, $post_id ): whether to print the &#039;listing&#039; (RealEstateListing) or the &#039;breadcrumbs&#039; (BreadcrumbList) on this listing page. Return false when your theme or plugin prints its own. This is the filter to coordinate duplicates with.<\/li>\n<li>doctorx_listing_schema_data( $data, $post_id, $values ): the RealEstateListing data before it is printed; return an empty array to print none for this listing.<\/li>\n<li>doctorx_listing_schema_breadcrumbs( $trail, $post_id ): the trail as array( name, url ) steps, home first, the listing last. DoctorX Neighbourhood Guides inserts the neighbourhood here.<\/li>\n<li>doctorx_listing_schema_place_type( $type, $values ): the schema.org type of the property (Accommodation, House, SingleFamilyResidence, Apartment, ApartmentComplex, Residence or Place).<\/li>\n<li>doctorx_listing_schema_post_types( $types ): the post types whose pages get listing data.<\/li>\n<\/ul>\n<h3>Functions<\/h3>\n<ul>\n<li>doctorx_listing_schema_for_post( $post_id ): the RealEstateListing data a page carries, as an array.<\/li>\n<li>doctorx_listing_schema_claim(): this page&#039;s listing data is printed by someone else; print none.<\/li>\n<\/ul>\n<p>When it prints a trail, the plugin calls doctorx_seo_breadcrumb_claim() (DoctorX SEO), and it does not print one when doctorx_seo_breadcrumb_claimed() says a trail is already taken. It also tells DoctorX SEO&#039;s structured-data check that Google documents no requirements for RealEstateListing.<\/p>\n<h3>WP-CLI<\/h3>\n<ul>\n<li>wp doctorx-listing-schema render &lt;id&gt;: the structured data of a listing page, as JSON.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">DoctorX Neighbourhood Guides<\/h2>\n\n\n\n<p>Guides are posts of type drx_neighbourhood. The figures are in the custom field _drx_guide_stats; the area in _drx_guide_key (a stable lowercase id, &quot;city&#8211;neighbourhood&quot;), _drx_guide_city and _drx_guide_area.<\/p>\n<h3>Shortcode<\/h3>\n<p>&#91;doctorx_neighbourhood_stats&#93; places the figures (and the homes listed, unless listings=&quot;no&quot;) inside a guide&#039;s text instead of after it.<\/p>\n<h3>Filters and actions<\/h3>\n<ul>\n<li>doctorx_guides_area_for_listing( $area, $values, $post_id, $grouping ): the area a listing counts in, as array( key, city, area ), or null to leave it out.<\/li>\n<li>doctorx_guides_snapshot( $figures, $area ): the figures stored for a guide; add your own.<\/li>\n<li>doctorx_guides_figures_rows( $rows, $figures, $guide_id ): the rows shown on a guide (label =&gt; text).<\/li>\n<li>doctorx_guides_ai_instructions( $instructions ): the instructions given to the AI for an introduction.<\/li>\n<li>doctorx_guides_post_type_args( $args ): register_post_type() arguments.<\/li>\n<li>doctorx_guides_draft_created( $guide_id, $area ) (action): a draft guide was created.<\/li>\n<\/ul>\n<h3>WP-CLI<\/h3>\n<ul>\n<li>wp doctorx-neighbourhood-guides areas: the areas found in the listings.<\/li>\n<li>wp doctorx-neighbourhood-guides build &#91;&#8211;create&#93; &#91;&#8211;ai=&lt;n&gt;&#93;: update the figures; create drafts for new areas.<\/li>\n<li>wp doctorx-neighbourhood-guides show &lt;id&gt;: a guide&#039;s figures as JSON.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">DoctorX Listing Video Uploader<\/h2>\n\n\n\n<ul class=\"drx-dev-facts\"><li><strong>Shortcodes:<\/strong> <code>&#91;doctorx_listing_video&#93;<\/code><\/li><li><strong>Blocks:<\/strong> <code>doctorx-listing-video\/tour<\/code><\/li><\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">No developer hooks beyond the shortcodes and blocks above.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">DoctorX Agent Profile theme<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">No developer hooks beyond the shortcodes and blocks above.<\/p>\n\n\n","protected":false},"excerpt":{"rendered":"<p>What developers can build on: hooks, shortcodes, blocks, REST routes and WP-CLI commands, taken from each plugin&#8217;s own readme and from what this site has registered. DoctorX Listing Data Model Use these functions rather than the custom fields directly. They are available once plugins have loaded; save listings on or after the init action. Check [&hellip;]<\/p>\n","protected":false},"author":0,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"footnotes":""},"class_list":["post-99","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/demo.doctorx.ca\/realtor\/wp-json\/wp\/v2\/pages\/99","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/demo.doctorx.ca\/realtor\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/demo.doctorx.ca\/realtor\/wp-json\/wp\/v2\/types\/page"}],"replies":[{"embeddable":true,"href":"https:\/\/demo.doctorx.ca\/realtor\/wp-json\/wp\/v2\/comments?post=99"}],"version-history":[{"count":2,"href":"https:\/\/demo.doctorx.ca\/realtor\/wp-json\/wp\/v2\/pages\/99\/revisions"}],"predecessor-version":[{"id":110,"href":"https:\/\/demo.doctorx.ca\/realtor\/wp-json\/wp\/v2\/pages\/99\/revisions\/110"}],"wp:attachment":[{"href":"https:\/\/demo.doctorx.ca\/realtor\/wp-json\/wp\/v2\/media?parent=99"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}