How to Migrate from the Legacy SOAP API to BrokerAPI

You can read the full API documentation to get your own sense of how the new API looks like. However, if you already have an existing integration with Hemnet’s legacy SOAP API this guide might help you get started.

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 Not yet implemented. Let us know if you miss it!
postnr visitingAddress.postcode
postort visitingAddress.postalArea
telefon phoneNumber
mailadress emailAddress
urlHemsida websiteUrl
urlObjektlista N/A This field has been removed.
urlLogotyp N/A This field has been removed. Can be customized through Kundportalen.
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”, “Sale”, and “Update market state” actions.
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 later, which should cover most cases where brokers asks for free-text fields for this.
nyproduktion N/A Deprecated in favour of isPreOwned (nyproduktion == !isPreOwned).
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 yearlyArrendeFee “Arrende” is a unique Swedish legal term so no precise English translation is available.
tomtratt yearlyLeaseholdFee
boarea livingArea
biarea supplementalArea
tomtarea landArea
kodMattenhetTomtarea N/A Land area is always in meters squared now.
avgiftBorattsforening monthlyFee This is now a generic “Fee” field. Add other required fees here too.
driftskostnad yearlyRunningCosts
antalRumText N/A No longer used.
antalRumSiffra numberOfBedrooms
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
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 else 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 Will be replaced with a way to force Hemnet to redownload an image from the same URL later.

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.