Moving a website to V2

One key, one host, the same property fields. For anyone who builds or looks after an agency website on Loop.

Today a website reads its properties from apiv3.loop.software (network.api) and sends its enquiries and reads its team through V1 at api.loop.software. Everything a website needs is now in V2, at api.loop.software/v2, with one key.

The property data is unchanged: the same fields, with the same names, plus the office's name (teamName). What changes is the key, the addresses, the paging, and a few behaviours, all listed below. The old addresses keep working while you move.

🚧

What breaks if you only change the host

  • The key goes in Authorization: Bearer, not x-api-key.
  • Results are in data and pagination, not results.
  • Enum values are names ("forSale"), not numbers (2).
  • Enquiries answer 201 Created, not 200 with { "value": true }.

1. Get a V2 key

One client key covers every office in the agency. To show one office's properties, filter with teamIds. A team id outside your key's agency answers 403 Forbidden.

TodayV2
KeyOne team key per office, as x-api-key: <guid>One client key for the agency, as Authorization: Bearer lp_live_…
OfficesOther offices' keys in teamAPIListteamIds, a list of team ids
PermissionsEverything the key could reachScopes, from the Website preset
curl "https://apiv3.loop.software/api/sales-properties?fetch=20" \
  -H "x-api-key: 3f2a9c1e-0000-0000-0000-000000000000"
curl "https://api.loop.software/v2/website/sales/properties?limit=20" \
  -H "Authorization: Bearer lp_live_…"
📘

Ask support for the Website preset. It gives the key read:sales-website, read:lettings-website, read:commercial-website and write:enquiries.

2. Change the addresses

Properties and teams

TodayV2Scope
GET apiv3.loop.software/api/sales-propertiesGET /v2/website/sales/propertiesread:sales-website
GET apiv3.loop.software/api/sales-properties/{lifecycleId}GET /v2/website/sales/properties/{id}read:sales-website
GET apiv3.loop.software/api/lettings-propertiesGET /v2/website/lettings/propertiesread:lettings-website
GET apiv3.loop.software/api/lettings-properties/{lifecycleId}GET /v2/website/lettings/properties/{id}read:lettings-website
GET apiv3.loop.software/api/sales-properties/search-statsGET /v2/website/sales/properties/search-statsread:sales-website
GET apiv3.loop.software/api/lettings-properties/search-statsGET /v2/website/lettings/properties/search-statsread:lettings-website
GET apiv3.loop.software/api/sales-properties/by-listing/{listingId}GET /v2/website/sales/properties/by-listing/{listingId}read:sales-website
GET apiv3.loop.software/api/lettings-properties/by-listing/{listingId}GET /v2/website/lettings/properties/by-listing/{listingId}read:lettings-website
GET api.loop.software/teamGET /v2/website/teams, GET /v2/website/teams/{id}either website scope

A property's {id} is the same lifecycle id as today: salesLifecycleId or lettingsLifecycleId in search results.

Enquiries

Today (V1)V2Scope
POST /property/residential/sales/{id}/viewingPOST /v2/sales/enquiries with "type": "viewing"write:enquiries
POST /property/residential/sales/{id}/call-backPOST /v2/sales/enquiries with "type": "callBack"write:enquiries
POST /property/residential/sales/{id}/informationPOST /v2/sales/enquiries with "type": "information"write:enquiries
POST /property/residential/lettings/{id}/viewing, /call-back, /informationPOST /v2/lettings/enquiries with the same three typeswrite:enquiries
POST /marketing/enquiries/sellerPOST /v2/sales/valuation-requestswrite:enquiries
Not in V2 yet: keep using these

These network.api addresses have no V2 version so far. Keep using them, and tell us if your site depends on one.

network.apiWhat it does
api/sales-properties/{lifecycleId}/sales, {lifecycleId}/documents, random-imagesSales history, documents, images
api/direct-viewings/…Booking a viewing directly
api/teams/… other than your own team, and api/network/…Other teams and the network

3. Property search

Parameters. All of today's parameters work as before, with two changes: limit replaces fetch (1 to 100, default 20), and teamIds (team ids) replaces teamAPIList (other teams' keys).

So lat, lng, range, searchText, minBeds, minPrice, maxPrice, minRent, maxRent, propertyTypes, propertyTypeList, letPeriod, webListingStatuses, refId, roleType, sortOrder and the …Only and exclude… switches are unchanged.

The response. Results come in V2's paging envelope. Each item has the same fields as today, plus teamName: the office the property belongs to, alongside its phone and email, so you can route a lead without another call.

{
  "results": [ … ],
  "totalResults": 57, "returnedResults": 20, "more": true,
  "offset": 0, "perPage": 20, "page": 1, "totalPages": 3
}
{
  "data": [
    { "webMarketingStatus": "forSale", "propertyType": "semiDetached",
      "teamName": "Example Estates Harrogate", … }
  ],
  "pagination": { "offset": 0, "limit": 20, "hasMore": true, "total": 57 }
}
Paging fields, old to new
TodayV2
resultsdata
totalResultspagination.total
morepagination.hasMore
offset, perPagepagination.offset, pagination.limit
page, totalPages, returnedResultsGone: work them out from offset, limit, total and the length of data
Enum values: numbers become names

This applies to every field and parameter that holds one of a fixed set of values. The API reference lists every value for each field. The matching text fields (propertyTypeText, parkingText and so on) are unchanged.

TodayV2
"webMarketingStatus": 2"webMarketingStatus": "forSale"
"propertyType": 3"propertyType": "semiDetached"
sortOrder=4sortOrder=mostRecentlyAdded

Search form ranges

GET /v2/website/sales/properties/search-stats and GET /v2/website/lettings/properties/search-stats return today's figures for your search form's dropdowns, with the same names: totalCount, minPrice, maxPrice and avgPrice (minRent, maxRent and avgRent for lettings), minBedrooms and maxBedrooms. They cover the same properties the search shows, and take teamIds in place of teamAPIList.

V2 adds minBathrooms, maxBathrooms and propertyTypes: the property types you have on the market, with how many of each. Each value is what the search's propertyTypes parameter takes, so a dropdown built from it never offers a type with nothing to show.

{
  "totalCount": 57,
  "minPrice": 125000, "maxPrice": 650000, "avgPrice": 310500.5,
  "minBedrooms": 1, "maxBedrooms": 5,
  "minBathrooms": 1, "maxBathrooms": 3,
  "propertyTypes": [
    { "value": "detached", "name": "Detached House", "count": 20 },
    { "value": "flat", "name": "Flat", "count": 15 },
    { "value": "semiDetached", "name": "Semi-Detached House", "count": 22 }
  ]
}

With nothing on the market, every figure is 0 and propertyTypes is empty. Today's call answers with an empty body.

4. Property pages

GET /v2/website/sales/properties/{id} and GET /v2/website/lettings/properties/{id} return the same fields as today's detail calls, plus teamName.

To look a property up by its website listing id, as today's by-listing calls do, use GET /v2/website/sales/properties/by-listing/{listingId} or GET /v2/website/lettings/properties/by-listing/{listingId}. They return the property exactly as the calls above do.

🚧

Only properties listed on the website. These calls return a property only if it has a listing on the website, whatever that listing's status. Any other property answers 404 Not Found, as does one outside your key's agency. Today these calls return any property.

5. Teams

GET /v2/website/teams lists every active office your key covers, in name order. GET /v2/website/teams/{id} returns one. With a client key, one call gives you every office, where V1 needed a key per office.

curl "https://api.loop.software/team" -H "x-api-key: <first office's key>"
curl "https://api.loop.software/team" -H "x-api-key: <second office's key>"
# …and so on for each office
curl "https://api.loop.software/v2/website/teams" \
  -H "Authorization: Bearer lp_live_…"

Each team has the fields V1's team returns, with the same names, and these changes:

Team fields: added and changed
FieldChange
id, clientIdAdded
membersAdded: the office's current agents (leavers aren't listed)
prospectingOutcodes, matchingOutcodesAdded: for routing a lead to an office by postcode
primaryDarkColour and the other brand coloursNow filled in (V1 always returned them empty)
linksAdded: the office's sales and lettings properties
apiKeyNo longer returned
dateCreatedA date rather than text

6. Enquiries and valuation requests

Send the same details as today. The field names match what V2 returns when you read an enquiry back. Call the API from your server, never from the browser: anyone who can see the key can use it.

curl -X POST "https://api.loop.software/v2/sales/enquiries" \
  -H "Authorization: Bearer lp_live_…" \
  -H "Content-Type: application/json" \
  -d '{
    "salesLifecycleListingId": 99120,
    "type": "viewing",
    "firstName": "Sam",
    "lastName": "Example",
    "emailAddress": "[email protected]",
    "preferredTimes": ["2026-11-07T10:00:00Z"]
  }'
$response = wp_remote_post('https://api.loop.software/v2/sales/enquiries', [
    'headers' => [
        'Authorization' => 'Bearer ' . LOOP_API_KEY,
        'Content-Type' => 'application/json',
    ],
    'body' => wp_json_encode([
        'salesLifecycleListingId' => 99120,
        'type' => 'viewing',
        'firstName' => 'Sam',
        'lastName' => 'Example',
        'emailAddress' => '[email protected]',
        'preferredTimes' => ['2026-11-07T10:00:00Z'],
    ]),
]);

if (wp_remote_retrieve_response_code($response) !== 201) {
    // A 400 lists each problem under the field it's about.
}
const res = await fetch("https://api.loop.software/v2/sales/enquiries", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.LOOP_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    salesLifecycleListingId: 99120,
    type: "viewing",
    firstName: "Sam",
    lastName: "Example",
    emailAddress: "[email protected]",
    preferredTimes: ["2026-11-07T10:00:00Z"],
  }),
});

if (res.status !== 201) {
  // A 400 lists each problem under the field it's about.
}

A valuation request with a client key also needs teamId: the office the request is for. It can include propertyType and bedrooms too.

Enquiry fields, V1 to V2
V1V2
{id} in the addressGone (V1 ignored it)
ListingIdsalesLifecycleListingId (lettings: lettingsLifecycleListingId), the same id
Title, FirstName, LastNametitle, firstName, lastName
EmailAddress, PhoneDaytime, PhoneEveningemailAddress, phoneDaytime, phoneEvening
Address, Postcode, Commentaddress, postcode, comment
PossibleDate1, PossibleDate2, PossibleDate3preferredTimes: a list of up to three ISO 8601 times
DateMoving, ReasonForMovedateMoving, reasonForMove
PropertyToSell, PropertyToRent, FinancialAdvice, PartExchangepropertyToSell, propertyToRent, financialAdvice, partExchange
SendBrochureGone (V1 never used it)
Valuation: MinSalesValuation, SalesValuation, MaxSalesValuationsalesValuation: { min, value, max } (rental: rentalValuation)
Valuation: DateReceived, ExternalIdGone (Loop sets the date)
V1V2
Success200 with { "value": true }201 Created with the whole enquiry and a Location header
A listing your key can't reach, or none given401, or 200 with "value": false400 with the reason
Which office gets the leadThe key's officeThe office the listing belongs to
Alerts, automatic replies and phone notificationsSales onlySales, lettings and valuation requests, as for a portal lead
One of those failingCould fail the request after the enquiry was savedNever fails the request

7. Errors

V2 errors come back as ProblemDetails JSON, with a title, a detail and the status.

StatusMeans
400Something in the request isn't valid; errors says what
401No key, or the key isn't valid
403The key lacks the scope, or a filter names an office outside its agency
404Not found, or not one your key can see
429Too many requests; wait for the time in Retry-After
503Property search is unavailable
❗️

A 503 from search is not "no results". Show the visitor "try again shortly", never an empty results page.

Checklist

  1. Get a client key with the Website preset.
  2. Send it as Authorization: Bearer … instead of x-api-key.
  3. Change the addresses (step 2).
  4. Swap fetch for limit and teamAPIList for teamIds.
  5. Read results from data and pagination.
  6. Build the search form's ranges and property type list from search-stats.
  7. Compare enum values with names, not numbers.
  8. Expect 404 for a property page that isn't listed on the website.
  9. Move the enquiry forms to the V2 field names, and expect 201.
  10. Handle 503 from search as "try again", not as an empty page.

Did this page help you?