How to Migrate from the Legacy SOAP API to BrokerAPI

Main differences

  1. The new API is authenticated using expiring tokens instead of via IP.
  2. The new API is in English.
  3. The new API has dedicated error responses.
  4. The new API publishes listings using separate endpoints.
  5. The new API supports partial updates.
  6. The new API requires other tooling for code generation.

Interoperability with the old API

Using new API on records created with SOAP

You can load listings and broker agencies created through the old API using the new API, which should help you get started.

In some cases the old API asked you to use your internal IDs to identify records while the new API asks you to use Hemnet’s own IDs instead. If you don’t have any Hemnet IDs stored on these records you could use the different “List” actions to match Hemnet IDs with your own IDs and store them in your records. That way you can now use the new API even for older records in your database.

Using SOAP on records from the new API

Note: While it is supported and recommended that you interact with older records through the new API, there is no guarantee that using the old API on records modified with the new API will work.

As an example, we support fetching a listing that was created using SOAP in the new API, and we support updating it from the new API. We don’t guarantee that updating that record with the old API (after changing it with the new API) will have semantics that matches the original API.

It is still technically possible and in some cases the effect might be indistinguishable, but Hemnet makes no guarantees that this will continue to work indefinitely. At some point it might even become impossible and cause the SOAP request to fail.

You can deal with this by tagging your records with the API that changed them last. If the new API changed it last then the old implementation should probably not try to change it anymore.

You can always use our test environment to see the effects of this to determine how strict you want to be.

Recommendations

Hemnet recommend that you:

  1. Make sure to start saving information the new API would need to operate on the record when using the old API.
  2. Prepare your existing records by fetching the missing information.
  3. Test updating and creating a few records in the test environment after fetching this information.
  4. When you feel confident, use the new API to modify and create new records instead of the old API, one model at a time.
  5. Stop reading using the old API.

Authentication

In order to get your first authentication tokens you need to log in to the API portal. Here you can create a key, which will show you the secret token once. Save this somewhere safe and provide your application with it.

A key has a randomized ID, which is returned along with the token when a Key is created.

NOTE: Currently an auth token consists of <ID>.<SECRET>. This format is not guaranteed to remain and the secret token should be regarded as an opaque string by all systems.

Keys expire after some time, so it is important to make sure you can change the key easily at runtime.

Providing secret tokens to API requests

Place the secret token inside the Authorization header. If your secret token is 84Qx3dBR7CMV9p-1HsvA.v2KqPYU2YVKozxxdg152, then the header should look like this:

Authorization: Bearer 84Qx3dBR7CMV9p-1HsvA.v2KqPYU2YVKozxxdg152

Testing authentication

A simple way to test your authentication is to access the GET /keys route, which should always give a fast non-empty result back if you are authenticated.

Automatically dealing with expiry

There is an API to create new keys that you can use to automatically deal with key renewal without human intervention.

In the following examples, each customer in the system has their own API key for auditing and security but this is up to you if you want to use this method or not.

Start by checking the current key’s status by fetching the Key details:

GET /keys/4Y0BP-mbkuv40_44aJfG
{
  "key": {
    "id": "4Y0BP-mbkuv40_44aJfG",
    "name": "Prod 2017:Q3 - Customer 45",
    "expiresAt": "2017-12-31T00:00:00+02:00",
    "lastUsedAt": "2017-10-13T14:32:11+02:00",
    "createdBy": {},
    "state": "active"
  }
}

Here you can see that the key is active, but it will expire at 2017-12-31. If this date is close you can create a new key:

POST /keys
{
  "name": "Prod 2017:Q4 - Customer 45",
}
{
  "authToken": "84Qx3dBR7CMV9p-1HsvA.v2KqPYU2YVKozxxdg152",
  "key": {
    "id": "84Qx3dBR7CMV9p-1HsvA",
    "name": "Prod 2017:Q4 - Customer 45",
    "expiresAt": "2018-07-07T00:00:00+02:00",
    "lastUsedAt": null,
    "createdBy": {
      "type": "key",
      "name": "Prod 2017:Q3 - Customer 45",
      "identifier": "4Y0BP-mbkuv40_44aJfG"
    },
    "state": "active"
  }
}

Store the new Key’s ID and auth token in your database.

customer.change_key(created_key.key.id, created_key.auth_token)

Now use the new key for the next request. If you are sure nothing is using the old key you can revoke it manually, or wait for it to expire.

schedule_key_revoke_job(2.minutes, old_key.id)
DELETE /keys/4Y0BP-mbkuv40_44aJfG

Changes to domain models

The new API is in English so all existing fields in the SOAP API is now translated to English as well. Further, the API is not using the exact same domain model so not all fields can be translated cleanly from one to the other.

It is recommended that you read through the new models and get familiar with them before you read this section.

Main concepts

Here’s a quick summary of the model name changes:

  • “Objekt” is now “Listing”.
  • “Mäklarfirma” is now “Broker Agency”.
  • “Mäklare” (person) is now “Broker”.
  • “Mäklarsystem” is now “Broker System”.
  • “Visning” is now “Showing”.
  • “Bild” is now “Listing Image”.
  • The old type ID system for listings is now split into two fields: tenure and housingForm.
  • “Bostadsrättsförening” is now “(Housing) Cooperative”.

MaklareToAdd

Here is a basic summary of the differences between the old MaklareToAdd and the new BrokerAgency.

Old name New name Notes
supplier N/A This is implied through the API Key being used in the request.
broker externalId Your own ID for the broker.
namn name
besoksadress visitingAddress.street
postadress N/A We are still working on this field.
postnr visitingAddress.postcode
postort visitingAddress.postalArea
telefon N/A We are still working on this field.
mailadress emailAddress
urlHemsida N/A We are still working on this field.
urlObjektlista N/A This field has been removed.
datumLogo N/A Just like urlLogotyp, this is deprecated in favor of branding features in Kundportalen.
timestamp_ms N/A No longer needed.

ObjektToAdd

Here is a basic summary of the differences between the old ObjectToAdd and the new Listing.

Old name New name Notes
supplier N/A This is implied through the API Key being used in the request.
broker brokerAgencyId This is Hemnet’s ID instead of your own internal ID.
maklarsystemObjektId externalId Your own ID for the listing.
status N/A Replaced by “Publish”, “Unpublish” and “Sale” actions. Old status 2 for upcoming is replaced with the (Coming soon!) isUpcoming field.
objekttyps tenure and housingForm There is a dedicated section on this below.
prisKronor N/A We calculate this ourselves now.
prisAnnons + valutakod askingPrice
omrade N/A This field is not supported. Alternatives are being discussed. Get in touch if you want to give feedback!
gatuadress streetAddress
postnummer postCode
postadress postalArea
lanKommunkod N/A We calculate this ourselves from the coordinates now.
land N/A Only Swedish listings are accepted in the Listings API.
byggar constructionSpan This is now a structured field and not a free-text field. A place to put major renovations will be added soon, which should cover most cases where brokers asks for free-text fields for this.
byproduktion (Coming soon!) isNewConstruction true when no one has lived in the property before, false otherwise.
annonsUnderBegagnat N/A Not needed anymore.
taxeringsvardeTotalt + taxeringsvardeByggnad taxAssessment The fields have changed semantics quite a lot, but the basic idea of having taxation information is still here.
arrende (Coming soon!) yearlyArrendeFee “Arrende” is a unique Swedish legal term so no precise English translation is available.
tomtratt (Coming soon!) yearlyLeaseholdFee
boarea livingArea
biarea supplementalArea
tomtarea landArea
kodMattenhetTomtarea N/A Land area is always in meters squared now.
avgiftBorattsforening monthlyFee + (Coming soon!) monthlyFeeExplanation This is now a generic “Fee” field. Add other required fees here too.
driftskostnad yearlyRunningCosts + (Coming soon!) yearlyRunningCostsExplanation
antalRumText N/A No longer used.
antalRumSiffra numberOfBedrooms + (Coming soon!) numberOfRooms Use numberOfRooms to stick with the old semantics of this field, while numberOfBedrooms has other semantics.
visningstids The Showing model Added on already existing listings.
xKoordinat + yKoordinat coordinates Projection have changed from SWREF99 to WGS84/GPS. This is the same projection that is used on most online maps and devices.
sokordstextmassa N/A Only text in actual descriptions are now searchable.
urlBeskrivning urls.brokerPage
ansvarigMaklareNamn broker.name
ansvarigMaklareMailadress broker.emailAddress
ansvarigMaklareTelefon broker.phoneNumber
bildUrls The ListingImage model Added on already existing listings.
kortForsaljningstext description
byteskrav N/A This functionality has been removed from Hemnet.
timestamp_ms N/A No longer needed.
saljfras tagline
urlBildLista urls.allImages
bud biddingStatus (Coming soon!) Actual bids can also be added through the Bid model.
exekutivAuktion isForeclosure
saljareNamn, etc. Updated through a specific action.
lagenhetsnummer apartmentNumber Note: This must be the Skatteverket definition now. A housing cooperative’s own numbers must not be provided here.
bostadsrattsforening N/A We will replace this with something newer later.
fastighetsbeteckning propertyDesignation

BildToAdd

BildToAdd is replaced with ListingImages that you create on existing listings.

Old name New name Notes
storBildUrl url
ordningsnummer index Images will be ordered by this number when shown to users. Smaller numbers are shown first.
bildspelsindex N/A Completely replaced with index.
senastUppdaterad N/A (Coming soon!) Will be replaced with a way to force Hemnet to redownload an image from the same URL.

VisningstidToAdd

VisningstidToAdd is replaced by the Showing model that you add to existing listings.

Old name New name Notes
datum N/A See the next field.
N/A isAllDay Will make showing render as an “All-day” event, without a start end end time. End date, if provided, will still be shown.
visningstidStart startAt Full date + time. Use midnight if you only care about the date. See isAllDay.
visningstidSlut endAt Full date + time. Use midnight if you only care about the date. See isAllDay.
visningstidKommentar description

Objekttyps (Tenure and Housing form)

This part of the API has undergone pretty big changes. It is really recommended that you read up on Tenure and HousingForm in the API documentation first so you understand how they are supposed to work.

The biggest change is that more combinations can be expressed, but it is also very important that you treat housing form series correct when you implement this on your side in case we make changes to them in the future. Adding or removing a housing form is not regarded as a breaking change as there is a defined behavior that any client should follow on an unknown HousingForm code.

Dealing with unknown HousingForm codes

If you get an unknown code, like 620, then you should treat it as 600 in your system. If we remove a known code, like 601, you will get 600 back from the API if you read the current value.

This means that you should not perform any naive diffing of this number as you are not guaranteed to get the same number back as you put in.

Here is one suggested way of representing the different codes in a language with sum types + enums (in this case, Rust):

#[derive(Copy, Debug, PartialEq)]
enum HousingForm {
  // 1xx series
  House, // 100
  // Room if left for unknown codes in the 1xx series, but they are still
  // identified as belonging to that series.
  UnknownHouse(i32),

  // 2xx series
  TerracedHouse, // 200
  RowHouse,      // 201
  LinkedHouse,   // 202
  TwinHouse,     // 203
  UnknownTerracedHouse(i32),

  // ...

  // Truly unknown, which would be numbers outside of the valid range or for
  // series that are just reserved.
  Unknown(i32),
}

impl From<i32> for HousingForm {
  fn from(code: i32) -> HousingForm {
    use HousingForm::*;
    // Every code can now be matched to a proper type.
    match code {
      100 => House,
      101..199 => UnknownHouse(code),
      200 => TerracedHouse,
      201 => RowHouse,
      202 => LinkedHouse,
      203 => TwinHouse,
      204..299 => UnknownTerracedHouse(code),
      // ...
      _ => Unknown(code),
    }
  }
}

impl From<HousingForm> for i32 {
  fn from(form: HousingForm) -> i32 {
    use HousingForm::*;
    match form {
      House => 100,
      // ...
      // For the unknown codes, the original code can still be derived.
      UnknownHouse(code) | UnknownTerracedHouse(code) | Unknown(code) => code,
    }
  }
}

Here is an example of a different strategy, this time in the dynamically typed language Ruby:

class HousingForm
  # Original code can be read out as an attribute
  attr_reader :code

  # Declare series ranges
  SERIES = {
    (100..199) => :house,
    (200..299) => :terraced_house,
    # ...
  }.freeze

  # Keep lookups for ranges too
  RANGES = SERIES.invert.freeze

  # Declare known codes
  CODES = {
    100 => :house,
    200 => :terraced_house,
    201 => :row_house,
    202 => :linked_house,
    # ...
  }.freeze

  # Generate a full lookup table at code load time
  SYMBOL_LOOKUP = SERIES.reduce({}) { |(range, series_name), accumulator|
    range.each do |code|
      accumulator[code] = :"unknown_#{series_name}"
    end
    accumulator
  }.merge(CODES).freeze

  def initialize(code)
    @code = code
  end

  # Returns :house on 100, :unknown_house on 101 and :unknown on 1.
  def symbol
    SYMBOL_LOOKUP.fetch(code, :unknown)
  end

  # Inside the range, they are part of the series even if the code itself is
  # not understood.
  def house_series?
    RANGES.fetch(:house).cover?(code)
  end

  # If code is not on the list of supported codes, then it is not supported by us.
  def supported?
    CODES.keys.include?(code)
  end
end

Mapping existing codes

You might need help to map your existing codes to the new codes. We assume you have your own internal representation that you already had to map to Hemnet types before so this list might not be optimal for you. It might still give you a good starting point in case you are having trouble mapping your internal types.

Old Hemnet Type Tenure Housing Form Notes
100 Owned House (100)
110 Owned House (100)
120 Owned Twin House (203)
130 Tenant ownership Terraced House (200)
131 Tenant ownership Terraced House (200)
190 Unknown House (100)
200 Owned Vacation Home (400) Could also be 401 if you know it’s a house.
210 Owned Winterized Vacation Home (402)
220 Tenant ownership Vacation Home (400) Could also be 401 if you know it’s a house.
300 Owned Homestead (600)
310 Owned Agricultural Estate (602)
320 Owned Foresting Estate (603)
330 Owned Estate Without Cultivation (601)
400 Rental Apartment (400)
410 Tenant ownership Apartment (400)
420 Rental Apartment (400)
430 Other Apartment (400)
440 Other Apartment (400)
450 Owned Apartment (400)
500 Owned Plot (500) Identical
900 Unknown Other (900)

Some of the mappings above are not perfect, so please adjust according to the data you have in your system before implementing this.

Another way of testing this is to actually load some of your already published listings over the new API and looking at how they are represented. This is not something to rely on fully as Hemnet has been forced to apply a blind mapping according to the table above for existing records. In some cases the mappings might not be the correct values, but on most cases they should be close enough to help you find the best combination.

Error handling

Most errors should be declared in the API specification, but there are always the possibility of unexpected errors to prop up. There are also a few errors that are considered “global”, like for example not being authenticated.

Errors should all look like this:

{
  "errors": [
    {
      "type": "ErrorType",
      "message": "ErrorMessage",
      "extraFieldsDependingOnErrorType": "..."
    }
  ]
}

That is, the document should only have a single key called errors which contains an array of errors. All errors will have a type and a message. Depending on the type more fields could be present.

You can parse them into structured error types on your side.

Generic errors

When no more specific error type is appropriate, an error will become a GenericError. They do not have any extra fields.

{
  "errors": [
    {
      "type": "GenericError",
      "message": "Persistence failed because of cosmic rays. Try again!"
    }
  ]
}

Validation errors

Validation errors will be emitted as an ValidationError type and have the following extra fields:

  • field – The name of the field with the validation error.
  • fieldMessage – An error message that is scoped to the field. See the example below.
{
  "errors": [
    {
      "type": "ValidationError",
      "message": "urls.allImages is not a valid URL",
      "field": "urls.allImages",
      "fieldMessage": "is not a valid URL"
    }
  ]
}

The intention of field is that it should exactly match the name of the field used in the request (using dot-notation for nested fields). If they do not match, that is a bug and we’d love it if you could report it to us.

fieldMessage is supposed to be a message you could attach to the underlying input field or show in contexts where the problem is obvious. message is instead the full message that can be logged or otherwise shown to a developer as it contains the field name that is used in the API (which might not be a user-friendly name).

In the example above, “is not a valid URL” could be shown in a error message right under the input field for the URL. “urls.allImages is not a valid URL” would not be appropriate to show there.

Showing messages in Swedish

We’d love to hear from you if you’d like to have the error messages translated to Swedish so we can find a solution that covers your needs.

There are many ways of representing errors and not all of them might be appropriate for all cases. Real-world examples and someone to talk to would help us immensely.

For now you could opt to just show “There is a problem with this input” to the user in their language and hope that they understand what the error is. In the more complicated cases you could replicate our validation logic on your side to give users quicker feedback, but that is only valid for more obvious types like email addresses, URLs, numbers, and so on.

When to publish listings

The new API is built to support a workflow where a listing is updated in small steps until the user is happy with the data. Then publication happens as a separate request without also making changes to it.

This allows you to wait to publish until all images are completed, or to show other feedback to users regarding information that is recommended to add before publishing.

Most users will be on contracts with Hemnet where each individual listing needs to be approved in Kundportalen before it can be published. This is something that needs to be done before the publication if you want to completely control the time of publication using the API.

Thankfully the API can tell you if the listing is approved or not, which is also something you can show your users.

Some users are on contracts that allow auto-publishing without explicit approvals. In those cases the listing will appear to be approved almost immediately after the listing is created without the user doing anything.

NOTE: Currently approving in Kundportalen will also publish the listing automatically without an API call. This will change in the near future to require action at both ends.

Unpublishing and marking as sold

If the user do not want the listing to show up on Hemnet anymore they can unpublish just like before.

However, if the listing is supposed to be unpublished because it has been sold, then the listing should not be unpublished. Instead it should be marked as sold. Doing this ensures Hemnet will be able to correctly treat the listing lifecycle, which helps with subscriptions, metrics and tracking turnarounds.

Partial updates

When updating a record (using the PATCH verb) you do not have to send the complete record like with the older SOAP API. Send only the keys you want to change and any keys that are left out will be unchanged.

As an example, updating a Showing with the following document will only change the end date:

PATCH /listings/:listing_id/showings/:showing_id
{
  "endAt": "2018-07-07T12:00:00+02:00"
}

If you include a key with the value null it will be unset, when possible. In this example, the end time of the showing is instead removed:

{
  "endAt": null
}

Sending partial changes like this ensures that the change will happen faster and also makes it possible to make several update requests at the same time that still applies safely.

It is important that your internal models allow updates to differentiate between “no change” and “null”. There are many ways of modelling this, from wrapping types in monads, to keeping a separate list of which fields to pass to the request, or using purely dynamic types.

// Wrapping attributes in monads (Rust)
struct ShowingUpdate {
  // Optional DateTime, e.g. "a new value or make no change at all".
  start_at: Option<DateTime>,

  // endAt is an optional field (`Option<DateTime>`), wrapped in an `Option`
  // for "a new value or make no change at all".
  end_at: Option<Option<DateTime>>,

  // ...
}

// A custom enum is also possible to avoid `Option<Option<T>>`.
enum OptionalValueChange<T> {
  Unchanged,
  ChangeTo(T),
  Unset,
}

enum RequiredValueChange<T> {
  Unchanged,
  ChangeTo(T),
}

impl<T> Default for OptionalValueChange<T> {
  fn default() -> Self { OptionalValueChange::Unchanged }
}

impl<T> Default for RequiredValueChange<T> {
  fn default() -> Self { RequiredValueChange::Unchanged }
}

// Example:
update.start_at = RequiredValueChange::ChangeTo(time)
update.end_at = OptionalValueChange::Unset
update.description = OptionalValueChange::Unchanged
// Keeping list of attributes to send (Java)
class ShowingUpdateBuilder {
  public ShowingUpdateBuilder changeEndAt(value: DateTime) {
    this.changedFields.add("endAt");
    this.endAt = value;
    return this;
  }

  public ShowingUpdateBuilder unsetEndAt() {
    this.changedFields.add("endAt");
    this.endAt = null;
    return this;
  }

  public ShowingUpdateBuilder forgetChangeToEndAt() {
    this.changedFields.remove("endAt");
    this.endAt = null;
    return this;
  }
}
# Purely dynamic type, using a Hash/Map/Dictionary (Ruby)
class ShowingUpdate
  def initialize
    @updates = Hash.new
  end

  def end_at=(value)
    @updates["endAt"] = value
  end

  def forget_end_at
    @updates.delete("endAt")
  end

  def to_json(*args)
    @updates.to_json(*args)
  end
end

There are, of course, other ways of doing this, but three examples might be enough for you to find a solution that fits with your current data models.

Code generation

The existing SOAP API means that a lot of integrators could use code generation to not have to deal with manually implementing structures or keep track of the different SOAP method signatures.

The new API also supports code generation since it is based on the Swagger specification (now Open API). When reading the API documentation, add a .json extension to the URL to get the Swagger specification for the API. This JSON can then be fed to a code generator for Swagger.

It’s important to not mix generated code with your own custom code in case you want to regenerate the client code after Hemnet updates the specification with new fields or models. Make sure you namespace the generated code and treat it just like a foreign interface that you compose on top of. If you are using a staticly-typed language you should get compiler errors pointing you to where you need to change your own interfaces to match the new client changes, making migrations less painful.