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).
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.
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.
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=&redirect_on_deny=(true|false)]
| 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.
POST https://auth.dotloop.com/oauth/token?grant\_type=authorization\_code&code=<code>&redirect_uri=<redirect_url>&state=<state>
<encode_base64(ClientID:ClientSecret)>
-
Example
-
ClientId:
69bcf590-71b7-41a4-a039-a1d290edca11 -
ClientSecret:
3415e381-bdc4-49b7-bde2-69b3c5cd6447 -
Resulting Header:
Authorization: Basic NjliY2Y1OTAtNzFiNy00MWE0LWEwMzktYTFkMjkwZWRjYTExOjM0MTVlMzgxLWJkYzQtNDliNy1iZGUyLTY5YjNjNWNkNjQ0Nw==
-
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:*"
}
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>
Authorization: Basic <encode_base64(ClientID:ClientSecret)>
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. |
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>
All APIs listed below are available under a common base path:
https://api-gateway.dotloop.com/public/v2/
All paths below are relative to this url, e.g. [https://api-gateway.dotloop.com/public/v2/loop-it](https://api-gateway.dotloop.com/public/v2/loop-it)
This API is a JSON API, so request and response payload are expected to be of content type application/json if not marked otherwise.
Each API request requires a valid Access Token to be presented in an Authorization header, e.g.
Authorization: Bearer 0b043f2f-2abe-4c9d-844a-3eb008dcba67
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>
| 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 |
> 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"
}
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"
}
}
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.
Retrieve account details
Required scope: account:read |
GET /account
None
Status: 200 OK
{
"data": {
"id": 1,
"firstName": "Brian",
"lastName": "Erwin",
"email": "brianerwin@newkyhome.com",
"defaultProfileId": 42
}
}
List all profiles associated with the user.
GET /profile
Required scope: profile:read |
None
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
},
...
]
}
Retrieve an individual profile by id.
Required scope: profile:read |
GET /profile/:profile_id
None
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
}
}
Create a new profile.
Required scope: profile:write |
POST /profile
| 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 |
{
"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"
}
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"
}
}
Update an existing profile by id.
- This API allows partial updates
- Required scope:
profile:write
PATCH /profile/:profile_id
| 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 |
{
"name": "My Changed Profile Name",
"company": "My New Company"
}
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"
}
}
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=&filter=&include_details=true]
| 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) |
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"
},
...
]
}
Retrieve an individual loop by id.
Required scope: loop:read |
GET /profile/:profile_id/loop/:loop_id
None
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.
Create a new loop.
Required scope: loop:write |
POST /profile/:profile_id/loop
| Name | Type | Description |
|---|---|---|
| name | string | the name of the loop (max 200 chars) |
| status | string | status of the loop |
| transactionType | string | type of transaction |
{
"name": "Atturo Garay, 3059 Main, Chicago, IL 60614",
"status": "PRE_LISTING",
"transactionType": "LISTING_FOR_SALE"
}
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"
}
}
Update an existing loop by id.
| * This API allows partial updates * Required scope: loop:write |
PATCH /profile/:profile_id/loop/:loop_id
| Name | Type | Description |
|---|---|---|
| name | string | the name of the loop (max 200 chars) |
| status | string | status of the loop |
| transactionType | string | type of transaction |
{
"status": "SOLD"
}
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"
}
}
Retrieve loop details by id.
Required scope: loop:read |
GET /profile/:profile_id/loop/:loop_id/detail
| 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 |
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",
...
},
...
}
}
Update loop details by id.
| * This API allows partial updates * Required scope: loop:write |
PATCH /profile/:profile_id/loop/:loop_id/detail
See Get Loop Details above.
{
"Financials": {
"Purchase/Sale Price": "342342"
}
}
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",
...
},
...
}
}
List all folders in a loop
Required scope: loop:read |
GET /profile/:profile_id/loop/:loop_id/folder[?include_documents=<include_documents>]
| Name | Type | Description |
|---|---|---|
| include_documents | boolean | Include a list of all documents in all folders |
Status: 200 OK
{
"meta": {
"total": 4
},
"data": [
{
"id": 423424,
"name": "Disclosures",
"created": "2017-05-17T01:18:37Z",
"updated": "2017-05-30T21:42:17Z"
},
...
]
}
Retrieve an individual folder by id.
Required scope: loop:read |
GET /profile/:profile_id/loop/:loop_id/folder/:folder_id[?include_documents=<include_documents>]
| Name | Type | Description |
|---|---|---|
| include_documents | boolean | include a list of all documents in the folder |
Status: 200 OK
{
"data":{
"id": 423424,
"name": "Disclosures",
"created": "2017-05-17T01:18:37Z",
"updated": "2017-05-30T21:42:17Z"
}
}
Create a new folder.
Required scope: loop:write |
POST /profile/:profile_id/loop/:loop_id/folder/
| Name | Type | Description |
|---|---|---|
| name | string | the name of the folder (max ??? chars) |
{
"name": "Disclosures"
}
Status: 201 Created
{
"data":{
"id": 423424,
"name": "Disclosures",
"created": "2017-05-17T01:18:37Z",
"updated": "2017-05-30T21:42:17Z"
}
}
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
| Name | Type | Description |
|---|---|---|
| name | string | the name of the folder (max ??? chars) |
{
"name": "Disclosures (renamed)"
}
Status: 200 OK
{
"data":{
"id": 423424,
"name": "Disclosures (renamed)"
"created": "2017-05-17T01:18:37Z",
"updated": "2017-05-30T21:42:17Z"
}
}
List all documents in a loop
Required scope: loop:read |
GET /profile/:profile_id/loop/:loop_id/folder/:folder_id/document
None
Status: 200 OK
{
"meta": {
"total": 3
},
"data": [
{
"id": 561621,
"filename": "disclosures.pdf",
"created": "2017-05-17T01:18:37Z",
"updated": "2017-05-17T01:18:37Z"
}, ...
]
}
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
None
Status: 200 OK
{
"data": {
"id": 561621,
"name": "disclosures.pdf",
"created": "2017-05-17T01:18:37Z",
"updated": "2017-05-17T01:18:37Z"
}
}
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= content-length: XXX
-- Content-Disposition: form-data; name="file"; fileName="disclosures.pdf" Content-Type: application/pdf
----| Name | Type | Description |
|---|---|---|
| fileName | string | fileName of the pdf |
$ 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/
Status: 201 OK
{
"data": {
"id": 561621,
"name": "my_disclosures.pdf",
"created": "2017-05-17T01:18:37Z",
"updated": "2017-05-17T01:18:37Z"
}
}
List all loop participants in a loop
Required scope: loop:read |
GET /profile/:profile_id/loop/:loop_id/participant
None
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"
}
]
}
Retrieve loop participants details of an individual loop participant.
Required scope: loop:read |
GET /profile/:profile_id/loop/:loop_id/participant/:participant_id
None
Status: 200 OK
{
"data": {
"id": 2355,
"fullName": "Brian Erwin",
"email": "brianerwin@newkyhome.com",
"role": "BUYER",
"Phone": "(555) 555-5555"
}
}
Add a new loop participant
* Required scope: loop:write |
POST /profile/:profile_id/loop/:loop_id/participant
| Name | Type | Description |
|---|---|---|
| fullName | string | First and last name of the participant |
| 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.
{
"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"
}
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"
}
}
Update an existing participant
| * This API allows partial updates * Required scope: loop:write |
PATCH /profile/:profile_id/loop/:loop_id/participant/:participant_id
| Name | Type | Description |
|---|---|---|
| fullName | string | First and last name of the participant |
| 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 |
{
"email": "brian@gmail.com"
}
Status: 200 OK
{
"data": {
"id": 2355,
"fullName": "Brian Erwin",
"email": "brian@gmail.com",
"role": "BUYER",
"Phone": "(555) 555-5555"
}
}
Delete an existing participant by id.
Required scope: loop:write |
DELETE /profile/:profile_id/loop/:loop_id/participant/:participant_id
none
Status: 204 No Content
List all task lists in a loop
Required scope: loop:read |
GET /profile/:profile_id/loop/:loop_id/tasklist/
None
Status: 200 OK
{
"meta": {
"total": 3
},
"data": [
{
"id": 1234,
"name": "My Tasks"
}, ..
]
}
Retrieve an individual task list.
Required scope: loop:read |
GET /profile/:profile_id/loop/:loop_id/tasklist/:task_list_id
None
Status: 200 OK
{
"data": {
"id": 1234,
"name": "My Tasks"
}
}
List all task items in a task list
Required scope: loop:read |
GET /profile/:profile_id/loop/:loop_id/tasklist/:task_list_id/task
None
Status: 200 OK
{
"meta": {
"total": 4
},
"data": [
{
"id": 125736485,
"name": "contract",
"due": "2016-10-21T00:00:00-04:00",
"completed": true
},
...
]
}
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
None
Status: 200 OK
{
"data": {
"id": 125736485,
"name": "contract",
"due": "2016-10-21T00:00:00-04:00"
}
}
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>]
| Name | Type | Description |
|---|---|---|
| batch_size | integer | size of batch returned (default=20, max=100) |
| batch_number | integer | batch/page number (default=1) |
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. |
List all contacts in the user account.
Required scope: contact:read |
GET /contact[?batch_size=<batch_size>&batch_number=<batch_number>&filter=]
| 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> |
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"
},
...
]
}
Retrieve an individual contact by id.
Required scope: contact:read |
GET /contact/:contact_id
None
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"
}
}
Create a new contact.
Required scope: contact:write |
POST /contact
| Name | Type | Description |
|---|---|---|
| firstName | string | first name |
| lastName | string | last name |
| 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 |
{
"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"
}
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"
}
}
Update an existing contact by id.
| * This API allows partial updates * Required scope: contact:write |
PATCH /contact/:contact_id
| Name | Type | Description |
|---|---|---|
| firstName | string | first name |
| lastName | string | last name |
| 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 |
{
"home": "(415) 888 8888"
}
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"
}
}
Delete an existing contact by id.
Required scope: contact:write |
DELETE /contact/:contact_id
none
Status: 204 No Content
List all loop templates in the profile
Required scope: template:read or loop:write |
GET /profile/:profile_id/loop-template
None
Status: 200 OK
{
"meta": {
"total": 5
},
data: [
{
"id": 423,
"profileId": 732453,
"name": "My Loop Template",
"transactionType": "PURCHASE_OFFER",
"shared": true,
"global": false
},
...
]
}
Retrieve an individual loop template by id.
Required scope: template:read or loop:write |
GET /profile/:profile_id/loop-template/:loop_template_id
None
Status: 200 OK
{
"data": {
"id": 423,
"profileId": 732453,
"name": "My Loop Template",
"transactionType": "PURCHASE_OFFER",
"shared": true,
"global": false
}
}
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:
-
Creating a subscription
-
Receiving a webhook event because of the subscription
-
How client applications may want to fetch Public API data based on the event.
Step 1 - Create a subscription to "User 100’s" contacts:
Step 2 - "User 100" creates a contact while using Dotloop:
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)
The following table defines the event types that can be subscribed to.
(See also Webhooks Event Targets and Corresponding Event Types under Types/Constants)
| 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 |
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" }
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.
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.
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) |
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 |
<br>{<br> ...<br> "event": {<br> "id": "123" /* loop id */<br> }<br>}<br> |
CONTACT_CREATED, CONTACT_UPDATED, CONTACT_DELETED |
<br>{<br> ...<br> "event": {<br> "id": "456" /* contact id */<br> }<br>}<br> |
PROFILE_UPDATED, USER_PROFILE_ACTIVATED, USER_PROFILE_DEACTIVATED, USER_ADDED_TO_PROFILE, USER_REMOVED_FROM_PROFILE |
<br>{<br> ...<br> "event": {<br> "id": "444" /* profile id */<br> }<br>}<br> |
LOOP_MERGED |
<br>{<br> ...<br> "event": {<br> "id": "777", /* original loop id */<br> "fromId": "777", /* original loop id */<br> "toId": "888", /* new loop id */<br> }<br>}<br> |
LOOP_PARTICIPANT_CREATED, LOOP_PARTICIPANT_UPDATED, LOOP_PARTICIPANT_DELETED |
<br>{<br> ...<br> "event": {<br> "id": "777", /* loop id */<br> "participantId": "999" /* participant id */<br> }<br>}<br> |
SUBSCRIPTION_REMOVED, SUBSCRIPTION_DISABLED |
<br>{<br> ...,<br> "event": {<br> "targetType": "PROFILE",<br> "targetId": 789,<br> "eventTypes": ["LOOP_CREATED", "LOOP_UPDATED"],<br> "url": "https://foobar.com/callbacks/dotloopwebhook",<br> "externalId": "your_external_id",<br> "signingKey": "super_secret_key",<br> "tenantId": "tenant_id",<br> "enabled": "false",<br> "revision": 1<br> }<br>}<br> |
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.
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.
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.
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 } ] } }
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.
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.
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.
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);
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.
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 |
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=&next_cursor=<next_cursor>]
| 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 |
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"
}
}
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
None
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
}
}
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
| 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. |
{
"targetType": "USER",
"targetId": 7083432,
"eventTypes": ["CONTACT_CREATED", "CONTACT_UPDATED"],
"url": "https://foobar.com/callbacks/dotloopwebhook",
"signingKey": "super_secret_key",
"externalId": "user_id_in_db"
}
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
}
}
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
| 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. |
{
"externalId": "user-guuid-abcd-123efg"
}
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
}
}
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
none
Status: 204 No Content
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>]
| 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 |
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": {}
}
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
None
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
}
]
}
}
-
ADMIN- optional fields:
ID,License #
- optional fields:
-
APPRAISER- optional fields:
ID,License #
- optional fields:
-
BUYER_ATTORNEY- optional fields:
ID,License #
- optional fields:
-
BUYER- optional fields:
Marital Status
- optional fields:
-
BUYING_AGENT- optional fields:
Fax,ID,License #
- optional fields:
-
BUYING_BROKER- optional fields:
Fax,ID,License #
- optional fields:
-
ESCROW_TITLE_REP- optional fields:
ID,License #
- optional fields:
-
HOME_IMPROVEMENT_SPECIALIST- optional fields:
ID,License #
- optional fields:
-
HOME_INSPECTOR- optional fields:
ID,License #
- optional fields:
-
HOME_SECURITY_PROVIDER- optional fields:
ID,License #
- optional fields:
-
HOME_WARRANTY_REP- optional fields:
ID,License #
- optional fields:
-
INSPECTOR- optional fields:
ID,License #
- optional fields:
-
INSURANCE_REP- optional fields:
ID,License #
- optional fields:
-
LANDLORD- optional fields:
ID,License #
- optional fields:
-
LISTING_AGENT- optional fields:
Fax,ID,License #
- optional fields:
-
LISTING_BROKER- optional fields:
Fax,ID,License #
- optional fields:
-
LOAN_OFFICER- optional fields:
ID,License #
- optional fields:
-
LOAN_PROCESSOR- optional fields:
ID,License #
- optional fields:
-
MANAGING_BROKER- optional fields:
ID,License #
- optional fields:
-
MOVING_STORAGE- optional fields:
ID,License #
- optional fields:
-
OTHER- optional fields:
ID,License #
- optional fields:
-
PROPERTY_MANAGER- optional fields:
ID,License #
- optional fields:
-
SELLER_ATTORNEY- optional fields:
ID,License #
- optional fields:
-
SELLER- optional fields:
Marital Status
- optional fields:
-
TENANT_AGENT- optional fields:
ID,License #
- optional fields:
-
TENANT- optional fields:
ID,License #,Marital Status
- optional fields:
-
TRANSACTION_COORDINATOR- optional fields:
ID,License #
- optional fields:
-
UTILITIES_PROVIDER- optional fields:
ID,License #
- optional fields:
| Custom roles are not listed here |
-
INDIVIDUAL -
TEAM -
OFFICE -
COMPANY -
ASSOCIATION -
NATIONAL_PARTNER
-
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
-
-
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
-
-
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.
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 |
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.
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"
}
]
}
-
05/20/2025
- Added support for new Webhook event types Webhooks Event Targets and Corresponding Event Types
-
08/14/2024
- Added support for 301 Redirect for Loop Merges, Get a Loop
-
05/20/2024
- Added support for webhook Subscription Events and documentation
-
10/30/2023
-
Introduce Webhooks (Initial Release) as an "Initial Release" public api feature to create, list and update subscriptions, and list events.
-
Introduced api documentation for Webhook Subscriptions and Webhook Events
-
Added constants for Webhooks Event Delivery Statuses
-
Added constants for Webhooks Event Targets and Corresponding Event Types
-
-
09/06/2018
- Added support to filter for multiple transaction types, e.g.
transaction_status=PRE_OFFER|PRE_LISTINGLoop API
- Added support to filter for multiple transaction types, e.g.
-
08/08/2018
- Adding support for updating custom loop template fields in Loop Detials API
-
04/20/2018
-
Include document metadata in Folder API via "include_documents" parameter
-
roleis 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
updatedfield for loops was not updated upon document uploads
- Fix issue where
-
09/20/2017
- Global Templates are now applied at loop creation time
-
08/24/2017
-
Fix issue where
updatedfield for loops was not updated upon task changes -
Add
updatedtimestamp 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 Foundwas 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 supportupdated_minparameter which is now deprecated) Examples: Contact API, Loop API -
2 new Loop filters: a)
created_minto list all loops created after a specific time, b)transaction_typeto list all loops of the specified transaction type -
New sort field: Sort Loops by
createdtimestamp -
Include Loop details in summaries via
?include_details=true
-
-
06/28/2017
-
FIX: templates can now also be read with
loop:writescope 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_urivia 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 /loopresponses -
FIX: Fix
403 Forbiddenissue affecting some users
-
-
05/31/2017
-
Loops and Contacts returned with
createdtimestamp (in addition toupdatedfield) -
Custom contact or loop participant roles are supported now
-
-
05/17/2017
-
Loop list supports new query param
updated_minto select loops updated since a timestamp -
requiresTemplateflag 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
loopUrlproperty now -
Contacts API supports
companyandroleproperty 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_sizeandbatch_number -
Contacts API changes
-
contact items now have a last
updatedtimestamp -
introduce
updated_minquery param to select contacts updated since a timestamp
-
-
-
04/05/2017
- Introducing Loop Tasks APIs
-
03/23/2017
-
Introduced
deactivatedflag for profiles -
Loop-it™ returns now mobile-ready URLs
-
Loop task count (total/completed) returned in loop APIs
-
-
03/08/2017
-
Introduced
defaultflag for profiles -
Added new
GET /accountAPI (token requiresaccount:readscope to be able to access this api)
-
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:
-
You may be attempting to access a non-
INDIVIDUALprofile (e.g.OFFICEorCOMPANYprofiles) -
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
-
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:readscope (required:loop:writeorloop:*)




