Skip to content

Instantly share code, notes, and snippets.

@karlmikko
Last active June 24, 2024 23:56
Show Gist options
  • Save karlmikko/66b7b68401826e4281093692ef5868cc to your computer and use it in GitHub Desktop.
Save karlmikko/66b7b68401826e4281093692ef5868cc to your computer and use it in GitHub Desktop.
LockedOn Query API

LockedOn API

How to get Access

Access is issued to a connected partner on a per office basis. Access requires authorization from the Office and LockedOn Support.

If you are connected with more than 1 office, you will receive an API key per office.

Limits

API Access partners have a concurrency limit applied, this is done on a per LockedOn server basis. Http status 503 is returned to let you know you are over the currency limit. A data packet is also returned to let you know the concurrency limit. This limit is subject to change at anytime.

Syntax

Queries are expressed in EDN data structures. See Edn Syntax.

All API queries start with a vector [] of root queries from a known entity.

Entities are described as an entity ident, a map of identity attribute and value, generally this is keyword and uuid. eg {:properties/uuid #uuid "dd7b7f72-2f0e-4e0b-8cba-c8e2d2c2c859"}

Root Queries and Sub Queries are expressed using maps {}. Where the single key is a keyword (with our without parms) and the value being a vector of sub queries.

Attributes are expressed using keywords :keyword.

A special symbol * is used to express a wildcard that is expanded to all value attributes on an entity. (with some exceptions detailed in schema)

Query Params are expressed with a list () around the query keyword with a map of params as the second list item.

There is a special symbol API_OFFICE used as a root helper for API Access Partners. This will enable you to run sub queries from the given office.

[{API_OFFICE [:offices/uuid]}]

This example will return the :offices/uuid for the given API Access Token.

Url and Headers

Query api endpoint: POST https://newapi.lockedoncloud.com/api/query with Content-Type: application/edn.

API Access Tokens are JWT's with office identifing data. This is to be supplied as the Authorization header with Token prefix. eg Authorization: Token dhwnklailjliadnf.lqernlnaiuig3enaoubiupebnroinoianebuyuknehdfghdfghdfgha=.dninqinerelkn

The API servers support returning data in a few different types:

  • application/edn
  • application/json
  • application/transit+json
  • application/transit+msgpack

If there are other data formats you would like supported please contact support.

Query Params

All sort params are available to be used on any join/root query attribute.

[{API_OFFICE ;; can't use on special symbols as they already include params in-built
  [{(:offices/properties {:limit 1})
    [:properties/uuid]}]}]

Example showing use of params for getting 1 property ident from the office.

In all the examples below, only the params map will be shown. All of the params can be use in conjunction with one another. eg filters and pagination can be used together.

Ident / Root Query

A known Ident can be used as the params map to start a root query from a known entity.

IMPORTANT: the keyword used as the root attribute must match that of that namespace of the ident identity attribute. Eg below showing the :properties keyword matching the :properties/uuid

[{(:properties {:properties/uuid #uuid "dd7b7f72-2f0e-4e0b-8cba-c8e2d2c2c859"}) 
  [:properties/uuid]}]

In this example we pass params of a property ident to start a query from a specific entity we are interested in.

Sorting

IMPORTANT: there is no default sort order, nor is sort consistent between requests. It is important to add sort order when doing pagination.

:sort is a vector of maps defining the data to be sorted on and direction. Many sort maps can be supplied as fallback sort order if the sort comparison is equal. eg: sort by suburb then sub sort by street then sub sort by uuid.

Each sort definition contains 4 keys :query / :path, :nil and :direction .

:direction (optional, default: asc) must be either :asc or :desc. This is will change the direction of the sort.

{:query [:properties/uuid] :direction :asc}

:nil (optional, default depending on sort order) must be either :first or :last. This is will change how nil value is treated in sort. ie have nil values first in the results or last in the results.

{:query [:properties/uuid] :direction :asc :nil :first}

:query (required) Sub query to return data to sort on - will use the first value found. These queries enjoy the full support of the query engine, including nested sorting.

{:query [{:properties/address [:addresses/display-address]}] :direction :asc}

:path (is a short hand for :query allowing for compact query expression for value to sort on)

{:path [:properties/address :addresses/display-address] :direction :asc}

This example is translated to:

{:path [{(:properties/address {:limit 1}) [:addresses/display-address]}] :direction :asc}

Putting it all together

{:sort [{:path [:properties/address :addresses/display-address] :direction :asc} {:query [:properties/uuid] :direction :asc}]}

Example of full sort param supplied to sort collection by display address then subsort ident.

Pagination

There are 4 pagination params :limit :offset :drop and :take.

:take (integer) take n items from the front of the collection, very similar to :limit

:drop (integer) ignore n items from the front of the collection, similar to :offset

:limit (integer) very similar to :take, works with :offset

:offset (integer) drops :limit times :offset items from the front of the collection.

Generally you will either use :take and :drop OR :limit and :offset.

Stats

:stats (boolean, default false) alters the collection data type to be a map containing stats of the collection :total, :collection and :params

:total (integer) number of items in the collection before any pagination and with filters applied.

:params (map) returned copy of the supplied params. can be useful to have stateless pagination/filters working from returned data only.

:collection this will be the data as would normally be returned.

Time Travel

Our query engine supports query at a point in time. By default, the :as-of is current time.

:as-of (inst) of the point in time you want to query.

This can be useful in doing many queries with consistent results. eg make a query, get results, make another query based on those results. Specific eg: get a list of properties, then query details on each property. Or to work through a pagination list of properties to avoid timeouts.

Additionally, in our system nothing is ever deleted, and setting :as-of to a point in time before a retraction was issued will still yeild results as of that time.

{:as-of #inst "2020-03-05"}

Teleport

:teleport and :teleport-or are used to change the strucutre of the result. This can also be used to join to sub queries together into 1 collection.

:teleport-or allows you to run multiple queries with fallback queries. Takes a vector of queries. All left queries should be a join attribute.

:teleport is implemented in terms of :teleport-or with 1 teleport option.

:teleport can also be used to change the attribute in the returned data. Allowing you to query the same join multiple times.

Filters

Filters are specific per entity type. Details of filters are described in the schema including the underlying implementation details.

For more information about specific filters please contact our support. Additionally we are happy to add more filters for specific use cases.

Schema

Schema is available via GET http://newapi.lockedoncloud.com/api/schema best called with header Accept: application/edn to see nuanced data types.

A basic viusal took is available https://jsfiddle.net/karlmikko/bnfcL8k4/93/

Examples

Examples are run with the following headers, not all examples will have sample output. (All example tokens are fake)

Accept: application/edn
Content-type: application/edn
Authorization: Token afnasdnfqelkqbrej.asjbfkluhkdalbflkubasdilbfklueqwaskjdnflkjbajyhrbeqhjbejqkbrvkeq.ajknbeqklbrqhbeyb7yibavj

Basic Office Details

[{API_OFFICE 
  [:offices/uuid
   {:offices/company-detail 
    [*]}]}] ;; note the use of the wild card and how the result is many attributes.

Returns

{:offices 
 {:offices/uuid #uuid "c0f0a352-16fc-4171-bfc9-318b78246c83", 
  :offices/company-detail {:company-details/country-code-iso2 "AU", 
                           :schema/variant :company-details, 
                           :company-details/trading-as "LockedOn Interactive Pty Ltd", 
                           :company-details/uuid #uuid "728db255-450a-4078-8764-aa21d3ca6ce8", 
                           :company-details/timezone "Australia/Sydney", 
                           :company-details/name "LockedOn Real Estate", 
                           :company-details/signature-summary "Let's list and sell"}}}

Property List

[{API_OFFICE 
  [{(:offices/properties {:limit 100 ;; keep the pages small to avoid the change of timing out for very large queries - this will vary with size of sub query
                          :offset 0
                          :stats true
                          :filters {:ready-to-market #{true} ;; only those published
                                    :status #{:properties/listed}}}) ;; only listed properties
    [:properties/uuid]}]}] ;; This sub query could contain more details like address etc if you need it

Property Details

See property.edn

Client List

  [:offices/uuid
   {(:offices/clients {:limit 1 
                       :offset 0
                       :stats true
                       :filters {:clients/triggered.gt #inst "2021-06-09"} ;; date of last start of sync
                       :as-of #inst "2021-07-28T04:52:00" ;; date of current start of sync
                       :sort [{:query [:clients/uuid] :direction :asc}]})
    [:clients/uuid
     :clients/triggered]}]

Client details

See client.edn

[*
{:clients/notes [*]}
#:clients {:company-detail [*]}
{(:clients/contacts
{:sort [{:path [:contacts/order], :direction :asc}]})
;; this portion is the contact query - this can be used in the property.edn if you want the details of who did the enquiry if you are not syncing the clients separately
[*
#:contacts {:company-detail [*]}
#:contacts {:addresses [* #:addresses {:geo-location [*]}]}
#:contacts {:avatar [*]}
#:contacts {:name [*]}
{(:contacts/phone-numbers
{:sort [{:path [:phone-numbers/primary], :direction :desc}]})
[* #:phone-numbers {:label [*]}]}
{(:contacts/emails
{:sort [{:path [:emails/primary], :direction :desc}]})
[*
#:emails {:label [*]}
#:emails {:failed-status
[:tracking-performances/status
:tracking-performances/failed-reason]}]}]}]
;; Below is a fairly complete property query - this can be dropped in place in the property list example
[*
#:office-users {:_properties [:office-users/uuid #:office-users {:contact [* #:contacts {:name [*]}]}]}
{:office-users/_properties [:office-users/uuid #:office-users {:contact [* #:contacts {:name [*]}]}]}
#:properties {:property-type [* #:tags {:tags [*]}]}
{:properties/enquiries [{:enquiries/checkin [{:checkins/contact [:contacts/uuid]}]}
{:enquiries/contact [:contacts/uuid]}]}
{:properties/checkins [{:checkins/contact [:contacts/uuid]}]}
{:properties/sales-offers [{:sales-offers/purchaser [* {:clients/contacts [:contacts/uuid]}]}]}
#:properties {:primary-agent
[:office-users/uuid]}
#:properties {:secondary-assign-to
[:office-users/uuid]}
#:properties {:inspection-times
[*
#:calendar {:occurance [*]}]}
#:properties {:appraisal
[*
{:appraisals/appointments
[*
#:calendar {:venue-address [*]}
#:calendar {:occurance [*]}]}
#:appraisals {:appraised-min-price [*]}
#:appraisals {:appraised-max-price [*]}]}
#:properties {:listing
[*
#:listings {:listing-agent-primary
[:office-users/uuid]}
#:listings {:listing-agent-secondary
[:office-users/uuid]}
#:listings {:listing-agents
[:office-users/uuid]}
#:listings {:listing-pas
[:office-users/uuid]}
#:listings {:vendor-managers
[:office-users/uuid]}
#:listings {:admin-assistant
[:office-users/uuid]}
#:listings {:social-media-coordinator
[:office-users/uuid]}
#:listings {:listings-details [* #:tags {:price [*]}]}
#:listings {:auction
[*
#:calendar {:venue-address [*]}
#:calendar {:occurance [*]}]}
#:listings {:vendor-price [*]}
#:listings {:agent-price [*]}
#:listings {:market-price [*]}
#:listings {:vpa-agreed-amount [*]}
#:listings {:commission-rate
[*
#:part-prices {:assignment
[*
#:price-assignments {:assign-to
[*
#:assign-tos {:office-user
[:office-users/uuid
#:office-users {:contact
[*
#:contacts {:name
[*]}]}]}]}]}]}]}
#:properties {:sales-advice
[*
#:sales-advices {:sales-agents
[:office-users/uuid
#:office-users {:contact
[*
#:contacts {:name
[*]}]}]}
#:sales-advices {:sales-agents-secondary
[:office-users/uuid
#:office-users {:contact
[*
#:contacts {:name
[*]}]}]}
#:sales-advices {:vendors
[*
#:sales-parties {:clients
[*
#:clients {:contacts
[*
#:contacts {:name
[*]}]}]}
#:sales-parties {:solicitor
[*
#:clients {:company-detail
[*
#:company-details {:addresses
[*]}
{(:company-details/phone-numbers
{:limit
1
:sort
[{:path
[:phone-numbers/primary]
:direction
:desc}]})
[*
#:phone-numbers {:label
[*]}]}
{(:company-details/emails
{:limit
1
:sort
[{:path
[:phone-numbers/primary]
:direction
:desc}]})
[*
#:emails {:label
[*]}]}]}
#:clients {:contacts
[*
#:contacts {:name
[*]}
{(:contacts/phone-numbers
{:limit
1
:sort
[{:path
[:phone-numbers/primary]
:direction
:desc}]})
[*
#:phone-numbers {:label
[*]}]}
{(:contacts/emails
{:limit
1
:sort
[{:path
[:phone-numbers/primary]
:direction
:desc}]})
[*
#:emails {:label
[*]}]}]}]}
#:sales-parties {:solicitor-contact
[*
#:contacts {:name
[*]}
{(:contacts/phone-numbers
{:limit 1
:sort
[{:path
[:phone-numbers/primary]
:direction
:desc}]})
[*
#:phone-numbers {:label
[*]}]}
{(:contacts/emails
{:limit 1
:sort
[{:path
[:phone-numbers/primary]
:direction
:desc}]})
[*
#:emails {:label
[*]}]}]}]}
#:sales-advices {:purchasers
[*
#:sales-parties {:solicitor
[*
#:clients {:company-detail
[*
#:company-details {:addresses
[*]}
{(:company-details/phone-numbers
{:limit
1
:sort
[{:path
[:phone-numbers/primary]
:direction
:desc}]})
[*
#:phone-numbers {:label
[*]}]}
{(:company-details/emails
{:limit
1
:sort
[{:path
[:phone-numbers/primary]
:direction
:desc}]})
[*
#:emails {:label
[*]}]}]}
#:clients {:contacts
[*
#:contacts {:name
[*]}
{(:contacts/phone-numbers
{:limit
1
:sort
[{:path
[:phone-numbers/primary]
:direction
:desc}]})
[*
#:phone-numbers {:label
[*]}]}
{(:contacts/emails
{:limit
1
:sort
[{:path
[:phone-numbers/primary]
:direction
:desc}]})
[*
#:emails {:label
[*]}]}]}]}
#:sales-parties {:solicitor-contact
[*
#:contacts {:name
[*]}
{(:contacts/phone-numbers
{:limit 1
:sort
[{:path
[:phone-numbers/primary]
:direction
:desc}]})
[*
#:phone-numbers {:label
[*]}]}
{(:contacts/emails
{:limit 1
:sort
[{:path
[:phone-numbers/primary]
:direction
:desc}]})
[*
#:emails {:label
[*]}]}]}
{(:sales-parties/clients
{:sort
[{:path [:clients/uuid]
:direction :desc}]})
[*
{(:clients/contacts
{:sort
[{:path [:contacts/order]
:direction :asc}]
:limit 1})
[*
#:contacts {:name [*]}
#:contacts {:addresses [*]}
#:contacts {:phone-numbers
[*
#:phone-numbers {:label
[*]}]}]}]}]}
#:sales-advices {:contract-price
[* #:prices {:tax [*]}]}
#:sales-advices {:commission-amount
[* #:prices {:tax [*]}]}
#:sales-advices {:outstanding-marketing
[* #:prices {:tax [*]}]}
#:sales-advices {:outstanding-monies
[* #:prices {:tax [*]}]}
#:sales-advices {:deposit-required
[* #:prices {:tax [*]}]}
#:sales-advices {:deposit-held
[* #:prices {:tax [*]}]}]}
#:properties {:property-details
[*
#:tags {:tags [*]}
#:tags {:default-tag [*]}
#:tags {:measurement [*]}
#:tags {:measurement-min [*]}
#:tags {:measurement-max [*]}
#:tags {:price [*]}
#:tags {:price-min [*]}
#:tags {:price-max [*]}]}
{(:properties/campaign-items {:stats true})
[*
#:expense-items {:price [*]}
#:expense-items {:category-tag [:tags/label]}
#:expense-items {:publication-tag [:tags/label]}]}
#:properties {:vpa-payments [* #:vpa-payments {:price [*]}]}
#:properties {:vpa-agreed-amount [*]}
#:properties {:internal-expenses [#:expense-items {:price [*]}]}
#:properties {:marketing
[*
{(:marketing-items/floorplans
{:sort [{:path [:files/order], :direction :asc}]})
[*]}
{(:marketing-items/images
{:sort [{:path [:files/order], :direction :asc}]})
[*]}
#:marketing-items {:virtual-tours
[*
#:urls {:label
[:tags/uuid
:tags/label
:tags/tag
:tags/locked]}]}
#:marketing-items {:websites
[*
#:urls {:label
[:tags/uuid
:tags/label
:tags/tag
:tags/locked]}]}
#:marketing-items {:videos
[*
#:urls {:label
[:tags/uuid
:tags/label
:tags/tag
:tags/locked]}]}]}
#:properties {:land-details
[*
#:tags {:tags [*]}
#:tags {:default-tag [*]}
#:tags {:measurement [*]}
#:tags {:measurement-min [*]}
#:tags {:measurement-max [*]}
#:tags {:price [*]}
#:tags {:price-min [*]}
#:tags {:price-max [*]}]}
#:properties {:property-tags
[:schema/variant :tags/uuid :tags/tag :tags/label]}
#:properties {:tenants
[:tenants/uuid
#:tenants {:client
[*
{(:clients/contacts
{:sort
[{:path [:contacts/order]
:direction :asc}]
:limit 1})
[*
#:contacts {:name [*]}
#:contacts {:addresses [*]}
#:contacts {:phone-numbers
[*
#:phone-numbers {:label [*]}]}
#:contacts {:emails
[*
#:emails {:label [*]}]}]}]}]}
#:properties {:vendor-type [*]}
#:properties {:vendors
[*
#:sales-parties {:solicitor-contact
[*
#:contacts {:name [*]}
{(:contacts/phone-numbers
{:limit 1
:sort
[{:path [:phone-numbers/primary]
:direction :desc}]})
[* #:phone-numbers {:label [*]}]}
{(:contacts/emails
{:limit 1
:sort
[{:path [:phone-numbers/primary]
:direction :desc}]})
[* #:emails {:label [*]}]}]}
#:sales-parties {:solicitor
[*
#:clients {:company-detail
[*
#:company-details {:addresses
[*]}
{(:company-details/phone-numbers
{:limit 1
:sort
[{:path
[:phone-numbers/primary]
:direction :desc}]})
[*
#:phone-numbers {:label
[*]}]}
{(:company-details/emails
{:limit 1
:sort
[{:path
[:phone-numbers/primary]
:direction :desc}]})
[*
#:emails {:label [*]}]}]}
#:clients {:contacts
[*
#:contacts {:name [*]}
{(:contacts/phone-numbers
{:limit 1
:sort
[{:path
[:phone-numbers/primary]
:direction :desc}]})
[*
#:phone-numbers {:label
[*]}]}
{(:contacts/emails
{:limit 1
:sort
[{:path
[:phone-numbers/primary]
:direction :desc}]})
[*
#:emails {:label
[*]}]}]}]}
{(:sales-parties/clients
{:sort [{:path [:clients/uuid], :direction :desc}]})
[*
{(:clients/contacts
{:sort
[{:path [:contacts/order], :direction :asc}]
:limit 1})
[*
#:contacts {:name [*]}
#:contacts {:addresses [*]}
#:contacts {:phone-numbers
[* #:phone-numbers {:label [*]}]}]}]}]}
#:properties {:vendor-documents [:files/uuid]}
#:properties {:contract-documents [:files/uuid]}
#:properties {:rural-details
[*
#:tags {:tags [*]}
#:tags {:default-tag [*]}
#:tags {:measurement [*]}
#:tags {:measurement-min [*]}
#:tags {:measurement-max [*]}
#:tags {:price [*]}
#:tags {:price-min [*]}
#:tags {:price-max [*]}]}
#:properties {:rental-details
[*
#:tags {:tags [*]}
#:tags {:default-tag [*]}
#:tags {:measurement [*]}
#:tags {:measurement-min [*]}
#:tags {:measurement-max [*]}
#:tags {:price [*]}
#:tags {:price-min [*]}
#:tags {:price-max [*]}]}
#:properties {:portal-subscriptions
[*
:portal-subscriptions/property-status
{(:portal-subscriptions/required-checklist
{:sort
[{:path [:tags/boolean], :direction :desc}
{:path [:tags/label], :direction :asc}]})
[*]}
#:portal-subscriptions {:primary-agent
[*
#:assign-tos {:office-user
[*
#:office-users {:contact
[*
#:contacts {:name
[*]}]}]}]}
#:portal-subscriptions {:secondary-agent
[*
#:assign-tos {:office-user
[:office-users/uuid]}]}
#:portal-subscriptions {:vic-soi [*]}
#:portal-subscriptions {:schedule [*]}
{(:portal-subscriptions/upload-to
{:sort
[{:path [:selected-portals/active]
:direction :desc}
{:path
[:selected-portals/portal
:active-portals/external-service
:external-services/label]
:direction :asc}]})
[*
#:selected-portals {:portal
[*
#:active-portals {:external-service
[*]}]}]}]}
#:properties {:address [* #:addresses {:geo-location [*]}]}]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment