1. Overview

1.1. What’s New

Webhooks

Dotloop Webhooks is currently in the Initial Release phase. Webhooks allow client users to subscribe to specific events that occur in dotloop. When an event occurs, dotloop will send an HTTPS POST payload to the webhook’s configured URL. Detailed information can be found in the Webhooks (Initial Release) section. API Documentation can be found in Webhook Subscriptions and Webhook Events sections.

Loop-It Facade API

We now provide a simple 'Facade' API which allows client applications to create a loop and populate data into it via a single request, which includes inserting property information, adding loop participants into the contacts directory, and creating a loop from loop templates etc.

New Authentication Scheme

We introduce a new authentication scheme (OAuth2) which gets client applications access to user accounts upon user’s consent.

New Request / Response Schemas

All APIs and their corresponding request/response schemas have been refreshed/updated - we continue to support existing integrations using dotloop’s external API.

Read/Write Access

We introduce a new set of APIs in addition to GET APIs already available in external API, so clients can now for the first time create and update new resources (e.g. create/update loops or profiles via POST and PATCH).

1.2. Authentication

Dotloop’s Public API Version 2 makes use of the OAuth 2.0 protocol for authentication and authorization and initially support scenarios for web server applications only (3-legged-OAuth).

By using this protocol, we allow dotloop users to control data access by third-party applications and provide users the ability to revoke previously granted access at a later time.

Authentication and API Access

1.2.1. Client Registration

To register your application to integrate with this API, please request access at http://info.dotloop.com/developers. Upon registration, we will issue you a client id and client secret which are prerequisites in order to use the API.

1.2.2. Obtaining Access Token

In order to obtain an access token to access any resources on behalf of a dotloop user, client applications need to obtain an access token for each user provided the user gives his consent. Access tokens are short-living and expire usually after 12 hours, hence need to be refreshed once expired. For more information on the Oauth2 protocol, e.g. error codes please see RFC6749.

Step 1 - Obtain an Authorization Code

The first step towards acquisition of access and refresh tokens is to obtain an Authorization Code. The code will be issued after the user approved access to the client application to his account. To prompt the user to give his approval, redirect the user to (usually done in a popup window):

https://auth.dotloop.com/oauth/authorize?response_type=code&client_id=<client_id>&redirect_uri=<redirect_url>[&state=<state>&redirect_on_deny=(true|false)]
Parameters
Name Type Description

response_type

string

[required] only code is supported today

client_id

string

[required] client id (UUID issued when registring client application)

redirect_uri

string

[required] URL the user agent gets redirected to with authorization code after user consent

state

string

[optional] random string to protect against CSRF

redirect_on_deny

boolean

[optional] defines whether the action behind deny button redirects to redirect_uri or just closes the browser window; [true|false] (default: false)

Once the user approved the request of your client to access his account, we’ll issue a user agent redirect (302 with Location header) back to the URL provided (redirect_uri param) with the authorization code added in the query, ie. <redirect_url>?code=<code>. Please not that the state param is recommended and should be used to protect your site against CSRF.

Step 2 - Acquire Access/Refresh Token

With the code received in step 1, the client application can obtain an access token by making a request against the /token endpoint.

The /token endpoint requires an authentication header (HTTP Basic Authentication) sent by the client application.
Request
POST https://auth.dotloop.com/oauth/token?grant_type=authorization_code&code=<code>&redirect_uri=<redirect_url>&state=<state>
Authorization: Basic <encode_base64(ClientID:ClientSecret)>
  • Example

    • ClientId: 69bcf590-71b7-41a4-a039-a1d290edca11

    • ClientSecret: 3415e381-bdc4-49b7-bde2-69b3c5cd6447

    • Resulting Header:

      Authorization: Basic NjliY2Y1OTAtNzFiNy00MWE0LWEwMzktYTFkMjkwZWRjYTExOjM0MTVlMzgxLWJkYzQtNDliNy1iZGUyLTY5YjNjNWNkNjQ0Nw==
Response
Status: 200 OK
{
  "access_token": "0b043f2f-2abe-4c9d-844a-3eb008dcba67",
  "token_type": "Bearer",
  "refresh_token": "19bfda68-ca62-480c-9c62-2ba408458fc7",
  "expires_in": 43145,
  "scope": "profile:*, loop:*"
}

1.2.3. Refreshing Access Token after expiration

Access tokens have a short lifetime and need to be refreshed every 12 hours. The client application can either pro-actively refresh tokens before they expire and lazily refresh them upon receiving an authentication error (401 Unauthenticated ) when accessing the API.

POST https://auth.dotloop.com/oauth/token?grant_type=refresh_token&refresh_token=<refresh_token>
Header
Authorization: Basic <encode_base64(ClientID:ClientSecret)>
Response
Status: 200 OK
{
  "access_token": "86609772-aa95-4071-ad7f-25ad2d0be295",
  "token_type": "Bearer",
  "refresh_token": "19bfda68-ca62-480c-9c62-2ba408458fc7",
  "expires_in": 43199,
  "scope": "account:read, profile:*, loop:*, contact:*, template:read"
}
When refreshing access tokens, any previously issued access token becomes invalid. If you manage tokens in a clustered environment, make sure to share/use and refresh the token once across your cluster instances to avoid race conditions during the token update when triggered from multiple instances concurrently.

1.2.4. Access Revocation

A client or the actual user of the client may decide to disconnect from dotloop and revoke previously given permission to his dotloop account. To revoke access, the client application should call the following endpoint which will invalidate access and refresh token for future use.

POST https://auth.dotloop.com/oauth/token/revoke?token=<access_token>

1.3. Endpoint

All APIs listed below are available under a common base path:

All paths below are relative to this url, e.g. https://api-gateway.dotloop.com/public/v2/loop-it

1.4. Content Type

This API is a JSON API, so request and response payload are expected to be of content type application/json if not marked otherwise.

1.5. Authorization Header

Each API request requires a valid Access Token to be presented in an Authorization header, e.g.

Authorization: Bearer 0b043f2f-2abe-4c9d-844a-3eb008dcba67

2. Loop-It™

The Loop-It™ API makes it easy to create a new Loop and and populate various details into the loop, e.g. setup loop participant’s contact data into the contacts directory, pulls listing property data, authenticates the caller if an NRDS Id or MLS Agent Id is available to get access to form templates, etc.

Required scope: loop:write

POST /loop-it?profile_id=<profile_id>

2.1. Parameters

Name Type Description

profile_id

integer

[optional] Id of the individual profile the loop will be created in; required in case the account has more than one profile

transactionType

string

[required] Type of transaction (see addendum)

status

string

[required] Status of the loop (see addendum)

name

string

[required] Name of the loop, usually either property address line or lead name (max 200 chars)

streetName

string

[optional] Street name

streetNumber

string

[optional] Street number

unit

string

[optional] Unit number

city

string

[optional] City

state

string

[optional] State

zipCode

string

[optional] Zip code

county

string

[optional] County

country

string

[optional] Country

participants.fullName

string

[optional] Participant’s full name

participants.email

string

[optional] Participant’s email address

participants.role

string

[optional] Participant’s role

templateId

integer

[optional or required] Loop Template Id: (note: my be required by the user’s organization (parent profile)

mlsPropertyId

string

[optional] MLS Property Id

mlsId

string

[optional] MLS Id required to search listing

mlsAgentId

string

[optional] MLS Agent Id

nrdsId

string

[optional] NRDS Id

Please be aware of that access to loops is currently restricted to INDIVIDUAL profiles only

2.2. Example

POST /loop-it?profile_id=4711
{
  "name": "Brian Erwin",
  "transactionType": "PURCHASE_OFFER",
  "status": "PRE_OFFER",
  "streetName": "Waterview Dr",
  "streetNumber": "2100",
  "unit": "12",
  "city": "San Francisco",
  "zipCode": "94114",
  "state": "CA",
  "country": "US",
  "participants": [
    {
      "fullName": "Brian Erwin",
      "email": "brianerwin@newkyhome.com",
      "role": "BUYER"
    },
    {
      "fullName": "Allen Agent",
      "email": "allen.agent@gmail.com",
      "role": "LISTING_AGENT"
    },
    {
      "fullName": "Sean Seller",
      "email": "sean.seller@yahoo.com",
      "role": "SELLER"
    }
  ],
  "templateId": 1424,
  "mlsPropertyId": "43FSB8",
  "mlsId": "789",
  "mlsAgentId": "123456789"
}

2.3. Response

The response contains a property loopUrl, which can be used to redirect the user to the loop on dotloop.com.

Status: 201 Created
{
  "data": {
    "id": 34308,
    "profileId": 4711,
    "name": "Brian Erwin",
    "transactionType": "PURCHASE_OFFER",
    "status": "PRE_OFFER",
    "created": "2017-05-30T21:42:17Z",
    "updated": "2017-05-31T23:27:11Z",
    "loopUrl": "https://www.dotloop.com/m/loop?viewId=34308"
  }
}

2.4. Design Guidelines

In order to allow users to easily spot and interact with Dotloop’s Loop-It™ functionality in 3rd-party products, we advise to implement and visualize the feature as a Loop-It™ button.

As an example, the Loop-It™ Button could be rendered next to a property listing, which allows an agent to easily create a loop associated with the listed property. Upon response from the Loop-It™ API, which contains a perma-link to the created loop, the agent gets prompted by the 3rd party application, whether he wants to transition into dotloop to continue manage loop details or move the transaction forward.

_Loop-It™_ Button

Download Links: PNG | SVG

3. Account

3.1. Get Account Details

Retrieve account details

Required scope: account:read
GET /account

3.1.1. Parameters

None

3.1.2. Response

Status: 200 OK
{
  "data": {
    "id": 1,
    "firstName": "Brian",
    "lastName": "Erwin",
    "email": "brianerwin@newkyhome.com",
    "defaultProfileId": 42
  }
}

4. Profiles

4.1. List all Profiles

List all profiles associated with the user.

GET /profile
Required scope: profile:read

4.1.1. Parameters

None

4.1.2. Response

Status: 200 OK
{
  "meta": {
    "total": 3
  },
  "data": [
    {
      "id": 3,
      "name": "My Profile",
      "type": "INDIVIDUAL",
      "company": "MyCompany",
      "phone": "+0 (123) 456 7890",
      "fax": "+0 (123) 456 7890",
      "address": "1234 Wall St",
      "city": "New York",
      "state": "NY",
      "zipCode": "10005",
      "default": true,
      "requiresTemplate": true
    },
    ...
  ]
}

4.2. Get a Profile

Retrieve an individual profile by id.

Required scope: profile:read

GET /profile/:profile_id

4.2.1. Parameters

None

4.2.2. Response

Status: 200 OK
{
  "data": {
    "id": 3,
    "name": "My Profile",
    "type": "INDIVIDUAL",
    "company": "MyCompany",
    "phone": "+0 (123) 456 7890",
    "fax": "+0 (123) 456 7890",
    "address": "1234 Wall St",
    "city": "New York",
    "state": "NY",
    "zipCode": "10005",
    "requiresTemplate": true
  }
}

4.3. Create a Profile

Create a new profile.

Required scope: profile:write

POST /profile

4.3.1. Parameters

Name Type Description

name

string

profile name

company

string

company name

phone

string

phone number

address

string

address line

city

string

city

zipCode

string

zip code

state

string

state

country

string

country

4.3.2. Example

{
  "name": "My Profile",
  "company": "MyCompany",
  "phone": "+0 (123) 456 7890",
  "fax": "+0 (123) 456 7890",
  "address": "1234 Wall St",
  "city": "New York",
  "state": "NY",
  "zipCode": "10005"
}

4.3.3. Response

Status: 201 Created
{
  "data": {
    "id": 3,
    "type": "INDIVIDUAL",
    "name": "My Profile",
    "company": "MyCompany",
    "phone": "+0 (123) 456 7890",
    "fax": "+0 (123) 456 7890",
    "address": "1234 Wall St",
    "city": "New York",
    "state": "NY",
    "zipCode": "10005"
  }
}

4.4. Update a Profile

Update an existing profile by id.

  • This API allows partial updates

  • Required scope: profile:write

PATCH /profile/:profile_id

4.4.1. Parameters

Name Type Description

name

string

profile name

company

string

company name

phone

string

phone number

address

string

address line

city

string

city

zipCode

string

zip code

state

string

state

country

string

country

4.4.2. Example

{
  "name": "My Changed Profile Name",
  "company": "My New Company"
}

4.4.3. Response

Status: 200 OK
{
  "data": {
    "id": 3,
    "type": "INDIVIDUAL",
    "name": "My Changed Profile Name",
    "company": "My New Company",
    "phone": "+0 (123) 456 7890",
    "fax": "+0 (123) 456 7890",
    "address": "1234 Wall St",
    "city": "New York",
    "state": "NY",
    "zipCode": "10005"
  }
}

5. Loop Summaries

5.1. List all Loops

List all loops associated with a profile.

Required scope: loop:read

GET /profile/:profile_id/loop[?batch_size=<batch_size>&batch_number=<batch_number>&sort=<sort>&filter=<filter>&include_details=true]

5.1.1. Parameters

Name Type Description

batch_size

integer

[optional] size of batch returned (default=20, max=100)

batch_number

integer

[optional] batch/page number (default=1)

sort

string

[optional] string which contains the sort category and optionally the sort direction (default ascending); format: <category>[:asc|desc], e.g. address or address:asc produce the same results. Possible sort categories: default, address, created, updated, purchase_price, listing_date, expiration_date, closing_date, review_submission_date

filter

String

[optional] format: <filter_key>=<filtervalue>, filter keys: updated_min=<timestamp>, created_min=<timestamp>, transaction_type=<type>[|<type>|…​], transaction_status=<status>[|<status>|…​]

include_details

boolean

[optional] flag to include loop details with each record returned; [true|false] (default: false)

5.1.2. Response

Status: 200 OK
{
  "meta": {
    "total": 10
  },
  "data": [
    {
      "id": 34308,
      "name": "Atturo Garay, 3059 Main, Chicago, IL 60614",
      "status": "ARCHIVED",
      "transactionType": "PURCHASE_OFFER",
      "totalTaskCount": 5,
      "completedTaskCount": 3,
      "updated": "2017-05-30T21:42:17Z",
      "created": "2017-05-17T01:18:37Z",
      "loopUrl": "https://www.dootloop.com/m/loop?viewId=34308"
    },
    ...
  ]
}

5.2. Get a Loop

Retrieve an individual loop by id.

Required scope: loop:read

GET /profile/:profile_id/loop/:loop_id

5.2.1. Parameters

None

5.2.2. Response

Status: 200 OK
{
  "data": {
    "id": 34308,
    "name": "Atturo Garay, 3059 Main, Chicago, IL 60614",
    "status": "ARCHIVED",
    "transactionType": "PURCHASED",
    "totalTaskCount": 5,
    "completedTaskCount": 3,
    "updated": "2017-05-30T21:42:17Z",
    "created": "2017-05-17T01:18:37Z",
    "loopUrl": "https://www.dootloop.com/m/loop?viewId=34308"
  }
}
Status: 301 Moved Permanently

In some scenarios, two separate loops can be merged together, which can change the original loop ID. In those cases, attempting to access the original loop ID will produce a 301 response that points to the new loop. Any clients that persist loop IDs should account for this scenario and be able to update any references when a 301 is encountered.

See the support article for more information on the loop merging process: [Merge Loops](https://support.dotloop.com/s/article/Merge-Loops).

The 301 response will have a Location header present with a path to redirect to the new Loop View.

Headers

Location: /public/v2/profile/3/loop/30004

To get the Loop use the value from the Location header to do the next call.

When Autoredirect is enabled the second call will be done automatically and will get you a response as shown in 200 Response.

5.3. Create a Loop

Create a new loop.

Required scope: loop:write

POST /profile/:profile_id/loop

5.3.1. Parameters

Name Type Description

name

string

the name of the loop (max 200 chars)

status

string

status of the loop

transactionType

string

type of transaction

5.3.2. Example

{
  "name": "Atturo Garay, 3059 Main, Chicago, IL 60614",
  "status": "PRE_LISTING",
  "transactionType": "LISTING_FOR_SALE"
}

5.3.3. Response

Status: 201 Created
{
  "data": {
    "id": 34308,
    "profileId": 23483,
    "name": "Atturo Garay, 3059 Main, Chicago, IL 60614",
    "transactionType": "LISTING_FOR_SALE",
    "status": "PRE_LISTING",
    "totalTaskCount": 5,
    "completedTaskCount": 3,
    "created": "2017-05-17T01:18:37Z",
    "updated": "2017-05-17T01:18:37Z",
    "loopUrl": "https://www.dootloop.com/m/loop?viewId=34308"
  }
}

5.4. Update a Loop

Update an existing loop by id.

  • This API allows partial updates

  • Required scope: loop:write

PATCH /profile/:profile_id/loop/:loop_id

5.4.1. Parameters

Name Type Description

name

string

the name of the loop (max 200 chars)

status

string

status of the loop

transactionType

string

type of transaction

5.4.2. Example

{
  "status": "SOLD"
}

5.4.3. Response

Status: 200 OK
{
  "data": {
    "id": 34308,
    "name": "Atturo Garay, 3059 Main, Chicago, IL 60614",
    "transactionType": "LISTING_FOR_SALE",
    "status": "SOLD",
    "totalTaskCount": 5,
    "completedTaskCount": 3,
    "updated": "2017-05-30T21:42:17Z",
    "created": "2017-05-17T01:18:37Z",
    "loopUrl": "https://www.dootloop.com/m/loop?viewId=34308"
  }
}

6. Loop Details

6.1. Get Loop Details

Retrieve loop details by id.

Required scope: loop:read

GET /profile/:profile_id/loop/:loop_id/detail

6.1.1. Parameters

Details Section Field Type Description

'Property Address'

'Country'

string

'Property Address'

'Street Number'

string

'Property Address'

'Street Name'

string

'Property Address'

'Unit Number'

string

'Property Address'

'City'

string

'Property Address'

'State/Prov'

string

'Property Address'

'Zip/Postal Code'

string

'Property Address'

'County'

string

'Property Address'

'MLS Number'

string

'Property Address'

'Parcel/Tax ID'

string

'Financials'

'Purchase/Sale Price'

string

'Financials'

'Sale Commission Rate'

string

'Financials'

'Sale Commission Split % - Buy Side'

string

'Financials'

'Sale Commission Split % - Sell Side'

string

'Financials'

'Sale Commission Total'

string

'Financials'

'Earnest Money Amount'

string

'Financials'

'Earnest Money Held By'

string

'Financials'

'Sale Commission Split $ - Buy Side'

string

'Financials'

'Sale Commission Split $ - Sell Side'

string

'Contract Dates'

'Contract Agreement Date'

string

date string, e.g. 01/31/2017

'Contract Dates'

'Closing Date'

string

date string, e.g. 01/31/2017

'Offer Dates'

'Inspection Date'

string

date string, e.g. 01/31/2017

'Offer Dates'

'Offer Date'

string

date string, e.g. 01/31/2017

'Offer Dates'

'Offer Expiration Date'

string

date string, e.g. 01/31/2017

'Offer Dates'

'Occupancy Date'

string

date string, e.g. 01/31/2017

'Offer Dates'

'Offer Date'

string

date string, e.g. 01/31/2017

'Contract Info'

'Transaction Number'

string

'Contract Info'

'Class'

string

'Contract Info'

'Type'

string

'Referral'

'Referral %'

string

'Referral'

'Referral Source'

string

'Listing Information'

'Expiration Date'

string

date string, e.g. 01/31/2017

'Listing Information'

'Listing Date'

string

date string, e.g. 01/31/2017

'Listing Information'

'Original Price'

string

'Listing Information'

'Current Price'

string

'Listing Information'

'1st Mortgage Balance'

string

'Listing Information'

'2nd Mortgage Balance'

string

'Listing Information'

'Other Liens'

string

'Listing Information'

'Description of Other Liens'

string

'Listing Information'

'Homeowner's Association'

string

'Listing Information'

'Homeowner's Association Dues'

string

'Listing Information'

'Total Encumbrances'

string

'Listing Information'

'Property Includes'

string

'Listing Information'

'Property Excludes'

string

'Listing Information'

'Remarks'

string

'Geographic Description'

'MLS Area'

string

'Geographic Description'

'Legal Description'

string

'Geographic Description'

'Map Grid'

string

'Geographic Description'

'Subdivision'

string

'Geographic Description'

'Lot'

string

'Geographic Description'

'Deed Page'

string

'Geographic Description'

'Deed Book'

string

'Geographic Description'

'Section'

string

'Geographic Description'

'Addition'

string

'Geographic Description'

'Block'

string

'Property'

'Year Built'

string

'Property'

'Bedrooms'

string

'Property'

'Square Footage'

string

'Property'

'School District'

string

'Property'

'Type'

string

'Property'

'Bathrooms'

string

'Property'

'Lot Size'

string

6.1.2. Response

Status: 200 OK
{
  "data": {
    "Property Address": {
      "Country": "USA",
      "Street Number": "333",
      "Street Name": "Main St",
      "Unit Number": "123",
      "City": "San Francisco",
      "State/Prov": "CA",
      "Zip/Postal Code": "94105",
      "County": "USA",
      ...
    },
    "Financials": {
      "Sale Commission Rate": "3",
      "Sale Commission Split % - Buy Side": "50",
      "Sale Commission Split % - Sell Side": "50",
      "Sale Commission Total": "10000",
      "Sale Commission Split $ - Buy Side": "50",
      "Sale Commission Split $ - Sell Side": "20000",
      ...
    },
    ...
  }
}

6.2. Update Loop Details

Update loop details by id.

  • This API allows partial updates

  • Required scope: loop:write

PATCH /profile/:profile_id/loop/:loop_id/detail

6.2.1. Parameters

See Get Loop Details above.

6.2.2. Example

{
    "Financials": {
      "Purchase/Sale Price": "342342"
    }
}

6.2.3. Response

Status: 200 OK
{
  "data": {
    "Property Address": {
      "Country": "USA",
      "Street Number": "333",
      "Street Name": "Main St",
      "Unit Number": "123",
      "City": "San Francisco",
      "State/Prov": "CA",
      "Zip/Postal Code": "94105",
      "County": "USA",
      ...
    },
    "Financials": {
      "Purchase/Sale Price": "342342",
      "Sale Commission Rate": "3",
      "Sale Commission Split % - Buy Side": "50",
      "Sale Commission Split % - Sell Side": "50",
      "Sale Commission Total": "10000",
      "Sale Commission Split $ - Buy Side": "50",
      "Sale Commission Split $ - Sell Side": "20000",
      ...
    },
    ...
  }
}

7. Loop Folders

7.1. List all Folders

List all folders in a loop

Required scope: loop:read

GET /profile/:profile_id/loop/:loop_id/folder[?include_documents=<include_documents>]

7.1.1. Parameters

Name Type Description

include_documents

boolean

Include a list of all documents in all folders

7.1.2. Response

Status: 200 OK
{
  "meta": {
    "total": 4
  },
  "data": [
    {
      "id": 423424,
      "name": "Disclosures",
      "created": "2017-05-17T01:18:37Z",
      "updated": "2017-05-30T21:42:17Z"
    },
    ...
  ]
}

7.2. Get a Folder

Retrieve an individual folder by id.

Required scope: loop:read

GET /profile/:profile_id/loop/:loop_id/folder/:folder_id[?include_documents=<include_documents>]

7.2.1. Parameters

Name Type Description

include_documents

boolean

include a list of all documents in the folder

7.2.2. Response

Status: 200 OK
{
  "data":{
    "id": 423424,
    "name": "Disclosures",
    "created": "2017-05-17T01:18:37Z",
    "updated": "2017-05-30T21:42:17Z"
  }
}

7.3. Create a Folder

Create a new folder.

Required scope: loop:write

POST /profile/:profile_id/loop/:loop_id/folder/

7.3.1. Parameters

Name Type Description

name

string

the name of the folder (max ??? chars)

7.3.2. Example

{
  "name": "Disclosures"
}

7.3.3. Response

Status: 201 Created
{
"data":{
    "id": 423424,
    "name": "Disclosures",
    "created": "2017-05-17T01:18:37Z",
    "updated": "2017-05-30T21:42:17Z"
  }
}

7.4. Update a Folder

Update an existing folder by id.

  • This API allows partial updates

  • Required scope: loop:write

PATCH /profile/:profile_id/loop/:loop_id/folder/:folder_id

7.4.1. Parameters

Name Type Description

name

string

the name of the folder (max ??? chars)

7.4.2. Example

{
  "name": "Disclosures (renamed)"
}

7.4.3. Response

Status: 200 OK
{
  "data":{
    "id": 423424,
    "name": "Disclosures (renamed)"
    "created": "2017-05-17T01:18:37Z",
    "updated": "2017-05-30T21:42:17Z"
  }
}

8. Loop Documents

8.1. List all Documents

List all documents in a loop

Required scope: loop:read

GET /profile/:profile_id/loop/:loop_id/folder/:folder_id/document

8.1.1. Parameters

None

8.1.2. Response

Status: 200 OK
{
  "meta": {
    "total": 3
  },
  "data": [
    {
      "id": 561621,
      "filename": "disclosures.pdf",
      "created": "2017-05-17T01:18:37Z",
      "updated": "2017-05-17T01:18:37Z"
    }, ...
  ]
}

8.2. Get a Document

Retrieve an individual document by document_id

Required scope: loop:read

GET /profile/:profile_id/loop/:loop_id/folder/:folder_id/document/:document_id
Accept: application/json

8.2.1. Parameters

None

8.2.2. Response

Status: 200 OK
{
  "data": {
    "id": 561621,
    "name": "disclosures.pdf",
    "created": "2017-05-17T01:18:37Z",
    "updated": "2017-05-17T01:18:37Z"
  }
}

8.3. Upload a Document

Upload a individual document (binary) via multipart form post

Required scope: loop:write

POST /profile/:profile_id/loop/:loop_id/folder/:folder_id/document/
content-type: multipart/form-data; boundary=<BOUNDARY>
content-length: XXX


--<BOUNDARY>
Content-Disposition: form-data; name="file"; fileName="disclosures.pdf"
Content-Type: application/pdf

<binary data>
--<BOUNDARY>--

8.3.1. Parameters

Name Type Description

fileName

string

fileName of the pdf

8.3.2. Example using curl:

$ curl -F "file=@\"/Users/you/Documents/disclosures.pdf\";fileName=\"my_disclosures.pdf\";type=application/pdf" -H "Authorization: Bearer <token>" https://api-gateway.dotloop.com/public/v2/profile/:profile_id/loop/:loop_id/folder/:folder_id/document/

8.3.3. Response

Status: 201 OK
{
  "data": {
    "id": 561621,
    "name": "my_disclosures.pdf",
    "created": "2017-05-17T01:18:37Z",
    "updated": "2017-05-17T01:18:37Z"
  }
}

9. Loop Participants

9.1. List all Loop Participants

List all loop participants in a loop

Required scope: loop:read

GET /profile/:profile_id/loop/:loop_id/participant

9.1.1. Parameters

None

9.1.2. Response

Status: 200 OK
{
  "meta": {
    "total": 3
  },
  "data": [
    {
      "id": 2355,
      "fullName": "Brian Erwin",
      "email": "brianerwin@newkyhome.com",
      "role": "BUYER",
      "Phone": "(555) 555-5555"
    },
    {
      "id": 57567,
      "fullName": "Allen Agent",
      "email": "allen.agent@gmail.com",
      "role": "LISTING_AGENT",
      "Phone": "(555) 555-1234",
      "Company Name":  "Allen Realty"
    },
    {
      "id": 24743,
      "fullName": "Sean Seller",
      "email": "sean.seller@yahoo.com",
      "role": "SELLER",
      "Street Name": "123",
      "Street Number": "Main St.",
      "City": "Cincinnati",
      "Zip/Postal Code": "45123",
      "Country":  "USA",
      "Cell Phone": "(555) 555-4444"
    }
  ]
}

9.2. Get a Loop Participant

Retrieve loop participants details of an individual loop participant.

Required scope: loop:read

GET /profile/:profile_id/loop/:loop_id/participant/:participant_id

9.2.1. Parameters

None

9.2.2. Response

Status: 200 OK
{
  "data": {
    "id": 2355,
    "fullName": "Brian Erwin",
    "email": "brianerwin@newkyhome.com",
    "role": "BUYER",
    "Phone": "(555) 555-5555"
  }
}

9.3. Add a Loop Participant

Add a new loop participant

  • Required scope: loop:write

POST /profile/:profile_id/loop/:loop_id/participant

9.3.1. Parameters

Name Type Description

fullName

string

First and last name of the participant

email

string

participant email

role

string

participant role

'Street Name'

string

[optional] street number of participant’s address

'Street Number'

string

[optional] street name of participant’s address

'City'

string

[optional] city of participant’s address

'State/Prov'

string

[optional] state/providence of participant’s address

'Zip/Postal Code'

string

[optional] postal code of participant’s address

'Unit Number'

string

[optional] unit # of participant’s address

'Country'

string

[optional] country of participant’s address

'Phone'

string

[optional] participant phone number

'Cell Phone'

string

[optional] participant mobile number

'Company Name'

string

[optional] participant company

Additional role-specific fields can also be provided. See Built-in Contact/Loop Participant Roles for details.

9.3.2. Example

{
  "fullName": "Brian Erwin",
  "email": "brian@gmail.com",
  "role": "BUYER",
  "Street Name": "123",
  "Street Number": "Main St.",
  "City": "Cincinnati",
  "Zip/Postal Code": "45123",
  "Country":  "USA",
  "Phone": "(555) 555-5555",
  "Cell Phone": "(555) 555-4444",
  "Company Name":  "Buyer's Company"
}

9.3.3. Response

Status: 201 Created
{
  "data": {
    "id": 2355,
    "fullName": "Brian Erwin",
    "email": "brianerwin@newkyhome.com",
    "role": "BUYER",
    "Street Name": "123",
    "Street Number": "Main St.",
    "City": "Cincinnati",
     "Zip/Postal Code": "45123",
     "Country":  "USA",
     "Phone": "(555) 555-5555",
     "Cell Phone": "(555) 555-4444",
     "Company Name":  "Buyer's Company"
  }
}

9.4. Update a Loop Participant

Update an existing participant

  • This API allows partial updates

  • Required scope: loop:write

PATCH /profile/:profile_id/loop/:loop_id/participant/:participant_id

9.4.1. Parameters

Name Type Description

fullName

string

First and last name of the participant

email

string

participant email

role

string

participant role

'Street Name'

string

street number of participant’s address

'Street Number'

string

street name of participant’s address

'City'

string

city of participant’s address

'State/Prov'

string

state/providence of participant’s address

'Zip/Postal Code'

string

postal code of participant’s address

'Unit Number'

string

unit # of participant’s address

'Country'

string

country of participant’s address

'Phone'

string

participant phone number

'Cell Phone'

string

participant mobile number

'Company Name'

string

participant company

9.4.2. Example

{
  "email": "brian@gmail.com"
}

9.4.3. Response

Status: 200 OK
{
  "data": {
    "id": 2355,
    "fullName": "Brian Erwin",
    "email": "brian@gmail.com",
    "role": "BUYER",
    "Phone": "(555) 555-5555"
  }
}

9.5. Delete a Loop Participant

Delete an existing participant by id.

Required scope: loop:write

DELETE /profile/:profile_id/loop/:loop_id/participant/:participant_id

9.5.1. Parameters

none

9.5.2. Response

Status: 204 No Content

10. Loop Tasks

10.1. List all Loop Task Lists

List all task lists in a loop

Required scope: loop:read

GET /profile/:profile_id/loop/:loop_id/tasklist/

10.1.1. Parameters

None

10.1.2. Response

Status: 200 OK
{
  "meta": {
    "total": 3
  },
  "data": [
    {
      "id": 1234,
      "name": "My Tasks"
    }, ..
  ]
}

10.2. Get a Loop Task List

Retrieve an individual task list.

Required scope: loop:read

GET /profile/:profile_id/loop/:loop_id/tasklist/:task_list_id

10.2.1. Parameters

None

10.2.2. Response

Status: 200 OK
{
  "data": {
    "id": 1234,
    "name": "My Tasks"
  }
}

10.3. List all Loop Task List Items

List all task items in a task list

Required scope: loop:read

GET /profile/:profile_id/loop/:loop_id/tasklist/:task_list_id/task

10.3.1. Parameters

None

10.3.2. Response

Status: 200 OK
{
  "meta": {
    "total": 4
  },
  "data": [
    {
      "id": 125736485,
      "name": "contract",
      "due": "2016-10-21T00:00:00-04:00",
      "completed": true
    },
    ...
  ]
}

10.4. Get a Loop Task List item

Retrieve an individual task list item.

Required scope: loop:read

GET /profile/:profile_id/loop/:loop_id/tasklist/:task_list_id/task/:task_list_item_id

10.4.1. Parameters

None

10.4.2. Response

Status: 200 OK
{
  "data": {
    "id": 125736485,
    "name": "contract",
    "due": "2016-10-21T00:00:00-04:00"
  }
}

11. Loop Activities

11.1. List all Loop Activities

List all activities for a loop

Required scope: loop:read

GET /profile/:profile_id/loop/:loop_id/activity[?batch_size=<batch_size>&batch_number=<batch_number>]

11.1.1. Parameters

Name Type Description

batch_size

integer

size of batch returned (default=20, max=100)

batch_number

integer

batch/page number (default=1)

11.1.2. Response

Status: 200 OK
{
  "meta": {
    "total": -1
  },
  "data": [
    {
      "message": "User One viewed document Agency Disclosure Statement - Seller",
      "date": "2017-01-09T13:10:14Z"
    },
    ...
  ]
}
The meta/total count is currently returned as -1, ie in order to know how many activities are present the caller needs to paginate the the entire result.

12. Contacts

12.1. List all Contacts

List all contacts in the user account.

Required scope: contact:read

GET /contact[?batch_size=<batch_size>&batch_number=<batch_number>&filter=<filter>]

12.1.1. Parameters

Name Type Description

batch_size

integer

size of batch returned (default=20, max=100)

batch_number

integer

batch/page number (default=1)

filter

String

format: <filter_key>=<filtervalue>, filter keys: updated_min=<timestamp>

12.1.2. Response

Status: 200 OK
{
  "meta": {
    "total": 10
  },
  "data": [
    {
      "id": 3603862,
      "firstName": "Brian",
      "lastName": "Erwin",
      "email": "brianerwin@newkyhome.com",
      "home": "(415) 8936 332",
      "office": "(415) 1213 656",
      "fax": "(415) 8655 686",
      "address": "2100 Waterview Dr",
      "city": "San Francisco",
      "zipCode": "94114",
      "state": "CA",
      "country": "US",
      "updated": "2017-04-20T03:48:30Z"
    },
    ...
  ]
}

12.2. Get a Contact

Retrieve an individual contact by id.

Required scope: contact:read

GET /contact/:contact_id

12.2.1. Parameters

None

12.2.2. Response

Status: 200 OK
{
  "data": {
    "id": 3603862,
    "firstName": "Brian",
    "lastName": "Erwin",
    "email": "brianerwin@newkyhome.com",
    "home": "(415) 8936 332",
    "office": "(415) 1213 656",
    "fax": "(415) 8655 686",
    "address": "2100 Waterview Dr",
    "city": "San Francisco",
    "zipCode": "94114",
    "state": "CA",
    "country": "US",
    "updated": "2017-04-20T03:48:30Z"
  }
}

12.3. Create a Contact

Create a new contact.

Required scope: contact:write

POST /contact

12.3.1. Parameters

Name Type Description

firstName

string

first name

lastName

string

last name

email

string

email address

home

string

home phone number

office

string

office phone number

fax

string

fax number

address

string

address line

city

string

city

zipCode

string

zip code

state

string

state

country

string

country

12.3.2. Example

{
  "firstName": "Brian",
  "lastName": "Erwin",
  "email": "brianerwin@newkyhome.com",
  "home": "(415) 8936 332",
  "office": "(415) 1213 656",
  "fax": "(415) 8655 686",
  "address": "2100 Waterview Dr",
  "city": "San Francisco",
  "zipCode": "94114",
  "state": "CA",
  "country": "US"
}

12.3.3. Response

Status: 201 Created
{
  "data": {
    "id": 3603862,
    "firstName": "Brian",
    "lastName": "Erwin",
    "email": "brianerwin@newkyhome.com",
    "home": "(415) 8936 332",
    "office": "(415) 1213 656",
    "fax": "(415) 8655 686",
    "address": "2100 Waterview Dr",
    "city": "San Francisco",
    "zipCode": "94114",
    "state": "CA",
    "country": "US",
    "updated": "2017-04-20T03:48:30Z"
  }
}

12.4. Update a Contact

Update an existing contact by id.

  • This API allows partial updates

  • Required scope: contact:write

PATCH /contact/:contact_id

12.4.1. Parameters

Name Type Description

firstName

string

first name

lastName

string

last name

email

string

email address

home

string

home phone number

office

string

office phone number

fax

string

fax number

address

string

address

city

string

city

zipCode

string

zip code

state

string

state

country

string

country

12.4.2. Example

{
  "home": "(415) 888 8888"
}

12.4.3. Response

Status: 200 OK
{
  "data": {
    "id": 3603862,
    "firstName": "Brian",
    "lastName": "Erwin",
    "email": "brianerwin@newkyhome.com",
    "home": "(415) 888 8888",
    "office": "(415) 1213 656",
    "fax": "(415) 8655 686",
    "address": "2100 Waterview Dr",
    "city": "San Francisco",
    "zipCode": "94114",
    "state": "CA",
    "country": "US",
    "updated": "2017-04-20T03:48:30Z"
  }
}

12.5. Delete a Contact

Delete an existing contact by id.

Required scope: contact:write

DELETE /contact/:contact_id

12.5.1. Parameters

none

12.5.2. Response

Status: 204 No Content

13. Loop Templates

13.1. List all Loop Templates

List all loop templates in the profile

Required scope: template:read or loop:write

GET /profile/:profile_id/loop-template

13.1.1. Parameters

None

13.1.2. Response

Status: 200 OK
{
  "meta": {
    "total": 5
  },
  data: [
    {
      "id": 423,
      "profileId": 732453,
      "name": "My Loop Template",
      "transactionType": "PURCHASE_OFFER",
      "shared": true,
      "global": false
    },
    ...
  ]
}

13.2. Retrieve an Individual Loop Template

Retrieve an individual loop template by id.

Required scope: template:read or loop:write

GET /profile/:profile_id/loop-template/:loop_template_id

13.2.1. Parameters

None

13.2.2. Response

Status: 200 OK
{
  "data": {
    "id": 423,
    "profileId": 732453,
    "name": "My Loop Template",
    "transactionType": "PURCHASE_OFFER",
    "shared": true,
    "global": false
  }
}

14. Webhooks (Initial Release)

14.1. Webhooks Overview

Webhooks is currently in the initial release phase and available to API clients by request. Please contact support to request access.

Webhooks is a Dotloop Public API feature. The Webhooks feature is managed by API client applications registered with the Dotloop Public API via /subscription and /subscription/:subscription_id/event endpoints. Integrating applications are able to configure webhooks individually per authorized dotloop user and their profiles, using the appropriate access tokens.

A dotloop user may have webhooks enabled for as many integrating applications as they have connected to their account. These webhook configurations have no interference or interaction with webhook configurations created by other integrating applications.

Below are diagrams outlining an example using a USER CONTACT_CREATED subscription and event:

  1. Creating a subscription

  2. Receiving a webhook event because of the subscription

  3. How client applications may want to fetch Public API data based on the event.

Step 1 - Create a subscription to "User 100’s" contacts:

Create a subscription

Step 2 - "User 100" creates a contact while using Dotloop:

Receive webhook event

Step 3 - The integrating application will likely want to fetch the created/updated resource to sync or diff the record:
(see Webhook Events & Related API Resources)

Fetch based on event

14.2. Subscriptions Overview

The following table defines the event types that can be subscribed to.
(See also Webhooks Event Targets and Corresponding Event Types under Types/Constants)

14.2.1. Event Types By Target Type Table

Target Type Event Types

PROFILE

LOOP_CREATED

LOOP_UPDATED

LOOP_MERGED

LOOP_PARTICIPANT_CREATED

LOOP_PARTICIPANT_UPDATED

LOOP_PARTICIPANT_DELETED

USER

CONTACT_CREATED

CONTACT_UPDATED

CONTACT_DELETED

PROFILE_UPDATED

USER_PROFILE_ACTIVATED

USER_PROFILE_DEACTIVATED

USER_ADDED_TO_PROFILE

USER_REMOVED_FROM_PROFILE

Create Subscription Request
POST /public/v2/subscription HTTP/1.1
Content-Type: application/json
Authorization: {{BEARER_TOKEN}}
...
{
  "targetType": "PROFILE",
  "targetId": 789,
  "eventTypes": ["LOOP_CREATED", "LOOP_UPDATED"],
  "url": "https://foobar.com/callbacks/dotloopwebhook",
  "externalId": "your_external_id"
  "signingKey": "super_secret_key"
}
ExternalId

The externalId property is a configuration on the Subscription resource. This is intended to be a foreign key in the integrating system, and is included in the body of each webhook event as subscriptionExternalId. See Receiving Webhook Events below.

Subscription Authorization

To receive profile events, the user-owner of the subscription must have access to the profile. If access to a profile is lost, the subscription will be disabled. This subscription will not be automatically re-enabled by dotloop if access is re-granted to the profile - client applications will need to re-enable via the Public API. Consider the following example:

Jane Doe has "Manage Loops" access to an "office profile". Jane has previously connected your Client Application to her Dotloop account.

This office profile has many loops in it, created by various agents ("child profiles") working in loops that can be viewed by this profile. Your Client Application creates a subscription to this profile, for "Loop Created & Loop Updated" events.

Your Client Application server receives these events for some time, until Jane leaves the company, at which point an administrator on the office profile removes Jane. Jane’s subscription is automatically disabled, and no more events will be received.

Subscription Events

In the event a subscription is disabled or deleted, a "good-bye" event will be delivered to the webhook url configured for the subscription. Subscription events follow the same format as application events, but will contain the subscription configuration in the "event" field (see Event Request for details).

Subscription Event Type Description

SUBSCRIPTION_REMOVED

Event generated when a subscription is permanently deleted, typically by user removing api access ( see Delete a Subscription)

SUBSCRIPTION_DISABLED

Event generated when a subscription is suspended via enabled = false (see Update a Subscription)

14.3. Receiving Webhook Events

14.3.1. Event Request

POST https://fooBar.com/callbacks/dotloopWebhook
Content-Type: application/json
X-DOTLOOP-SIGNATURE: {{COMPUTED_SIGNATURE_HASH}}
X-DOTLOOP-TIMESTAMP: 1691763097001
...
{
  "eventId": "3bc982f6-7029-40ae-81aa-62f93a5ea1a8",
  "createdOn": "2022-11-01T00:00:00Z",
  "subscriptionId": "7dbf7306-6015-48aa-8e9b-205363514d32",
  "subscriptionExternalId": "FOO_BAR_DB_ID",
  "profileId": "5678",
  "eventType": "LOOP_CREATED",
  "event": {
    "id": "154684513548"
  }
}

Please note that the event field contains a polymorphic object, consisting of ids. There will always be an id field, this is the id of the eventType target.

Event Type Event

LOOP_CREATED,
LOOP_UPDATED

{
  ...
  "event": {
    "id": "123" /* loop id */
  }
}

CONTACT_CREATED,
CONTACT_UPDATED,
CONTACT_DELETED

{
  ...
  "event": {
    "id": "456" /* contact id */
  }
}

PROFILE_UPDATED,
USER_PROFILE_ACTIVATED,
USER_PROFILE_DEACTIVATED,
USER_ADDED_TO_PROFILE,
USER_REMOVED_FROM_PROFILE

{
  ...
  "event": {
    "id": "444" /* profile id */
  }
}

LOOP_MERGED

{
  ...
  "event": {
    "id": "777",     /* original loop id */
    "fromId": "777", /* original loop id */
    "toId": "888",   /* new loop id      */
  }
}

LOOP_PARTICIPANT_CREATED,
LOOP_PARTICIPANT_UPDATED,
LOOP_PARTICIPANT_DELETED

{
  ...
  "event": {
    "id": "777",           /* loop id */
    "participantId": "999" /* participant id */
  }
}

SUBSCRIPTION_REMOVED,
SUBSCRIPTION_DISABLED

{
  ...,
  "event": {
    "targetType": "PROFILE",
    "targetId": 789,
    "eventTypes": ["LOOP_CREATED", "LOOP_UPDATED"],
    "url": "https://foobar.com/callbacks/dotloopwebhook",
    "externalId": "your_external_id",
    "signingKey": "super_secret_key",
    "tenantId": "tenant_id",
    "enabled": "false",
    "revision": 1
  }
}

14.3.2. At-Least-Once Delivery

Dotloop will attempt to deliver each webhook event at least once. There may be times where you receive an event twice or multiple times, your application should be able to handle this.

14.3.3. Multiple Event Types

In some cases, a single action can trigger multiple events. For example, adding a loop participant to a loop can trigger both LOOP_UPDATED and LOOP_PARTICIPANT_CREATED events. Clients can expect to receive both events if they are subscribed to both. This is not the only example of this, so it is good practice to fetch the proper API Resource for each event received, see Webhook Events & Related API Resources.

14.3.4. Failure Handling

Dotloop will attempt to POST events immediately to the url defined in a subscription. If the response from the client server is anything but a 2xx success code, we will schedule the event for another delivery. These retries follow a back-off policy of 30s, 1min, 15min, 30min, 1hr, 2hr, 4hr, and 8hr, after which the event will be marked as failed to deliver. Failed events can still be viewed via api endpoint but dotloop will no longer attempt to resend them. There is currently no mechanism to redrive events.

Please note that if a subscription results in too many failed events in succession we may disable the subscription.

14.3.5. Webhook Request Timeout

Dotloop will wait for a response from the client server for 5 seconds. If no response is received, the https connection will be terminated and the request will be marked as failed, the event will be scheduled for another delivery if applicable.

A sample event with a timeout failure:

{
  data: {
    id: '194d74f5-6bdf-415e-b0dc-bfae1aa75ae',
    subscriptionId: 'bc08a81e-e42c-4dfc-bc23-a560234f3b8e',
    createdOn: '2023-10-30T17:41:55.975Z',
    eventType: 'LOOP_CREATED',
    eventData: { id: '70558693' },
    deliveryStatus: 'SCHEDULED',
    deliveryAttempts: 1,
    responseData: [
      {
      responseCode: 0,
      responseBody: 'Timeout duration of 5000ms has been reached.',
      url: 'https://foobar.com/callbacks/dotloopwebhook',
      requestSentAt: '2023-11-27T21:28:01.512Z',
      durationMs: 5260
      }
    ]
  }
}

14.3.6. Verifying Webhook Events

Since webhook endpoints are accepting post requests from the internet, we implement two ways to verify the authenticity and integrity of events posted to a client’s endpoint.

Subscription Signing Key

When a client creates a subscription, a signingKey can be provided (see the above request body for creating a subscription). All events posted from this subscription will have a header X-DOTLOOP-SIGNATURE: {{COMPUTED_SIGNATURE_HASH}}. Client’s can use this hash to verify the body and timestamp of the event has not been tampered with or impersonated. More on this below.

Subscriptions without a signingKey will not have this header.

Timestamp

Events always have a timestamp header, X-DOTLOOP-TIMESTAMP: 1691763097001. This header’s value is a timestamp in seconds since epoch. This is to prevent replay attacks, where an event can be "replayed" by someone multiple times, even with a "valid" signature header. Clients should use this timestamp to verify an event falls within their acceptable time range.

Computing The Signature

To compute the signature, you will need to compose a string where the timestamp and raw json body are concatenated, separated by a dot (.). As an example:

const signed_content = `${X-DOTLOOP-TIMESTAMP}.${body}`

Dotloop uses an HMAC with SHA-1 to sign its webhooks. Below is an example implementation:

import crypto from 'crypto';

const signed_content = `${X-DOTLOOP-TIMESTAMP}.${body}`;
const secret_bytes = Buffer.from("super_secrety_key", "utf8");
const hmac = crypto.createHmac("sha1", secret_bytes);

hmac.update(signed_content);

const hash = hmac.digest("hex");
console.log(hash);

14.3.7. Querying For Events

All events can be queried via the Dotloop Public Api. Refer to the Webhook Events API documentation below. Notice that the shape for received events and queried events from the API are quite different. Events returned via the API contain additional information about the most recent success, response, or failure of the event’s delivery attempt.

14.3.8. Events TTL

Dotloop will store events for 90 days, after which they can no longer be retrieved.

This is a list of all webhook events and the related API resources that can be fetched to get more information about the event.

Event Types Resource(s)

LOOP_CREATED

Get a Loop, Get Loop Details

LOOP_UPDATED

Get a Loop, Get Loop Details

LOOP_MERGED

Get a Loop, Get Loop Details

LOOP_PARTICIPANT_CREATED

Get a Loop Participant

LOOP_PARTICIPANT_UPDATED

Get a Loop Participant

LOOP_PARTICIPANT_DELETED

Get Loop Details

CONTACT_CREATED

Get a Contact

CONTACT_UPDATED

Get a Contact

CONTACT_DELETED

none

PROFILE_UPDATED

Update a Profile

USER_PROFILE_ACTIVATED

none

USER_PROFILE_DEACTIVATED

none

USER_ADDED_TO_PROFILE

Create a Profile

USER_REMOVED_FROM_PROFILE

none

SUBSCRIPTION_REMOVED

none

SUBSCRIPTION_DISABLED

none

15. Webhook Subscriptions

15.1. List all Subscriptions

List all subscriptions associated with the current user token.

SCOPE: Subscriptions are scoped to the API Client itself. Only the client that created the subscription can list the subscription.

GET /subscription[?enabled=<enabled>&next_cursor=<next_cursor>]

15.1.1. Parameters

Name Type Description

enabled

boolean

[optional] flag to return only enabled subscriptions (default: false)

next_cursor

string

[optional] fetch the next batch/page from this cursor

15.1.2. Response

Status: 200 OK
{
  "data": [{
    "id": "4370bfdb-af96-430c-9871-90badf4d5608",
    "targetType": "PROFILE",
    "targetId": 789,
    "externalId": "some_external_id",
    "url": "https://foobar.com/callbacks/dotloopwebhook",
    "eventTypes": ["LOOP_CREATED", "LOOP_UPDATED"],
    "signingKey": "super_secret_key",
    "enabled": true
  },{
    "id": "d5871d89-c901-4a8f-9b7b-f9f183624edc",
    "targetType": "USER",
    "targetId": 100,
    "externalId": "user_id_in_db",
    "url": "https://foobar.com/callbacks/dotloopwebhook",
    "eventTypes": ["CONTACT_CREATED", "CONTACT_UPDATED"],
    "signingKey": "super_secret_key",
    "enabled": true
  }],
  "meta": {
    "nextCursor: "base64_string"
  }
}

15.2. Get a Subscription

Retrieve an individual subscription by id.

Scope: Subscriptions are scoped to the API Client itself. Only the client that created the subscription can get the subscription.

GET /subscription/:subscription_id

15.2.1. Parameters

None

15.2.2. Response

Status: 200 OK
{
  "data": {
    "id": "4370bfdb-af96-430c-9871-90badf4d5608",
    "targetType": "PROFILE",
    "targetId": 789,
    "externalId": "your_external_id",
    "url": "https://foobar.com/callbacks/dotloopwebhook",
    "eventTypes": ["LOOP_CREATED", "LOOP_UPDATED"],
    "signingKey": "super_secret_key",
    "enabled": true
  }
}

15.3. Create a Subscription

Create a new subscription.

  • Required scope: contact:read or loop:read depending on the subscription targetType

  • Your subscription endpoint must close the connection within 5 seconds, please see Webhook Request Timeout for more information.

  • Please review Receiving Webhook Events for thorough documentation on how to handle and verify webhook events.

POST /subscription

15.3.1. Parameters

Name Type Description

targetType

string

Type of target (USER or PROFILE)

targetId

number

A dotloop user id or profile id depending on the targetType.

eventTypes

string list

List of event types to deliver. Refer to Event Types By Target Type Table.

url

string

The url you wish to have webhook events POST to.
Must be HTTPS.
Max 512 characters.

signingKey

string

[optional] Secret key to be used for generating event signature header.
Max 128 characters.

externalId

string

[optional] An identifier, useful to you, that will be included in the body of every webhook event.
Max 128 characters.

15.3.2. Example

{
  "targetType": "USER",
  "targetId": 7083432,
  "eventTypes": ["CONTACT_CREATED", "CONTACT_UPDATED"],
  "url": "https://foobar.com/callbacks/dotloopwebhook",
  "signingKey": "super_secret_key",
  "externalId": "user_id_in_db"
}

15.3.3. Response

Status: 200 OK
{
  "data": {
    "id": "ab0e1759-9419-4c51-ae03-aeb296f815ef",
    "targetType": "USER",
    "targetId": 7083432,
    "eventTypes": ["CONTACT_CREATED", "CONTACT_UPDATED"],
    "url": "https://foobar.com/callbacks/dotloopwebhook",
    "externalId": "user_id_in_db",
    "signingKey": "super_secret_key",
    "enabled": true
  }
}

15.4. Update a Subscription

Update an existing subscription by id.

  • Scope: contact:read or loop:read depending on the subscription targetType

  • Setting the enabled parameter to false will suspend the subscription and generate a SUBSCRIPTION_DISABLED event

PATCH /subscription/:subscription_id

15.4.1. Parameters

Name Type Description

eventTypes

string list

List of event types to deliver. Refer to Event Types By Target Type Table.

url

string

The url you wish to have webhook events POST to.
Must be HTTPS.
Max 512 characters.

signingKey

string

Secret key to be used for generating event signature header.
Max 128 characters.

externalId

string

An identifier, useful to you, that will be included in the body of every webhook event.
Max 128 characters.

enabled

boolean

Enable or disable the subscription.

15.4.2. Example

{
  "externalId": "user-guuid-abcd-123efg"
}

15.4.3. Response

Status: 200 OK
{
  "data": {
    "id": "ab0e1759-9419-4c51-ae03-aeb296f815ef",
    "targetType": "USER",
    "targetId": 7083432,
    "eventTypes": ["CONTACT_CREATED", "CONTACT_UPDATED"],
    "url": "https://foobar.com/callbacks/dotloopwebhook",
    "externalId": "user-guuid-abcd-123efg",
    "signingKey": "super_secret_key",
    "enabled": true
  }
}

15.5. Delete a Subscription

Delete an existing subscription by id.

  • Scope: Subscriptions are scoped to the API Client itself. Only the client that created the subscription can delete the subscription.

  • When a subscription is deleted, a SUBSCRIPTION_REMOVED event will be sent to the client webhook url.

DELETE /subscription/:subscription_id

15.5.1. Parameters

none

15.5.2. Response

Status: 204 No Content

16. Webhook Events

16.1. List all Events

List all events associated with the subscription.

  • SCOPE: Subscriptions are scoped to the API Client itself. Only the client that created the subscription can list the subscription.

  • Events are stored for 90 days, see Events TTL.

GET /subscription/:subscription_id/event[?delivery_status=<delivery_status>&next_cursor=<next_cursor>]

16.1.1. Query Parameters

Name Type Description

delivery_status

string

[optional] filter events by a delivery status, see Webhooks Event Delivery Statuses

next_cursor

string

[optional] fetch the next batch/page from this cursor

16.1.2. Response

Status: 200 OK
{
  "data": [{
    "id": "5f6667e3-4b65-4025-9d64-f8d695b3ebb5",
    "subscriptionId": "4370bfdb-af96-430c-9871-90badf4d5608",
    "createdOn": "2023-11-01T20:20:08.186Z",
    "eventType": "LOOP_UPDATED",
    "eventData": {
      "id": 6611120
    },
    "deliveryStatus": "SUCCESS",
    "deliveryAttempts": 1,
    "responseData": [
      {
        "responseCode": 200,
        "responseHeaders": {
          "content-type": "application/json; charset=utf-8",
          
        },
        "responseBody": "Hello World",
        "url": "https://foobar.com/callbacks/dotloopwebhook",
        "requestSentAt": "2023-11-01T20:20:10.186Z",
        "durationMs": 5260
      }
    ]
  },{
    "id": "a22e5a45-f9db-4250-8157-c7e2a5d21ab9",
    "subscriptionId": "4370bfdb-af96-430c-9871-90badf4d5608",
    "createdOn": "2023-11-01T20:19:50.722Z",
    "eventType": "LOOP_CREATED",
    "eventData": {
      "id": 6611120
    },
    "deliveryStatus": "SUCCESS",
    "deliveryAttempts": 1,
    "responseData": [
      {
        "responseCode": 200,
        "responseHeaders": {
          "content-type": "application/json; charset=utf-8",
          
        },
        "responseBody": "Hello World",
        "url": "https://foobar.com/callbacks/dotloopwebhook",
        "requestSentAt": "2023-11-01T20:19:55.722Z",
        "durationMs": 5260
      }
    ]
  }],
  "meta": {}
}

16.2. Get an Event

Retrieve an individual event by id.

  • Scope: Subscriptions are scoped to the API Client itself. Only the client that created the subscription can get the subscription.

  • Events are stored for 90 days, see Events TTL.

GET /subscription/:subscription_id/event/:event_id

16.2.1. Parameters

None

16.2.2. Response

Status: 200 OK
{
  "data": {
    "id": "5f6667e3-4b65-4025-9d64-f8d695b3ebb5",
    "subscriptionId": "4370bfdb-af96-430c-9871-90badf4d5608",
    "createdOn": "2023-11-01T20:20:08.186Z",
    "eventType": "LOOP_UPDATED",
    "eventData": {
      "id": 6611120
    },
    "deliveryStatus": "SUCCESS",
    "deliveryAttempts": 1,
    "responseData": [
      {
        "responseCode": 200,
        "responseHeaders": {
          "content-type": "application/json; charset=utf-8",
          
        },
        "responseBody": "Hello World",
        "url": "https://foobar.com/callbacks/dotloopwebhook",
        "requestSentAt": "2023-11-01T20:19:55.722Z",
        "durationMs": 5260
      }
    ]
  }
}

17. Addendum

17.1. Types / Constants

17.1.1. Built-in Contact/Loop Participant Roles

  • ADMIN

    • optional fields: ID, License #

  • APPRAISER

    • optional fields: ID, License #

  • BUYER_ATTORNEY

    • optional fields: ID, License #

  • BUYER

    • optional fields: Marital Status

  • BUYING_AGENT

    • optional fields: Fax, ID, License #

  • BUYING_BROKER

    • optional fields: Fax,ID,License #

  • ESCROW_TITLE_REP

    • optional fields: ID, License #

  • HOME_IMPROVEMENT_SPECIALIST

    • optional fields: ID, License #

  • HOME_INSPECTOR

    • optional fields: ID, License #

  • HOME_SECURITY_PROVIDER

    • optional fields: ID, License #

  • HOME_WARRANTY_REP

    • optional fields: ID, License #

  • INSPECTOR

    • optional fields: ID, License #

  • INSURANCE_REP

    • optional fields: ID, License #

  • LANDLORD

    • optional fields: ID, License #

  • LISTING_AGENT

    • optional fields: Fax, ID, License #

  • LISTING_BROKER

    • optional fields: Fax, ID, License #

  • LOAN_OFFICER

    • optional fields: ID, License #

  • LOAN_PROCESSOR

    • optional fields: ID, License #

  • MANAGING_BROKER

    • optional fields: ID, License #

  • MOVING_STORAGE

    • optional fields: ID, License #

  • OTHER

    • optional fields: ID, License #

  • PROPERTY_MANAGER

    • optional fields: ID, License #

  • SELLER_ATTORNEY

    • optional fields: ID, License #

  • SELLER

    • optional fields: Marital Status

  • TENANT_AGENT

    • optional fields: ID, License #

  • TENANT

    • optional fields: ID, License #, Marital Status

  • TRANSACTION_COORDINATOR

    • optional fields: ID, License #

  • UTILITIES_PROVIDER

    • optional fields: ID, License #

Custom roles are not listed here

17.1.2. Profile Types

  • INDIVIDUAL

  • TEAM

  • OFFICE

  • COMPANY

  • ASSOCIATION

  • NATIONAL_PARTNER

17.1.3. Loop Transactions corresponding Statuses

  • PURCHASE_OFFER

    • PRE_OFFER

    • UNDER_CONTRACT

    • SOLD

    • ARCHIVED

  • LISTING_FOR_SALE

    • PRE_LISTING

    • PRIVATE_LISTING

    • ACTIVE_LISTING

    • UNDER_CONTRACT

    • SOLD

    • ARCHIVED

  • LISTING_FOR_LEASE

    • PRE_LISTING

    • PRIVATE_LISTING

    • ACTIVE_LISTING

    • UNDER_CONTRACT

    • LEASED

    • ARCHIVED

  • LEASE_OFFER

    • PRE_OFFER

    • UNDER_CONTRACT

    • LEASED

    • ARCHIVED

  • REAL_ESTATE_OTHER

    • NEW

    • IN_PROGRESS

    • DONE

    • ARCHIVED

  • OTHER

    • NEW

    • IN_PROGRESS

    • DONE

    • ARCHIVED

17.1.4. Webhooks Event Targets and Corresponding Event Types

  • USER

    • CONTACT_CREATED

    • CONTACT_UPDATED

    • CONTACT_DELETED

    • PROFILE_UPDATED

    • USER_PROFILE_ACTIVATED

    • USER_PROFILE_DEACTIVATED

    • USER_ADDED_TO_PROFILE

    • USER_REMOVED_FROM_PROFILE

  • PROFILE

    • LOOP_CREATED

    • LOOP_UPDATED

    • LOOP_MERGED

    • LOOP_PARTICIPANT_CREATED

    • LOOP_PARTICIPANT_UPDATED

    • LOOP_PARTICIPANT_DELETED

17.1.5. Webhooks Event Delivery Statuses

  • PENDING

    • description: The event has been created and is waiting to be scheduled.

  • PENDING_RETRY

    • description: The previous delivery attempt did not result in a 2xx response, retry is waiting to be scheduled.

  • SCHEDULED

    • description: The event is scheduled to be delivered immediately or at its next interval, see back-off policy in Failure Handling.

  • SUCCESS

    • description: The event was successfully delivered.

  • FAILED

    • description: Delivery attempts were exhausted, no further attempts will be executed.

  • DISABLED

    • description: The subscription for this event has been disabled, no delivery attempt will be executed.

18. Client Errors

18.1. HTTP status codes

This API follows the common HTTP status code semantics. List of possible Client errors:

Error Code Description

400 Bad Request

The request is invalid, e.g. request payload can’t be parsed

401 Unauthorized

The access token is invalid or expired. Obtain a new token using the refresh token and insert into request header: Authorization: Bearer <access token>

403 Forbidden

The request is denied, e.g. you don’t have the privileges to access or create the resource

404 Not Found

The requested resource does not exist

422 Unprocessable Entity

The syntax of the request is correct but semantically erroneous

429 Too Many Requests

The caller exceeded the rate limit

18.2. Error responses

Error responses may contain one or many error items. A human-readable explanation may be provided in the detail property. The source may indicate to the origin of the error and code provides an application specific error code.

18.2.1. Example

Status: 400 OK
{
  "errors": [
    {
      "code": "123",
      "source": { "pointer": "/data/attributes/profileid" },
      "detail": "Profile id invalid"
    },
    {
      "source": { "parameter": "include" },
      "detail": "include param required but not present"
    }
  ]
}

19. Changelog

  • 05/20/2025

  • 08/14/2024

    • Added support for 301 Redirect for Loop Merges, Get a Loop

  • 05/20/2024

  • 10/30/2023

  • 09/06/2018

    • Added support to filter for multiple transaction types, e.g. transaction_status=PRE_OFFER|PRE_LISTING Loop API

  • 08/08/2018

  • 04/20/2018

    • Include document metadata in Folder API via "include_documents" parameter

    • role is now a required field when creating Loop Participants

  • 03/13/2018

    • Added additional data fields to participants api

  • 11/01/2017

    • Fix issue where opening downloaded documents via the API triggered a print dialog to appear

  • 10/04/2017

    • Fix issue where updated field for loops was not updated upon document uploads

  • 09/20/2017

    • Global Templates are now applied at loop creation time

  • 08/24/2017

    • Fix issue where updated field for loops was not updated upon task changes

    • Add updated timestamp for document entities

  • 07/26/2017

    • Added support to filter for multiple transaction types, e.g. transaction_type=PURCHASE_OFFER|LISTING_FOR_SALE

    • Introducing Folder API to create, list and rename folders

    • Introducing Document API to upload and download documents

    • FIX: 404 Not Found was returned in some users whose default profile id is not set correctly.

  • 07/12/2017

    • Introduce Activity API which retrieve all loop activities

    • New Filter syntax with param ?filter=…​ (we continue to support updated_min parameter which is now deprecated) Examples: Contact API, Loop API

    • 2 new Loop filters: a) created_min to list all loops created after a specific time, b) transaction_type to list all loops of the specified transaction type

    • New sort field: Sort Loops by created timestamp

    • Include Loop details in summaries via ?include_details=true

  • 06/28/2017

    • FIX: templates can now also be read with loop:write scope as they might be required to create a loop

    • FIX: Loop-It™ does not fail if the a participant is added with the same email as the account email (ignored)

  • 06/16/2017

    • Allow redirect to the redirect_uri via new &redirect_on_deny=[true|false] param, which allows client applications to control the experience if user denies access, see here.

    • FIX: Show correct total counts (in meta section) in GET /loop responses

    • FIX: Fix 403 Forbidden issue affecting some users

  • 05/31/2017

    • Loops and Contacts returned with created timestamp (in addition to updated field)

    • Custom contact or loop participant roles are supported now

  • 05/17/2017

    • Loop list supports new query param updated_min to select loops updated since a timestamp

    • requiresTemplate flag on profile which defines whether a template id is required to create a loop

    • FIX: transaction type in the request takes precedence over loop template transaction type now

  • 05/10/2017

    • Loop Summaries contains loopUrl property now

    • Contacts API supports company and role property now

    • Loop list can be sorted now (e.g. …​&sort=purchase_price:desc)

  • 04/19/2017

    • Added the ability to add, update and delete Loop Participants

    • Paginate thru loop and contact lists via new params batch_size and batch_number

    • Contacts API changes

      1. contact items now have a last updated timestamp

      2. introduce updated_min query param to select contacts updated since a timestamp

  • 04/05/2017

  • 03/23/2017

    • Introduced deactivated flag for profiles

    • Loop-it™ returns now mobile-ready URLs

    • Loop task count (total/completed) returned in loop APIs

  • 03/08/2017

    • Introduced default flag for profiles

    • Added new GET /account API (token requires account:read scope to be able to access this api)

20. FAQ

Are there more than one access or refresh token valid for a particular client/user combination at a given time?

No, there can be only 1 token valid at a time. This also means if you refresh an access token, any previously issued access token for that user will be invalidated.

Does my application need to share refresh/access token across instances in a clustered environment?

Yes, otherwise all instances within your cluster may start racing to refresh the access token.

Are there request/rate limits in place?

Yes. We rate limit client requests in order to protect our service against abuse or DOS attacks. Today we allow each client application to make up to 100 requests per minute for a user. Once client exceed this limit, they’ll receive a 429 Error response and need to wait till the rate limit gets reset. Clients can track their limits by evaluating the following response headers which indicate the actual limit, the remaining calls and when the limit gets reset (ms), e.g.:

X-RateLimit-Limit: 100
X-RateLimit-Remaining: 34
X-RateLimit-Reset: 32000

I’m getting a 403 ACCESS DENIED error returned from the API - what could be wrong?

There could be multiple reasons for a 403 error returned by the API:

  1. You may be attempting to access a non-INDIVIDUAL profile (e.g. OFFICE or COMPANY profiles)

  2. You may be using the wrong token when accessing a profile. Tokens are issued on behalf of a user, so you can only access resources which are the user has permission to access

  3. The scope of you client may be incorrect hence the client is not authorized to make the API request. Example: The application is trying to update a loop but the application has only loop:read scope (required: loop:write or loop:*)