Getting Started
Introduction
HOVER transforms smartphone photos of any property into an accurate, interactive 3D model. Thousands of exterior contractors and many of the top 10 U.S. insurance carriers are using HOVER to create accurate estimates, quickly process claims, and show homeowners what different materials and colors will look like on their home. All a customer has to do is download the HOVER app from the Apple or Google Play app store and take 8 photos of any property with the app. They will then receive the 3D model and comprehensive measurements for the property.
The HOVER API provides programmatic access to our ecosystem to build robust integrations with other software systems in order to streamline workflows and save time. From a high level, the HOVER API allows you to:
- Create jobs with pre-populated information that is assigned to a user or homeowner to complete the photo capture using the HOVER app.
- Set up webhooks to listen for status updates about jobs.
- When complete, retrieve job assets, measurements, and link to the 3D model.
Setting up Sandbox
The best way to learn the HOVER API is by playing around in our Sandbox environment. This mirrors Production functionality but does not mirror Production data. Additionally, we do not submit photos to our 3D reconstruction pipeline but instead have test jobs to help you recreate the end-to-end flow without having to use the app.
Creating your account
- Sign up for a HOVER Sandbox account.
- Provide your full name, email, & password and click Submit.
- Confirm your email.
- Provide a company name, phone number, & zip code and click Complete. (It's fine if this is fake information.)
- Confirm your account via the "Welcome to HOVER" email.
Creating your OAuth Client ID & Secret
We use organzation-level OAuth 2.0 authentication for all of our integrations. Application credentials can be created and managed in the Developer section of your settings.
- Navigate to the Developer section of settings in your Sandbox account.
- Click the Create New Integration button.
- Provide your integration name, redirect URI, description, & logo and click Save Integration.
- Once created, click Edit next to your new Integration.
- There you will be able to retreive your Client ID & Secret along with editing the application information and resetting the secret.
Postman Demo
To help walk you through some of the most common API workflows we recommend using a REST client called Postman. Simply click the button below to import a pre-made collection of examples and a Sandbox environment variable template.
Requirements
In order to use this demo you will need the following:
- Postman for Mac, Windows, or Linux.
- HOVER Sandbox OAuth Client ID & Secret.
Setting up your environment
Using the Run in Postman button above will also install an environment called "Sandbox (Template)". This will allow you to populate information once and use it throughout the demo.
In Postman, in the top right-hand corner, click the gear icon to open the Manage Environments window.
Click the "HOVER - Sandbox (Template)" text.
Where you see
your_client_id
,your_client_secret
, andyour_callback_url
replace them with your Integration OAuth information from the Sandbox Developer settings. Also make sure that those values get copied over to the Current Value column as well.Click Update.
Close the Manage Environments window.
In the top right environment drop down, choose the "HOVER - Sandbox (Template)" environment to set it as active.
OAuth Authentication
In order to start making your first API calls, you will need to configure OAuth and get your Access Token. This has been set up at the collection level so that you only have to do it once and it will apply to all API calls you make.
On the HOVER API collection folder, click the elipsis (...) icon and then Edit.
On the Edit Collection screen, click the Authorization tab.
Click the Get New Access Token button.
Populate the following information:
- Grant Type: Authorization Code
- Callback URL:
{{callback_url}}
- Auth URL:
{{url}}/oauth/authorize
- Access Token URL:
{{url}}/oauth/token
- Client ID:
{{client_id}}
- Client Secret:
{{client_secret}}
- Client Authentication: Send as Basic Auth header
If you have the "HOVER - Sandbox (Template)" environment selected, Postman will automatically interpret those variables for you. Click Request Token.
Sign in to your Sandbox account and authorize the application.
Once approved, you should see a new box that contains Access Token and other information. Scroll down & click Use Token.
Click Update.
Making your first API call
Now that you have everything set up you can start using the HOVER API. Let's begin by creating your first job.
Click the "Getting Started (Creating a Job)" folder.
Click the "Create a Job" POST request.
Click on the Body tab.
Complete at least the
job[name]
,job[location_line_1]
,job[location_city]
,job[location_region]
,job[location_postal_code]
, andjob[location_country]
form fields.Click Send.
When you get a 201 response code you have just created your first job!
You can now use the
id
value in the next request to get the job's details.
Authentication - OAuth 2.0
We currently support OAuth. OAuth 2.0 is an authorization framework that enables applications to obtain limited access to HTTP services.
Org Level Authentication
Using OAuth you will be authenticated as an org rather than a user. When authenticated as an org it is important to specify which user the integration is taking an action on behalf of. To do that you'll need to include a current_user_id
or current_user_email
parameter to specify which user to associate the action with. This user must be included in the org you are authorized under, and you cannot specify an action on behalf of a user outside your org. If you do not include this parameter on endpoints that require it, you will get an error response. Since most endpoints currently require this parameter we'll only mention it on endpoints that don't.
There are a variety of OAuth 2.0 libraries available including but not limited to .NET, Ruby, Java, PHP, Python and Node.js which can be found here. The example below is using the OAuth2 library available for Ruby. Additional information on OAuth 2.0 can be found here.
Important: We recommend performing a periodic audit of your apps authorized to use HOVER. If an admin who created the HOVER integration left your company, you should reauthorize your application to ensure any previously generated access/refresh tokens are revoked.
Authentication Code Flow
require 'oauth2'
#Use the oauth2 Ruby gem/library
client_id = "CLIENT_ID"
client_secret = "CLIENT_SECRET"
redirect_uri = "https://yourdomain.com/oauth/callback"
client = OAuth2::Client.new(client_id, client_secret, :site => "https://hover.to")
# Send user to authorization URL
client.auth_code.authorize_url(:redirect_uri => redirect_uri)
# When our system redirects back to the 3rd party's redirect_uri(oauth callback url) the 3rd party system uses the Grant Token to get a Access Token
code = 'GRANT_TOKEN_FROM_CALLBACK'
# Makes an API request to get an access token for the authorization code flow.
token = client.auth_code.get_token(code, :redirect_uri => redirect_uri)
The authorization code flow is used when an application exchanges an authorization code for an access token. The user first is directed to HOVER to authorize
the requesting integration. Once that is done, the user is redirected to the specified redirect_uri
with the authorization_code
in the query string, for example - https://hover.to/?code=authorization_code
. The application can then use this authorization_code
to request the Access Token and Refresh Token pair from the Access Token endpoint. For more detailed info, check out the RFC spec.
This request requires a client ID and client secret which are given to you when you create an integration. The keys are available under your integration within the HOVER Developer Settings. Please refer to our Getting Started Guide for detailed instructions on how to create an integration. Note that the client ID is how we identify the integration, and the client secret is how we verify that the access and refresh token requests are coming from your client. The client secret should be stored securely, and not be exposed externally as it should only be known by you.
Note that the authorization process can initiate from HOVER's side. In this case, the user redirected to your application's callback URL may not have an active session on your website. Therefore, you may need to redirect to your login page, and then back to the OAuth callback URL with the Grant Token. In case the Grant Token expires before the user logs in and gets redirected back to your callback URL you will need to send the user back to our authorization URL for a fresh grant token.
Retrieving an Authorization Code
require "net/http"
url = "https://hover.to/oauth/authorize?response_type=code&client_id=#{client_id}&redirect_uri=#{redirect_uri}"
uri = URI.parse(url)
request = Net::HTTP::Get.new(uri)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true if 'https' == uri.scheme
curl -v -x GET 'http://hover.to/oauth/authorize?response_type=code&client_id=#{client_id}&redirect_uri=#{redirect_uri}'
Authorization URL
GET https://hover.to/oauth/authorize
Request Parameters
Parameter | Description |
---|---|
client_id | Key provided by HOVER. This is how we identify the integration. |
response_type | Value MUST be set to "code". |
redirect_uri | After a user successfully authorizes your application we'll redirect them to this URL. We'll include a URL parameter called "code" that contains the authorization code granted to your application. |
Retrieving an Access Token
require "net/http"
require "JSON"
url = "https://hover.to/oauth/token?grant_type=authorization_code&code=#{authorization_code}&client_id=#{client_id}&client_secret=#{client_secret}&redirect_uri=#{redirect_uri}"
uri = URI.parse(url)
request = Net::HTTP::Post.new(uri)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true if 'https' == uri.scheme
response = http.request(request)
response = JSON.parse(response.body)
curl -v -X POST --data 'client_id=#{client_id}&client_secret=#{client_secret}&code=#{authorization_code}&grant_type=authorization_code&redirect_uri=#{redirect_uri}' 'https://hover.to/oauth/token'
When successful you'll get a 200 response code with a JSON indicating the access_token and refresh_token.
{
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOnsib3JnX2lkIjoyNTl9LCJkYXRhIjp7ImFwcGxpY2F0aW9uX2lkIjoxMTB9LCJleHAiOjE1NjI5NTk3MTd9.G_bmSAdMKWpUIlTImDIJMp3bvOx9VYDCoILmk8WltP0",
"token_type": "bearer",
"expires_in": 7200,
"refresh_token": "96201e2fffdaeea7922d43441dbf74200e3a2575af640617307c1567c3abd1f6",
"created_at": 1562952517,
"owner_id": 259,
"owner_type": "orgs"
}
When there is an error you will get a 401 Unauthorized error with the following JSON responses. This is either due to incorrect parameters or using the wrong authorization_code.
{
"error": "invalid_grant",
"error_description": "The provided authorization grant is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client."
}
{
"error": "invalid_request",
"error_description": "The request is missing a required parameter, includes an unsupported parameter value, or is otherwise malformed."
}
Once you have the authorization_code, you can use it to acquire the initial access_token. Access tokens expire every 2 hours hence a refresh_token is included to enable you to acquire new tokens programatically.
Access Token endpoint
POST https://hover.to/oauth/token
Request Parameters
Parameter | Description |
---|---|
grant_type | Value MUST be set to "authorization_code". |
code | The authorization code received from the authorization server. |
client_id | Key provided by HOVER. This is how we identify the integration. |
client_secret | Key provided by HOVER. This is how we verify the client. This value should be stored securely. |
redirect_uri | Must be identical to that used in the authorization request. |
How to Use the Access token
Once the authorization is complete, you only need to pass the access token in the header of each request. The client_id and client_secret do not need to be passed in requests except for when retrieving the access token for the first time. We require current_user_id
or current_user_email
parameter in some requests as well in order to associate the action with a user as mentioned above.
curl -X POST -H "Authorization: Bearer #{token} " "https://hover.to/api/v2/jobs"
`
Refreshing the Access Token
require "net/http"
require "JSON"
url = "https://hover.to/oauth/token?grant_type=refresh_token&refresh_token=#{refresh_token}&client_id=#{client_id}&client_secret=#{client_secret}"
uri = URI.parse(url)
request = Net::HTTP::Post.new(uri)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true if 'https' == uri.scheme
response = http.request(request)
response = JSON.parse(response.body)
curl -X POST \ 'https://hover.to/oauth/token?grant_type=refresh_token&refresh_token=#{refresh_token}&client_id=#{client_id}&client_secret=#{client_secret}'
When successful you'll get a 200 response code with a JSON indicating the updated
access_token
andrefresh_token
.
{
"access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOnsib3JnX2lkIjoyNTl9LCJkYXRhIjp7ImFwcGxpY2F0aW9uX2lkIjoxMTB9LCJleHAiOjE1NjI5NTk5OTN9.FYX3rViO0QQr-my8MpboFOu2OYGpysku4mwoQk8GFu8",
"token_type": "bearer",
"expires_in": 7200,
"refresh_token": "67680ddc55acb1d79dd34652973eec3213fbe68792bcb9f666c0cb08554e2802",
"created_at": 1562952793,
"owner_id": 259,
"owner_type": "orgs"
}
The refresh token will be returned in the initial token request and should be stored. The token endpoint can then be used to retrieve a new token. Note that when you use the refresh_token
to retrieve a new token, you will also receive an updated refresh_token
. If you do not update the refresh_token
each time you will receive an authorization error.
Access Token Endpoint
When there is an error you will get a 401 Unauthorized error with the following JSON responses. This is either due to incorrect parameters or using the wrong
refresh_token
.
{
"error": "invalid_grant",
"error_description": "The provided authorization grant is invalid, expired, revoked, does not match the redirection URI used in the authorization request, or was issued to another client."
}
{
"error": "invalid_request",
"error_description": "The request is missing a required parameter, includes an unsupported parameter value, or is otherwise malformed."
}
POST https://hover.to/oauth/token
Parameter | Description |
---|---|
grant_type | Value MUST be set to "refresh_token". |
refresh_token | The refresh token from the token request. |
client_id | Key provided by HOVER. This is how we identify the integration. |
client_secret | Key provided by HOVER. This is how we verify the client. This value should be stored securely. |
Standard Workflows
With the HOVER API, you can achieve a variety of workflows to suit the needs of your integration. To streamline the design and implementation of the integration, listed below are summarized step-by-step workflows catered to several different ways you can utilize the HOVER API:
Workflow 1 - Customer Relationship Manager (CRM)/Claims Management System (CMS) Workflow
Workflow 2 - Direct to Homeowner (HO)/Independent Adjuster(IA) Workflow
CRM/CMS Workflow
This workflow should be used when the capturing user is within your HOVER Org. This is the case for most customers.
{
"event": "job-state-changed",
"webhook_id": 230293,
"job_external_identifier": "order01239181",
"job_id": 19402492,
"state": "complete",
"content-available": 1,
"job_estimated_hours_to_completion": 1.3902
}
Create a HOVER job using the Create a Job endpoint. Ensure that you specify the user email parameter (this will be the user that is assigned in HOVER to take the photos). This request will return a job_id which can be used to download the measurement information, PDF and images once completed.
The user can then access this job within the HOVER app. Once they have completed the capture HOVER will start sending webhooks for job state updates. Alternatively, if you have a mobile app you can use deep links to move between apps seamlessly.
Listen for webhooks until the job is labeled as complete. Match job by the HOVER
job_id
field which is returned in step 1 when the job is created.Use any of our measurement output formats to download the HOVER measurements.
HO/IA Workflow
This workflow should be used when the capturing user is either a homeowner, or is not part of your HOVER Org. This is the case in many IA scenarios, where the end user might be working with multiple companies.
{
"event": "capture-request-state-changed",
"capture_request_id": 4290429,
"state": "connected"
}
{
"event": "job-state-changed",
"webhook_id": 230293,
"job_external_identifier": "order01239181",
"job_id": 19402492,
"state": "complete",
"content-available": 1,
"job_estimated_hours_to_completion": 1.3902
}
Create a HOVER capture request using the Create a Capture Request endpoint. This request will return a job_id which can be used to download the measurement information, PDF and images once completed.
The user can then access this job within the HOVER app once they download and sign-up. You can choose to handle the communication to the user using our deep links via email or text, or HOVER can send the communications on your behalf. Once they have completed the user sign-up and capture, HOVER will send webhooks for capture request and job state updates.
Listen for webhooks until the job is labeled as complete. Match job by the HOVER
job_id
field which is returned in step 1 when the job is created.Use any of our measurement output formats to download the HOVER measurements.
Capture Only Workflow
This workflow could be used for jobs that do not require measurements right away. Users can submit job photos and order measurements later on.
Create a HOVER job using the Create a Job endpoint with deliverable_id 7.
The user can start the capture on HOVER app.
Listen for webhooks until the job is labeled as submitting. This indicates that photos have been uploaded successfully and would be available via API via the images endpoints.
Whenever you are ready to upgrade the job, create a deliverable change request using the create capture requests endpoint.
Listen for webhooks until the job is labeled as complete.
Use any of our measurement output formats to download the HOVER measurements.
Webhooks
Webhooks are used to notify your application when the jobs it created have changed state. This is useful to know when a job is complete and its measurements are available. Additionally, you can know when photos have been successfully uploaded, or if a job has failed.
After a webhook is created it isn't active until it's been verified. The verification process involves sending a request to the URL specified when creating the webhook. That request will include the code needed to verify the webhook. You then make a POST request to webhook verification endpoint with the code provided, and we will begin posting status updates.
If there is an issue sending a webhook, the response from your server will be saved in the webhook's last_error
attribute.
High Level Workflow
Create a webhook and specify the endpoint HOVER should POST to. HOVER will POST a verification code to the specified URL.
Verify a webhook by making a POST request to the specified endpoint with the code.
Listen for state changes, using the
job_id
or thejob_external_identifier
to identify the HOVER property.If you need to change the endpoint, or you no longer want to receive webhooks, Delete the webhook.
Create a Webhook
require 'net/http'
require 'json'
token='your OAuth authorization token'
url = "https://hover.to/api/v2/webhooks"
uri = URI.parse(url)
request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer #{token}"
request.set_form_data({
"webhook[url]" => "https://data-collector.example.com/partners/hover/webhook",
"webhook[content_type]" => "json"
})
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true if 'https' == uri.scheme
response = http.request(request)
data = JSON.parse(response.body)
When successful you'll get a 201 response code with a JSON representation of the webhook in the response body.
{
"id": 4,
"owner_id": 10,
"owner_type": "Partner",
"url": "https://example.com/api/hover/v1/webhook",
"verified_at": null,
"content_type": "json",
"hmac_secret": "CvEbyJ1y0bOmg7YmPwLh9AgLHRzgBJysIpuRb7cP5GsyAF/167+VlgLGnzmjvxfIiStnQBpuydDgbURAudSoig==",
"last_error": null,
"created_at": "2017-11-03T02:39:07.221Z",
"updated_at": "2017-11-03T02:39:07.221Z"
}
When there's an error creating the webhook you'll get a 422 response code. The JSON in the response body will explain the error.
{"url":["can't be blank"]}
This endpoint creates a webhook. Once the webhook is created, we will POST a verification code to the specified URL.
The webhook will be created in the scope of the org you are authorized under. It will only report on events that happen within that org. This includes events from any user within the organization for jobs created both in app or through API.
The response includes an HMAC secret. Whenever we send requests to the url specified when creating the webhook we'll include an HMAC signature in the "Authorization" header. You can use the webhook's unique HMAC secret to generate the same signature from the webhook requests you receive. If the signature you generate matches the signature in the request header you know the request came from us. The method for generating our HMAC signatures is explained here.
HTTP Request
POST https://hover.to/api/v2/webhooks
URL Parameters
Parameter | Description |
---|---|
webhook[url] | The URL the webhook should POST to |
webhook[content_type] | Optional and defaults to x-www-form-urlencoded . When it's given a valid value, webhook will include specified content type in POST header. Currently supports x-www-form-urlencoded and json |
Verify a Webhook
require 'net/http'
verification_code = 'verification code posted to the webhook URL'
url = "https://hover.to/api/v2/webhooks/#{verification_code}/verify"
uri = URI.parse(url)
request = Net::HTTP::Put.new(uri)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true if 'https' == uri.scheme
response = http.request(request)
When successful you'll get a 200 response code with no body.
When un-successful you'll get a 404 response code with no body.
This endpoint verifies a webhook. Authentication is not required. Until a webhook is verified, we will not begin posting status updates.
HTTP Request
PUT https://hover.to/api/v2/webhooks/<CODE>/verify
URL Parameters
Parameter | Description |
---|---|
code | The secret code needed to verify the webhook. When you create a webhook this code is posted immediately as part of your first webhook request to the URL you specify. |
Re-send webhook verification code
require 'net/http'
webhook_id = 'your webhook ID returned in the first JSON response'
token = 'your OAuth authorization token'
url = "https://hover.to/api/v2/webhooks/#{webhook_id}/request_verification"
uri = URI.parse(url)
request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer #{token}"
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true if 'https' == uri.scheme
response = http.request(request)
When successful you'll get a 204 response code with no body.
When un-successful you'll get a 404 response code with no body.
This endpoint re-sends the verification code needed to verify a webhook. The verification code will be in the context of the HOVER Organization you are currently authorized in.
HTTP Request
POST https://hover.to/api/v2/webhooks/<WEBHOOK-ID>/request_verification
Receiving Webhooks
When you create a webhook you'll specify a URL. For each event that we send webhook notifications for we'll make a POST request to that URL. Each webhook request will include an "event" attribute that let's you know what type of even it's for. The rest of the data included is specific to the event.
"job-state-changed" Event
When a job's state changes you'll receive a webhook with the following post body.
{
"event": "job-state-changed",
"webhook_id": 230293,
"job_external_identifier": "order01239181",
"job_id": 19402492,
"state": "processing_upload",
"content-available": 1,
"job_estimated_hours_to_completion": 1.3902
}
The "state" attribute lets you know which state the job just entered. New states may be created in the future. Here is a list of the current states:
State | Description |
---|---|
uploading | Waiting for the contractor or homeowner to take pictures of the home and submit them. |
processing_upload | The uploaded data has been received and we're verifying the uploaded data and authorizing the uploaded user. |
submitting | The uploaded data is being submitted to our processing pipeline. |
working | We're turning the 2D images you uploaded into a photo realistic 3D model and measurements. |
waitlisted | Your account is on our waitlist. The job will stay in this state until we take your account of the waitlist. |
waiting_approval | The user that uploaded this job must get approval from their org's administrator before the job will progress. |
retrieving | 3D model and measurements are ready. When the job is in this state we're fetching the results from our processing pipeline. |
processing | Apply partner specific branding and other final processing |
paying | We're collecting payment for the job |
complete | All done. Your results are paid for and available. |
failed | We weren't able to finish processing this job. |
cancelled | This job was cancelled before we finished processing it. |
requesting_corrections | The client was unhappy with the job and we're sending it back to the processing pipeline for corrections. |
processing_upload_for_improvements | More images were uploaded for the job after processing was done. Let's pre-process those images and get them ready for the pipeline. |
requesting_improvements | We've received and processed an upload for an already complete or failed job. In this state we're sending the job back to the processing pipeline for improvements. |
"inspection-checklist-created" Event
When an inspection checklist has been created, you'll receive a webhook with the following post body.
{
"event": "inspection-checklist-created",
"inspection_checklist_id": 573,
"job_id": 333
}
"inspection-checklist-updated" Event
When an inspection checklist has been updated, you'll receive a webhook with the following post body.
{
"event": "inspection-checklist-updated",
"inspection_checklist_id": 573,
"job_id": 333
}
"webhook-verification-code" Event
When you create a webhook we immediately send a request with the "webhook-verification-code" event. You'll need to use the included code to verify your webhook and activate it.
{
"event": "webhook-verification-code",
"webhook_id": 230293,
"code": "03947FE2-28C3-4EB1-AD44-B4258B3345BB"
}
"capture-request-state-changed" Event
After creating a capture request you'll receive webhooks letting you know about it's state changes. Here's an example
{
"capture_request_id": 4290429,
"state": "complete",
"event": "capture-request-state-changed",
"job_estimated_hours_to_completion": 0.0,
"job_client_identifier": "test-client-identifier-992759",
"job_id": 19402492,
"job_external_identifier": "order01239181"
}
Potential capture request states:
State | Description |
---|---|
new | The request is new and the homeowner hasn't connected yet. |
connected | The capture request has been associated with a user record belonging to the homeowner. |
complete | The job has been captured. |
deleted | The request was deleted before the job was captured. |
"deliverable-change-request-state-changed" Event
When a deliverable change request's state changes you'll receive a webhook with the following post body.
{
"event": "deliverable-change-request-state-changed",
"webhook_id": 230293,
"job_id": 19402492,
"state": "complete",
"deliverable_change_request_id": 3234
}
Potential deliverable change request states:
State | Description |
---|---|
first_attempt | We're still making our first attempt to upgrade this job's deliverable |
waiting_for_job_to_finish | The job wasn't in a state that allowed a deliverable upgrade. In this state we wait for the job to be finished so that we can request the upgrade afterward. |
second_attempt | We weren't able to upgrade the job's deliverable the first time, so we waited for it to finish. In this state we make our second and final attempt to upgrade the job's deliverable. |
complete | The upgrade was completed and the user will get the new requested deliverable |
failed | We weren't able to upgrade this job. |
List All Webhooks
require 'net/http'
require 'json'
url = "https://hover.to/api/v2/webhooks"
uri = URI.parse(url)
request = Net::HTTP::Get.new(uri)
request["Authorization"] = "Bearer #{token}"
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true if 'https' == uri.scheme
response = http.request(request)
data = JSON.parse(response.body)
The example above produces this response.
{
"pagination": {
"current_page": 1,
"total_pages": 1,
"total": 2
},
"results": [
{
"id": 4,
"owner_id": 10,
"owner_type": "Partner",
"url": "https://example.com/api/hover/v1/webhook",
"last_error": null,
"verified_at": "2017-11-03T02:43:08.221Z",
"created_at": "2017-11-03T02:39:07.221Z",
"updated_at": "2017-11-03T02:39:07.221Z"
},
{
"id": 8,
"owner_id": 10,
"owner_type": "Partner",
"url": "https://data-collector.example.com/partners/hover/webhook",
"last_error": "Code: 500, Body: {'error': 'internal server error'}",
"verified_at": null,
"created_at": "2017-11-03T02:39:07.221Z",
"updated_at": "2017-11-03T02:39:07.221Z"
}
]
}
This endpoint retrieves all webhooks belonging to the authenticated organization.
HTTP Request
GET https://hover.to/api/v2/webhooks
Delete a Specific Webhook
require 'net/http'
webhook_id = 'your webhook ID returned in the first JSON response'
token = 'Auth_Token'
url = "https://hover.to/api/v2/webhooks/#{webhook_id}"
uri = URI.parse(url)
request = Net::HTTP::Delete.new(uri)
request["Authorization"] = "Bearer #{token}"
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true if 'https' == uri.scheme
response = http.request(request)
The above command returns an EMPTY response body.
This endpoint deletes a specific webhook. You need to be authenticated as the organization that owns the webhook.
HTTP Request
DELETE https://hover.to/api/v2/webhooks/<WEBHOOK-ID>
URL Parameters
Parameter | Description |
---|---|
ID | The ID of the webhook to delete |
Webhook Signature Verification
require 'net/http'
require 'digest/md5'
require 'base64'
require 'json'
require 'openssl'
# The incoming request. Normally your server would be recieving this.
# Here we construct it for testing.
url = "https://hover.to"
uri = URI.parse(url)
incoming_request = Net::HTTP::Post.new(uri)
incoming_request.content_type = 'application/json'
incoming_request.body = {
"event" =>"webhook-verification-code",
"code" => "9ed069d5-97d6-42f0-bc09-423aada6cc0e",
"webhook_id" => 244
}.to_json
incoming_request['DATE'] = 'Fri, 14 Sep 2018 13:23:40 GMT'
incoming_request['Content-MD5'] = Digest::MD5.base64digest(incoming_request.body)
incoming_request['Authorization'] = 'APIAuth 244:OuSqLQ06ch/ANESLo7vp5En7KTo='
# A function to authenticate incoming requests
def authenticate_webhook(request, hmac_id, hmac_secret)
canonical_string = [
request.content_type,
Digest::MD5.base64digest(request.body),
request.path,
request['DATE']
].join(",")
digest = OpenSSL::Digest.new('sha1')
signature = Base64.strict_encode64(OpenSSL::HMAC.digest(digest, hmac_secret, canonical_string))
header = "APIAuth #{hmac_id}:#{signature}"
request['Authorization'] == header
end
# Use the authentication function
hmac_secret = "YupC68b+PV+qUxyJj2yRImSvftkpcD3Bh1ip7vUC2Fnk4LS+IdL9nOq8eK267At3UxFztmKJXTGI41zDVOkPMg=="
hmac_id = 244
result = authenticate_webhook(incoming_request, hmac_id, hmac_secret)
puts "Authenticated: #{result}"
Our webhooks include a "Authorization" header with an HMAC signature. By using the hmac_secret
included in the response JSON when you first create the webhook, you can generate this signature. If the generated signature matches the HMAC signature included in the header, you can verify the that the webhook is coming from us. The steps outlined below indicate how to generate this signature:
Create a canonical string using the HTTP headers from the incoming webhook using the content-type, content-MD5, request URI and the timestamp. If content-type or content-MD5 are not present, then a blank string should be used in their place. If the timestamp isn't present, a valid HTTP date is automatically added to the request. The canonical string string is computed as follows:
canonical_string = 'content-type,content-MD5,request URI,timestamp'
Use the
canonical_string
to create the signature which is a Base64 encoded SHA1 HMAC, using your private hmac_secret key. This key should be kept secret, and it is known only to you and HOVER. This is returned in the first request when you create the webhook.Confirm that the signature included in the incoming webhook header matches the one you generated. We use the same process to generate the signature on our outbound webhooks. If they match, you can verify the webhook came from us.
Authorization = APIAuth webhook_id:signature_from_step_2
Note that the webhook_id
is unique to each webhook you create, and is included in the JSON response when creating the webhook.
ex: 'Authorization' = 'APIAuth 244:OuSqLQ06ch/ANESLo7vp5En7KTo='
Capture Requests
Capture requests are used to invite another user outside of your Org to capture a job for you. For example a contractor can create a capture request that's sent to a homeowner.
The Homeowner or Pro User captures the job and both the capturing user and creator get access to the job once complete.
Once a capture request is created either the user requesting the capture or the user receiving the capture request can capture it.
After creation we'll send notifications to capturing_user_email
and capturing_user_phone
, instructing them to download the app and take the photos.
It's possible for either of the users involved in a capture request to complete the capture. To know who actually took the photos you can look at the job's "captured_user_id" attribute.
The job is created along with the capture request. If you would like to create a job without notifying an external stakeholder to capture the property, use the create a job endpoint to create a job record.
Create a Capture Request
require 'net/http'
require 'json'
token='your OAuth authorization token'
url = 'https://hover.to/api/v2/capture_requests'
uri = URI.parse(url)
request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer #{token}"
request.set_form_data({
"capture_request[capturing_user_name]" => "Luke Groundrunner",
"capture_request[capturing_user_email]" => "[email protected]",
"capture_request[capturing_user_phone]" => "123-456-7890",
"capture_request[job_attributes][name]" => "Capture Request Test Job",
"capture_request[job_attributes][location_line_1]" => "634 2nd St.",
"capture_request[job_attributes][location_line_2]" => "Suite 300",
"capture_request[job_attributes][location_city]" => "San Francisco",
"capture_request[job_attributes][location_region]" => "California",
"capture_request[job_attributes][location_postal_code]" => "94107",
"capture_request[job_attributes][deliverable_id]" => 3,
"current_user_email" => "[email protected]"
})
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true if 'https' == uri.scheme
response = http.request(request)
data = JSON.parse(response.body)
When successful you'll get a 201 response code with the following JSON in the response body.
{
"id": 41440,
"capturing_user_name": "Luke Groundrunner",
"capturing_user_phone": "",
"capturing_user_email": "[email protected]",
"capturing_user_id": 332141,
"requesting_user_id": 307444,
"identifier": "Zjz-Nw",
"claim_number": null,
"state": "connected",
"signup_type": "homeowner",
"job_attributes": {
"name": "Capture Request Test Job",
"location_line_1": "634 2nd St.",
"location_line_2": "Suite 300",
"location_city": "San Francisco",
"location_region": "California",
"location_postal_code": "94107",
"external_identifier": "",
"deliverable_id": 3,
"client_identifier": "be0d7649-c510-4a5b-a1de-17a56224eda7"
},
"captured_user_id": null,
"pending_job_id": 1500715,
"requesting_user": {
"name": "Han Duet",
"email": "[email protected]",
"org": {
"id": 200016,
"name": "Capture Request Test Job",
"preferences": {
"external_identifier_label": "Lead Number",
"external_identifier_required": false
}
}
},
"org_id": 200016
}
When there's an error creating the capture request you'll get a 422 response code. The JSON in the response body will explain the error.
{
"capturing_user_name": ["can't be blank"],
"job_attributes": ["must have address"]
}
This endpoint creates a capture request. A job record is created at the same time. You can include any parameters supported by the Create a Job endpoint in the job_attributes attribute of the capture request. Those attributes will be used when creating the job that the capture request endpoint creates.
HTTP Request
POST https://hover.to/api/v2/capture_requests
URL Parameters
Parameter | Description |
---|---|
capture_request[capturing_user_name] | Required. The name of the person that will be capturing the property. Most likely the homeowner's name. |
capture_request[capturing_user_email] | Required. The email address of the person this capture request will be sent to. |
capture_request[capturing_user_phone] | Optional. The phone number of the person this capture request will be sent to. |
capture_request[job_attributes][name] | Optional. A name for this job |
capture_request[job_attributes][deliverable_id] | Optional. The deliverable you want assigned to the request. Supported deliverable_ids: 2(Roof Only), 3(Complete), 5(Total Living Area Plus), 6(Total Living Area), 7(Capture Only, Photos Only) |
capture_request[job_attributes][location_line_1] | Required. First line of this job's address |
capture_request[job_attributes][location_line_2] | Optional. Second line of this job's address |
capture_request[job_attributes][location_city] | Optional. The city of this job's address |
capture_request[job_attributes][location_region] | Optional. The state of this job's address |
capture_request[job_attributes][location_postal_code] | Optional. The postal code of this job's address |
capture_request[job_attributes][external_identifier] | Optional. A unique identifier generated by the client. This can be anything you want and won't be changed. It can be used to reference an ID in the client application's database. |
capture_request[job_attributes][suppress_email] | Optional. If a value other than false is given for this attribute then all emails related to this job will be suppressed. That includes capture request emails. |
capture_request[job_attributes][wallet_id] | Optional. By passing this parameter you can specify the billing information for the job. |
current_user_email or current_user_id | Required. The internal user that you want to assign the job to. |
capture_request[signup_type] | Optional. This defaults to homeowner, indicating that if the user is not signed up yet, they will be signed up as a Homeowner. If you set this parameter to pro, the user will be signed up as a pro user within whatever HOVER partner is sending the request. |
Get Download/Signup/Capture Link
require 'net/http'
url = "https://hover.to/api/v2/capture_requests/#{capture_request_id}"
uri = URI.parse(url)
request = Net::HTTP::Get.new(uri)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true if 'https' == uri.scheme
response = http.request(request)
if response.code == '302'
capture_link = response.header['location']
else
puts "Error: HTTP #{response.code}"
end
curl --location --request GET 'https://hover.to/api/v2/capture_requests/#{capture_request_id}'
When successful you'll get a 302 response code with the redirect URL in the "Location" header.
If the capture request cannot be found you'll get a 404 response code with an empty response body.
This endpoint returns a URL to download our mobile application, signup, and associate the new user account with the capture request.
The CaptureRequest-ID
maps to the identifier
field returned in the JSON response when initially creating the Capture Request. Note that this request does not require an access token.
HTTP Request
GET https://hover.to/api/v2/capture_requests/<CaptureRequest-ID>
List Capture Requests
require 'net/http'
require 'json'
token = 'your OAuth authorization token'
url = "https://hover.to/api/v2/[email protected]"
uri = URI.parse(url)
request = Net::HTTP::Get.new(uri)
request["Authorization"] = "Bearer #{token}"
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true if 'https' == uri.scheme
response = http.request(request)
data = JSON.parse(response.body)
curl --location --request GET 'https://hover.to/api/v2/[email protected]' \
--header 'Authorization: Bearer #{token}'
When successful you'll get a 200 response code with the following JSON in the response body.
{
"requested_captures": {
"pagination": { "current_page": 1, "total_pages": 1, "total": 1 },
"results": {
"id": 4820,
"capturing_user_name": "Loak Groundrunner",
"capturing_user_phone": "555-555-5555",
"capturing_user_email": "[email protected]",
"job_attributes": {
"name": "Capture Request Test Job",
"location_line_1": "634 2nd St.",
"location_line_2": "Suite 300",
"location_city": "San Francisco",
"location_region": "California",
"location_postal_code": "94107"
},
"claim_number": null,
"requesting_user_id": 48420,
"pending_job_id": 320932,
"job_id": null,
"identifier": "C313C1F6-EC32-4719-B1E9-EC0BE88C49DC",
"requesting_user": {
"name": "Hand Duet",
"email": "[email protected]"
},
"created_at": "2017-11-03T02:39:07.221Z",
"updated_at": "2017-11-03T02:39:07.221Z"
}
},
"capture_requests": {
"pagination": { "current_page": 1, "total_pages": 1, "total": 0 },
"results": []
}
}
This endpoint lists capture requests. The response has two paginated lists. "capture_requests", and "requested_captures". Each is a list of capture requests. The "requested_captures" list contains the captures that this user requested. The "capture_requests" list contains capture requests that another user requested this user capture.
If you're authenticated as an org, then the "requested_captures" list includes all captures requests requested by anyone in that org. And the "capture_requests" list contains all capture requests that users of that org have been invited to capture.
Determining Capture Request State
- If job_id and capturing_user_id is null then the capture request is in the "new" state.
- if job_id is null and capturing_user_id isn't then the capture request is in the "connected" state.
- If job_id and capturing_user_id are set then the request is in the "complete" state.
HTTP Request
GET https://hover.to/api/v2/capture_requests
URL Parameters
Parameter | Description |
---|---|
current_user_email | The email for the capturing user. |
Check for Existing Capture Request
require "net/http"
require 'json'
token = "your OAuth authorization token"
url = 'https://hover.to/api/v2/capture_requests/[email protected]'
uri = URI.parse(url)
request = Net::HTTP::Get.new(uri)
request["Authorization"] = "Bearer #{token}"
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true if 'https' == uri.scheme
response = http.request(request)
data = JSON.parse(response.body)
curl --location --request GET 'https://hover.to/api/v2/capture_requests/[email protected]' \
--header 'Authorization: Bearer #{token}'
When successful you'll get a 200 response code with the following JSON in the response body.
{
"pending_capture_request_exists": true,
"capturing_user_exists": false,
"requesting_user_name": "Han Duet"
}
Checks to see if there is an existing capture request waiting for the user specified by the email parameter.
HTTP Request
GET https://hover.to/api/v2/capture_requests/exists
URL Parameters
Parameter | Description |
---|---|
Check if there's a capture request for this email address. |
Delete a Capture Request
require 'net/http'
token = "your OAuth authorization token"
url = "https://hover.to/api/v2/capture_requests/#{capture_request_id}[email protected]"
uri = URI.parse(url)
request = Net::HTTP::Delete.new(uri)
request["Authorization"] = "Bearer #{token}"
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true if 'https' == uri.scheme
response = http.request(request)
curl --location --request DELETE 'https://hover.to/api/v2/capture_requests/#{capture_request_id}' \
--header 'Authorization: Bearer #{token}'
If we were able to delete the capture request you'll get a 200 response code
If we weren't able to delete the capture request you'll get a 422 response code with an explanation of what went wrong
{
"job": ["has been captured"]
}
Deletes a capture request. Capture requests can't be deleted after the job is captured. They can be deleted anytime before that. The corresponding job state in which a capture request can be deleted is uploading
HTTP Request
DELETE https://hover.to/api/v2/capture_requests/<CaptureRequest-ID>
URL Parameters
Parameter | Description |
---|---|
ID | The ID of the capture request to delete |
current_user_email | The user you want to delete the capture request on behalf of. This would usually be an admin user. |
Resend Notifications To Capturing User
require 'net/http'
token = "your OAuth authorization token"
url = "https://hover.to/api/v2/capture_requests/#{capture_request_id}/resend_notifications"
uri = URI.parse(url)
request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer #{token}"
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true if 'https' == uri.scheme
response = http.request(request)
curl --location --request POST 'https://hover.to/api/v2/capture_requests/#{capture_request_id}/resend_notifications' \
--header 'Authorization: Bearer #{token}'
If the capture request exists and you have permission to access it you'll always get a 200 with an empty response body
Resends the email and SMS message to remind the capturing user to sign up and take the photos.
HTTP Request
POST https://hover.to/api/v2/capture_requests/<CaptureRequest-ID>/resend_notifications
Jobs
Create A Job
require 'net/http'
require 'json'
token = "your OAuth authorization token"
url = "https://hover.to/api/v1/jobs"
uri = URI.parse(url)
request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer #{token}"
request.set_form_data({
"job[name]" => "Test Job",
"job[location_line_1]" => "634 2nd St.",
"job[location_line_2]" => "Suite 300",
"job[location_city]" => "San Francisco",
"job[location_region]" => "California",
"job[location_postal_code]" => "94107",
"job[location_lat]" => 37.773336,
"job[location_lon]" => -122.405547,
"job[customer_name]" => "Luke Groundrunner",
"job[customer_email]" => "[email protected]",
"job[customer_phone]" => "123-456-7890",
"current_user_email" => "[email protected]"
})
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true if 'https' == uri.scheme
response = http.request(request)
data = JSON.parse(response.body)
When successful you'll get a 201 response code with the following JSON in the response body.
{
"id": 11,
"name": "Test Job",
"location_line_1": "634 2nd St.",
"location_line_2": "Suite 300",
"location_city": "San Francisco",
"location_region": "California",
"location_postal_code": "94107",
"location_country": "United States",
"location_lat": "37.773336",
"location_lon": "-122.405547",
"created_at": "2018-03-14T20:29:54.366Z",
"updated_at": "2018-03-14T20:29:54.537Z",
"uploaded_at": null,
"reported_at": null,
"customer_name": null,
"customer_phone": null,
"customer_email": null,
"archive_number": null,
"archived_at": null,
"customer_first_name": null,
"example": false,
"quote_user_id": null,
"customer_contact_only_by_email": false,
"customer_contact": false,
"contractor_estimate_sent_to": null,
"shared": false,
"original_job_id": null,
"machete_features": [],
"customer_notes": null,
"estimates_count": 0,
"org_job_accesses_assigned_lead_count": 0,
"deliverable_id": 2,
"mobile_application_id": null,
"estimated_hours_to_completion": 0.0,
"approved": true,
"completed_at": null,
"approved_at": null,
"first_completed_at": null,
"approving_user_id": null,
"test_state": "no",
"external_identifier": null,
"archived": false,
"org_id": 25,
"user_id": 30,
"captured_user_id": 30,
"state": "uploading",
"images": [{"id": 2}, {"id": 3}],
"via_job_share": null,
"via_org_job_accesses": [
{
"id": 12,
"org_id": 25,
"lead_state": "assigned",
"kind": "creator",
"ordering_state": "lead"
}
],
"via_job_assignments": [{"id": 12, "kind": "creator"}]
}
When there's an error creating the job you'll get a 422 response code.
This endpoint creates a job. The mobile application can then be used to add images to the job and put it through the processing pipeline. It's not necessary to create a job using this endpoint in most cases. Using the mobile app to capture a property automatically creates a job when it uploads the images. Creating a capture request also creates an associated job automatically.
This endpoint, when used with Org-level OAuth 2.0, requires a params[current_user_email]
or params[current_user_id]
. Otherwise, a 422 unprocessable entity may be returned as a response.
HTTP Request
POST https://hover.to/api/v1/jobs
URL Parameters
Parameter | Description |
---|---|
job[customer_name] | The name of the customer. Usually the homeowner. |
job[customer_email] | The email address of the customer. Usually the homeowner. |
job[customer_phone] | The phone number of the customer. Usually the homeowner. |
job[name] | A name for this job |
job[location_line_1] | First line of this job's address |
job[location_line_2] | Second line of this job's address |
job[location_city] | The city of this job's address |
job[location_region] | The state of this job's address |
job[location_postal_code] | The postal code of this job's address |
job[location_postal_lat] | The postal code of this job's address |
job[location_postal_lon] | The postal code of this job's address |
job[location_country] | The postal code of this job's address |
job[deliverable_id] | Supported deliverable_ids: 2(Roof Only), 3(Complete), 5(Total Living Area Plus), 6(Total Living Area), 7(Capture Only, Photos Only) |
job[customer_contact_only_by_email] | true or false |
job[test_state] | This optional parameter causes a job to be a "test" job and to skip our processing pipeline. Supported values are "complete" and "failed". "complete" auto completes your job with example results. "failed" automatically fails the job. This parameter is optional and shouldn't be used on real jobs. |
job[external_identifier] | A unique identifier generated by the client. This can be anything you want and won't be changed. It can be used to reference an ID in the client application's database. |
job[suppress_email] | If a value other than false is given for this attribute then all emails related to this job will be suppressed. That includes capture request emails. |
job[wallet_id] | Optional. By passing this parameter you can specify the billing information for the job. |
current_user_email | Required. The internal user that you want to assign the job to. Alternatively you can also use the current_user_id param. |
current_user_id | Required. The internal user that you want to assign the job to. Alternatively you can also use the current_user_email param. |
Test Jobs
require 'net/http'
require 'json'
token = 'your OAuth authorization token'
url = "https://hover.to/api/v2/jobs/#{job_id}/set_test_state.json"
uri = URI.parse(url)
request = Net::HTTP::Get.new(uri)
request.set_form_data({"current_user_id" => job.user_id, "state" => "complete"})
request["Authorization"] = "Bearer #{token}"
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true if 'https' == uri.scheme
response = http.request(request)
data = JSON.parse(response.body)
curl --location --request PATCH 'https://hover.to/api/v2/jobs/#{job_id}/set_test_state.json?state=completed' \
--header 'Authorization: Bearer #{token}'
When successful you'll get a 200 response code with an empty body. If there was an error you'll get a 422 response code with a JSON response explaining the problem
{
"test_state": ["cannot set while in the \"working\" state"]
}
This endpoint makes a job a test job and sets it's final state. After getting a successful response from this endpoint the job will skip the real processing pipeline and return fake results. Payment will also be skipped. You will not be charged for test jobs.
Setting the test state only works when the job is in the uploading or processing_upload states. Once the job has progressed beyond that point it's already entered our processing pipeline and it's too late to set it's test state.
By default captured_user_id will be updated to the job creator's id. For capture request job that the capturing user performed the capture, pass in the capturing_user_captured flag and the captured_user_id will be updated to capturing user's id.
If params[original_job_id]
is given, the test job will be copying the images from the original job if the test job is
in the uploading state; the test job will be copying the model files (e.g. measurements) from the original job if the
test job is in the processing_upload state. You must have access to the original job or the endpoint will return 404.
The original job must also be in the complete state or the endpoint will return 422. params[state]
will be ignored
and the test job will eventually end up in the complete state.
HTTP Request
PATCH https://hover.to/api/v2/jobs/<Job-ID>/set_test_state.json
URL Parameters
Parameter | Type | Description |
---|---|---|
state | string | Supported values are "complete" and "failed". If you pass complete the job will be completed and returned with example results. If you pass failed the job will be failed with no results. |
original_job_id | integer | Optional. Specify the original_job_id that you want to copy the image and model files from for the test job. params[state] will be ignored if this param is given. |
capturing_user_captured | string | Optional. It can be any string. If this flag exists and the job has pending capture request, the captured_user_id will be updated to capturing user's id. |
List Jobs
require 'net/http'
require 'json'
token='your OAuth authorization token'
url = "https://hover.to/api/v2/jobs?states[]=processing&states[]=complete&mine=true"
uri = URI.parse(url)
request = Net::HTTP::Get.new(uri)
request["Authorization"] = "Bearer #{token}"
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true if 'https' == uri.scheme
response = http.request(request)
data = JSON.parse(response.body)
curl --location --request GET 'https://hover.to/api/v2/jobs/' \
--header 'Authorization: Bearer #{token}'
When successful you'll get a 200 response code with the following JSON in the response body.
{
"pagination": {
"current_page": 1,
"total": 30,
"total_pages": 2
},
"meta": {
"request_received_at": 1475066300
},
"results": [
{
"archived": false,
"name": "user shared",
"customer_notes": null,
"deliverable_id": 4,
"location_line_1": "634 2nd St.",
"location_line_2": "Suite 300",
"location_city": "San Francisco",
"location_region": "California",
"location_postal_code": "94107",
"location_country": "United States",
"location_lat": "41.585462",
"location_lon": "-73.878935",
"id": 24,
"estimated_hours_to_completion": 0.0,
"approved": true,
"example": false,
"updated_at": "2018-03-14T20:27:47.834Z",
"completed_at": null,
"approved_at": null,
"created_at": "2018-03-14T20:27:45.823Z",
"org_id": 49,
"user_id": 50,
"captured_user_id": 50,
"search_rank": null,
"state": "complete",
"via_job_share": 1,
"via_org_job_accesses": [],
"via_job_assignments": []
},
{
"archived": false,
"name": "Job X",
"customer_notes": null,
"deliverable_id": 2,
"location_line_1": "634 2nd St.",
"location_line_2": "Suite 300",
"location_city": "San Francisco",
"location_region": "California",
"location_postal_code": "94107",
"location_country": "United States",
"location_lat": "41.585462",
"location_lon": "-73.878935",
"id": 33,
"estimated_hours_to_completion": 0.0,
"approved": true,
"example": false,
"updated_at": "2018-03-14T20:27:47.712Z",
"completed_at": null,
"approved_at": null,
"created_at": "2018-03-14T20:27:47.561Z",
"org_id": 45,
"user_id": 42,
"captured_user_id": 42,
"search_rank": null,
"state": "paying",
"via_job_share": null,
"via_org_job_accesses": [
{
"id": 37,
"org_id": 45,
"lead_state": "assigned",
"kind": "creator",
"ordering_state": "lead"
}
],
"via_job_assignments": [{"id": 36, "kind": "creator"}]
}
]
}
This endpoint lists jobs.
There are several ways you might have access to a job. One way is via a "JobShare", a mechanism used to share jobs via email, sms, social media, and links. Another way is through our "OrgJobAccess" join table that associates jobs with orgs. You can see which method gave you access to this job by looking at the "via_job_share" and "via_org_job_accesses" attributes in the JSON. You can revoke access to a job using the OrgJobAccess's "id" attribute.
HTTP Request
GET https://hover.to/api/v2/jobs
URL Parameters
For the array type parameters passing multiple values returns jobs that match any of those values. It's not required that the returned jobs match all values, just one. For each supported value that isn't given in the array jobs matching that value are excluded unless they match another value for the same parameter. If no values are given the filter is not applied. Passing this for states "['processing', 'needs_photos', 'needs_approval']" would result in jobs that are in any of those states, but are not in any of the other supported states "['complete', 'failed']".
Parameter | Type | Description |
---|---|---|
states | Array | Supported states: "complete", "failed", "processing", "needs_approval", "needs_photos". |
types | Array | Supported types: "standard", "connect", "prospect". |
deliverable_id | Array | Supported deliverable_ids: 2(Roof Only), 3(Complete), 5(Total Living Area Plus), 6(Total Living Area), 7(Capture Only, Photos Only) |
archived | Boolean | Defaults to either. When 'true' we only return archived jobs. When 'false' archived jobs will be excluded. |
example | Boolean | Defaults to either. When 'true' we only return example jobs. When 'false' example jobs will be excluded. |
mine | Boolean | Defaults to either. When 'true' we only return the logged in user's jobs. When 'false' we only returned jobs belonging to the team of the logged in user, excluding jobs belonging to the logged in user. |
search | String | Filter jobs by search text, minimum 3 characters required |
sort_order | String | Defaults to 'DESC'. 'ASC' or 'DESC' |
sort_by | String | Defaults to 'updated_at' when searching. 'completed_at', 'created_at', or 'updated_at' |
page | Number | Defaults to 1. Return this page of results |
per | Number | Defaults to 25. Return this many jobs per page. |
updated_since | Number | Seconds since the EPOCH. Only return jobs that have been updated since this time. |
Get A Job's Details
require 'net/http'
require 'json'
token='your OAuth authorization token'
url = "https://hover.to/api/v2/jobs/481456"
uri = URI.parse(url)
request = Net::HTTP::Get.new(uri)
request["Authorization"] = "Bearer #{token}"
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true if 'https' == uri.scheme
response = http.request(request)
data = JSON.parse(response.body)
curl --location --request GET 'https://hover.to/api/v2/jobs/#{job_id}' --header 'Authorization: Bearer #{token}'
When successful you'll get a 200 response code with the following JSON in the response body.
{
"id": 481456,
"name": "1928 Haverwood",
"location_country": null,
"location_line_1": "1928 Haverwood Lane",
"location_line_2": null,
"location_city": "Plano",
"location_region": "Texas",
"location_postal_code": "75226",
"location_country": "USA",
"location_lat": "-112.3930",
"location_lon": "42.23929",
"created_at": "2017-12-11T20:11:43.851Z",
"updated_at": "2017-12-11T21:44:59.057Z",
"uploaded_at": null,
"reported_at": null,
"customer_name": null,
"customer_phone": null,
"customer_email": null,
"archive_number": null,
"archived_at": null,
"customer_first_name": null,
"example": false,
"quote_user_id": null,
"customer_contact_only_by_email": false,
"customer_contact": null,
"contractor_estimate_sent_to": null,
"shared": false,
"original_job_id": null,
"machete_features": ["measurements"],
"customer_notes": null,
"estimates_count": 0,
"org_job_accesses_assigned_lead_count": 0,
"deliverable_id": 3,
"mobile_application_id": 6,
"estimated_hours_to_completion": 3.96169773287556,
"approved": true,
"completed_at": "2017-12-11T21:44:59.079Z",
"approved_at": null,
"first_completed_at": "2017-12-11T21:44:59.079Z",
"approving_user_id": null,
"archived": false,
"org_id": 12161,
"user_id": 5,
"captured_user_id": 5,
"state": "complete",
"images": [
{"id": 5734793},
{"id": 5734794},
{"id": 5734795},
{"id": 5734796},
{"id": 5734800},
{"id": 5734801},
{"id": 5734797},
{"id": 5734798},
{"id": 5734799}
],
"additional_captured_jobs": [
{"id": 481458},
{"id": 481459},
{"id": 481464}
],
"via_job_share": null
}
HTTP Request
GET https://hover.to/api/v2/jobs/<Job-ID>
Get A Job's Primary Image
require 'net/http'
token = 'your OAuth authorization token'
url = "https://hover.to/api/v2/jobs/481456.jpg"
uri = URI.parse(url)
request = Net::HTTP::Get.new(uri)
request["Authorization"] = "Bearer #{token}"
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true if 'https' == uri.scheme
response = http.request(request)
File.open("image.jpg", "wb") do |file|
file.write(response.body)
end
curl --location --request GET 'https://hover.to/api/v2/jobs/481456.jpg' \
--header 'Authorization: Bearer #{token}'
When successful you'll be redirected to the image file.
HTTP Request
GET /api/v2/jobs/<Job-ID>.jpg
Get A Job's Measurements
require 'net/http'
token = 'your OAuth authorization token'
url = "https://hover.to/api/v2/jobs/481456/measurements.pdf"
uri = URI.parse(url)
request = Net::HTTP::Get.new(uri)
request["Authorization"] = "Bearer #{token}"
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true if 'https' == uri.scheme
response = http.request(request)
File.open("measurements.pdf", "wb") do |file|
file.write(response.body)
curl --location --request GET 'https://hover.to/api/v2/jobs/481456/measurements.pdf' \
--header 'Authorization: Bearer #{token}'
When successful you'll be redirected to the measurement file or with a JSON in the response body.
Notice the only thing that changes in the URLs below is the extension. This specifies the format the measurements will be returned in.
HTTP Request
GET /api/v2/jobs/<Job-ID>/measurements.pdf
GET /api/v2/jobs/<Job-ID>/measurements.json
GET /api/v2/jobs/<Job-ID>/measurements.xlsx
GET /api/v2/jobs/<Job-ID>/measurements.xml
Parameter | Value | Description |
---|---|---|
version | This option is available only for the JSON format and is optional. By default the 3D representation of the model is returned. | |
summarized_json | For summarized measurement data, use version=summarized_json |
|
full_json | For summarized and detailed measurement data, use version=full_json |
|
sketch_json | (Total Living Area only) For JSON-formatted file of the Total Livng Area footprint and measurements, use version=sketch_json |
See Job Measurement Data Summarized Version
See Job Measurement Data Full Version
See 3D Representation of a Job
Revoke An Org's Access To A Job
require 'net/http'
token='your OAuth authorization token'
url = "https://hover.to/api/v1/org_job_accesses/3290"
uri = URI.parse(url)
request = Net::HTTP::Delete.new(uri)
request["Authorization"] = "Bearer #{token}"
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true if 'https' == uri.scheme
response = http.request(request)
curl --location --request DELETE 'https://hover.to/api/v1/org_job_accesses/3290' \
--header 'Authorization: Bearer #{token}'
When successful you'll receive a 204 response code and no body.
When there is an error you'll receive a 422 response code with a json explaining what went wrong.
This endpoint removes an org's access to a job. You can get the ID of an OrgJobAccess record from the job's index/list response.
HTTP Request
DELETE /api/v1/org_job_accesses/<OrgJobAccess-ID>
Revoke A User's Access To A Job
require 'net/http'
token='your OAuth authorization token'
url = "https://hover.to/api/v1/job_assignments/34920"
uri = URI.parse(url)
request = Net::HTTP::Delete.new(uri)
request["Authorization"] = "Bearer #{token}"
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true if 'https' == uri.scheme
response = http.request(request)
curl --location --request DELETE 'https://hover.to/api/v1/job_assignments/34920' \
--header 'Authorization: Bearer #{token}'
When successful you'll receive a 204 response code and no body.
When there is an error you'll receive a 422 response code with a json formatted explanation of what went wrong.
This endpoint removes a user's access to a job. You can get the ID of the JobAssignment record from the job's index/list response.
HTTP Request
DELETE /api/v1/job_assignments/<JobAssignment-ID>
Re-Assign A Job To A User
require 'net/http'
url = "https://hover.to/api/v2/jobs/42902/reassign"
uri = URI.parse(url)
request = Net::HTTP::Post.new(uri)
request.set_form_data({
"user_id" => 239,
"access_token" => oauth_token
})
http = Net::HTTP.new(uri.host, uri.port)
response = http.request(request)
curl --location --request POST 'https://hover.to/api/v2/jobs/42902/reassign' \
--header 'Authorization: Bearer #{token}' \
--form 'user_id=239'
When successful you'll receive a 204 response code and no body.
When there is an error you'll receive a 40x response code
This endpoint re-assigns a job to the user specified by the user_id parameter.
It will remove the creator JobAssignments before assigning this job to the specified user. So if a job is created by user A and is captured by user B, and you use this endpoint to re-assign it to user C, user A will lose access and user C will gain access. User C will become the job creator and gains the ability to re-assign. User B is still the capturer and will maintain the job access.
When logged in as the job creator or an org admin from the same org, you can assign job to another user in this org.
When authenticated as an org, you can re-assign jobs under this org to another user under this org.
When authenticated as a partner, you can re-assign jobs from orgs under this partner to another user from orgs under this partner.
HTTP Request
POST https://hover.to/api/v2/jobs/<Job-ID>/reassign
Parameter | Type | Description |
---|---|---|
user_id | Integer | The ID of the user you want to assign the job to. |
access_token | string | Oauth token. Required for org and partner level authentications. |
Archive Job Access
require 'net/http'
token = "your OAuth authorization token"
url = "https://hover.to/api/v1/jobs/12345/archive"
uri = URI.parse(url)
request = Net::HTTP::Put.new(uri)
request["Authorization"] = "Bearer #{token}"
request.set_form_data({
"current_user_id" => 123
})
http = Net::HTTP.new(uri.host, uri.port)
response = http.request(request)
curl --location --request PUT 'https://hover.to/api/v2/jobs/12345/reassign' \
--header 'Authorization: Bearer #{token}' \
--form 'user_id=123'
When successful you'll receive a 204 response code and no body.
When trying to archive an archived job or a job that cannot be found under your current authenticated role, you will receive 404 response code.
When archiving a job as a partner but not all accesses can be archived, you'll receive a 422 response code. Operation will be rolled back and no change will be made.
When there is an authentication error, you'll receive a 401 response code.
This endpoint archives the job access. After archiving, this job will no longer show up on user's property list.
When logged in as the job creator, user with access to the job, or job org admin, you can archive job access for current user org
When authenticated as the job creator's org, you can archive job access for current org
When authenticated as the job creator's partner, you can archive all job accesses at once
HTTP Request
PUT https://hover.to/api/v1/jobs/<Job-ID>/archive
Parameter & Type | Description |
---|---|
See current user | The user who you impersonated to archive the job. |
Job Exports
Get A Job As ESX Export
Provided your Org has the ESX Export feature enabled, you can get an ESX (Xactimate compatible) file from this endpoint.
require 'net/http'
token = "your OAuth authorization token"
job_id= 2160222
url = "https://hover.to/api/v2/jobs/#{job_id}/exports/esx"
uri = URI.parse(url)
request = Net::HTTP::Get.new(uri)
request["Authorization"] = "Bearer #{token}"
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true if 'https' == uri.scheme
response = http.request(request)
while response.code == '301' || response.code == '302'
response = http.get(URI.parse(response.header['location']))
end
if response.code == '200'
File.open("example.esx", "wb") do |file|
file.write(response.body)
end
else
puts "Error: HTTP #{response.code}"
end
curl --location --output example.esx --request GET 'https://hover.to/api/v2/jobs/2055501/exports/esx' \
--header 'Authorization: Bearer #{token}'
When successful you'll be redirected to the associated ESX file.
HTTP Request
GET https://hover.to/api/v2/jobs/<Job-ID>/exports/esx
Job Shares
Job shares can be useful for providing read only access to another HOVER user, someone without a HOVER account, or 3rd party mobile apps that want to make API requests directly.
Sharing A Job
When successful or trying to create a job share that already exists, you'll receive a 201 response code with the job share json.
{
"job_share": {
"id": 1,
"job_id": 123,
"user_id": 456,
"recipient_id": null,
"token": "TOKEN",
"external_service": null,
"external_identifier": null,
"expired": false,
"expires_at": "2014-12-16T02:47:49.626Z",
"url": "https://hover.to/shared/TOKEN",
"image_url": "https://hover.to/api/v1/job_shares/1/image.png?share_token=TOKEN",
"video_url": "https://hover.to/api/v1/models/789/video.mp4?share_token=TOKEN",
"open_graph_url": "https://hover.to/shared/TOKEN",
"street_address": "Pope Road, SF, California.",
"state": null
}
}
When there is an error you'll receive a 422 response code.
You can use this endpoint to retrieve job share token, or share your job through email or social media. Passing in optional parameters can trigger different actions.
To share to an external service you must first request a share token from the server. This token will allow a visitor to access the shared resources with or without a HOVER account.
The open graph based sharing methods supported by most social networks are supported by sharing the open_graph_url
attribute instead of the url
. For older methods you can share the url attribute.
HTTP Request
POST https://hover.to/api/v1/job_shares
Parameters | Description |
---|---|
job_share[job_id] | The job that you would like to share with others. |
Optional. Job share recipient email address(es). Separate emails by comma if passing multiple emails. If it's provided, we will email recipients with a link to view the job you've shared with them. | |
share_token | Optional. This can be used instead of job_id in order to update an existing job share. |
external_service | Optional. This field is used to help us track how this job share was meant to be used. We don't take any action with this data besides using it to look at popularity across different sharing methods, or to find an incomplete set of links to shared jobs on social media. For example, if you intend to post this to facebook you might pass "Facebook" as external_service and the ID of the facebook post as the external_identifier. |
external_identifier | Same as above. |
current_user_email | Required. Or use current_user_id. |
current_user_id | Required. Or use current_user_email. |
After Sharing
After successfully posting to the external service you should Update Job Share with the ID of the post on the external service. When posting to facebook for example you get back a "post_id" attribute that looks like this "752113671538317_752903771459307".
When Sharing Fails
If something goes wrong when sharing or the user cancels, we can Delete Job Share.
List Job Shares
When successful, you'll receive a 200 response code with json body.
{
"job_shares": [
{
"id": 123,
"job_id": 456,
"user_id": 789,
"recipient_id": null,
"token": "TOKEN",
"external_service": null,
"external_identifier": null,
"expired": false,
"expires_at": "2017-04-11T18:37:12.260Z",
"url": "https://hover.to/shared/TOKEN",
"image_url": "https://hover.to/api/v1/models/123/video.jpg?version=image",
"video_url": "https://hover.to/api/v1/models/456/video.mp4?share_token=TOKEN",
"open_graph_url": "https://hover.to/shared/TOKEN",
"street_address": "456 Hover Way",
"state": null
},
{
"id": 28327,
"job_id": 987,
"user_id": 654,
"recipient_id": null,
"token": "TOKEN",
"external_service": null,
"external_identifier": null,
"expired": false,
"expires_at": "2017-04-07T21:42:15.101Z",
"url": "https://hover.to/shared/TOKEN",
"image_url": "https://hover.to/api/v1/models/876/video.jpg?version=image",
"video_url": "https://hover.to/api/v1/models/543/video.mp4?share_token=TOKEN",
"open_graph_url": "https://hover.to/shared/TOKEN",
"street_address": "456 Hover Way",
"state": null
}
],
"meta": {
"pagination": {
"total": 2,
"total_count": 2,
"current_page": 1,
"next_page": null,
"prev_page": null,
"total_pages": 1
}
}
}
Retrieve a list of job shares available to you
HTTP Request
GET https://hover.to/api/v1/job_shares
Parameters | Description |
---|---|
job_id | Optional. If presents, it will filter results by job_id |
Retrieve Job Share
When successful, you'll receive a 200 response code with json body.
{
"job_share": {
"id": 123,
"job_id": 456,
"user_id": 789,
"recipient_id": null,
"token": "TOKEN",
"external_service": null,
"external_identifier": null,
"expired": false,
"expires_at": "2017-04-11T18:37:12.260Z",
"url": "https://hover.to/shared/TOKEN",
"image_url": "https://hover.to/api/v1/models/49415/video.jpg?version=image",
"video_url": "https://hover.to/api/v1/models/49415/video.mp4?share_token=TOKEN",
"open_graph_url": "https://hover.to/shared/TOKEN",
"street_address": "456 Hover Way",
"state": null
}
}
When there is an error you'll receive a 404 response code.
Retrieve a job share available to you. It looks up job share by id. If share_token is provided and an associated job share is found, it will return that job share instead.
HTTP Request
GET https://hover.to/api/v1/job_shares/<Job-Share-ID>
Parameters | Description |
---|---|
share_token | Optional. Token that is used to look up a job share. |
Update Job Share
When successful, you'll receive a 204 with no body.
When there is an error you'll receive a 404 or 422 response code.
Update your existing job share
HTTP Request
PATCH https://hover.to/api/v1/job_shares/<Job-Share-ID>
Parameters | Description |
---|---|
Optional. Recipient email. | |
external_service | Optional. Facebook, Google+, etc... |
external_identifier | Optional. External ID provided by external services. |
sender_email | Optional. Job share sender's email. |
sender_name | Optional. Job share sender's name. |
sender_text | Optional. Job share sender's customized text. |
Delete Job Share
When successful, you'll receive a 204 with no body.
When there is an error you'll receive a 404 response code.
Delete your job share
HTTP Request
DELETE https://hover.to/api/v1/job_shares/<Job-Share-ID>
Job Styles
List Job Styles
require 'net/http'
require 'api_auth'
url = "https://hover.to/api/v2/jobs/<Job-ID>/styles"
uri = URI.parse(url)
request = Net::HTTP::Get.new(uri)
signed_request = ApiAuth.sign!(request, HOVER_PARTNER_ID, HMAC_SECRET_KEY)
http = Net::HTTP.new(uri.host, uri.port)
response = http.request(signed_request)
data = JSON.parse(response.body)
When successful you'll get a 200 response code with the following JSON in the response body.
[
{
"id": 1,
"job_id": 1,
"user_id": 1,
"org_id": 4,
"name": "Job Style 1",
"created_at": "2018-05-23T18:14:04.239Z",
"updated_at": "2018-05-23T18:14:04.239Z"
}
]
HTTP Request
GET https://hover.to/api/v2/jobs/<Job-ID>/styles
Get A Job Style's Details
require 'net/http'
require 'api_auth'
url = "https://hover.to/api/v2/jobs/<Job-ID>/styles/<Job-Style-ID>"
uri = URI.parse(url)
request = Net::HTTP::Get.new(uri)
signed_request = ApiAuth.sign!(request, HOVER_PARTNER_ID, HMAC_SECRET_KEY)
http = Net::HTTP.new(uri.host, uri.port)
response = http.request(signed_request)
data = JSON.parse(response.body)
When successful you'll get a 200 response code with the following JSON in the response body.
{
"id": 2,
"job_id": 1,
"user_id": 1,
"org_id": 4,
"name": "Job Style 1",
"image": {
"url": "https://s3.amazonaws.com/hover.to/Hyperion/JobStyle/2/image.jpg",
"metadata": {},
"key": "Hyperion/JobStyle/2/image.jpg"
},
"model_json_v3": {
"url": "https://s3.amazonaws.com/hover.to/Hyperion/JobStyle/2/model_json_v3.json",
"metadata": {},
"key": "Hyperion/JobStyle/2/model_json_v3.json"
},
"machete_json": {
"url": "https://s3.amazonaws.com/hover.to/Hyperion/JobStyle/2/machete_json.json",
"metadata": {},
"key": "Hyperion/JobStyle/2/machete_json.json"
},
"created_at": "2018-05-23T18:14:04.239Z",
"updated_at": "2018-05-23T18:14:04.239Z"
}
HTTP Request
GET https://hover.to/api/v2/jobs/<Job-ID>/styles/<Job-Style-ID>
Get A Job Style's Image
require 'net/http'
require 'api_auth'
url = "https://hover.to/api/v2/jobs/<Job-ID>/styles/<Job-Style-ID>.jpg"
uri = URI.parse(url)
request = Net::HTTP::Get.new(uri)
signed_request = ApiAuth.sign!(request, HOVER_PARTNER_ID, HMAC_SECRET_KEY)
http = Net::HTTP.new(uri.host, uri.port)
response = http.request(signed_request)
When successful you'll receive a 302 response code and a redirect url to s3.
Redirects to the image url on s3.
HTTP Request
GET https://hover.to/api/v2/jobs/<Job-ID>/styles/<Job-Style-ID>.jpg
Get A Job Style's Model JSON V3
require 'net/http'
require 'api_auth'
url = "https://hover.to/api/v2/jobs/<Job-ID>/styles/<Job-Style-ID>/model_json_v3"
uri = URI.parse(url)
request = Net::HTTP::Get.new(uri)
signed_request = ApiAuth.sign!(request, HOVER_PARTNER_ID, HMAC_SECRET_KEY)
http = Net::HTTP.new(uri.host, uri.port)
response = http.request(signed_request)
When successful you'll receive a 302 response code and a redirect url to s3.
Redirects to the model_json_v3 url on s3.
HTTP Request
GET https://hover.to/api/v2/jobs/<Job-ID>/styles/<Job-Style-ID>/model_json_v3
Get A Job Style's Machete JSON
require 'net/http'
require 'api_auth'
url = "https://hover.to/api/v2/jobs/<Job-ID>/styles/<Job-Style-ID>/machete_json"
uri = URI.parse(url)
request = Net::HTTP::Get.new(uri)
signed_request = ApiAuth.sign!(request, HOVER_PARTNER_ID, HMAC_SECRET_KEY)
http = Net::HTTP.new(uri.host, uri.port)
response = http.request(signed_request)
When successful you'll receive a 302 response code and a redirect url to s3.
Redirects to the machete_json url on s3.
HTTP Request
GET https://hover.to/api/v2/jobs/<Job-ID>/styles/<Job-Style-ID>/machete_json
Create Job Style
require 'net/http'
require 'api_auth'
url = "https://hover.to/api/v2/jobs/<Job-ID>/styles"
uri = URI.parse(url)
request = Net::HTTP::Post.new(uri)
request.set_form_data({
"job_style[name]" => "Job Style 1"
})
signed_request = ApiAuth.sign!(request, HOVER_PARTNER_ID, HMAC_SECRET_KEY)
http = Net::HTTP.new(uri.host, uri.port)
response = http.request(signed_request)
data = JSON.parse(response.body)
When successful you'll get a 201 response code with the following JSON in the response body.
- `image_upload_url` is the direct s3 upload url for image (PUT, expires in 10 minutes)
- `model_json_v3_upload_url` is the direct s3 upload url for model_json_v3 (PUT, expires in 10 minutes)
- `machete_json_upload_url` is the direct s3 upload url for machete_json (PUT, expires in 10 minutes)
- `files_uploaded_url` is the PUT url for notifying our system that the files have been uploaded to s3.
{
"id": 2,
"job_id": 1,
"user_id": 1,
"org_id": 4,
"name": "Job Style 1",
"created_at": "2018-05-23T18:14:04.239Z",
"updated_at": "2018-05-23T18:14:04.239Z",
"image_upload_url": "https://s3.amazonaws.com/hover.to/Hyperion/JobStyle/2/image.jpg",
"model_json_v3_upload_url": "https://s3.amazonaws.com/hover.to/Hyperion/JobStyle/2/model_json_v3.jpg",
"machete_json_upload_url": "https://s3.amazonaws.com/hover.to/Hyperion/JobStyle/2/machete_json.jpg",
"files_uploaded_url": "https://hover.to/api/v2/jobs/1/styles/2/files_uploaded"
}
When there's an error creating the job style you'll get a 422 response code. The JSON in the response body will explain the error.
{
"error": {
"job_style": {
"name": ["can't be blank"]
}
}
}
This endpoint creates a job style. It only accepts the name of the job style.
For image, model_json_v3, or machete_json, use the upload urls in the response.
After successfully uploading the files to s3, make a PUT request to the files_uploaded_url
to notify our system.
HTTP Request
POST https://hover.to/api/v2/jobs/<Job-ID>/styles
URL Parameters
Parameter | Type | Description |
---|---|---|
job_style[name] | String | Name of the job style |
Update Job Style
require 'net/http'
require 'api_auth'
url = "https://hover.to/api/v2/jobs/<Job-ID>/styles/<Job-Style-ID>"
uri = URI.parse(url)
request = Net::HTTP::Put.new(uri)
request.set_form_data({
"job_style[name]" => "Job Style 1"
})
signed_request = ApiAuth.sign!(request, HOVER_PARTNER_ID, HMAC_SECRET_KEY)
http = Net::HTTP.new(uri.host, uri.port)
response = http.request(signed_request)
data = JSON.parse(response.body)
When successful you'll get a 200 response code with the following JSON in the response body.
- `image_upload_url` is the direct s3 upload url for image (PUT, expires in 10 minutes)
- `model_json_v3_upload_url` is the direct s3 upload url for model_json_v3 (PUT, expires in 10 minutes)
- `machete_json_upload_url` is the direct s3 upload url for machete_json (PUT, expires in 10 minutes)
- `files_uploaded_url` is the PUT url for notifying our system that the files have been uploaded to s3.
{
"id": 2,
"job_id": 1,
"user_id": 1,
"org_id": 4,
"name": "Job Style 1",
"created_at": "2018-05-23T18:14:04.239Z",
"updated_at": "2018-05-23T18:14:04.239Z",
"image_upload_url": "https://s3.amazonaws.com/hover.to/Hyperion/JobStyle/2/image.jpg",
"model_json_v3_upload_url": "https://s3.amazonaws.com/hover.to/Hyperion/JobStyle/2/model_json_v3.jpg",
"machete_json_upload_url": "https://s3.amazonaws.com/hover.to/Hyperion/JobStyle/2/machete_json.jpg",
"files_uploaded_url": "https://hover.to/api/v2/jobs/1/styles/2/files_uploaded"
}
When there's an error updating the job style you'll get a 422 response code. The JSON in the response body will explain the error.
{
"error": {
"job_style": {
"name": ["can't be blank"]
}
}
}
This endpoint updates a job style. It only updates the name of the job style.
To update image, model_json_v3, or machete_json, use the upload urls in the response.
After successfully uploading the files to s3, make a PUT request to the files_uploaded_url
to notify our system.
You may only update job styles that you have created.
HTTP Request
PUT https://hover.to/api/v2/jobs/<Job-ID>/styles/<Job-Style-ID>
URL Parameters
Parameter | Type | Description |
---|---|---|
job_style[name] | String | Name of the job style |
Files Uploaded
require 'net/http'
require 'api_auth'
url = "https://hover.to/api/v2/jobs/<Job-ID>/styles/<Job-Style-ID>/files_uploaded"
uri = URI.parse(url)
request = Net::HTTP::Put.new(uri)
signed_request = ApiAuth.sign!(request, HOVER_PARTNER_ID, HMAC_SECRET_KEY)
http = Net::HTTP.new(uri.host, uri.port)
response = http.request(signed_request)
When successful you'll receive a 204 response code and no body.
Make a PUT request to this endpoint to notify our system that files have been uploaded to s3 for the job style.
HTTP Request
PUT https://hover.to/api/v2/jobs/<Job-ID>/styles/<Job-Style-ID>/files_uploaded
Job Wireframe Images
This section shows how to access the wireframe images of a job. The available wireframe images and their corresponding versions are shown as below:
Image Name | Versions |
---|---|
wireframe_front.png | top, compass |
wireframe_front_right.png | top, compass |
wireframe_right.png | top, compass |
wireframe_right_back.png | top, compass |
wireframe_back.png | top, compass |
wireframe_back_left.png | top, compass |
wireframe_left.png | top, compass |
wireframe_left_front.png | top, compass |
wireframe_roof.png | top, lengths, areas, pitches |
wireframe_footprint.png | - |
wireframe_total_living_area.png | - |
Here are the description for each available version:
Versions | Description |
---|---|
top | Bird's eye view image with the corresponding side (i.e. front, right_back) facing down |
compass | Compass image showing the direction of the corresponding bird's eye view image |
lengths | Length measurement image of the roof |
areas | Area measurement image of the roof |
pitches | Numerical measurement image of the steepness of the roof |
The URL format for accessing the wireframe images or versions is
https://hover.to/api/v2/jobs/<Job-ID>/wireframe_images/<Image-Name>.png?version=<Version>
List Job wireframe images
require 'net/http'
require 'api_auth'
url = "https://hover.to/api/v2/jobs/<Job-ID>/wireframe_images"
uri = URI.parse(url)
request = Net::HTTP::Get.new(uri)
signed_request = ApiAuth.sign!(request, HOVER_PARTNER_ID, HMAC_SECRET_KEY)
http = Net::HTTP.new(uri.host, uri.port)
response = http.request(signed_request)
data = JSON.parse(response.body)
When successful you'll get a 200 response code with the following JSON in the response body.
{
"wireframe_front": {
"url": "https://s3.amazonaws.com/some_paths/wireframe_front.png",
"metadata": {
"visible_labels": [
"SI-1",
"D-2",
"D-3",
"D-1"
]
},
"key": "some_paths/wireframe_front.png",
"compass": {
"url": "https://s3.amazonaws.com/some_paths/wireframe_front_compass.png",
"metadata": {},
"key": "some_paths/wireframe_front_compass.png"
},
"top": {
"url": "https://s3.amazonaws.com/some_paths/wireframe_front_top.png",
"metadata": {},
"key": "some_paths/wireframe_front_top.png"
}
},
"wireframe_front_right": {
"url": "https://s3.amazonaws.com/some_paths/wireframe_front_right.png",
"metadata": {
"visible_labels": [
"SI-2",
"SI-1",
"W-1",
"D-2",
"D-3",
"D-1"
]
},
"key": "some_paths/wireframe_front_right.png",
"compass": {
"url": "https://s3.amazonaws.com/some_paths/wireframe_front_right_compass.png",
"metadata": {},
"key": "some_paths/wireframe_front_right_compass.png"
},
"top": {
"url": "https://s3.amazonaws.com/some_paths/wireframe_front_right_top.png",
"metadata": {},
"key": "some_paths/wireframe_front_right_top.png"
}
},
"wireframe_right": {
"url": "https://s3.amazonaws.com/some_paths/wireframe_right.png",
"metadata": {
"visible_labels": [
"SI-2",
"W-1"
]
},
"key": "some_paths/wireframe_right.png",
"compass": {
"url": "https://s3.amazonaws.com/some_paths/wireframe_right_compass.png",
"metadata": {},
"key": "some_paths/wireframe_right_compass.png"
},
"top": {
"url": "https://s3.amazonaws.com/some_paths/wireframe_right_top.png",
"metadata": {},
"key": "some_paths/wireframe_right_top.png"
}
},
"wireframe_right_back": {
"url": "https://s3.amazonaws.com/some_paths/wireframe_right_back.png",
"metadata": {
"visible_labels": [
"SI-2",
"SI-3",
"W-1",
"W-2"
]
},
"key": "some_paths/wireframe_right_back.png",
"compass": {
"url": "https://s3.amazonaws.com/some_paths/wireframe_right_back_compass.png",
"metadata": {},
"key": "some_paths/wireframe_right_back_compass.png"
},
"top": {
"url": "https://s3.amazonaws.com/some_paths/wireframe_right_back_top.png",
"metadata": {},
"key": "some_paths/wireframe_right_back_top.png"
}
},
"wireframe_back": {
"url": "https://s3.amazonaws.com/some_paths/wireframe_back.png",
"metadata": {
"visible_labels": [
"SI-3",
"W-2"
]
},
"key": "some_paths/wireframe_back.png",
"compass": {
"url": "https://s3.amazonaws.com/some_paths/wireframe_back_compass.png",
"metadata": {},
"key": "some_paths/wireframe_back_compass.png"
},
"top": {
"url": "https://s3.amazonaws.com/some_paths/wireframe_back_top.png",
"metadata": {},
"key": "some_paths/wireframe_back_top.png"
}
},
"wireframe_back_left": {
"url": "https://s3.amazonaws.com/some_paths/wireframe_back_left.png",
"metadata": {
"visible_labels": [
"SI-4",
"SI-3",
"W-2"
]
},
"key": "some_paths/wireframe_back_left.png",
"compass": {
"url": "https://s3.amazonaws.com/some_paths/wireframe_back_left_compass.png",
"metadata": {},
"key": "some_paths/wireframe_back_left_compass.png"
},
"top": {
"url": "https://s3.amazonaws.com/some_paths/wireframe_back_left_top.png",
"metadata": {},
"key": "some_paths/wireframe_back_left_top.png"
}
},
"wireframe_left": {
"url": "https://s3.amazonaws.com/some_paths/wireframe_left.png",
"metadata": {
"visible_labels": [
"SI-4",
"D-4"
]
},
"key": "some_paths/wireframe_left.png",
"compass": {
"url": "https://s3.amazonaws.com/some_paths/wireframe_left_compass.png",
"metadata": {},
"key": "some_paths/wireframe_left_compass.png"
},
"top": {
"url": "https://s3.amazonaws.com/some_paths/wireframe_left_top.png",
"metadata": {},
"key": "some_paths/wireframe_left_top.png"
}
},
"wireframe_left_front": {
"url": "https://s3.amazonaws.com/some_paths/wireframe_left_front.png",
"metadata": {
"visible_labels": [
"SI-4",
"SI-1",
"D-2",
"D-4",
"D-1"
]
},
"key": "some_paths/wireframe_left_front.png",
"compass": {
"url": "https://s3.amazonaws.com/some_paths/wireframe_left_front_compass.png",
"metadata": {},
"key": "some_paths/wireframe_left_front_compass.png"
},
"top": {
"url": "https://s3.amazonaws.com/some_paths/wireframe_left_front_top.png",
"metadata": {},
"key": "some_paths/wireframe_left_front_top.png"
}
},
"wireframe_footprint": {
"url": "https://s3.amazonaws.com/some_paths/wireframe_footprint.png",
"metadata": {},
"key": "some_paths/wireframe_footprint.png"
},
"wireframe_roof": {
"url": "https://s3.amazonaws.com/some_paths/wireframe_roof.png",
"metadata": {},
"key": "some_paths/wireframe_roof.png",
"areas": {
"url": "https://s3.amazonaws.com/some_paths/wireframe_roof_areas.png",
"metadata": {},
"key": "some_paths/wireframe_roof_areas.png"
},
"top": {
"url": "https://s3.amazonaws.com/some_paths/wireframe_roof_top.png",
"metadata": {},
"key": "some_paths/wireframe_roof_top.png"
},
"lengths": {
"url": "https://s3.amazonaws.com/some_paths/wireframe_roof_lengths.png",
"metadata": {},
"key": "some_paths/wireframe_roof_lengths.png"
},
"pitches": {
"url": "https://s3.amazonaws.com/some_paths/wireframe_roof_pitches.png",
"metadata": {},
"key": "some_paths/wireframe_roof_pitches.png"
}
}
}
The response lists the S3 URLs of all wireframe images and versions for the corresponding job. Each url is valid to access for 10 minutes.
To access an image individually, i.e. wireframe_front_right, make a GET request to
https://hover.to/api/v2/jobs/<Job-ID>/wireframe_images/wireframe_front_right.png
.
To access an image version individually, i.e. compass of wireframe_front_right, make a GET request to
https://hover.to/api/v2/jobs/<Job-ID>/wireframe_images/wireframe_front_right.png?version=compass
.
When available, visible_labels
for each facade are included within their corresponding metadata
attribute.
HTTP Request
GET https://hover.to/api/v2/jobs/<Job-ID>/wireframe_images
Get a Job wireframe image
Redirects to the image URL on S3.
require 'net/http'
require 'api_auth'
url = "https://hover.to/api/v2/jobs/<Job-ID>/wireframe_images/wireframe_front.png?version=top"
uri = URI.parse(url)
request = Net::HTTP::Get.new(uri)
signed_request = ApiAuth.sign!(request, HOVER_PARTNER_ID, HMAC_SECRET_KEY)
http = Net::HTTP.new(uri.host, uri.port)
response = http.request(signed_request)
data = JSON.parse(response.body)
When successful you'll receive a 302 response code and a redirect URL to S3.
The redirected S3 URL is valid to access for 10 minutes.
HTTP Request
GET https://hover.to/api/v2/jobs/<Job-ID>/wireframe_images/wireframe_front.png?version=top
URL Parameters
Parameter | Type | Description |
---|---|---|
version | String | Optional. Example versions: "top", "compass", "areas", "lengths", "pitches". |
Deliverable Change Requests
Deliverable change requests provide a way to request that a job's deliverable be changed during the job's life cycle. For each deliverable change there may be a corresponding payment for the difference between the new deliverable and what's already been paid for the job.
Only one active deliverable change request can exist for a job at a time. Attempting to create another while one is pending updates the pending one.
Create or Update
require 'net/http'
require 'api_auth'
url = "https://hover.to/api/v2/deliverable_change_requests"
uri = URI.parse(url)
request = Net::HTTP::Post.new(uri)
request.set_form_data({
"deliverable_change_request[job_id]" => 123,
"deliverable_change_request[new_deliverable_id]" => 3
})
signed_request = ApiAuth.sign!(request, HOVER_PARTNER_ID, HMAC_SECRET_KEY)
http = Net::HTTP.new(uri.host, uri.port)
response = http.request(signed_request)
data = JSON.parse(response.body)
When successful you'll get a 201 response code with the following JSON in the response body.
{
"id": 1,
"job_id": 123,
"user_id": 456,
"new_deliverable_id": 3,
"old_deliverable_id": 1,
"changed_at": null,
"created_at": "2018-03-14T20:29:54.366Z",
"updated_at": "2018-03-14T20:29:54.537Z",
"state": "first_attempt"
}
When there's an error creating the deliverable change request you'll get a 422 response code.
This endpoint provides a way to request that a job's deliverable be changed.
HTTP Request
POST https://hover.to/api/v2/deliverable_change_requests
URL Parameters
Parameter | Description |
---|---|
deliverable_change_request[job_id] | The id of the job whose deliverable will be changed. |
deliverable_change_request[new_deliverable_id] | Supported deliverable_ids: 2(roof only), 3(complete) |
Allowable Deliverable Change Requests
Old Deliverable | New Deliverable |
---|---|
Roof Only (2) | Complete (3) |
Total Living Area (6) | Roof Only (2) |
Total Living Area Plus (5) | Roof Only (2) |
Total Living Area (6) | Complete (3) |
Total Living Area Plus (5) | Complete (3) |
Capture Only (7) | Roof Only (2) |
Capture Only (7) | Complete (3) |
View all for a job
require 'net/http'
require 'api_auth'
url = "https://hover.to/api/v2/deliverable_change_requests?job_id=123"
uri = URI.parse(url)
request = Net::HTTP::Get.new(uri)
signed_request = ApiAuth.sign!(request, HOVER_PARTNER_ID, HMAC_SECRET_KEY)
http = Net::HTTP.new(uri.host, uri.port)
response = http.request(signed_request)
data = JSON.parse(response.body)
When successful you'll get a 200 response code with the following JSON in the response body.
{
"results": [{
"id": 1,
"job_id": 123,
"user_id": 456,
"new_deliverable_id": 3,
"old_deliverable_id": 1,
"changed_at": null,
"created_at": "2018-03-14T20:29:54.366Z",
"updated_at": "2018-03-14T20:29:54.537Z",
"state": "first_attempt"
}]
}
This endpoint provides a way to view all deliverable change requests for a job.
HTTP Request
GET https://hover.to/api/v2/deliverable_change_requests
URL Parameters
Parameter | Description |
---|---|
job_id | The id of the job to view deliverable change requests for. |
Get the upgrade price
require 'net/http'
require 'api_auth'
url = "https://hover.to/api/v2/deliverable_change_requests/price?deliverable_change_request[job_id]=123&deliverable_change_request[new_deliverable_id]=3"
uri = URI.parse(url)
request = Net::HTTP::Get.new(uri)
signed_request = ApiAuth.sign!(request, HOVER_PARTNER_ID, HMAC_SECRET_KEY)
http = Net::HTTP.new(uri.host, uri.port)
response = http.request(signed_request)
data = JSON.parse(response.body)
When successful you'll get a 200 response code with the following JSON in the response body.
{
"owed": 2400,
"paid": 2000,
"pending": 0,
}
This endpoint provides a way to view the price to upgrade to a new deliverable.
Example scenario:
You paid $25 for a roof only job. After it was completed you want to see the price to upgrade to the complete deliverable which costs $50. The response is
{"owed": 2500, "paid" 2500, "pending", 0}
HTTP Request
GET https://hover.to/api/v2/deliverable_change_requests/price
URL Parameters
Parameter | Description |
---|---|
deliverable_change_request[job_id] | The id of the job whose upgrade price will be returned. |
deliverable_change_request[new_deliverable_id] | Supported deliverable_ids: 2(roof only), 3(complete) |
Delete
require 'net/http'
require 'api_auth'
url = "https://hover.to/api/v2/deliverable_change_requests/1"
uri = URI.parse(url)
request = Net::HTTP::Delete.new(uri)
signed_request = ApiAuth.sign!(request, HOVER_PARTNER_ID, HMAC_SECRET_KEY)
http = Net::HTTP.new(uri.host, uri.port)
response = http.request(signed_request)
data = JSON.parse(response.body)
When successful you'll get a 204 response code with no response body
When there's an error you'll get a 422 response code. An example response is
{
"base": ["Cannot be deleted in this state"]
}
This endpoint provides a way to delete a pending change request. Pending requests can usually be deleted, but there is short period of time when they can't. If you encounter an error, you should consider reloading the job from the API, it's deliverable may have changed.
HTTP Request
DELETE https://hover.to/api/v2/deliverable_change_requests/<Job-ID>
Orgs
List Orgs
require 'net/http'
token='your OAuth authorization token'
url = "https://hover.to/api/v1/orgs"
uri = URI.parse(url)
request = Net::HTTP::Get.new(uri)
request["Authorization"] = "Bearer #{token}"
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true if 'https' == uri.scheme
response = http.request(request)
data = JSON.parse(response.body)
curl --location --request GET 'https://hover.to/api/v1/orgs' \
--header 'Authorization: Bearer #{token}'
When successful you'll get a 200 response code with the following JSON in the response body.
{
"orgs": [
{
"id": 174,
"parent_id": null,
"name": "Cool Roofing Company",
"ancestral_name": "Hover > Pro > Cool Roofing Company",
"updated_at": "2018-04-11T16:55:27.378Z",
"discounts": [ ],
"on_waitlist": false,
"plan_billing_cycle": "monthly",
"deliverable_id": null,
"created_at": "2018-04-11T16:55:27.358Z",
"client_branding_logo_url": null,
"customer_display_name": "Cool Roofing Company",
"external_partner_identifier": null,
"preferences": {
"id": 348,
"org_id": 174,
"created_at": "2018-04-11T16:55:27.371Z",
"updated_at": "2018-04-11T16:55:27.371Z",
"default_acl_template": null,
"preferred_siding_waste_factor": null,
"preferred_roofing_waste_factor": null,
"external_identifier_label": "Identifier",
"external_identifier_required": false
},
"plan": {
"id": 117,
"name": "Premium",
"partner_id": 1053,
"created_at": "2018-04-11 16:59:49 UTC",
"updated_at": "2018-04-11 16:59:49 UTC",
"tier": "premium",
},
"wallet": {
"id": 174,
"org_id": 174,
"balance": 0,
"billing_address_line_1": null,
"billing_address_line_2": null,
"billing_address_city": null,
"billing_address_region": null,
"billing_address_postal_code": null,
"billing_address_country": null,
"billing_email": null,
"card_brand": null,
"card_last4": null,
"cardholder_name": null
},
"features": [
]
}
],
"meta": {
"pagination": {
"total": 2,
"total_count": 2,
"current_page": 1,
"next_page": null,
"prev_page": null,
"total_pages": 1
}
}
}
HTTP Request
GET https://hover.to/api/v1/orgs
URL Parameters
Parameter | Type | Description |
---|---|---|
search | string | If this is present only org's who's names or IDs match this string will be returned. |
Create Org
require 'net/http'
token='your OAuth authorization token'
url = "https://hover.to/api/v1/orgs"
uri = URI.parse(url)
request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer #{token}"
request.set_form_data({
"org[name]" => "The Best Roofing Company Around",
"org[external_partner_identifier]" => "brand-id-239802"
})
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true if 'https' == uri.scheme
response = http.request(request)
data = JSON.parse(response.body)
curl --location --request POST 'https://hover.to/api/v1/orgs' \
--header 'Authorization: Bearer #{token}' \
--form 'org[name]=The Best Roofing Company Around' \
--form 'org[external_partner_identifier]=brand-id-239802'
Success: HTTP 201 with a JSON response body
{
"org": {
"id": 43920
}
}
Error: HTTP 422 with a JSON response explaining the error.
{
"name": ["can't be blank"],
}
Creates an org.
HTTP Request
POST https://hover.to/api/v1/orgs
URL Parameters
Parameter | Description |
---|---|
org[name] | The Org's name as it's displayed to logged in users that are members of the org. |
org[customer_display_name] | The Org's name as it's displayed to customers when doing things like capture requests |
org[external_partner_identifier] | Any arbitrary string you want to save. |
org[settings_attributes][deliverable_id] | The restricted deliverable(s) for this org: 2(Roof Only), 3(Complete), 5(Total Living Area Plus), 6(Total Living Area), 7(Capture Only, Photos Only). Members of the org will only be able to request this deliverable. (If not set, deliverable defaults to partner's deliverables.) |
org[preferences_attributes][external_identifier_label] | The label that will be used for a job's "external_identifier" attribute when displaying the external identifier in front-end clients. |
org[preferences_attributes][external_identifier_required] | This controls if front-end clients will require users to enter an external identifier when creating a job. It's also validated on the back-end. |
org[preferences_attributes][preferred_roofing_waste_factor] | |
org[preferences_attributes][preferred_siding_waste_factor] |
Update Org
require 'net/http'
token='your OAuth authorization token'
url = "https://hover.to/api/v1/orgs/#{org_id}"
uri = URI.parse(url)
request = Net::HTTP::Patch.new(uri)
request["Authorization"] = "Bearer #{token}"
request.set_form_data({
"org[name]" => "The Best Roofing Company Around",
})
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true if 'https' == uri.scheme
response = http.request(request)
data = JSON.parse(response.body)
curl --location --request PATCH 'https://hover.to/api/v1/orgs/#{org_id}' \
--header 'Authorization: Bearer #{token}' \
--form 'org[name]=The Best Roofing Company Around'
Success: HTTP 204 with an empty response body.
Error: HTTP 422 with a JSON response explaining the error.
{
"name": ["can't be blank"],
}
Update an org.
HTTP Request
PATCH https://hover.to/api/v1/orgs/<Org-ID>
URL Parameters
Parameter | Description |
---|---|
org[name] | The Org's name as it's displayed to logged in users that are members of the org. |
org[customer_display_name] | The Org's name as it's displayed to customers when doing things like capture requests |
org[external_partner_identifier] | Any arbitrary string you want to save. |
org[settings_attributes][deliverable_id] | The restricted deliverable(s) for this org: 2(Roof Only), 3(Complete), 5(Total Living Area Plus), 6(Total Living Area), 7(Capture Only, Photos Only). Members of the org will only be able to request this deliverable. (If not set, deliverable defaults to partner's deliverables.) |
org[preferences_attributes][external_identifier_label] | The label that will be used for a job's "external_identifier" attribute when displaying the external identifier in front-end clients. |
org[preferences_attributes][external_identifier_required] | This controls if front-end clients will require users to enter an external identifier when creating a job. It's also validated on the back-end. |
org[preferences_attributes][preferred_roofing_waste_factor] | |
org[preferences_attributes][preferred_siding_waste_factor] |
Change Org's Plan
require 'net/http'
token='your OAuth authorization token'
url = "https://hover.to/api/v1/orgs/#{org_id}/update_plan"
uri = URI.parse(url)
request = Net::HTTP::Put.new(uri)
request["Authorization"] = "Bearer #{token}"
request.set_form_data({
"org[plan_id]" => 6,
})
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true if 'https' == uri.scheme
response = http.request(request)
data = JSON.parse(response.body)
curl --location --request PUT 'https://hover.to/api/v1/orgs/#{org_id}/update_plan' \
--header 'Authorization: Bearer #{token}' \
--form 'org[plan_id]=6'
Success: HTTP 200 with a JSON representation of the org as a response.
Error: HTTP 422 with a JSON response explaining the error.
{
"plan_id": ["Invalid plan for partner"],
}
Changes an org's plan.
HTTP Request
PUT https://hover.to/api/v1/orgs/<Org-ID>/update_plan
URL Parameters
Parameter | Description |
---|---|
org[plan_billing_cycle] | The Org's billing cycle. Valid values are "monthly" and "yearly" |
org[plan_id] | The ID of the Org's Plan. |
Users
List Users
require 'net/http'
require 'api_auth'
url = "https://hover.to/api/v2/users.json"
uri = URI.parse(url)
request = Net::HTTP::Get.new(uri)
signed_request = ApiAuth.sign!(request, HOVER_PARTNER_ID, HMAC_SECRET_KEY)
http = Net::HTTP.new(uri.host, uri.port)
response = http.request(signed_request)
data = JSON.parse(response.body)
When successful you'll get a 200 response code with the following JSON in the response body.
{
"pagination": {
"current_page": 1,
"total": 30,
"total_pages": 2
},
"results": [
{
"id": 155036,
"first_name": "first",
"last_name": "last",
"email": "[email protected]",
"time_zone": "Pacific Time (US \u0026 Canada)",
"aasm_state": "invited",
"updated_at": "2018-02-07T00:30:49.546Z",
"created_at": "2018-02-07T00:30:07.319Z",
"require_job_approval": false,
"last_sign_in_at": null,
"acl_template": "admin",
"test_data": false
},
{
"id": 154894,
"first_name": "Max",
"last_name": "Hover",
"email": "[email protected]",
"time_zone": "Central Time (US \u0026 Canada)",
"aasm_state": "activated",
"updated_at": "2018-02-07T00:26:52.246Z",
"created_at": "2018-02-06T12:04:39.721Z",
"require_job_approval": false,
"last_sign_in_at": null,
"acl_template": "admin",
"test_data": false
}
]
}
If you're authenticated as a user we'll return users in that user's org.
If you're authenticated as an org we'll return users in that org.
HTTP Request
GET https://hover.to/api/v2/users
URL Parameters
Passing multiple parameters returns users that match any of those parameters. It's not required that the returned users match all parameters, just one. So "jobs_need_approval=true&admin=true" returns both administrators and users that need to have their jobs approved. The exception to this rule is the search parameter. Searching with filters will behave like this "(search AND (filter1 OR filter2 OR filter3))".
Parameter | Type | Description |
---|---|---|
jobs_need_approval | boolean | Defaults to either. If you pass true users that require job approval will be returned. If you pass false users that do not require job approval will be returned. |
org_id | integer | By default all user that your ACL gives you access to will be returned. If you specify an org_id only users in that org will be returned. |
admin | boolean | Defaults to either. If you pass true admins will be returned. If you pass false no admins will be returned. |
state | string or array | Defaults to any. If you pass "invited" users that have been invited, but not activated will be returned. If you pass "activated" users that are activated(members) will be returned. You can also pass an array of states an users matching any of those states will be returned. |
search | string | Filter users by search text. Fields included in search: name, email |
sort_order | string | By default we sort by "recent activity", which is the user's updated_at column descending. If you give this parameter it needs to have a value of "ASC" or "DESC" and it will cause users to be sorted by their first name ascending or descending. |
page | integer | Defaults to 1. Return this page of results |
per | integer | Defaults to 25. Return this many users per page. |
Check For Existing User
require 'net/http'
require 'api_auth'
url = "https://hover.to/api/partners/v1/users/[email protected]"
uri = URI.parse(url)
request = Net::HTTP::Get.new(uri)
signed_request = ApiAuth.sign!(request, HOVER_PARTNER_ID, HMAC_SECRET_KEY)
http = Net::HTTP.new(uri.host, uri.port)
response = http.request(signed_request)
data = JSON.parse(response.body)
When successful you'll get a 200 response code with the following JSON in the response body.
{
"exists": true,
"exists_under_partner": true
}
Returns a true or false value to let you know if a user with a given email address already exists. This endpoint is only available to partners.
HTTP Request
GET https://hover.to/api/partners/v1/users/exists
URL Parameters
Parameter | Description |
---|---|
Lookup an existing user account using this email address |
Create User
require 'net/http'
require 'api_auth'
url = "https://hover.to/api/v1/users"
uri = URI.parse(url)
request = Net::HTTP::Post.new(uri)
request.set_form_data({
"app_id" => "hover",
"user[email]" => "[email protected]",
"user[first_name]" => "First",
"user[last_name]" => "Last",
})
signed_request = ApiAuth.sign!(request, HOVER_PARTNER_ID, HMAC_SECRET_KEY)
http = Net::HTTP.new(uri.host, uri.port)
response = http.request(signed_request)
data = JSON.parse(response.body)
When successful you'll get a 201 response code with the following JSON in the response body.
{
"id": 5,
"first_name": "First",
"last_name": "Last",
"email": "[email protected]",
"time_zone": "Central Time (US \u0026 Canada)",
"upload_secret": "563379BA-3C0E-470E-A635-3EC0175A97FE",
"aasm_state" : "activated",
"last_sign_in_at": "2018-02-07T00:26:52.246Z",
"created_at": "2018-02-07T00:26:52.246Z",
"mobile_phone": "555-555-5555",
"office_phone": "555-555-5555",
"preferred_identifier": "",
"is_homeowner": true,
"orgs": [
{
"id": 1,
"name": "Homeowners",
"example_jobs_cloned": true,
"plan": {
"id": 42,
"name": "Homeowners",
}
}
]
}
When there's an error creating the capture request you'll get a 422 response code. The JSON in the response body will explain the error.
{
"email": ["can't be blank"],
}
This endpoint creates a user. By default the user will have a HOVER "Homeowner" account. You can decide where a new user ends up by specifying the "app_id" parameter. If you didn't pass an "app_id" parameter, or if the one you used results in a "Homeowner" account, you can still make the user a pro.
HTTP Request
POST https://hover.to/api/v1/users/create_beta
URL Parameters
Parameter | Description |
---|---|
app_id | Controls which partner and org a user ends up in. By default users are signed up as HOVER users. If you're a partner with your own "app_id" use it here to ensure your users are registered to you in our system. |
user[email] | Email address |
user[first_name] | |
user[last_name] | |
user[mobile_phone] | |
user[office_phone] | |
user[password] | |
user[password_confirmation] | |
user[preferred_identifier] | Any arbitrary string |
user[capture_request_identifier] | |
user[require_job_approval] | Require administrator approval before user is allowed to process jobs. "true" or "false" |
Make User A Pro
require 'net/http'
require 'api_auth'
url = "https://hover.to/api/v1/orgs/upgrade/users/<USER-ID>/upgrade_to_pro"
uri = URI.parse(url)
request = Net::HTTP::Post.new(uri)
request.set_form_data({
"app_id" => "hover",
"org[name]" => "Pretty Good Construction Services",
"org[parent_app_id]" => "hover",
"wallet[billing_address_postal_code]" => "94103",
"wallet[billing_address_country]" => "United States"
})
signed_request = ApiAuth.sign!(request, HOVER_PARTNER_ID, HMAC_SECRET_KEY)
http = Net::HTTP.new(uri.host, uri.port)
response = http.request(signed_request)
data = JSON.parse(response.body)
Success: HTTP 200 or 202
{
"id": 5,
"first_name": "First",
"last_name": "Last",
"email": "[email protected]",
"time_zone": "Central Time (US \u0026 Canada)",
"upload_secret": "563379BA-3C0E-470E-A635-3EC0175A97FE",
"aasm_state" : "activated",
"last_sign_in_at": "2018-02-07T00:26:52.246Z",
"created_at": "2018-02-07T00:26:52.246Z",
"mobile_phone": "555-555-5555",
"office_phone": "555-555-5555",
"preferred_identifier": "",
"is_homeowner": false,
"orgs": [
{
"id": 2,
"name": "Pretty Good Construction Services",
"example_jobs_cloned": true,
"plan": {
"id": 93,
"name": "Professional",
}
}
]
}
{
"email": ["can't be blank"],
}
This endpoint upgrades a homeowner user to a professional.
HTTP Request
POST https://hover.to/api/v1/orgs/upgrade/users/<USER-ID>/upgrade_to_pro
URL Parameters
Parameter | Description |
---|---|
user[email] | Email address |
user[first_name] | |
user[last_name] | |
user[mobile_phone] | |
user[office_phone] | |
user[password] | |
user[password_confirmation] | |
user[preferred_identifier] | Any arbitrary string |
user[capture_request_identifier] | |
user[require_job_approval] | Require administrator approval before user is allowed to process jobs. "true" or "false" |
org[name] | |
org[parent_id] | |
org[parent_app_id] | |
org[plan_id] | |
org[external_partner_identifier] | |
org[customer_display_name] | |
wallet[billing_address_line_1] | |
wallet[billing_address_line_2] | |
wallet[billing_address_city] | |
wallet[billing_address_region] | |
wallet[billing_address_postal_code] | |
wallet[billing_address_country] |
Reset A Users Password
require 'net/http'
require 'api_auth'
url = "https://hover.to/api/v1/password/issue_token.json"
uri = URI.parse(url)
request = Net::HTTP::Post.new(uri)
signed_request = ApiAuth.sign!(request, HOVER_PARTNER_ID, HMAC_SECRET_KEY)
http = Net::HTTP.new(uri.host, uri.port)
response = http.request(signed_request)
data = JSON.parse(response.body)
When successful you'll get a 204 with no response body.
When successful you'll get a 400 with no response body.
Emails a user instructions to reset their password.
HTTP Request
POST https://hover.to/api/v1/password/issue_token.json
URL Parameters
Parameter | Description |
---|---|
user[email] | Send password instructions to the user that has this email address. |
Move User
require 'net/http'
require 'api_auth'
url = "https://hover.to/api/v1/orgs/<ORG-ID>/users/<USER-ID>/move"
uri = URI.parse(url)
request = Net::HTTP::Patch.new(uri)
request.set_form_data({
"to_org_id" => 3920,
})
signed_request = ApiAuth.sign!(request, HOVER_PARTNER_ID, HMAC_SECRET_KEY)
http = Net::HTTP.new(uri.host, uri.port)
response = http.request(signed_request)
data = JSON.parse(response.body)
Success: HTTP 204
Failure: HTTP 422
{
"error": {
"user": ["User is not movable: List, of, reasons, why."]
}
}
This endpoint moves a user from one org to another. This removes all access to the original org and grants access to the destination org specified by the to_org_id
parameter. All of the users jobs will be moved with them. If the user had any failed payments in their previous org those payments and their associated goods will be moved with the user. Once in the destination org those failed payments will be retried with the destination org's billing information.
HTTP Request
PATCH https://hover.to/api/v1/orgs/<ORG-ID>/users/<USER-ID>/move
URL Parameters
Parameter | Description |
---|---|
to_org_id | The ID of the destination org. |
Remove User From Org
require 'net/http'
require 'api_auth'
url = "https://hover.to/api/v1/orgs/<ORG-ID>/users/<USER-ID>/archive"
uri = URI.parse(url)
request = Net::HTTP::Patch.new(uri)
signed_request = ApiAuth.sign!(request, HOVER_PARTNER_ID, HMAC_SECRET_KEY)
http = Net::HTTP.new(uri.host, uri.port)
response = http.request(signed_request)
data = JSON.parse(response.body)
Success: HTTP 200
This endpoint removes a user from the org specified by the
HTTP Request
PATCH https://hover.to/api/v1/orgs/<ORG-ID>/users/<USER-ID>/archive
Invite User To Existing Org
require 'net/http'
require 'api_auth'
url = "https://hover.to/api/v1/orgs/<ORG-ID>/users/"
uri = URI.parse(url)
request = Net::HTTP::Post.new(uri)
request.set_form_data({
"user[email]" => "[email protected]",
"user[first_name]" => "First",
"user[last_name]" => "Last",
})
signed_request = ApiAuth.sign!(request, HOVER_PARTNER_ID, HMAC_SECRET_KEY)
http = Net::HTTP.new(uri.host, uri.port)
response = http.request(signed_request)
data = JSON.parse(response.body)
Success: HTTP 201
This endpoint invites a user to an existing org. If the user exists they will be given access to the org immediately. If the user does not exist a user account will be created for them and they'll be invited via email to set a password and join the team.
You must be authenticated as a user that is an admin of the org, or as the org itself.
HTTP Request
POST https://hover.to/api/v1/orgs/<ORG-ID>/users.json
URL Parameters
Parameter | Description |
---|---|
acl_template | The ACL template to give the user that is being invited. Defaults to the org's default ACL template. Supported options: "admin", "homeowner", "pro_plus". If they aren't an administrator of the org or a homeowner use "pro_plus". |
user[email] | Email address |
user[first_name] | |
user[last_name] | |
user[mobile_phone] | |
user[office_phone] | |
user[preferred_identifier] | Any arbitrary string |
user[capture_request_identifier] | |
user[require_job_approval] | Require administrator approval before user is allowed to process jobs. "true" or "false" |
User Profile
require 'net/http'
require 'api_auth'
url = "https://hover.to/api/v2/users/profile"
uri = URI.parse(url)
request = Net::HTTP::Get.new(uri)
signed_request = ApiAuth.sign!(request, HOVER_PARTNER_ID, HMAC_SECRET_KEY)
http = Net::HTTP.new(uri.host, uri.port)
response = http.request(signed_request)
data = JSON.parse(response.body)
When successful you'll get a 200 response code with the following JSON in the response body.
{
"id": 5,
"first_name": "Elise",
"last_name": "Wood",
"email": "[email protected]",
"time_zone": "Central Time (US \u0026 Canada)",
"upload_secret": "563379BA-3C0E-470E-A635-3EC0175A97FE",
"orgs": [
{
"id": 1,
"name": "Elise's Roofing",
"example_jobs_cloned": true,
"plan": {
"id": 120,
"name": "Professional",
"partner_id": 10,
"machete": true,
"measurements": true,
"measurements_branding": false,
"ordering": false,
"basic_export": true,
"advanced_export": true,
"basic_design": false,
"advanced_design": true,
"basic_team_management": false,
"advanced_team_management": true,
"created_at": "2017-04-18T18:01:05.246Z",
"updated_at": "2017-05-22T17:26:25.089Z",
"default": false,
"visible": true,
"sort_index": 3,
"feature_icons": null,
"feature_bullets": null,
"email_receipt": true,
"auto_assign_leads": false,
"partner_analytics": false,
"org_analytics": true,
"allow_photo_only_capture": true,
"days_of_historical_job_access": 0,
"homeowner_lead_capture": true,
"saas": true,
"plan_after_trial_id": null,
"allows_instant_estimate": false,
"allow_upgrade_to_pro": true,
"allow_multi_family_residential_captures": false
}
}
],
"accesses": [
{
"org_id": 1,
"is_admin": true
}
]
}
If the user wasn't found you'll receive an authentication error.
Returns the profile of the authenticated user.
HTTP Request
GET https://hover.to/api/v2/users/profile
Images
require 'net/http'
require 'api_auth'
url = "https://hover.to/api/v2/images/23092381/rotated_image.jpg?version=medium"
uri = URI.parse(url)
request = Net::HTTP::Get.new(uri)
signed_request = ApiAuth.sign!(request, HOVER_PARTNER_ID, HMAC_SECRET_KEY)
http = Net::HTTP.new(uri.host, uri.port)
response = http.request(signed_request)
File.open("image.jpg", "wb") do |file|
file.write(response.body)
end
When successful you'll be redirected to the image file.
The Image API let's you download the images uploaded for a job. Two types of images are exposed. The original image as it was uploaded. And a processed version of the image that's been converted into a common format, had it's exif stripped, and it's orientation normalized so that all pixels are right side up.
To get an image you'll need to construct it's URL. The unique part of an image URL is the ID of the image. You can get image IDs by fetching the job's details.
When you request the image you will be redirected to another URL. Currently that's a pre-authenticated Amazon S3 URL. There you'll find the actual image file. Some clients will automatically follow the redirect and return the image. If you don't follow the redirect the final URL to the image can be found in the "Location" header of the response. That URL should function for at least 10 minutes before it expires.
Parameter | Description |
---|---|
version | The version of the image to return. This parameter is optional. By default the original resolution and quality is returned. |
Supported Versions
Name | Description |
---|---|
small | 200x200, 60% quality |
medium | 800x800, 60% quality |
low_quality | Original resolution, 50% quality |
The image as it was uploaded
GET /api/v2/images/<Image-ID>/image.jpg
The image after correcting orientation and removing orientation tags from the EXIF data.
GET /api/v2/images/<Image-ID>/rotated_image.jpg
Notes
List A Job's Notes
require 'net/http'
require 'api_auth'
url = "https://hover.to/api/v2/notes.json?job_id=31"
uri = URI.parse(url)
request = Net::HTTP::Get.new(uri)
signed_request = ApiAuth.sign!(request, HOVER_PARTNER_ID, HMAC_SECRET_KEY)
http = Net::HTTP.new(uri.host, uri.port)
response = http.request(signed_request)
data = JSON.parse(response.body)
When successful you'll get a 200 response code with the following JSON in the response body.
{
"results": [
{
"id": 12,
"job_id": 6,
"org_id": 17,
"user_id": 26,
"body": "The roof has a hole in it!",
"created_at": "2018-03-15T07:18:07.440Z",
"updated_at": "2018-03-15T07:18:07.440Z",
"user": {
"email": "[email protected]",
"first_name": "First",
"last_name": "Last"
}
}
]
}
The only error case is if the authenticated user doesn't have access to the job. You'll get a 401
This job only returns notes accessible to the authenticated user. If a job is accessible to multiple orgs, members of each org can only see notes from their team members. A user from one org cannot see the notes created by members of another org.
HTTP Request
GET https://hover.to/api/v2/notes.json
URL Parameters
Parameter | Type | Description |
---|---|---|
job_id | integer | The job to retrieve notes for |
Create A Note
require 'net/http'
require 'api_auth'
url = "https://hover.to/api/v2/notes.json"
uri = URI.parse(url)
request = Net::HTTP::Post.new(uri)
request.set_form_data({
'note[body]' => "This is the content of the note.",
'note[job_id]' => 6
})
signed_request = ApiAuth.sign!(request, HOVER_PARTNER_ID, HMAC_SECRET_KEY)
http = Net::HTTP.new(uri.host, uri.port)
response = http.request(signed_request)
data = JSON.parse(response.body)
When successful you'll get a 201 response code with the following JSON in the response body.
{
"id": 12,
"job_id": 6,
"org_id": 17,
"user_id": 26,
"body": "This is the content of the note.",
"created_at": "2018-03-15T07:18:07.440Z",
"updated_at": "2018-03-15T07:18:07.440Z",
"user": {
"email": "[email protected]",
"first_name": "First",
"last_name": "Last"
}
}
If there is a problem creating the note you'll get a 422 and a JSON response body explaining the problem.
{
"user_id": [
"Does not have access to job"
]
}
HTTP Request
POST https://hover.to/api/v2/notes.json
URL Parameters
Parameter | Type | Description |
---|---|---|
note[body] | String | The content of the note |
note[job_id] | integer | The job to add this note to |
Update A Note
require 'net/http'
require 'api_auth'
url = "https://hover.to/api/v2/notes/5.json"
uri = URI.parse(url)
request = Net::HTTP::Patch.new(uri)
request.set_form_data({
'note[body]' => "Updated note content.",
})
signed_request = ApiAuth.sign!(request, HOVER_PARTNER_ID, HMAC_SECRET_KEY)
http = Net::HTTP.new(uri.host, uri.port)
response = http.request(signed_request)
data = JSON.parse(response.body)
When successful you'll get a 201 response code with the following JSON in the response body.
{
"id": 12,
"job_id": 6,
"org_id": 17,
"user_id": 26,
"body": "Updated note content.",
"created_at": "2018-03-15T07:18:07.440Z",
"updated_at": "2018-03-15T07:18:07.440Z",
"user": {
"email": "[email protected]",
"first_name": "First",
"last_name": "Last"
}
}
The only problem you'll encounter is not having permission to update a note, 422. And the note not existing 404.
HTTP Request
PATCH https://hover.to/api/v2/notes/<Note-ID>.json
URL Parameters
Parameter | Type | Description |
---|---|---|
note[body] | String | The content of the note |
Delete A Note
require 'net/http'
require 'api_auth'
url = "https://hover.to/api/v2/notes/5.json"
uri = URI.parse(url)
request = Net::HTTP::Delete.new(uri)
signed_request = ApiAuth.sign!(request, HOVER_PARTNER_ID, HMAC_SECRET_KEY)
http = Net::HTTP.new(uri.host, uri.port)
response = http.request(signed_request)
data = JSON.parse(response.body)
When successful you'll get a 204 response code and no response
The only problem you'll encounter is not having permission to update a note, 422. And the note not existing 404.
HTTP Request
DELETE https://hover.to/api/v2/notes/<Note-ID>.json
Errors
The HOVER API uses the following error codes:
Error Code | Meaning |
---|---|
400 | Bad Request -- There is something wrong with your request. |
401 | Unauthorized -- Your API key or signing process is wrong. |
403 | Forbidden -- You are not allowed access to the requested resource. |
404 | Not Found -- The specified resource could not be found. |
406 | Not Acceptable -- You requested a format that isn't json. |
422 | Unprocessable Entity -- The create or update request you tried to make would result in an invalid record. Invalid Parameters. Response body format is {"attribute": [ "list", "of", "problems", "with", "attribute"]} |
500 | Internal Server Error -- We had a problem with our server. Try again later. |
503 | Service Unavailable -- We're temporarily offline for maintenance. Please try again later. |
JSON examples
Job Measurement Data Summarized Version
GET https://hover.to/api/v2/jobs/<Job-ID>/measurements.json?version=summarized_json
See Job Measurement Data Full Version
See Job Measurement Data Total Living Area
See Job Sketch JSON Total Living Area
See 3D Representation of a Job
When available you'll be redirected to the measurement file with the following JSON in the response body.
{
"area": {
"facades": {
"siding": 3350,
"other": 0
},
"openings": {
"siding": 205,
"other": 0
},
"unknown": {
"siding": null,
"other": 0
},
"total": {
"siding": 3555,
"other": 0
}
},
"openings": {
"quantity": {
"siding": 2,
"other": 0
},
"tops_length": {
"siding": 17.67,
"other": 0
},
"sills_length": {
"siding": 3.08,
"other": 0
},
"sides_length": {
"siding": 40.0,
"other": 0
},
"total_perimeter": {
"siding": 60.75,
"other": 0
}
},
"siding_waste": {
"zero": 3350,
"plus_10_percent": 3685,
"plus_18_percent": 3953,
"with_openings": 3368,
"openings_plus_10_percent": 3705,
"openings_plus_18_percent": 3974
},
"trim": {
"level_starter": {
"siding": 201.58,
"other": 0
},
"sloped_trim": {
"siding": 0,
"other": 0
},
"vertical_trim": {
"siding": 0,
"other": 0
}
},
"roofline": {
"eaves_fascia": {
"length": 120.0,
"avg_depth": null,
"soffit_area": null
},
"level_frieze_board": {
"length": 119.33,
"avg_depth": 0.17,
"soffit_area": 16
},
"rakes_fascia": {
"length": 103.92,
"avg_depth": null,
"soffit_area": null
},
"sloped_frieze_board": {
"length": 103.92,
"avg_depth": 0.17,
"soffit_area": 16
}
},
"corners": {
"inside_corners_qty": {
"siding": 0,
"other": null
},
"inside_corners_len": {
"siding": 0,
"other": null
},
"outside_corners_qty": {
"siding": 4,
"other": null
},
"outside_corners_len": {
"siding": 58.5,
"other": null
}
},
"accessories": {
"shutter_qty": {
"siding": 0,
"other": 0
},
"shutter_area": {
"siding": 0,
"other": 0
},
"vents_qty": {
"siding": 0,
"other": 0
},
"vents_area": {
"siding": 0,
"other": 0
}
},
"roof": {
"roof_facets": {
"area": 3118,
"total": 2,
"length": null
},
"ridges_hips": {
"area": null,
"total": 1,
"length": 60.0
},
"valleys": {
"area": null,
"total": 0,
"length": 0
},
"rakes": {
"area": null,
"total": 4,
"length": 103.92
},
"gutters_eaves": {
"area": null,
"total": 2,
"length": 120.0
},
"flashing": {
"area": null,
"total": 0,
"length": 0
},
"step_flashing": {
"area": null,
"total": 0,
"length": 0
},
"pitch": [
{
"roof_pitch": "4/12",
"area": 3118,
"percentage": 100.0
}
],
"waste_factor": {
"area": {
"zero": 3118,
"plus_5_percent": 3274,
"plus_10_percent": 3430,
"plus_15_percent": 3586,
"plus_20_percent": 3742
}
}
}
}
The availability of the fields depends on the job's deliverable type.
Field | Type | Deliverable | Description |
---|---|---|---|
area | Complete | ||
facades.siding | integer | In square feet. | |
facades.other | integer | In square feet. | |
openings.siding | integer | In square feet. | |
openings.other | integer | In square feet. | |
unknown.siding | null | - | |
unknown.other | integer | In square feet. | |
openings | Complete | ||
openings.quantity.siding | integer | Quantity. | |
openings.quantity.other | integer | Quantity. | |
openings.tops_length.siding | decimal | In feet. | |
openings.tops_length.other | decimal | In feet. | |
openings.sills_length.siding | decimal | In feet. | |
openings.sills_length.other | decimal | In feet. | |
siding_waste | Complete | ||
zero | Integer | In square feet. | |
plus_10_percent | integer | In square feet. | |
plus_18_percent | integer | In square feet. | |
with_openings | integer | In square feet. | |
openings_plus_10_percent | integer | In square feet. | |
openings_plus_18_percent | integer | In square feet. | |
trim | Complete | ||
level_starter.siding | decimal | In feet. | |
level_starter.other | decimal | In feet. | |
sloped_trim.siding | decimal | In feet. | |
sloped_trim.other | decimal | In feet. | |
vertical_trim.siding | decimal | In feet. | |
vertical_trim.other | decimal | In feet. | |
roofline | Complete | ||
eaves_fascia.length | decimal | In feet. | |
eaves_fascia.avg_depth | null | - | |
eaves_fascia.soffit_area | null | - | |
level_frieze_board.length | decimal | In feet. | |
level_frieze_board.avg_depth | decimal | In feet. | |
level_frieze_board.soffit_area | decimal | In square feet. | |
rakes_fascia.length | decimal | In feet. | |
rakes_fascia.avg_depth | null | - | |
rakes_fascia.soffit_area | null | - | |
sloped_frieze_board.length | decimal | In feet. | |
sloped_frieze_board.avg_depth | decimal | In feet. | |
sloped_frieze_board.soffit_area | integer | In square feet. | |
corners | Complete | ||
inside_corners_qty.siding | integer | Quantity. | |
inside_corners_qty.other | null | - | |
inside_corners_len.siding | decimal | In feet. | |
inside_corners_len.other | null | - | |
outside_corners_qty.siding | decimal | Quantity. | |
outside_corners_qty.other | null | - | |
outside_corners_len.siding | decimal | In feet. | |
outside_corners_len.other | null | - | |
accessories | Complete | ||
shutter_qty.siding | integer | Quantity. | |
shutter_qty.other | integer | Quantity. | |
shutter_area.siding | null | In square feet. | |
shutter_area.other | null | In square feet. | |
vents_qty.siding | integer | Quantity. | |
vents_qty.other | integer | Quantity. | |
vents_area.siding | null | In square feet. | |
vents_area.other | null | In square feet. | |
roof | Roof-Only or Complete | ||
roof_facets.area | integer | In square feet. | |
roof_facets.total | integer | Quantity. | |
roof_facets.length | null | _ | |
ridges_hips.area | null | _ | |
ridges_hips.total | integer | Quantity. | |
ridges_hips.length | decimal | In feet. | |
valleys.area | null | - | |
valleys.total | integer | Quantity. | |
valleys.length | decimal | In feet. | |
rakes.area | null | - | |
rakes.total | integer | Quantity. | |
rakes.length | decimal | In feet. | |
gutters_eaves.area | null | - | |
gutters_eaves.total | integer | Quantity. | |
gutters_eaves.length | decimal | In feet. | |
flashing.area | null | - | |
flashing.total | integer | Quantity. | |
flashing.length | decimal | In feet. | |
step_flashing.area | null | - | |
step_flashing.total | integer | Quantity. | |
step_flashing.length | decimal | In feet. | |
pitch | array | ||
- roof_pitch | string | ||
- area | integer | In square feet. | |
- percentage | decimal | ||
waste_factor.area.zero | integer | In square feet. | |
waste_factor.area.plus_5_percent | integer | In square feet. | |
waste_factor.area.plus_10_percent | integer | In square feet. | |
waste_factor.area.plus_15_percent | integer | In square feet. | |
waste_factor.area.plus_20_percent | integer | In square feet. |
Job Measurement Data Full Version
GET https://hover.to/api/v2/jobs/<Job-ID>/measurements.json?version=full_json
See Job Measurement Data Summarized Version
See Job Measurement Data Total Living Area
See Job Sketch JSON Total Living Area
See 3D Representation of a Job
When available you'll be redirected to the measurement file with the following JSON in the response body.
{
"version": 1.0,
"summary": {
"area": {
"facades": {
"siding": 3350,
"other": 0
},
"openings": {
"siding": 205,
"other": 0
},
"unknown": {
"siding": null,
"other": 0
},
"total": {
"siding": 3555,
"other": 0
}
},
"openings": {
"quantity": {
"siding": 2,
"other": 0
},
"tops_length": {
"siding": 17.67,
"other": 0
},
"sills_length": {
"siding": 3.08,
"other": 0
},
"sides_length": {
"siding": 40.0,
"other": 0
},
"total_perimeter": {
"siding": 60.75,
"other": 0
}
},
"siding_waste": {
"zero": 3350,
"plus_10_percent": 3685,
"plus_18_percent": 3953,
"with_openings": 3368,
"openings_plus_10_percent": 3705,
"openings_plus_18_percent": 3974
},
"trim": {
"level_starter": {
"siding": 201.58,
"other": 0
},
"sloped_trim": {
"siding": 0,
"other": 0
},
"vertical_trim": {
"siding": 0,
"other": 0
}
},
"roofline": {
"eaves_fascia": {
"length": 120.0,
"avg_depth": null,
"soffit_area": null
},
"level_frieze_board": {
"length": 119.33,
"avg_depth": 0.17,
"soffit_area": 16
},
"rakes_fascia": {
"length": 103.92,
"avg_depth": null,
"soffit_area": null
},
"sloped_frieze_board": {
"length": 103.92,
"avg_depth": 0.17,
"soffit_area": 16
}
},
"corners": {
"inside_corners_qty": {
"siding": 0,
"other": null
},
"inside_corners_len": {
"siding": 0,
"other": null
},
"outside_corners_qty": {
"siding": 4,
"other": null
},
"outside_corners_len": {
"siding": 58.5,
"other": null
}
},
"accessories": {
"shutter_qty": {
"siding": 0,
"other": 0
},
"shutter_area": {
"siding": 0,
"other": 0
},
"vents_qty": {
"siding": 0,
"other": 0
},
"vents_area": {
"siding": 0,
"other": 0
}
},
"roof": {
"roof_facets": {
"area": 3118,
"total": 2,
"length": null
},
"ridges_hips": {
"area": null,
"total": 1,
"length": 60.0
},
"valleys": {
"area": null,
"total": 0,
"length": 0
},
"rakes": {
"area": null,
"total": 4,
"length": 103.92
},
"gutters_eaves": {
"area": null,
"total": 2,
"length": 120.0
},
"flashing": {
"area": null,
"total": 0,
"length": 0
},
"step_flashing": {
"area": null,
"total": 0,
"length": 0
},
"pitch": [
{
"roof_pitch": "4/12",
"area": 3118,
"percentage": 100.0
}
],
"waste_factor": {
"area": {
"zero": 3118,
"plus_5_percent": 3274,
"plus_10_percent": 3430,
"plus_15_percent": 3586,
"plus_20_percent": 3742
}
}
},
"address": "634 2nd St, San Francisco, CA 94107",
"property_id": 12345,
"external_identifier": "an-external-identifier"
},
"footprint": {
"stories": ">1",
"perimeter": 141.93036972,
"area": 1048,
"wireframe_url": "https://hover.to/api/v2/jobs/<Job-ID>/wireframe_images/wireframe_footprint.png"
},
"elevations": {
"sides": {
"front": {
"total": 493,
"area_per_label": [
{
"SI-1": 323
},
{
"SI-2": 25
},
{
"SI-4": 145
}
]
},
"right": {
"total": 543,
"area_per_label": [
{
"SI-3": 81
},
{
"SI-5": 196
},
{
"SI-6": 23
},
{
"SI-7": 243
}
]
},
"left": {
"total": 322,
"area_per_label": [
{
"SI-11": 284
},
{
"SI-12": 38
}
]
},
"back": {
"total": 516,
"area_per_label": [
{
"SI-10": 389
},
{
"SI-8": 77
},
{
"SI-9": 50
}
]
}
}
},
"facades": {
"siding": [
{
"facade": "SI-1",
"area": 323,
"shutters": 0,
"vents": 0,
"area_with_waste_factor_calculation": {
"zero": 323,
"plus_10_percent": 355,
"plus_18_percent": 381,
"with_openings": 418,
"openings_plus_10_percent": 460,
"openings_plus_18_percent": 493
},
"trim": {
"level_starter": 20.5244407,
"sloped": 0.0,
"vertical": 5.64105764
},
"corners": {
"inside_number": 0,
"inside_length": 0.0,
"outside_number": 2,
"outside_length": 21.06920453
},
"roofline": {
"level_frieze_board": 0.31316037,
"sloped_frieze_board": 28.75674916
},
"openings": {
"tops": 15.15807605,
"sills": 15.15807605,
"sides": 42.27518182,
"openings_total": 7,
"labels": [
"W-7",
"W-5",
"W-3",
"W-4",
"W-6",
"W-1",
"W-2"
]
}
},
{
"facade": "SI-2",
"area": 25,
"shutters": 0,
"vents": 0,
"area_with_waste_factor_calculation": {
"zero": 25,
"plus_10_percent": 28,
"plus_18_percent": 30,
"with_openings": 25,
"openings_plus_10_percent": 28,
"openings_plus_18_percent": 30
},
"trim": {
"level_starter": 0.0,
"sloped": 6.92526853,
"vertical": 0.0
},
"corners": {
"inside_number": 1,
"inside_length": 2.61763106,
"outside_number": 1,
"outside_length": 5.12408381
},
"roofline": {
"level_frieze_board": 6.47982693,
"sloped_frieze_board": 0.0
},
"openings": {
"tops": 0.0,
"sills": 0.0,
"sides": 0.0,
"openings_total": 0,
"labels": [
]
}
}
],
"brick": [
{
"facade": "BR-1",
"area": 33,
"shutters": 0,
"vents": 0,
"openings": {
"openings_total": 0
}
},
{
"facade": "BR-2",
"area": 52,
"shutters": 0,
"vents": 0,
"openings": {
"openings_total": 4
}
}
]
},
"openings": {
"windows": [
{
"opening": "W-1",
"width_x_height": "31\" x 69\"",
"united_inches": "100\"",
"area": 15
},
{
"opening": "W-2",
"width_x_height": "31\" x 69\"",
"united_inches": "100\"",
"area": 15
}
],
"doors": [
{
"opening": "D-1",
"width_x_height": "36\" x 96\"",
"area": 24
},
{
"opening": "D-2",
"width_x_height": "36\" x 96\"",
"area": 24
}
]
},
"roof": {
"measurements": {
"ridges": 75.10744583,
"hips": 10.24373374,
"valleys": 44.41210524,
"rakes": 114.83583305,
"gutters_eaves": 121.81002309,
"flashing": 14.70389798,
"step_flashing": 32.53078363
},
"facets": [
{
"facet": "RF-2",
"area": 26,
"pitch": 4
},
{
"facet": "RF-8",
"area": 281,
"pitch": 12
}
],
"area": {
"facets": 9,
"total": 1811
},
"pitch": [
{
"roof_pitch": "4/12",
"area": 113,
"percentage": "6.26%"
},
{
"roof_pitch": "12/12",
"area": 1663,
"percentage": "91.81%"
},
{
"roof_pitch": "3/12",
"area": 35,
"percentage": "1.92%"
}
],
"wireframe_facets_url": "https://hover.to/api/v2/jobs/<Job-ID>/wireframe_images/wireframe_roof.png",
"wireframe_measurements_url": "https://hover.to/api/v2/jobs/<Job-ID>/wireframe_images/wireframe_roof.png?version=lengths",
"wireframe_areas_url": "https://hover.to/api/v2/jobs/<Job-ID>/wireframe_images/wireframe_roof.png?version=areas",
"wireframe_pitches_url": "https://hover.to/api/v2/jobs/<Job-ID>/wireframe_images/wireframe_roof.png?version=pitches"
},
"wireframes": {
"front": {
"wireframe_url": "https://hover.to/api/v2/jobs/<Job-ID>/wireframe_images/wireframe_front.png",
"compass_url": "https://hover.to/api/v2/jobs/<Job-ID>/wireframe_images/wireframe_front.png?version=compass",
"top_url": "https://hover.to/api/v2/jobs/<Job-ID>/wireframe_images/wireframe_front.png?version=top",
"metadata": {
"visible_labels": [
"SI-4",
"SI-2"
]
}
},
"front_right": {
"wireframe_url": "https://hover.to/api/v2/jobs/<Job-ID>/wireframe_images/wireframe_front_right.png",
"compass_url": "https://hover.to/api/v2/jobs/<Job-ID>/wireframe_images/wireframe_front_right.png?version=compass",
"top_url": "https://hover.to/api/v2/jobs/<Job-ID>/wireframe_images/wireframe_front_right.png?version=top",
"metadata": {
"visible_labels": [
"SI-7",
"SI-4"
]
}
},
"right": {
"wireframe_url": "https://hover.to/api/v2/jobs/<Job-ID>/wireframe_images/wireframe_right.png",
"compass_url": "https://hover.to/api/v2/jobs/<Job-ID>/wireframe_images/wireframe_right.png?version=compass",
"top_url": "https://hover.to/api/v2/jobs/<Job-ID>/wireframe_images/wireframe_right.png?version=top",
"metadata": {
"visible_labels": [
"SI-6",
"SI-7",
"SI-3"
]
}
},
"right_back": {
"wireframe_url": "https://hover.to/api/v2/jobs/<Job-ID>/wireframe_images/wireframe_right_back.png",
"compass_url": "https://hover.to/api/v2/jobs/<Job-ID>/wireframe_images/wireframe_right_back.png?version=compass",
"top_url": "https://hover.to/api/v2/jobs/<Job-ID>/wireframe_images/wireframe_right_back.png?version=top",
"metadata": {
"visible_labels": [
"SI-8"
]
}
},
"back": {
"wireframe_url": "https://hover.to/api/v2/jobs/<Job-ID>/wireframe_images/wireframe_back.png",
"compass_url": "https://hover.to/api/v2/jobs/<Job-ID>/wireframe_images/wireframe_back.png?version=compass",
"top_url": "https://hover.to/api/v2/jobs/<Job-ID>/wireframe_images/wireframe_back.png?version=top",
"metadata": {
"visible_labels": [
"SI-8"
]
}
},
"back_left": {
"wireframe_url": "https://hover.to/api/v2/jobs/<Job-ID>/wireframe_images/wireframe_back_left.png",
"compass_url": "https://hover.to/api/v2/jobs/<Job-ID>/wireframe_images/wireframe_back_left.png?version=compass",
"top_url": "https://hover.to/api/v2/jobs/<Job-ID>/wireframe_images/wireframe_back_left.png?version=top",
"metadata": {
"visible_labels": [
"SI-11",
"SI-10",
"BR-2"
]
}
},
"left": {
"wireframe_url": "https://hover.to/api/v2/jobs/<Job-ID>/wireframe_images/wireframe_left.png",
"compass_url": "https://hover.to/api/v2/jobs/<Job-ID>/wireframe_images/wireframe_left.png?version=compass",
"top_url": "https://hover.to/api/v2/jobs/<Job-ID>/wireframe_images/wireframe_left.png?version=top",
"metadata": {
"visible_labels": [
"SI-11",
"SI-12",
"BR-2"
]
}
},
"left_front": {
"wireframe_url": "https://hover.to/api/v2/jobs/<Job-ID>/wireframe_images/wireframe_left_front.png",
"compass_url": "https://hover.to/api/v2/jobs/<Job-ID>/wireframe_images/wireframe_left_front.png?version=compass",
"top_url": "https://hover.to/api/v2/jobs/<Job-ID>/wireframe_images/wireframe_left_front.png?version=top",
"metadata": {
"visible_labels": [
"SI-11"
]
}
}
},
"captured_images": [
"https://hover.to/api/v2/images/123/rotated_image.jpg?version=medium",
"https://hover.to/api/v2/images/124/rotated_image.jpg?version=medium",
"https://hover.to/api/v2/images/125/rotated_image.jpg?version=medium"
]
}
The availability of the fields depends on the job's deliverable type.
Field | Type | Deliverable | Description |
---|---|---|---|
summary | Roof-Only or Complete | Most of the fields are same as Job Measurement Data Summarized Version | |
address | string | Address of the job | |
property_id | integer | Job ID | |
external_identifier | string | External Identifier of the job | |
footprint | Complete | ||
stories | string | "1" or ">1" | |
perimeter | decimal | In feet. | |
area | integer | In square feet. | |
wireframe_url | string | Link to wireframe footprint png image. | |
elevations | Complete | ||
sides | hash | ||
- front.total | integer | In square feet. | |
- front.area_per_label | Array | Label as key/Area as value in square feet, i.e., [ { "SI-1": 323 }, { "SI-2": 25 } ] . |
|
- right.total | integer | In square feet. | |
- right.area_per_label | Array | Label as key/Area as value in square feet, i.e., [ { "SI-1": 323 }, { "SI-2": 25 } ] . |
|
- left.total | integer | In square feet. | |
- left.area_per_label | Array | Label as key/Area as value in square feet, i.e., [ { "SI-1": 323 }, { "SI-2": 25 } ] . |
|
- back.total | integer | In square feet. | |
- back.area_per_label | Array | Label as key/Area as value in square feet, i.e., [ { "SI-1": 323 }, { "SI-2": 25 } ] . |
|
facades | Complete | ||
siding | array | ||
- facade | string | Label name. | |
- area | integer | In square feet. | |
- shutters | integer | Quantity. | |
- vents | integer | Quantity. | |
- area_with_waste_factor_calculation | hash | ||
> zero | integer | In square feet. | |
> plus_10_percent | integer | In square feet. | |
> plus_18_percent | integer | In square feet. | |
> with_openings | integer | In square feet. | |
> openings_plus_10_percent | integer | In square feet. | |
> openings_plus_18_percent | integer | In square feet. | |
- trim | hash | ||
> level_starter | decimal | In feet. | |
> sloped | decimal | In feet. | |
> vertical | decimal | In feet. | |
- corners | hash | ||
> inside_number | integer | Quantity. | |
> inside_length | decimal | In feet. | |
> outside_number | outside_number | Quantity. | |
> outside_length | decimal | In feet. | |
- roofline | hash | ||
> level_frieze_board | decimal | In feet. | |
> sloped_frieze_board | decimal | In feet. | |
- openings | hash | ||
> tops | decimal | In feet. | |
> sills | decimal | In feet. | |
> sides | decimal | In feet. | |
> openings_total | integer | Quantity. | |
> labels | array | Array of labels, i.e., [ "W-1", "W-2" ] |
|
brick | array | ||
- facade | string | Label name. | |
- area | integer | In square feet. | |
- shutters | integer | Quantity. | |
- vents | integer | Quantity. | |
- openings | hash | ||
> openings_total | integer | Quantity. | |
openings | Complete | ||
windows | array | ||
> opening | string | Label name. | |
> width_x_height | string | '31" x 69"' |
|
> united_inches | string | '100"' |
|
> area | integer | In square feet. | |
doors | array | ||
> opening | string | Label name. | |
> width_x_height | string | '31" x 69"' |
|
> area | integer | In square feet. | |
roof | Roof-Only or Complete | ||
measurements | hash | ||
- ridges | decimal | In feet. | |
- hips | decimal | In feet. | |
- valleys | decimal | In feet. | |
- rakes | decimal | In feet. | |
- gutters_eaves | decimal | In feet. | |
- flashing | decimal | In feet. | |
- step_flashing | decimal | In feet. | |
facets | array | ||
> facet | string | Label name. | |
> area | integer | In square feet. | |
> pitch | integer | Number of inches it rises vertically for every 12 inches it extends horizontally. For example, if this number is 11, the pitch is then 11/12 and the slope angle is 42.51 degrees. | |
area | hash | ||
> facets | integer | Number of facets. | |
> total | integer | In square feet. | |
pitch | array | ||
> roof_pitch | string | "4/12" |
|
> area | integer | In square feet. | |
> percentage | string | The percentage of the roof area with the corresponding roof pitch. | |
wireframe_facets_url | string | Link to wireframe roof facets png image. | |
wireframe_measurements_url | string | Link to wireframe roof measurements png image. | |
wireframe_areas_url | string | Link to wireframe roof areas png image. | |
wireframe_pitches_url | string | Link to wireframe roof pitches png image. | |
wireframes | Complete | ||
front | hash | ||
> wireframe_url | string | Link to the corresponding wireframe png image. | |
> compass_url | string | Link to the corresponding wireframe compass png image. | |
> top_url | string | Link to the corresponding wireframe top (bird's eye view) png image. | |
> metadata | hash | Contain metadata like visible_labels if available. |
|
front_right | hash | Same as wireframes.front |
|
right | hash | Same as wireframes.front |
|
right_back | hash | Same as wireframes.front |
|
back | hash | Same as wireframes.front |
|
back_left | hash | Same as wireframes.front |
|
left | hash | Same as wireframes.front |
|
left_front | hash | Same as wireframes.front |
|
captured_images | array | Roof-Only or Complete | Links to the captured images. |
Job Measurement Data Total Living Area
GET https://hover.to/api/v2/jobs/<Job-ID>/measurements.json?version=summarized_json
See Job Measurement Data Summarized Version
See Job Measurement Data Full Version
See Job Sketch JSON Total Living Area
See 3D Representation of a Job
When available you'll be redirected to the measurement file with the following JSON in the response body.
{
"living_areas": {
"floors": {
"entities": [
{
"label": null,
"identifier": "1st_floor",
"area": 232,
"measured": true,
"floor": 1
},
{
"label": null,
"identifier": "2nd_floor",
"area": 50,
"measured": true,
"floor": 2
},
{
"label": null,
"identifier": "3rd_floor",
"area": 35,
"measured": true,
"floor": 3
},
{
"label": null,
"identifier": "4th_floor",
"area": 35,
"measured": true,
"floor": 4
},
{
"label": null,
"identifier": "5th_floor",
"area": 35,
"measured": true,
"floor": 5
}
],
"total_area": 387
}
},
"attached_structures": {
"basements": {
"entities": [
{
"label": null,
"identifier": "basement_custom",
"area": 123,
"measured": true
}
],
"total_area": 123
},
"built_in_garages": {
"entities": [
{
"label": null,
"identifier": "garage_built_in",
"area": 51,
"measured": true,
"cars": null
}
],
"total_area": 51
},
"attached_garages": {
"entities": [
{
"label": null,
"identifier": "garage_attached",
"area": 35,
"measured": true,
"cars": null
}
],
"total_area": 35
},
"basement_garages": {
"entities": [
{
"label": null,
"identifier": "garage_basement",
"area": 57,
"measured": true,
"cars": null
}
],
"total_area": 57
},
"detached_garages": {
"entities": [
{
"label": null,
"identifier": "garage_detached_1car",
"area": 220,
"measured": false,
"cars": 1
},
{
"label": null,
"identifier": "garage_detached_1car",
"area": 220,
"measured": false,
"cars": 1
},
{
"label": null,
"identifier": "garage_detached_2car",
"area": 440,
"measured": false,
"cars": 2
},
{
"label": null,
"identifier": "garage_detached_3car",
"area": 660,
"measured": false,
"cars": 3
},
{
"label": null,
"identifier": "garage_detached_4car",
"area": 880,
"measured": false,
"cars": 4
},
{
"label": null,
"identifier": "garage_detached_5car",
"area": 1100,
"measured": false,
"cars": 5
}
],
"total_area": 3520
},
"decks": {
"entities": [
{
"label": null,
"identifier": "deck",
"area": 81.5,
"measured": true
}
],
"total_area": 81.5
},
"covered_patios": {
"entities": [
{
"label": null,
"identifier": "covered_patio",
"area": 37,
"measured": true
}
],
"total_area": 37
},
"open_breezeways": {
"entities": [
{
"label": null,
"identifier": "open_breezeway",
"area": 44,
"measured": true
}
],
"total_area": 44
},
"enclosed_breezeways": {
"entities": [
{
"label": null,
"identifier": "enclosed_breezeway",
"area": 50,
"measured": true
}
],
"total_area": 50
},
"screened_porches": {
"entities": [
{
"label": null,
"identifier": "screened_porch",
"area": 35,
"measured": true
}
],
"total_area": 35
},
"enclosed_porches": {
"entities": [
{
"label": null,
"identifier": "enclosed_porch",
"area": 35,
"measured": true
}
],
"total_area": 35
},
"open_porches": {
"entities": [
{
"label": null,
"identifier": "open_porch",
"area": 43,
"measured": true
}
],
"total_area": 43
},
"screened_enclosures": {
"entities": [
{
"label": null,
"identifier": "screened_enclosure",
"area": 50,
"measured": true
}
],
"total_area": 50
},
"carports": {
"entities": [
{
"label": null,
"identifier": "carport",
"area": 50,
"measured": true
}
],
"total_area": 50
}
},
"version": "1.0"
}
The availability of the fields depends on the job's deliverable type.
Field | Type | Deliverable | Description |
---|---|---|---|
living_areas | Total Living Area | ||
floors | hash | ||
- entities.label | null | - | |
- entities.identifier | string | Floor identifier. | |
- entities.area | decimal | In square feet. | |
- entities.measured | boolean | Indicates if measured or not. | |
- entities.floor | integer | Floor number. | |
- total_area | decimal | In square feet. | |
attached_structures | Total Living Area | ||
basements | hash | ||
- entities.label | null | - | |
- entities.identifier | string | Indicates if area is same as first floor or custom. | |
- entities.area | decimal | In square feet. | |
- entities.measured | boolean | Indicates if measured or not. | |
- total_area | decimal | In square feet. | |
built_in_garages | hash | ||
- entities.label | null | - | |
- entities.identifier | string | Garage specs identifier. | |
- entities.area | decimal | In square feet. | |
- entities.measured | boolean | Indicates if measured or not. | |
- entities.cars | integer | Number of possible cars inside. | |
- total_area | decimal | In square feet. | |
attached_garages | hash | ||
- entities.label | null | - | |
- entities.identifier | string | Garage specs identifier. | |
- entities.area | decimal | In square feet. | |
- entities.measured | boolean | Indicates if measured or not. | |
- entities.cars | integer | Number of possible cars inside. | |
- total_area | decimal | In square feet. | |
basement_garages | hash | ||
- entities.label | null | - | |
- entities.identifier | string | Garage specs identifier. | |
- entities.area | decimal | In square feet. | |
- entities.measured | boolean | Indicates if measured or not. | |
- entities.cars | integer | Number of possible cars inside. | |
- total_area | decimal | In square feet. | |
detached_garages | hash | ||
- entities.label | null | - | |
- entities.identifier | string | Garage specs identifier. | |
- entities.area | decimal | In square feet. | |
- entities.measured | boolean | Indicates if measured or not. | |
- entities.cars | integer | Number of possible cars inside. | |
- total_area | decimal | In square feet. | |
decks | hash | ||
- entities.label | null | - | |
- entities.identifier | string | Deck identifier. | |
- entities.area | decimal | In square feet. | |
- entities.measured | boolean | Indicates if measured or not. | |
- total_area | decimal | In square feet. | |
covered_patios | hash | ||
- entities.label | null | - | |
- entities.identifier | string | Covered patio identifier. | |
- entities.area | decimal | In square feet. | |
- entities.measured | boolean | Indicates if measured or not. | |
- total_area | decimal | In square feet. | |
open_breezeways | hash | ||
- entities.label | null | - | |
- entities.identifier | string | Breezeway identifier. | |
- entities.area | decimal | In square feet. | |
- entities.measured | boolean | Indicates if measured or not. | |
- total_area | decimal | In square feet. | |
enclosed_breezeways | hash | ||
- entities.label | null | - | |
- entities.identifier | string | Breezeway identifier. | |
- entities.area | decimal | In square feet. | |
- entities.measured | boolean | Indicates if measured or not. | |
- total_area | decimal | In square feet. | |
screened_porches | hash | ||
- entities.label | null | - | |
- entities.identifier | string | Porch identifier. | |
- entities.area | decimal | In square feet. | |
- entities.measured | boolean | Indicates if measured or not. | |
- total_area | decimal | In square feet. | |
enclosed_porches | hash | ||
- entities.label | null | - | |
- entities.identifier | string | Porch identifier. | |
- entities.area | decimal | In square feet. | |
- entities.measured | boolean | Indicates if measured or not. | |
- total_area | decimal | In square feet. | |
open_porches | hash | ||
- entities.label | null | - | |
- entities.identifier | string | Porch identifier. | |
- entities.area | decimal | In square feet. | |
- entities.measured | boolean | Indicates if measured or not. | |
- total_area | decimal | In square feet. | |
screened_enclosures | hash | ||
- entities.label | null | - | |
- entities.identifier | string | Screened enclosure identifier. | |
- entities.area | decimal | In square feet. | |
- entities.measured | boolean | Indicates if measured or not. | |
- total_area | decimal | In square feet. | |
carports | hash | ||
- entities.label | null | - | |
- entities.identifier | string | Carport identifier. | |
- entities.area | decimal | In square feet. | |
- entities.measured | boolean | Indicates if measured or not. | |
- total_area | decimal | In square feet. |
Job Sketch JSON Total Living Area
GET https://hover.to/api/v2/jobs/<Job-ID>/measurements.json?version=sketch_json
See Job Measurement Data Summarized Version
See Job Measurement Data Full Version
See Job Measurement Data Total Living Area
See 3D Representation of a Job
The Total Living Area Sketch JSON is available to import into external applications.
3D Representation of a Job
GET https://hover.to/api/v2/jobs/<Job-ID>/measurements.json
See Job Measurement Data Summarized Version
See Job Measurement Data Full Version
See Job Measurement Data Total Living Area
See Job Sketch JSON Total Living Area
When available you'll be redirected to the file with the following JSON in the response body.
{
"points": {
"0": {
"position": [
-282.78138694,
377.31895297,
-4.10000019
],
"display_label": "P-1"
},
"1": {
"position": [
-282.78138694,
377.31895297,
171.27429406
],
"display_label": "P-2"
},
"2": {
"position": [
297.14195747,
363.46304135,
-4.10000019
],
"display_label": "P-3"
},
"3": {
"position": [
297.14195746,
363.4630408,
171.27429406
],
"display_label": "P-4"
},
"4": {
"position": [
-280.78195755,
377.27118128,
171.27429406
],
"display_label": "P-5"
},
"5": {
"position": [
295.14252807,
363.51081249,
171.27429406
],
"display_label": "P-6"
},
"6": {
"position": [
-280.78195755,
377.27118128,
-4.10000019
],
"display_label": "P-7"
},
"7": {
"position": [
295.14252807,
363.51081249,
-4.10000019
],
"display_label": "P-8"
},
"8": {
"position": [
-285.45689344,
377.38287794,
171.27429406
],
"display_label": "P-9"
},
"9": {
"position": [
299.81746396,
363.39911583,
171.27429406
],
"display_label": "P-10"
},
"10": {
"position": [
7.18028467,
370.3909969,
268.22467489
],
"display_label": "P-11"
},
"11": {
"position": [
224.31071929,
365.20317292,
80.3514896
],
"display_label": "P-12"
},
"12": {
"position": [
260.0514174,
364.34923252,
80.3514896
],
"display_label": "P-13"
},
"13": {
"position": [
260.0514174,
364.34923252,
-0.92251216
],
"display_label": "P-14"
},
"14": {
"position": [
224.31071929,
365.20317292,
-0.92251216
],
"display_label": "P-15"
},
"15": {
"position": [
279.85599097,
-360.02170383,
171.27429406
],
"display_label": "P-16"
},
"16": {
"position": [
279.85598944,
-360.02170379,
-4.10000019
],
"display_label": "P-17"
},
"17": {
"position": [
297.0941858,
361.46361197,
-4.10000019
],
"display_label": "P-18"
},
"18": {
"position": [
279.9037611,
-358.0222744,
-4.10000019
],
"display_label": "P-19"
},
"19": {
"position": [
279.9037611,
-358.0222744,
171.27429406
],
"display_label": "P-20"
},
"20": {
"position": [
297.0941858,
361.46361197,
171.27429406
],
"display_label": "P-21"
},
"21": {
"position": [
-300.06735497,
-346.16579217,
-4.10000019
],
"display_label": "P-22"
},
"22": {
"position": [
-300.06735344,
-346.16579221,
171.27429406
],
"display_label": "P-23"
},
"23": {
"position": [
-300.0195833,
-344.16636279,
-4.10000019
],
"display_label": "P-24"
},
"24": {
"position": [
-282.82915861,
375.31952358,
-4.10000019
],
"display_label": "P-25"
},
"25": {
"position": [
-282.82915861,
375.31952358,
171.27429406
],
"display_label": "P-26"
},
"26": {
"position": [
-300.0195833,
-344.16636279,
171.27429406
],
"display_label": "P-27"
},
"27": {
"position": [
-280.17164551,
-346.64113762,
-4.10000019
],
"display_label": "P-28"
},
"28": {
"position": [
-298.06792363,
-346.21354613,
-4.10000019
],
"display_label": "P-29"
},
"29": {
"position": [
277.85656159,
-359.97393208,
-4.10000019
],
"display_label": "P-30"
},
"30": {
"position": [
-106.4445293,
-350.79194474,
-4.10000019
],
"display_label": "P-31"
},
"31": {
"position": [
277.85656159,
-359.97393208,
171.27429406
],
"display_label": "P-32"
},
"32": {
"position": [
-298.06792363,
-346.21354613,
171.27429406
],
"display_label": "P-33"
},
"33": {
"position": [
282.53149747,
-360.08562888,
171.27429406
],
"display_label": "P-34"
},
"34": {
"position": [
-302.74286041,
-346.10184931,
171.27429406
],
"display_label": "P-35"
},
"35": {
"position": [
-10.10567516,
-353.09373924,
268.22467489
],
"display_label": "P-36"
},
"36": {
"position": [
-280.17164555,
-346.64113932,
152.64333963
],
"display_label": "P-37"
},
"37": {
"position": [
-106.44452916,
-350.79193883,
152.64333963
],
"display_label": "P-38"
},
"38": {
"position": [
282.48503452,
-362.03046483,
176.81884015
],
"display_label": "P-39"
},
"39": {
"position": [
299.86392795,
365.34399498,
176.81884015
],
"display_label": "P-40"
},
"40": {
"position": [
7.22675284,
372.33585448,
273.76922097
],
"display_label": "P-41"
},
"41": {
"position": [
-10.15214352,
-355.03860534,
273.76922097
],
"display_label": "P-42"
},
"42": {
"position": [
-285.41043049,
379.32771397,
176.81884015
],
"display_label": "P-43"
},
"43": {
"position": [
-302.78932391,
-348.04670829,
176.81884015
],
"display_label": "P-44"
}
},
"edges": {
"0": {
"points": [
0,
1
],
"length": 175.37429425,
"display_label": "E-1"
},
"1": {
"points": [
2,
3
],
"length": 175.37429425,
"display_label": "E-2"
},
"2": {
"points": [
1,
4
],
"length": 2.0,
"display_label": "E-3"
},
"3": {
"points": [
5,
3
],
"length": 2.0,
"display_label": "E-4"
},
"4": {
"points": [
6,
7
],
"length": 576.08884809,
"display_label": "E-5"
},
"5": {
"points": [
1,
8
],
"length": 2.67627006,
"display_label": "E-6"
},
"6": {
"points": [
3,
9
],
"length": 2.67627006,
"display_label": "E-7"
},
"7": {
"points": [
10,
8
],
"length": 308.35820203,
"display_label": "E-8"
},
"8": {
"points": [
9,
10
],
"length": 308.35820314,
"display_label": "E-9"
},
"9": {
"points": [
11,
12
],
"length": 35.75089812,
"display_label": "E-10"
},
"10": {
"points": [
12,
13
],
"length": 81.27400176,
"display_label": "E-11"
},
"11": {
"points": [
14,
11
],
"length": 81.27400176,
"display_label": "E-12"
},
"12": {
"points": [
13,
14
],
"length": 35.75089812,
"display_label": "E-13"
},
"13": {
"points": [
15,
16
],
"length": 175.37429425,
"display_label": "E-14"
},
"14": {
"points": [
17,
18
],
"length": 719.69121947,
"display_label": "E-15"
},
"15": {
"points": [
19,
20
],
"length": 719.69121947,
"display_label": "E-16"
},
"16": {
"points": [
21,
22
],
"length": 175.37429425,
"display_label": "E-17"
},
"17": {
"points": [
23,
24
],
"length": 719.69121947,
"display_label": "E-18"
},
"18": {
"points": [
25,
26
],
"length": 719.69121947,
"display_label": "E-19"
},
"19": {
"points": [
27,
28
],
"length": 17.90138556,
"display_label": "E-20"
},
"20": {
"points": [
29,
30
],
"length": 384.41076643,
"display_label": "E-21"
},
"21": {
"points": [
15,
31
],
"length": 2.0,
"display_label": "E-22"
},
"22": {
"points": [
32,
22
],
"length": 2.0,
"display_label": "E-23"
},
"23": {
"points": [
15,
33
],
"length": 2.67627006,
"display_label": "E-24"
},
"24": {
"points": [
34,
22
],
"length": 2.67627096,
"display_label": "E-25"
},
"25": {
"points": [
35,
33
],
"length": 308.35819702,
"display_label": "E-26"
},
"26": {
"points": [
34,
35
],
"length": 308.358209,
"display_label": "E-27"
},
"27": {
"points": [
36,
37
],
"length": 173.77669609,
"display_label": "E-28"
},
"28": {
"points": [
27,
36
],
"length": 156.74333981,
"display_label": "E-29"
},
"29": {
"points": [
37,
30
],
"length": 156.74333981,
"display_label": "E-30"
},
"30": {
"points": [
38,
39
],
"length": 727.58204398,
"display_label": "E-31"
},
"31": {
"points": [
39,
40
],
"length": 308.35819869,
"display_label": "E-32"
},
"32": {
"points": [
40,
41
],
"length": 727.58204405,
"display_label": "E-33"
},
"33": {
"points": [
41,
38
],
"length": 308.35820147,
"display_label": "E-34"
},
"34": {
"points": [
40,
42
],
"length": 308.35820648,
"display_label": "E-35"
},
"35": {
"points": [
42,
43
],
"length": 727.58200645,
"display_label": "E-36"
},
"36": {
"points": [
43,
41
],
"length": 308.35820455,
"display_label": "E-37"
}
},
"facades": {
"11333885": {
"sort_index": 0,
"edges": [
{
"id": 0,
"type": "outside_corners"
},
{
"id": 1,
"type": "outside_corners"
},
{
"id": 2,
"type": "level_bases"
},
{
"id": 3,
"type": "level_bases"
},
{
"id": 4,
"type": "level_bases"
},
{
"id": 5,
"type": "eaves"
},
{
"id": 6,
"type": "eaves"
},
{
"id": 7,
"type": "rakes"
},
{
"id": 8,
"type": "rakes"
},
{
"id": 9,
"type": "opening_tops"
},
{
"id": 10,
"type": "opening_sides"
},
{
"id": 11,
"type": "opening_sides"
},
{
"id": 12,
"type": "opening_bottoms"
}
],
"area": 126504.93937199,
"display_label": "SI-1"
},
"11333886": {
"sort_index": 1,
"edges": [
{
"id": 30,
"type": "eave"
},
{
"id": 31,
"type": "rake"
},
{
"id": 32,
"type": "ridge"
},
{
"id": 33,
"type": "rake"
}
],
"area": 224355.8895029,
"pitch": 4,
"original_pitch": 4,
"slope_angle": 18.3,
"display_label": "RF-1"
},
"11333887": {
"sort_index": 2,
"edges": [
{
"id": 32,
"type": "ridge"
},
{
"id": 34,
"type": "rake"
},
{
"id": 35,
"type": "eave"
},
{
"id": 36,
"type": "rake"
}
],
"area": 224355.88767116,
"pitch": 4,
"original_pitch": 4,
"slope_angle": 18.3,
"display_label": "RF-2"
},
"11333888": {
"sort_index": 3,
"edges": [
{
"id": 1,
"type": "outside_corners"
},
{
"id": 13,
"type": "outside_corners"
},
{
"id": 14,
"type": "level_bases"
},
{
"id": 15,
"type": "eaves"
}
],
"area": 126215.33969475,
"display_label": "SI-4"
},
"11333889": {
"sort_index": 4,
"edges": [
{
"id": 16,
"type": "outside_corners"
},
{
"id": 0,
"type": "outside_corners"
},
{
"id": 17,
"type": "level_bases"
},
{
"id": 18,
"type": "eaves"
}
],
"area": 126215.33969475,
"display_label": "SI-2"
},
"11333890": {
"sort_index": 5,
"edges": [
{
"id": 13,
"type": "outside_corners"
},
{
"id": 16,
"type": "outside_corners"
},
{
"id": 19,
"type": "level_bases"
},
{
"id": 20,
"type": "level_bases"
},
{
"id": 21,
"type": "level_bases"
},
{
"id": 22,
"type": "level_bases"
},
{
"id": 23,
"type": "eaves"
},
{
"id": 24,
"type": "eaves"
},
{
"id": 25,
"type": "rakes"
},
{
"id": 26,
"type": "rakes"
},
{
"id": 27,
"type": "opening_tops"
},
{
"id": 28,
"type": "opening_sides"
},
{
"id": 29,
"type": "opening_sides"
}
],
"area": 102172.21824448,
"display_label": "SI-3"
}
},
"groups": {
"siding": {
"facade_ids": [
11333885,
11333888,
11333889,
11333890
]
},
"roof": {
"facade_ids": [
11333886,
11333887
]
}
}
}
Definition | |
---|---|
Point | x, y and z coordinates of a 3D point. |
Edge | Line segment that joins two points. |
Facades | Set of edges that compose facade's exterior and holes. |
Group | Group of facades of the same type. Can be of type 'siding' or 'roof'. |
Field | Type | Description |
---|---|---|
points | object | |
[ID]¹ | object | Point ID. |
position | array | Numeric, representing x, y and z coordinates. |
display_label | string | Label associated to the point. |
edges | object | |
[ID]² | object | Edge ID. |
points | array | Numeric, with the IDs¹ of two points. |
length | Decimal | In inches. |
display_label | string | Label associated to the edge. |
facades | object | |
[ID]³ | object | Facade ID. |
sort_index | integer | |
edges | array | Array of objects. |
id | integer | Edge ID². |
type | string | |
area | decimal | In square feet. |
pitch | integer | Available only for roof facades. |
original_pitch | integer | Available only for roof facades. |
slope_angle | decimal | Available only for roof facades. |
display_label | string | Label associated to the facade. |
groups | object | |
siding | object | |
facades_ids | array | Array of facade IDs³. |
roof | object | |
facades_ids | array | Array of facade IDs³. |
Payments
List Payments
require 'net/http'
token='your OAuth authorization token'
url = "https://hover.to/api/v1/payments?state=complete&good_type=Job&good_id=123&month=7&year=2018&page=1¤t_user_id=5"
uri = URI.parse(url)
request = Net::HTTP::Get.new(uri)
request["Authorization"] = "Bearer #{token}"
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true if 'https' == uri.scheme
response = http.request(signed_request)
data = JSON.parse(response.body)
curl --location --request GET 'https://hover.to/api/v1/payments?state=complete&good_type=Job&good_id=123&month=7&year=2018&page=1¤t_user_id=5' \
--header 'Authorization: Bearer #{token}'
When successful you'll get a 200 response code with the following JSON in the response body.
{
"pagination": {
"current_page": 1,
"total": 30,
"total_pages": 2
},
"results": [
{
"id": 1,
"org_id": 4,
"good_id": 123,
"good_data": "2",
"base_price": 100,
"base_price_subsidy": 10,
"discount": 10,
"discount_subsidy": 0,
"pre_tax_balance_debit": 0,
"pre_tax_subtotal": 80,
"balance_debit": 10,
"total": 90,
"subtotal": 100,
"tax": 20,
"state": "complete",
"error": null,
"created_at": "2018-07-12T01:01:48.296Z",
"updated_at": "2018-07-12T01:01:48.296Z",
"good_type": "Job",
"payment_discounts": [],
"payment_deposits": [],
"good": {
"id": 123,
"deliverable_id": 2,
"name": "Job X",
"location_line_1": null,
"location_city": null,
"location_region": null
}
}
]
}
This endpoint lists payments.
Results are ordered by updated_at in descending order.
HTTP Request
GET https://hover.to/api/v1/payments
URL Parameters
When filtered by state "failed", it returns all payments in one of the states: ["failed_calculating_tax", "failed_charging_card", "failed_discounting_base_price"]. Alternatively you may also filter by one of those failure states individually.
Parameter | Type | Description |
---|---|---|
state | String | Optional. Example states: "complete", "refunded", "cancelled", "paying", "waiting", "failed", "reverting_failed_card_charge". |
good_type | String | Optional. Supported good_type values: "Job", "Org". Case sensitive. |
good_id | Integer | Optional. The id of the corresponding good (Job or Org). Only effective when used together with "good_type". |
month | Integer | Optional. The numeric representation of month (i.e. 1 for Jan, 2 for Feb) to filter by "paid_at". Only effective when used together with "year". |
year | Integer | Optional. The numeric representation of year (i.e. 2018) to filter by "paid_at". Only effective when used together with "month". |
page | integer | Defaults to 1. Return this page of results |
Get A Payment's Receipt
require 'net/http'
token='your OAuth authorization token'
url = "https://hover.to/api/v1/payments/#{payment_id}/receipt.pdf"
uri = URI.parse(url)
request = Net::HTTP::Get.new(uri)
request["Authorization"] = "Bearer #{token}"
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true if 'https' == uri.scheme
response = http.request(signed_request)
File.open("Payment-#{payment_id}.pdf", "wb") do |file|
file.write(response.body)
end
curl --location --output Payment-#{payment_id}.pdf --request GET 'https://hover.to/api/v1/payments/#{payment_id}/receipt.pdf' \
--header 'Authorization: Bearer #{token}'
When successful you'll be redirected to the receipt PDF or HTML.
This endpoint only supports "Job" payments which are in "complete" state.
Notice the only thing that changes in the URLs below is the extension. This specifies the format the receipt will be returned in.
HTTP Request
GET https://hover.to/api/v1/payments/<Payment-ID>/receipt.pdf
GET https://hover.to/api/v1/payments/<Payment-ID>/receipt.html
Get All Receipts for a Specific Job
This section shows how you can get all the receipts for a specific job. Suppose the <Job-ID>
is 12345
, you can
make a GET request to the following endpoint to get the list of payments (List Payments) for that job:
GET https://hover.to/api/v1/payments?good_type=Job&good_id=12345
From the response JSON, you can get a list of payment ids from the results
. Suppose there are 2 payments with payment
id 111
and 222
, you can then make 2 GET requests to the following endpoint
(Get A Payment's Receipt) to get the PDF file of the receipts:
GET https://hover.to/api/v1/payments/111/receipt.pdf
GET https://hover.to/api/v1/payments/222/receipt.pdf
Wallets
Within HOVER, payment information is identified as a wallet. Each wallet has a unique identifier. When creating a job or capture request the wallet id can be used to specify payment details. This can be done within in an organization, or from organization to organization through a wallet share.
Wallet Shares
Wallet shares enable you to share your wallet to another organization. This is useful when you are contracting out capture to another company, or in general when you need to pay for a job outside of your org.
Wallet shares are setup by a HOVER admin as well as preferred wallets. If you intend to use shared wallets please reach out to your HOVER contact to utilize this functionality.
The wallet share is done by passing a wallet_id when using the create a job or create a capture request endpoints. The wallet can also be selected when capturing a job on the mobile app.
List All Available Wallets
{
"pagination": {
"current_page": 1,
"total_pages": 1,
"total": 1
},
"results": [
{
"id": 259,
"preferred": false,
"shared": false,
"allowed_good_types": [
"Job",
"HoverModels::RoofEstimate",
"Deposit",
"Plan",
"OrgJobAccess",
"Org"
],
"org": {
"name": "HOVER Documentation",
"customer_display_name": "HOVER"
}
}
]
}
Returns a list of wallets your current org has access to. The response includes other orgs' wallets shared with you and your own wallet in the order of preferred shared wallet
> shared wallet
> your own wallet
. Your own wallet will always be in the response.
HTTP Request
GET https://hover.to/api/v2/wallets
URL Parameters
Parameter | Type | Description |
---|---|---|
per | integer | Optional. How many items to return, the default is 25. |
page | integer | Optional. Page number |
current_user_id | integer | Required. |
current_user_email | string | Required. |
Returned Wallet Attributes:
Attribute | Type | Description |
---|---|---|
allowed_good_types | array | What kind of goods this wallet can be used for. It can be Job, HoverModels::RoofEstimate, Deposit, Plan, OrgJobAccess or Org. |
id | integer | wallet_id |
org | json | Contains key-value pairs: name and customer_display_name. |
preferred | boolean | Is this a preferred shared wallet or not? |
shared | boolean | Is this a shared wallet or your own wallet? |
App to App Integration
Linking from mobile app to mobile app is supported through Deep Links in HOVER.
Deep Links
Create a job using the API. This will return a job identifier.
User opens the 3rd party app and sees details of a job created in step 1.
The user taps a button that invokes a deep link with a specific format. If you wanted to capture the property images for example, you could use the following format:
hihover://<company_name>/capture/identifier/1234567
. Note that<company_name>
is an arbitrary string so you can use anything to identify yourself. So, you can use<yc>
for<yourcompany>
if you'd like.The deep link will launch the HOVER app and ask the user to log in (if not already logged in).
On success, the app will perform the action described in the deep link.
List of actions that can be done through deep links:
- Photo Capture -
hihover://<company_name>/capture/identifier/<job_id>
- View 3D –
hihover://<company_name>/3d/identifier/<job_id>
- Property Details –
hihover://<company_name>/details/identifier/<job_id>