NAV Navbar
ruby cURL
  • Getting Started
  • Authentication - OAuth 2.0
  • Standard Workflows
  • Webhooks
  • Capture Requests
  • Jobs
  • Job Exports
  • Job Shares
  • Job Styles
  • Job Wireframe Images
  • Deliverable Change Requests
  • Orgs
  • Users
  • Images
  • Notes
  • Errors
  • JSON examples
  • Payments
  • Wallets
  • App to App Integration
  • 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:

    1. Create jobs with pre-populated information that is assigned to a user or homeowner to complete the photo capture using the HOVER app.
    2. Set up webhooks to listen for status updates about jobs.
    3. 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

    1. Sign up for a HOVER Sandbox account.
    2. Provide your full name, email, & password and click Submit.
    3. Confirm your email.
    4. Provide a company name, phone number, & zip code and click Complete. (It's fine if this is fake information.)
    5. 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.

    1. Navigate to the Developer section of settings in your Sandbox account.
    2. Click the Create New Integration button.
    3. Provide your integration name, redirect URI, description, & logo and click Save Integration.
    4. Once created, click Edit next to your new Integration.
    5. 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.

    Run in Postman

    Requirements

    In order to use this demo you will need the following:

    1. Postman for Mac, Windows, or Linux.
    2. 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.

    1. In Postman, in the top right-hand corner, click the gear icon to open the Manage Environments window.

      alt text

    2. Click the "HOVER - Sandbox (Template)" text.

      alt text

    3. Where you see your_client_id, your_client_secret, and your_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.

      alt text

    4. Click Update.

    5. Close the Manage Environments window.

    6. In the top right environment drop down, choose the "HOVER - Sandbox (Template)" environment to set it as active.

      alt text

    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.

    1. On the HOVER API collection folder, click the elipsis (...) icon and then Edit.

      alt text

    2. On the Edit Collection screen, click the Authorization tab.

      alt text

    3. Click the Get New Access Token button.

    4. 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.

      alt text

    5. Sign in to your Sandbox account and authorize the application.

      alt text

    6. Once approved, you should see a new box that contains Access Token and other information. Scroll down & click Use Token.

      alt text

    7. 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.

    1. Click the "Getting Started (Creating a Job)" folder.

      alt text

    2. Click the "Create a Job" POST request.

    3. Click on the Body tab.

    4. Complete at least the job[name], job[location_line_1], job[location_city], job[location_region], job[location_postal_code], and job[location_country] form fields.

      alt text

    5. Click Send.

    6. When you get a 201 response code you have just created your first job!

    7. 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.

    alt text

    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 and refresh_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:

    1. Workflow 1 - Customer Relationship Manager (CRM)/Claims Management System (CMS) Workflow

    2. 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.

    alt text

    {
      "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
    }
    
    1. 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.

    2. 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.

    3. 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.

    4. 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.

    alt text

    {
      "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
    }
    
    1. 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.

    2. 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.

    3. 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.

    4. 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.

    alt text

    1. Create a HOVER job using the Create a Job endpoint with deliverable_id 7.

    2. The user can start the capture on HOVER app.

    3. 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.

    4. Whenever you are ready to upgrade the job, create a deliverable change request using the create capture requests endpoint.

    5. Listen for webhooks until the job is labeled as complete.

    6. 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

    alt text

    1. Create a webhook and specify the endpoint HOVER should POST to. HOVER will POST a verification code to the specified URL.

    2. Verify a webhook by making a POST request to the specified endpoint with the code.

    3. Listen for state changes, using the job_id or the job_external_identifier to identify the HOVER property.

    4. 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:

    1. 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'

    2. 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.

    3. 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.
    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

    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
    email 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.
    email 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
    email 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
    email 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 value in the url. The user will lose all access to the org and it's associated records. If this is the only org the user is in a new HOVER Pro org will be created for them and they'll be placed into that org.

    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.

    alt text

    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&current_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&current_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.

    1. Create a job using the API. This will return a job identifier.

    2. User opens the 3rd party app and sees details of a job created in step 1.

    3. 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.

    4. The deep link will launch the HOVER app and ask the user to log in (if not already logged in).

    5. On success, the app will perform the action described in the deep link.

    List of actions that can be done through deep links: