• ¶

    Continuity Control API Documentation

    This file is automatically generated from the code publically available on GitHub.


  • ¶

    This is a Sinatra application that integrates with the Control API. Sinatra is a small Ruby web application framework that provides a DSL (domain specific language) for handling HTTP requests like get and post. It should be easy to read even if you don’t read Sinatra’s documentation.

    require 'sinatra'
    require 'sinatra/reloader' if development?
    require 'httparty'
    require 'json'
    require 'dotenv'
    require 'byebug'
    Dotenv.load
  • ¶

    This class uses a library called HTTParty to connect to the Control API. All API responses are JSON. HTTParty automatically detects this and parses into a Ruby object.

    For more information, see the HTTParty website.

    class ControlAPI
      include HTTParty
      base_uri ENV['CONTROL_API_BASE_URI']
    
      basic_auth ENV['CONTROL_API_KEY'], ENV['CONTROL_API_SECRET']
  • ¶

    Set the ‘Content-Type’ header to ‘application/json’ to ensure that the API accepts and returns JSON. This is currently the only supported format, but this will ensure you still get JSON if the API supports XML in the future.

      headers 'Content-Type' => 'application/json'
    end
    
    helpers do
      def parsed_body
        request.body.rewind
        ::JSON.parse(request.body.read)
      end
    end
  • ¶

    The root path in this application simply provides navigation.

    erb :view_name renders the file in views/view_name.erb. For example, this will end up rendering views/root.erb.

    get '/' do
      erb :root
    end
  • ¶

    GET /v1/status

    Check the API status. Useful for testing authentication without knowing other information.

    Example request

    GET /v1/status
    

    Example response

    HTTP 200 OK

    {"description":"up"}
    

    HTTP 401 Unauthorized

    The request was not properly authenticated.

    HTTP 500 Server Error

    Response fields

    • description: A text description of the API state.
    • error: A text description of any error in the API call.
    get '/status' do
      status = ControlAPI.get('/v1/status.json').parsed_response
    
      if status['error']
        "API error: #{status['error']}"
      else
        "API status: #{status['description']}"
      end
    end
  • ¶

    GET /v1/users

    Get all users, optionally filtering by emails, external employee IDs, or manager emails

    Example requests

    GET /v1/users
    GET /v1/users?email[]=gwashington@example.com&gbush@example.com
    GET /v1/users?employee_id[]=1234
    GET /v1/users?manager_email[]=mwashington@example.com
    

    Example responses

    HTTP 200 OK

    {
      "users": [
        {
          // Content from GET /v1/users/:email
          'email': 'gwashington@example.com',
          ...
        },
        {
          'email': gbush@example.com',
          ...
        }
      ]
    }
    

    HTTP 500 Server Error

    get '/users' do
      users = ControlAPI.get('/v1/users', query: params)
    
      erb :'users/index', locals: users.to_h
    end
  • ¶

    POST /v1/users

    Create a new user and send an invitation for setting password, etc.

    Example requests

    POST /v1/users
    Content-Type: application/json
    
    {
      "user": {
        "email": "abe@whitehouse.gov",
        "first_name": "Abraham",
        "last_name": "Lincoln"
      }
    }
    

    Request fields

    The following fields are allowed:

    • email: Required
    • first_name: Required
    • last_name: Required
    • manager_email
    • title
    • employee_id
    • review_on
    • started_on

    For field descriptions, please see GET /v1/users/:email.

    Example responses

    HTTP 200 OK

    Refer to GET /v1/users/:email response.

    HTTP 422 Unprocessable Entity

    {
      "errors": {
        "email": [
          "is invalid",
        ]
      }
    }
    

    HTTP 500 Server Error

    get '/users/new' do
      erb :'users/new'
    end
    
    post '/users' do
      user = ControlAPI.post('/v1/users', body: params.to_json)
    
      case user.response.code
      when '201'
        [200, erb(:'users/created', locals: { email: user['email'] })]
      when '422'
        [422, erb(:errors, locals: { errors: user['errors'] })]
      else
        [500, 'There was an error while processing your request']
      end
    end
  • ¶

    GET /v1/users/:email

    Get an individual user by their email

    Data Example

    {
      "path": "/v1/users/gwashington@example.com",
      "email": "gwashington@example.com",
      "first_name": "George",
      "last_name": "Washington",
      "created_at": "1732-02-22T12:34:56Z",
      "updated_at": "1799-12-14T12:34:56Z",
      "review_on": "2076-07-04",
      "started_on": "1789-04-30",
      "manager_path": "/v1/users/mwashington@example.com",
      "manager_email": "mwashington@example.com",
      "title": "President of the United States",
      "administrator": true,
      "employee_id": "1",
    }
    

    Data Fields

    • path: API path for this user
    • email: Primary email
    • first_name: Personal name
    • last_name: Surname
    • created_at: ISO8601 datetime of the creation of this user, in UTC
    • updated_at: ISO8601 datetime of the time at which this user was updated, in UTC
    • review_on: The ISO8601 date for the next review of this user
    • started_on: The ISO8601 date when this user’s employment started
    • manager_path: The API path for the user’s manager
    • manager_email: The email address of the user’s manager
    • title: The job title of the user
    • administrator: Whether or not the user has administrator access for their organization
    • employee_id: (string) This is the external employee ID of the user. The value is arbitrary and is assigned by their organization. However, it must be unique to their organization.
    
    get '/users/:email' do
      user = ControlAPI.get("/v1/users/#{params[:email]}")
    
      case user.response.code
      when '200'
        erb :'users/show', locals: user.to_h
      when '404'
        [404, 'Not found']
      else
        [500, 'There was an error while processing your request']
      end
    end
  • ¶

    PATCH /v1/users

    Updates a user. Note that changing a user’s email address will affect their identification in other API calls, as well as the email they use to log in.

    Example requests

    Refer to POST /v1/users request. NOTE: you can omit keys as necessary

    Example responses

    Refer to GET /v1/users/:email response

    get '/users/:email/edit' do
      user = ControlAPI.get("/v1/users/#{params[:email]}")
      erb :'users/edit', locals: user.to_h
    end
    
    post '/users/:email' do
      user = ControlAPI.patch("/v1/users/#{params[:email]}", body: params.to_json)
    
      case user.response.code
      when '200'
        erb :'users/show', locals: user.to_h
      when '422'
        [422, erb(:errors, locals: { errors: user['errors'] })]
      when '404'
        [404, 'Not found']
      else
        [500, 'There was an error while processing your request']
      end
    end
  • ¶

    DELETE /v1/users/:email

    “Soft delete” the user by disabling them. Associated records, such as completed ToDos, are not deleted.

    Example responses

    HTTP 204 No Content

    post '/users/:email/delete' do
      user = ControlAPI.delete("/v1/users/#{params[:email]}")
    
      case user.response.code
      when '204'
        [200, erb(:'users/deleted')]
      when '422'
        [422, erb(:errors, locals: { errors: user['errors'] })]
      when '404'
        [404, 'Not found']
      else
        [500, 'There was an error while processing your request']
      end
    end
  • ¶

    GET /v1/template_to_dos

    Get all the TemplateToDos for your organization. NOTE: not filtered by enabled/disabled state.

    Example requests

    GET /v1/template_to_dos #     GET /v1/template_to_dos?tags[]=annual&tags[]=security
    

    Request fields

    • tags: Only return TemplateToDos that are tagged with all of the specified tags.

    Example response

    HTTP 200 OK

    {
      "template_to_dos": [
        {
          "uuid": "12345678-1234-5678-1234-567812345678",
          "name": "Review server logs",
          "tags": ["security", "annual", "training"]
        }
      ]
    }
    

    HTTP 500 Server Error

    Response fields

    • template_to_dos: an Array of TemplateToDos, or an empty array [] if none match the given criteria
      • uuid: UUID for each TemplateToDo.
      • name: The human-readable name for each ToDo.
      • tags: The tags for each TemplateToDo, as an array of strings. If there are no tags, it will be an empty array.
    get '/template_to_dos' do
      template_to_dos = ControlAPI.get("/v1/template_to_dos", :query => params)
      erb :template_to_dos, :locals => template_to_dos
    end
  • ¶

    POST /v1/distributed_to_dos

    Asynchronously distribute a ToDo to the given assignees. (The work happens in a job queue.)

    Example request

    POST /v1/distributed_to_dos
    Content-Type: application/json
    
    {
      "distributed_to_do": {
        "template_to_do_uuid": "12345678-1234-5678-1234-567812345678",
        "due_on": "2013-11-29",
        "assignee_emails": ["bobama@example.com", "gwbush@example.com", "bclinton@example.com"],
        "field_values": {"field1": "value1", "field2": "value2"},
      }
    }
    

    Request fields

    • distributed_to_do: Required. Holds parameters for the DistributedToDo.
      • template_to_do_uuid: Required. The UUID for the TemplateToDo that will be distributed. This can be found via GET /v1/template_to_dos, or in Continuity Control under “Settings”.
      • due_on: Required. ISO8601 date of when the DistributedToDo is due, in your configured Time Zone.
      • assignee_emails: Required. An Array of email addresses of Users that will receive the DistributedToDos.
      • field_values: Dictionary (Object) of values to pre-fill in the DistributedToDo. Field names are available in Continuity Control under “Settings”.

    Example responses

    HTTP 202 Accepted

    {
      "uuid":"f81d4fae-7dec-11d0-a765-00a0c91e6bf6",
      "path":"/v1/distributed_to_dos/f81d4fae-7dec-11d0-a765-00a0c91e6bf6"
    }
    

    HTTP 401 Unauthorized

    The request was not properly authenticated.

    HTTP 422 Unprocessable Entity

    {
      "errors": {
        "template_to_do_uuid": [
          "does not exist",
        ],
        "assignee_emails": [
          "has email alice@example.com which does not exist for this organization",
          "has email bob@example.com which does not exist for this organization"
        ],
        "due_on": [
          "could not be parsed"
        ]
      }
    }
    

    HTTP 500 Server Error

    Response fields

    • uuid: A UUID for the DistributedToDo which will be created asynchronously. Please include it in any bug reports to Continuity.
    • path: The path where the DistributedToDo will be available once created.
    • errors: An object with one key per request field and an array of all the validation errors that apply to that field. The exact text of the error messages is not guaranteed and may change without warning.
    get '/distributed_to_dos/new' do
      erb :distributed_to_dos_new
    end
    
    post '/distributed_to_dos' do
      distributed_to_do = ControlAPI.post('/v1/distributed_to_dos',
                                          :body => params.to_json)
    
      case distributed_to_do.response.code
      when '202'
        [200, erb(:distributed_to_dos_post, :locals => distributed_to_do)]
      when '422'
        [422, erb(:errors, :locals => distributed_to_do)]
      else
        [500, 'There was an error while processing your request']
      end
    end
  • ¶

    GET /v1/distributed_to_dos/:uuid

    Get the current state of a distributed_to_do as found by an uuid.

    Example requests

    GET /v1/distributed_to_dos/f81d4fae-7dec-11d0-a765-00a0c91e6bf6
    

    Example response

    HTTP 200 OK

    {
      "uuid": "f81d4fae-7dec-11d0-a765-00a0c91e6bf6",
      "name": "Review server logs",
      "created_at": "2013-11-02T12:34:46.000Z",
      "completed_at": null,
      "due_on": "2013-11-08",
      "tags": ["security", "annual", "training"],
      "assignments": [
        {
          "email": "bobama@example.com",
          "finished_on": "2013-11-07",
          "fields": [
            {
              "uuid": "8edfc480-a174-0135-509d-1e3560338d6d",
              "todo_script_id": null,
              "label": "I was able to generate this report.",
              "value": true,
              "disabled": false
            }
          ]
        },
        {
          "email": "bclinton@example.com",
          "finished_on": null,
          "fields": [
            {
              "uuid": "7fcfc480-a174-0135-509d-1e3560338d6d",
              "todo_script_id": null,
              "label": "I was able to generate this report.",
              "value": null,
              "disabled": false
            }
          ]
        }
      ]
    }
    

    HTTP 401 Unauthorized

    The request was not properly authenticated.

    HTTP 404 Not Found

    Returned when the resource does not exist. Note that after a POST to /v1/distributed_to_dos that gives a 202, /v1/distributed_to_dos/:uuid would return a 404 until the resource is asynchronously created.

    HTTP 500 Server Error

    Response fields

    • uuid: UUID for this DistributedToDo.
    • name: The human-readable name for this DistributedToDo.
    • created_at: ISO8601 datetime of the creation of this DistributedToDo, in UTC.
    • completed_at: ISO8601 datetime of when the DistributedToDo was completed, in UTC. This is when all the assignments have been finished. May be null.
    • due_on: ISO8601 date of when the DistributedToDo is due, in your configured Time Zone.
    • tags: The tags for this DistributedToDo, as an array of strings. If there are no tags, it will be an empty array.
    • assignments: Array
      • email: User email of DistributedToDo assignment
      • finished_on: ISO8601 date on which the assignment was completed (in UTC), or null if not completed
      • fields: Array
        • uuid: UUID for the Field
        • todo_script_id: Friendly reference id for the Field,
        • label: The human-readable name for the Field
        • value: The value submitted by the assignee
        • disabled: Boolean, if the Field is disabled it is not visible to the assignee
    get '/distributed_to_dos/:uuid' do
      distributed_to_do = ControlAPI.get("/v1/distributed_to_dos/#{params[:uuid]}")
    
      case distributed_to_do.response.code
      when '200'
        erb :distributed_to_do, :locals => distributed_to_do.to_h # NOTE: this gives us locals that are keys in the distributed_to_do response
      when '404'
        [404, 'Not found']
      else
        [500, 'There was an error while processing your request']
      end
    end
  • ¶

    GET /v1/distributed_to_dos

    Get all the DistributedToDos for your organization. Each DistributedToDo in this “collection” GET request is in the same format as the “member” GET request.

    Example requests

    GET /v1/distributed_to_dos
    GET /v1/distributed_to_dos?created_after=2013-11-02
    GET /v1/distributed_to_dos?created_after=2013-11-02&created_before=2013-11-07
    GET /v1/distributed_to_dos?created_before=2013-11-07
    GET /v1/distributed_to_dos?late=true
    GET /v1/distributed_to_dos?complete=true
    GET /v1/distributed_to_dos?created_after=2013-11-02&created_before=2013-11-07&late=true&complete=false
    GET /v1/distributed_to_dos?tags[]=annual&tags[]=security
    

    Request fields

    • created_after: ISO8601 date of exclusive lower bound of created_at time.
    • created_before: ISO8601 date of exclusive upper bound of created_at time.
    • late: When true, respond with only “late” DistributedToDos. When false, respond with only “on time” DistributedToDos. When not present, do not filter on lateness.
    • complete: When true, respond with only “complete” DistributedToDos. When false, respond with only “incomplete” DistributedToDos. When not present, do not filter on completeness.
    • tags: Only return DistributedToDos that are tagged with all of the specified tags.

    late and complete can be combined to filter:

    late=false&complete=false  # incomplete and on time
    late=false&complete=true   # completed and on time
    late=true&complete=false   # incomplete and late (what most users will want)
    late=true&complete=true    # completed and late
    

    Example response

    HTTP 200 OK

    {
      "distributed_to_dos": [
        // Content from GET /v1/distributed_to_dos/:uuid
      ]
    }
    

    HTTP 500 Server Error

    Response fields

    • distributed_to_dos: an Array of DistributedToDos, or an empty array [] if none match the given criteria
    get '/distributed_to_dos' do
      one_week_ago_params = { :created_after => Date.today - 7 }
      filter_params = params.empty? ? one_week_ago_params : params
    
      distributed_to_dos = ControlAPI.get('/v1/distributed_to_dos', :query => filter_params)
      erb :distributed_to_dos, :locals => distributed_to_dos
    end
  • ¶

    Webhooks

    Webhooks provide information about events in near real-time. You provide a URL, and we’ll POST to it as events take place. When a HTTP 5XX response occurs, the Webhook is retried with an incremental backoff.

    Format

    Our webhooks have the following format:

    {
    
      "event": "EventName",
      "fired_at": "2014-06-24T15:32:05Z",
      "data": {
        // Depends on event, see below
      }
    }
    

    Fields

    • event: A named event, as documented below
    • fired_at: When the webhook was fired
    • data: an Object of data for the given event

    Implementation Requirements

    The receiver of a webhook MUST check event on each POST. An unknown event MUST be ignored.

    Examples of why this behavior is required:

    • If the event is not checked, and the receiver only looks at the data.name attribute, it could be a ToDo’s name instead of a user’s name.
    • If the event is not checked, a user could have been updated instead of created. The receiver could then take action on an “updated” event that was only intended for a “created” event (e.g., sending a welcome email).

    Example

    For a contrived example, if there were a VoteCast event, the webhook would provide data like the following in two separate POSTs:

    First POST:

    {
      "event": "VoteCast",
      "fired_at": "2000-11-07T15:32:05Z",
      "data": {
        "full_name": "Al Gore",
        "party": "Democrat"
      }
    }
    

    Second POST:

    {
      "event": "VoteCast",
      "fired_at": "2000-11-07T15:42:05Z",
      "data": {
        "full_name": "George W Bush",
        "party": "Republican"
      }
    }
    

    Events

    UserCreated

    The UserCreated event fires when a new user is created on Control.

    Data Example

    {
      "full_name": "George Washington",
      "first_name": "George",
      "last_name": "Washington",
      "email": "gwashington@example.com"
    }
    

    Data Fields

    • full_name: Formatted name, may be null
    • first_name: Personal name, may be null
    • last_name: Surname, may be null
    • email: Primary email

    DistributedToDoCompleted

    The DistributedToDoCompleted events fires when all the assignments of a ToDo are completed. That is, when the ToDo as a whole is completed, rather than an individual user’s assignment.

    The data is the same for GET /v1/distributed_to_dos/:uuid. Please refer to its documentation.

    post '/webhook' do
      event = parsed_body['event']
      data = parsed_body['data']
    
      case event
      when 'DistributedToDoCompleted'
        "Good job, team!  We completed the #{data['name']} ToDo!"
      when 'UserCreated'
        "Nice to meet you, #{data['full_name']}!"
      end
    end