Public property search API
Read and filter public Sparki properties for a website.
Release status
This guide describes the local property-search implementation prepared on 19 September 2026. The new filters must be deployed to the API before a website uses them. Publishing this guide alone does not deploy the API. The earlier live check confirmed listing reads, pagination and transaction-type filtering, but not working text search.
Endpoint and access
Send a JSON GraphQL POST to https://api.sparki.app/api with Content-Type: application/json. Public listing reads do not require an API key or an Authorization header. Identify the agency by its portal ID. Do not put an admin key, MCP OAuth token or Framer CMS credential in browser code.
The MCP connection is a separate, authorized portal integration. It is not the public website feed.
Listing query
query Properties(
$portalId: String!
$paginate: PaginationResponse
$filters: EventTypeFiltersType
) {
getPublicIntegration(id: $portalId) {
getAvailableEventTypes(paginate: $paginate, filters: $filters) {
count
page
perPage
edges {
id
title
city
price
currency
category
transactionType
status
hideDetailedAddress
}
}
}
}Example variables, replacing YOUR_PORTAL_ID:
{
"portalId": "YOUR_PORTAL_ID",
"paginate": { "page": 1, "perPage": 12 },
"filters": {
"transactionType": "to-sell,rent-out",
"search": "canal",
"city": "Amsterdam",
"minPrice": 100000,
"maxPrice": 900000,
"category": "apartment,house",
"status": "available,underOption"
}
}All filter fields are optional. Different filters combine with AND. Comma-separated values within transaction type, status or category combine with OR. Whitespace around values is ignored; enum values are case-sensitive. An omitted, null or blank text filter imposes no constraint.
search: Stringperforms case-insensitive literal substring matching against the public title in the requested language or the city.%,_and backslash are literal characters, not wildcard syntax. It does not search descriptions, street, house number, postcode, coordinates, internal fields or translation metadata. Setx-sparki-locale, such asnl-NL, to search and receive that language; otherwise the existing request/portal language fallback applies. A deliberately empty translation stays empty.city: Stringis a case-insensitive exact city match after trimming spaces. It is not a street/address search.minPrice: FloatandmaxPrice: Floatare inclusive, finite, non-negative bounds in the stored price units, not cents. There is no currency conversion. Properties with a null price do not match an active price bound. Minimum must not exceed maximum.transactionType: Stringacceptsto-sell,rent-out,service,portfolio-item. Useto-sell,rent-outfor sale/rental listings. The default remains compatible with existing callers and can include services; exclusion is explicit.status: Stringacceptsavailable,underOption,cancelled,closed,completed. It never overrides visibility rules.cancelledyields no public listings.closedandcompletedcan appear only when the property is marked sold and enabled for public sold display.category: Stringaccepts existing category values:apartment,house,parking,storage,studio,villa,chalet,townhouse,commercial-space,office,building-ground,other,taxation,selling-intake,renting-out-intake,search,valuation,energy-label,mortgage. The last service-oriented values remain supported for compatibility; combine with transaction type for a property-only feed.- The legacy
isAdmin: Booleaninput does not change public access and cannot reveal private properties.
There are no typed room-count, bathroom-count or living-area filters in this contract. Do not send invented filter names or assume unstructured detail values support numeric filtering.
Pagination and errors
PaginationResponse uses page: Float! and perPage: Float! in the existing schema. Both must be positive integers at runtime. When the whole argument is omitted or null, the defaults are page 1 and 10 items. If supplied, both fields are required by GraphQL. perPage must fit a signed 32-bit integer, and the calculated offset must be a JavaScript safe integer. Prefer small pages such as 12 or 50.
All filters apply before counting and pagination. count is the total number of matching properties, not the page length. Results sort by existing project order DESC, then id ASC for deterministic ties. Multiple images do not duplicate properties or inflate the count.
A valid search with no matches returns edges: [] and count: 0. A page beyond the last result returns an empty edges array while preserving the filtered count.
Unknown status/category/transaction values, invalid numeric ranges and invalid pagination return a GraphQL error with extensions.code: "BAD_USER_INPUT". Invalid GraphQL variable types fail input coercion before the resolver runs. Always inspect the response's errors; HTTP success alone does not establish a successful sync. A missing or inaccessible portal is not an empty-property reconciliation result.
Public visibility and address privacy
The query stays scoped to the requested portal. Deleted projects and direct-link-only projects are excluded, including projects made direct-link-only by free-plan reconciliation. Only existing publicly eligible active projects or opted-in sold projects appear. Filters cannot widen those rules.
For hideDetailedAddress: true, list and public detail results omit location, street, houseNumber, postalCode, latitude and longitude. City, region and country remain public. Filtering never inspects the precise address fields. Titles and descriptions are author-controlled public content; do not put a confidential address in public copy and expect address-field redaction to remove it.
Live bookings and hybrid CMS use
Keep availability and bookings in Sparki, outside Framer CMS. See the public website plugin guide for project-specific booking links and data-event-type-id button integration. The public API also has getAvailableEventType(id: String!) and appointmentOptions, but a custom availability/booking client is outside this search contract. Use the supported booking UI rather than recreating booking rules from cached listings.
A Framer CMS sync add-on is separate work. Match records by Sparki property ID. Fetch and validate every page before reconciling withdrawals. Never withdraw records after a failed, partial or GraphQL-error response. Stable tie ordering does not make several page requests an atomic snapshot when properties change during a run.
This work does not add signed change webhooks, deletion tombstones, updatedSince, Framer publishing or MCP demo-property creation/image upload. There is no verified end-to-end CMS synchronization contract yet.
Deployment checks still required
The search tests run against isolated PostgreSQL and the actual resolver/input types. Before launching a website, verify the deployed API version and the website's browser CORS behavior. A server adapter avoids browser CORS requirements but still needs appropriate access controls and request limits.
This guide does not promise a numeric rate limit, cache TTL, permanent image URLs, ordered image/alt-text contract or a completed direct-booking API specification. Those guarantees need separate verification and documentation. Keep CMS credentials server-side and do not ingest customer or inbox data.