General Principles

REpresentational State Transfer (REST) is a set of guidelines and best practices for API design. We believe in pragmatic REST design. In other words, our API follows RESTful principles, deviating only to make life easier for end users (you). RESTful principles include:

  • Uniform interface: Our URIs describe resources. Our URIs contain only nouns, not verbs. Those resources can be manipulated through standard HTTP verbs: GET, POST, and PATCH. All responses from the API return objects in JSON format, including errors.
  • Stateless: Our servers do not store any state information. Instead, each request to the API contains all the necessary information to handle the request. Ever had back-button issues when an API expected you to complete steps in a certain order? That's because it violated the statelessness principle.
  • Cachable: Caching certain API responses eliminates many client–server interactions, further improving scalability and performance.
  • HTTP response codes: We use standard HTTP response codes to indicate API succes and errors.
  • CORS: Cross Origin Resource Sharing (CORS) allows you to interact securely with our API from any client side browser or application.

Login authentication

When to use authentication via login: Login-based authentication is appropriate when communication with Honest Renter's servers will be entirely through your user's browser via AJAX. Honest Renter will manage session creation, authentication, and expiry. The key advantage over secret key authentication is its simplicity. Your website only needs front end files (e.g., html, css, javascript). You don't need to create or maintain any server-side scripts (e.g., php, asp, python, .net). You don't need to store any usernames or passwords in your own database. We take care of all the tricky bits for you. If you are a landlord or small property management company, you probably want authentication via login.

What do I need to start? You need an apiKey. Contact us to have us generate one for you.

Initial authentication: To log in a member, create a new session by posting his/her username and password to https://api.honestrenter.com/members/session. To log in an applicant, create a new session by posting an invitation code's value to https://api.honestrenter.com/applicants/session

Failed login: When there is no member or applicant matching the provided info, a 404 not found error is thrown. When the password is incorrect, a 400 error is thrown. After 10 failed attemps, a 422 Unprocessable Entity error is thrown, and the member is temporarily locked, preventing anyone from logging in as that member. The IP address will also be locked, temporarily preventing that IP address from logging in to anyone's account.

Successful login: If the login details are correct, the server will respond with a HTTP status code of 200. The response body will contain the corresponding member or applicant object in JSON format. The response header will contain two parameters to be used for authenticating subsequent API calls:

  1. HONR-Session: Contains information the server needs in order to authorize future requests. It describes who is logged in, the level of authorization granted, when the credentials will expire, and the API key used to generate the request. A typical value looks like {"apiKey":"jU89Bv9KL3tYzHm", "authorization":"member", "expires":1429940443, "person":"cykKrHNCjQ", "renewableUntil":1429949443}.
  2. HONR-Authentication-Token: A digest authenticating the contents of HONR-Session. It is a signature unique to the session information, allowing our API to ensure that the contents of HONR-Session are authentic and unmodified.

Subsequent authentication: After login credentials have been verified by the API, subsequent calls are authorized and authenticated using the session and authentication token respectively. The HONR-Session and HONR-Authentication-Token should be included in all future request headers for that person's session.

Lifetime of sessions: Sessions expire after about 60 minutes of inactivity. Activity can renew the session. If a session is more than 5 and less than 60 minutes old, any new request will produce a new HONR-Session and HONR-Authentication-Token returned in response headers. Those new values can be used instead of the old ones to extend the life of the session so that the person can continue working without having to re-supply their login credentials. To avoid sessions being renewed forever, there is a maximum life imposed on a session. A session is only renewable for up to 3 hours. After that, login credentials will have to be provided again. These lifetime limits on sessions help improve security by limiting the value of hijacking a session.

HTTPS required: To ensure that session credentials remain safe, all API requests must be made over HTTPS. Any call made over plain HTTP will fail.

Member login request

$.ajax({ data: "address=demo@honestrenter.com&password=testme", method: "POST", url: "https://api.honestrenter.com/members/session" });

Applicant login request

$.ajax({ data: 'value='+$("#invitation-code").val(), method: "POST", url: "https://api.honestrenter.com/applicants/session" });

Response headers

Connection: Keep-Alive Content-Length: 894 Content-Type: text/html;charset=UTF-8 Date: Sat, 18 Jul 2015 07:11:06 GMT HONR-Authentication-Token: ecf417f8a8fc7a7ced512c087412b1a6f44c60c21ff232b15fe83cccfe28141a HONR-Session: {"apiKey":"jU89Bv9KL3tYzHm","authorization":"member","expires":1437207067,"person":"cykKrHNCjQ","renewableUntil":1437214267} Keep-Alive: timeout=5, max=100 Server: Apache

Response body (for member login)

{ "data": { "id": "cykKrHNCjQ", "hasPassword": 1, "inboxFilters": "cykKrHNCjQ", "inviteSettings": "cykKrHNCjQ", "sendReportsByEmail": true, "users": [ "Lv8n4R4eR7" ], "accountsWithAccess": [], "birthdate": null, "created": 1391796078, "dateToDeleteIdentifyingInfo": null, "ethnicities": [], "emails": [ "lb3IHuvPqC" ], "gender": null, "name": { "title": null, "first": "Demo", "middle": null, "last": "Tester", "previousLast": null, "suffix": null }, "notes": [], "notifications": [], "otherEthnicity": "", "phones": [], "positions": null, "preferredEmail": "lb3IHuvPqC", "preferredPhone": null } }

Append the session info and authentication token in request headers for subsequent API calls

$.ajaxSetup({ beforeSend: function(xhr,settings) { xhr.setRequestHeader( 'HONR-Authentication-Token', honr.get('honr_authentication_token') ); xhr.setRequestHeader( 'HONR-Session', honr.get('honr_session') ); } });

Secret key authentication

When to use secret key authentication: Larger companies may already have online platforms that manage user login/authentication. When you have a service that already verifies login credentials, you probably don't want the hassle of a second login just for Honest Renter. Instead, you can generate API request headers containing all necessary credentials using your own back end scripts. The key advantages of using secret keys over login authentication are that:

  1. Communication between servers can be stateless. Your server can produce all the information needed to have a request processed by Honest Renter without needing to go through login scripts or remember session information. Statelessness is a core principle of RESTful APIs.
  2. Avoids the need to maintain separate login credentials for Honest Renter. Your users don't need separate usernames/passwords for Honest Renter. Applicants don't need invitation codes from Honest Renter. The whole point is that user authentication is left up to you. We just verify that you used the right secret key.

What do I need to start?

  1. An apiKey and a secretKey. Contact us to have us generate those for you.
  2. A way to authenticate your users (i.e., verify that people are logged in to your service). In other words, you need your own login system.
  3. A way to store the Honest Renter person id corresponding to the logged in user. Typically, this would require adding a field to your database to store that information for later retrieval.
  4. Some back end scripts capable of retrieving the person id and applying the secretKey to generate valid authentication token.

Initial authentication: A back end script (e.g., php, asp, python) on your server generates session information and uses your secret key to produce a valid authentication token. Include those in the HONR-Session and HONR-Authentication-Token HTTP headers of your request. See the sample PHP code (on the right) for producing the session and authentication token.

Failed authentication: A 401 error is thrown when you did not use the right secretKey, or when the authentication token did not match the session information, or when the session has expired.

Maximum session length: Expiry dates that are more than 3 hours in the future will be reset to 3 hours in the future. If the renewableUntil date is more than 24 hours in the future, it will be reset to 24 hours in the future.

What's in a session: Sessions are the same for both authentication via login and authentication via secret key. They describe who is logged in, the level of authorization granted, when the credentials will expire, and the API key used to generate the request. A typical value looks like {"apiKey":"jU89Bv9KL3tYzHm", "authorization":"member", "expires":1429940443, "person":"cykKrHNCjQ", "renewableUntil":1429949443}.

Generating the authentication token:

  1. Produce an associative array containing the following properties of the session: apiKey, authorization, expires, person, renewableUntil.
  2. Sort the array alphabetically by key.
  3. JSON encode the array to create an authentication string. An example string would be '{"apiKey":"jU89Bv9KL3tYzHm","authorization":"member", "expires":1429940443,"person":"cykKrHNCjQ","renewableUntil":1429949443}'
  4. If API requests will be made from a browser through AJAX, append the first 1000 character's of the browser's user agent to the authentication string. This helps prevent someone else on a different browser from using the same session information and authentication token.
  5. Generate an HMAC (hash-based authentication code) of the authentication string using the SHA256 digest algorithm. The resulting HMAC is the authentication token.

Subsequent authentication: You can continue using the same HONR-Session and HONR-Authentication-Token to make additional requests up to the expiry time of the session. The requests could come directly from your server, or from a user's browser.

HTTPS required: To ensure that session credentials remain safe, all API requests must be made over HTTPS. Any call made over plain HTTP will fail.

Keep the secretKey secret: As the name implies, is is important that your secretKey is kept secret. Store it somewhere outside of your website's document root. The secretKey should never be sent in any headers. Someone who knows your secret key could impersonate any of your users on Honest Renter. If you believe someone inappropriate may have gained access to your secret key, let us know so that we can generate a new one for you.

Sample PHP code to generate a session and authentication token

<?php header('Content-type: application/json; charset=utf-8'); #Your API key and secret key are provided by Honest Renter $apiKey = 'jU89Bv9KL3tYzHm'; $secretKey = 'rkH(U9zqst*KGPmqHK2sHhsw^36gwAqW'; #Set the expiry date 1 hour in the future $expires = time() + (60*60); #Set the renewable until date to 3 hours in the future $renewableUntil = time() + (60*60*3); #Retrieve the Honest Renter person id for the person making the request. #This information should be stored in your database. $sql = "SELECT person FROM users WHERE id = :id_of_logged_in_user;"; $handle = $connection->prepare($stmt); $handle->bindParam(:id_of_logged_in_user, $idOfLoggedInUser, PDO::PARAM_STR, 10); $handle->execute(); $honestRenterPersonId = $handle->fetch(PDO::FETCH_COLUMN, 0); #Define what information to include in the HONR-Session request header $sessionInfo = [ "apiKey" => $apiKey, "authorization" => "member", "expires" => $expires, "person" => $honestRenterPersonId, "renewableUntil" => $renewableUntil ]; #Make sure the session array is sorted alphabetically by array key ksort($sessionInfo); #JSON encode the session info to produce the session text to go in the HONR-Session HTTP request headers $sessionText = json_encode($sessionInfo); #Find the first 1000 characters of the browser user agent $userAgent = empty($_SERVER['HTTP_USER_AGENT']) ? '' : filter_var($_SERVER['HTTP_USER_AGENT'], FILTER_SANITIZE_STRING); $userAgent = substr($userAgent, 0, 1000); #The authentication string is a combination of the $sessionText and $userAgent $authenticationString = $sessionText.$userAgent; #Generate the HMAC to go in HONR-Authentication-Token HTTP request headers $authenticationToken = hash_hmac('sha256', $authenticationString, $authenticationKey); #Output the sessionText and authentication token $output = [ 'HONR-Session' => $sessionText, 'HONR-Authentication-Token' => $authenticationToken ]; $json = json_encode($output); echo $json; ?>

Authorization

What is authorization? Authorization means ensuring that someone has sufficient permissions to carry out the requested action. Every request must include information on the person making the request.

Candidate vs. member authorization: Each request must also include whether that person is currently granted "candidate" or "member" authorization credentials. Candidate credentials are given when someone logs in to take a survey using an invitation code. Member credentials are granted when a person has verified his/her username and password.

Failure: When person information is missing from the HONR-Session request header, a 401 - Unauthorized error is thrown. When person information is present but the person lacks permission to perform the requested action, a 403 - Forbidden error is thrown. When the person lacks permissions to complete a GET request for retrieving information, a 404 - Not Found error can be thrown, indicating that the server can neither confirm or deny the object's existence for that person.

Contents of HONR-Session request header

{ "apiKey":"jU89Bv9KL3tYzHm", "authorization":"member", "expires":1429940443, "person":"cykKrHNCjQ", "renewableUntil":1429949443 }

Saving session info to a client's browser

When response headers to a browser contain HONR-Session and HONR-Authentication-Token information, you should save that information. Subsequent API requests from the client's browser must include the session info and authentication token.

There are two main options for saving the information to client's browser:

Cookies: You can save the information in a cookie. One word of caution is that once a cookie is set, it is automatically included in all request headers for all file types. That's fine if everything is loaded over a secure https connection. However, if you load a stylesheet or image over http, you could potentially expose the session information. Exposed session information could allow someone to log in as that person for a few hours. That risk can be mitigated by setting the cookie as secure so that it will only be transmitted over https. The take home message is that if storing session information in cookies, make sure all files are loaded over secure https connections, or set the secure flag.

JavaScript's sessionStorage: JavaScript's sessionStorage in an HTML5 alternative to cookies. Unlike cookies, sessionStorage values are not included automatically included in all request headers. This helps reduce the risk of accidental exposure. Note that iOS disables sessionStorage during privacy mode. To avoid errors in iOS privacy mode, best practice is to detect whether sessionStorage is currently enabled. When sessionStorage is disabled, you can use cookies as a fall back solution.

Saving session info

//When an ajax request completes, check if session info is present in headers. If it is, save it. $(document).ajaxComplete(function(event, jqXHR, settings) { if (jqXHR.getResponseHeader('HONR-Authentication-Token') && jqXHR.getResponseHeader('HONR-Session')) { honr.set('honr_authentication_token', jqXHR.getResponseHeader('HONR-Authentication-Token')); honr.set('honr_session', jqXHR.getResponseHeader('HONR-Session')); } }); //Here are some javascript functions to save info to sessionStorage if available, or fall back to cookies (function(honr, $, undefined) { //Functions to create, read, and remove cookies honr.cookie = { create : function(name,value,days) { if (days) { var date = new Date(); date.setTime(date.getTime()+(days*24*60*60*1000)); var expires = "; expires="+date.toGMTString(); } else var expires = ""; document.cookie = name+"="+value+expires+"; path=/"; }, read : function (name) { var nameEQ = name + "="; var ca = document.cookie.split(';'); for(var i=0;i < ca.length;i++) { var c = ca[i]; while (c.charAt(0)==' ') c = c.substring(1,c.length); if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length,c.length); } return null; }, remove : function(name) { honr.cookie.create(name,"",-1); } } //Check if session storage can be written to. It is disabled in private mode in Safari. function isSessionStorageSupported() { try { localStorage.setItem('test', 'test'); localStorage.removeItem('test'); return true; } catch(e) { return false; } } //Check if cookies are enabled function areCookiesEnabled() { var cookieEnabled = (navigator.cookieEnabled) ? true : false; if (typeof navigator.cookieEnabled == "undefined" && !cookieEnabled) { document.cookie="testcookie"; cookieEnabled = (document.cookie.indexOf("testcookie") != -1) ? true : false; } return (cookieEnabled); } honr.sessionStorageEnabled = isSessionStorageSupported() ? true : false; honr.cookiesEnabled = areCookiesEnabled() ? true : false; honr.set = function(name, value) { if (honr.sessionStorageEnabled) sessionStorage.setItem(name, value); else honr.cookie.create(name, value, 1); } honr.get = function(name) { if (honr.sessionStorageEnabled) return sessionStorage.getItem(name); else return honr.cookie.read(name); } honr.remove = function(name) { if (honr.sessionStorageEnabled) sessionStorage.removeItem(name); else honr.cookie.remove(name); } }(window.honr = window.honr || {}, jQuery));

Errors

We use standard HTTP response codes to indicate success or failure of API requests. Codes in the 2xx range indicate success. Codes in the 4xx range indicate an error due to the information provided in the request (e.g. a required parameter was missing, the logged in person lacks permissions required to complete the request, etc.). Codes in the 5xx range indicate an error on Honest Renter's servers.

Successful requests (in the 2xx range) return objects in the "data" namespace. Failed requests (in the 4xx or 5xx range) return objects in the "error" namespace.

Errors are returned in the request body as a JSON formatted object. The "message" property contains text suitable for you to display to end users.

ATTRIBUTES

  • codeINT(3)
    The numeric HTTP status code. Will be one of the codes listed on the right.
  • messageTEXT
    A human-readable message giving more details about the error. These messages can be shown to your users.
  • typeVARCHAR(255)
    The name of the error. Will be one of the names listed on the right.

Error message format

{ "error": { "code": 422, "message": "Invalid email address.", "type": "Unprocessable entity" } }

HTTP Status Code Summary

200 - OK Everything worked as expected.
201 - Created The request has been fulfilled and resulted in a new resource being created.
204 - No content The server has successfully fulfilled the request but does not need to return a body.
400 - Bad Reqest Something went wrong when posting the information. This generic error only occurs when none of the other more specific 400 level error types apply. Check the message returned for details.
401 - Unauthorized Request not completed because it lacks valid credentials for the target resource. The session may have timed out or been unset. The person needs to log in again.
403 - Forbidden The person making the request lacks permissions to perform the requested action on the requested object. A different user (e.g., account administrator) may need to perform the action instead.
404 - Not Found The requested object doesn't exist, or the server cannot confirm the object's existence for the logged in user.
405 - Method Not Allowed A request was made of a resource using a request method not supported by that resource; for example, using GET on a form which requires data to be presented via POST, or using PATCH on a read-only resource.
408 - Request Timeout The request wasn't processed because it did not complete in time. It's safe to try again.
412 - Precondition Failed Missing a required parameter in the GET or POST data. Check the front end javascript to make sure all required values are being submitted. You may want to alert yourself whenever a 412 error is thrown because it likely indicates a bug in your code.
418 - Api Connection Failed to connect to the Honest Renter server for performing the API request.
419- Already Completed The response cannot be modified because the assessment for that administration has already been marked as completed.
422 - Unprocessable entity A value entered by someone on a form was invalid (e.g., invalid email address). You should display a message to the person so he or she can correct the error.
500, 502, 503, 504 - Sever Errors Something went wrong on Honest Renter's end. For most errors, our technical support team is automatically notified and we'll fix it promptly.

Expanding objects

Chained objects: Objects are often chained, meaning they are properties of other objects. For example, a person object contains an emails property which lists all the email objects associated with that person. Chained objects can be expanded using the expand request parameter. Expanding allows multiple chained objects to be retrieved in a single API call.

Available everywhere: The expand parameter is available on all API requests. All chained objects are expandable.

Always use JSON: Values of the "expand" paramater must always be in valid JSON format. To expand a person's email addresses, the expand parameter would be expand='["emails"]'. To expand both email addresses and phone numbers, the request would be expand='["emails","phones"]'. You can also do nested expansion requests. A request to expand both emails and phones properies of a pet's owner would be expand='{"owner":["emails","phones"]}'.

Dots: Dots are a convenient shorthand for nested expansions. A request to expand=["owner.emails"] of a pet will expand the owner property into a full person object, then expand the emails property for that person into a full list of email objects. You may find the dot notation more readable than writing out the equivalent JSON: expand={"owner":["emails"]}.

Simple expand request

$.ajax({ url: "https://api.honestrenter.com/pets/Ui5Y7CxBnY", method: "GET", data: 'expand=["size"]' });

Example response

{ "data": { "id": "Ui5Y7CxBnY", "ageCategory": 1, "breed": "Shih Tzu", "created": 1422523991, "otherType": null, "owner": "Ted", "size": { "id": 1, "label": "Small (e.g., Shih Tzu)" }, "type": 1 } }

Using dots to nest expansion requests

This request will expand the size and owner properties of the pet. Having "owner.emails" means that it will also expand the emails property of the owner.

$.ajax({ url: "https://api.honestrenter.com/pets/Ui5Y7CxBnY", method: "GET", data: 'expand=["size","owner.emails"]' });

Example response

{ "data": { "id": "Ui5Y7CxBnY", "ageCategory": 1, "breed": "Shih Tzu", "created": 1422523991, "otherType": null, "owner": { "id": "aYu8Bn92Br", "administrations": ["ui7Vbnc3d5"], "bestTimeToContact": null, "created": 1422523871, "currentlyEmployed": null, "driversLicense": null, "emergencyContacts": null, "explanationForBackgroundCheck": null, "explanationForEviction": null, "hasBeenEvicted": null, "hasExplanationForBackgroundCheck": null, "hasPet": true, "hasProblemsWithLandlord": null, "hasVehicle": null, "nationalIdentification": null, "problemsWithLandlord": null, "residences": [], "smokingStatus": 3, "pets": ["Ui5Y7CxBnY"], "vehicles": [], "accountsWithAccess": ["73iB3umxer"], "birthdate": 354960000, "dateToDeleteIdentifyingInfo": null, "ethnicities": [], "emails": [ { "id": "f1BvGhjL72", "address": "sample_email@notreal.com", "confirmed": true, "created": 1424945774, "flagged": null, "organization": null, "person": "aYu8Bn92Br" } ], "gender": 1, "name": { "title": null, "first": "Sample", "middle": null, "last": "Respondent, "previousLast": null, "suffix": null }, "notes": [], "notifications": [], "otherEthnicity": "", "phones": [], "positions": [], "preferredEmail": "f1BvGhjL72", "preferredPhone": null }, "size": { "id": 1, "label": "Small (e.g., Shih Tzu)" }, "type": 1 } }

Filters for fetching a list of objects

Fetching a list: For most objects, making a "GET" request on the collector returns a list of objects. For example, a "GET" request to https://api.honestrenter.com/pet-types returns a list of pet types.

Filters: Filters specify which objects to return in the list. Filters are listed in the query string.

Ampersands (&) Ampersands are equivalent to an SQL "AND" operator. When filtering pets, the filter "owner=Ted" searches for pets where the owner property has the value "Ted". The filter "type=1" restricts the lookup to pets where the type property has the value 1 (i.e., dogs). The filter "owner=Ted&type=1" will return dogs owned by Ted.

Commas Commas are equivalent to an SQL "OR" operator. The filter "type=1,2,3,4" will match pets where the type property has the value 1 or 2 or 3 or 4.

Combining ampersands with commas You can combine ampersands and commas for more complex queries. The filter "owner=Ted,Fred,Jim&type=1,2,3,4" will match any pets owned by Ted, Fred, or Jim having the type value of 1 or 2 or 3 or 4.

Limits Want to return only the first 10 results? Specify a limit of 10. When no limit is provided, the system uses the maximum limit for that object. For most objects, the maximum limit is 100. The limit used in the query is listed in the meta-data of the returned results.

Offset Offset allows you to start with the nth object in the returned list. For example, offset=20&limit=10 will display objects 20 through 30 in the list of matched results. When no offset is provided, defaults to 0. The offset used in a query is listed in the meta-data of the returned results.

Example request

$.ajax({ url: "https://api.honestrenter.com/pets", method: "GET", data: "owner=Ted&type=1" });

Example response

{ "data": [ { "id": "Ui5Y7CxBnY", "ageCategory": 1, "breed": "Shih Tzu", "created": 1422523991, "otherType": null, "owner": "Ted", "size": 1, "type": 1 } ], "limit": 100, "offset": 0 }

Example request

$.ajax({ url: "https://api.honestrenter.com/pets", method: "GET", data: "owner=Ted,Fred,Jim&type=1,2,3,4" });

Example response

{ "data": [ { "id": "Ui5Y7CxBnY", "ageCategory": 1, "breed": "Shih Tzu", "created": 1422523991, "otherType": null, "owner": "Ted", "size": 1, "type": 1 }, { "id": "ymBi73Y9Ut", "ageCategory": null, "breed": null, "created": 1422524728, "otherType": null, "owner": "Fred", "size": null, "type": 2 }, { "id": "zpKlB38Jhg", "ageCategory": null, "breed": null, "created": 1422529273, "otherType": null, "owner": "Jim", "size": null, "type": 3 } ], "limit": 3, "offset": 0 }

Specify fields to return

Wish you could make the API output just the fields you want? No problem! Just include a comma-sparated list of fields in the query string. Only those fields will be output in any objects (including chained objects) returned by the API.

Example request

$.ajax({ url: "https://api.honestrenter.com/pets/Ui5Y7CxBnY", method: "GET", data: "fields=id,breed" });

Example response

{ data : { "id": "Ui5Y7CxBnY", "breed": "Shih Tzu" } }

Versioning

All API requests must include "application/vnd.honestrenter.v1+json" in the Accept header. The "v1" at the end stands for "version 1." It tells our API how to prepare a response consistent with your system's expectations. The +json lets our system know to respond in json format. Currently we only support responses in JSON format, though we may add other options in future.

Minimizing changes: Changes are a pain in the neck for everyone. They're also a necessary evil. We'll make every effort to minimize the number of backwards incompatible changes to our API. When we have to release a backward-incompatible change, we will release a new version. The current version is version 1.

Changelog: All changes will be noted in our changelog, viewable on this API documentation. Currently there are no changes to report.

Example request

$.ajaxSetup({ beforeSend : function(xhr,settings) { xhr.setRequestHeader( 'Accept', 'Application/vnd.honestrenter.v1+json' ); } });

Changelog

No changes to report yet!