Getting Started

This document describes the Groove.id application programming interface. It allows you to automate your access to Groove.id, as well as exposing to additional advanced features.

The API is accessed by making HTTP requests via TLS. All requests should be made to the following endpoint:



There is a GUI tool to explore the API available in the Groove.id console, just navigate to /debug under your signin URL, e.g. https://signin.initech.biz/debug. This tool will automatically add GV1 authentication for your current browser session to requests.


The current version of the api is v1. By default your requests will be directed at the latest version of the api. To request a specific version of the API, specify the version in the X-Grooveid-Api-Version header when you make a request.

X-Grooveid-Api-Version: v1

We will not make backwards incompatible changes to the API without increasing the version number. We may add endpoints or add fields to objects without incrementing the version number. We will not change the semantic meaning of endpoints, the names or types of fields, or remove support for objects, fields or endpoints without increasing the version number.

User Agent

We encourage you to include an informative User-Agent (or X-User-Agent) header with each request you make. A good user agent string should include an email address or URL when we can make technical contact about issues with your tool.


User-Agent: acme_dir_sync/v1.2; Acme Corp Custom Directory Sync Tool (identity-team@example.com)


The main way to authenticate to the API is to use keys you generate in the Groove.id console. API keys are 56-characters long and being with the letters “gv”, for example: gvfoniuykysuayxvog0oevdnrzuynhmi5fqz2keanptobyjhnvycedjn.

You can pass the key in the Authorization header like:

curl -H "Authorization: Bearer API-KEY" https://api.groove.id/users/me

You can also pass the key as a parameter:

curl "https://api.groove.id/users/me?access_token=API-KEY"

Each API key is associated with scopes that define what kinds of actions are allowed. Set the scopes of your API keys in the Groove.id console. The scopes are:

  • user - normal user access.
  • audit - read only access to user and activity information.
  • sync - can make administrative changes only to users and groups.
  • admin - full access.

In addition to API keys, there are additional authentication mechanisms available:

  • GV1 - the protocol used internally by Groove.id
  • CFJWT - for applicationa hosted behind Cloudflare Access.
  • OAuth - for applications using an OAuth flow.


For most object types basic operations follow the REST model, using GET to fetch information without side effect, POST to create objects, PUT to modify them and DELETE to remove them.

Content Types

Unless otherwise noted, all requests and responses are JSON-encoded. You should provide a header similar to Accept: application/json, as appropriate. If your request includes a body, you should add Content-Type: application/json. You can expect Content-Type: application/json in all responses unless otherwise noted.

Creating an object

In general you create objects with a POST request to /:type. On success this will create the object returning a status of 201 Created and a response header X-Id which includes the assigned object ID and Location which will return something like /users/gCfBX3fRAHCFGb:

POST /users HTTP/1.1
Host: auth.groove.id
Authorization: Bearer API-KEY
Content-Length: 2
Content-Type: application/json


The response will be something like:

HTTP/1.1 201 Created
Date: Mon, 10 Dec 2018 21:40:29 GMT
Etag: "36058bcd35a2228c905169389ee6954e"
Last-Modified: Mon, 10 Dec 2018 21:40:29 GMT
Location: /users/gCfBX3fRAHCFGb
X-Create-Time: Mon, 10 Dec 2018 21:40:29 GMT

The X-Id header tells you the generated ID of the user, while Location tells you the full URI of the new object. Etag is an opaque value that will change whenever the object changes. X-Create-Time and Last-Modified tell you the timestamps that the server assigned to the object.

Some objects, such as VirtualHost are identified by name rather than a randomly generated identifier. For named objects, you must specify the identifier in the URI when creating the object:

POST /virtualhosts/signin.example.com HTTP/1.1
Host: auth.groove.id
Authorization: Bearer API-KEY
Content-Length: 2
Content-Type: application/json


The server’s response is otherwise the same.

Fetching an object

Use the GET method to fetch an individual object.

GET /users/Cq3OzybjHBxk2e HTTP/1.1
Host: auth.groove.id
Accept: application/json
Authorization: Bearer API-KEY
HTTP/1.1 200 OK
Cache-Control: private, max-age=0
Content-Length: 153
Content-Type: application/json
Date: Mon, 10 Dec 2018 21:44:48 GMT
Etag: "36058bcd35a2228c905169389ee6954e"
Expires: -1
Last-Modified: Mon, 10 Dec 2018 21:43:18 GMT
Vary: Origin
X-Create-Time: Mon, 10 Dec 2018 21:43:18 GMT
X-Id: Cq3OzybjHBxk2e
X-Trace: id=thgQbuQ&t=2018-12-10T21%3A44%3A48Z&v=ebc04fe.20181210155010


As with create, the response includes Etag, X-Id, X-Create-Time and X-Id headers.

Updating Objects

To update an object, make a PUT request.

You must include an If-Match header containing an Etag from a previous GET. If the object has changed since your GET, the request will be rejected with 412 Precondition Failed. If you see this error, you should re-fetch the object, re-apply your changes, and then retry the PUT.

PUT /users/Cq3OzybjHBxk2e HTTP/1.1
Host: auth.groove.id
Content-Type: application/json
Content-Length: 214
Authorization: Bearer API-KEY
If-Match: "36058bcd35a2228c905169389ee6954e"

  "HavePIN": false,
  "HaveTOTPSecret": false,
  "Active": false,
  "SuspendAfter": "0001-01-01T00:00:00Z",
  "SuspendBefore": "0001-01-01T00:00:00Z",
  "Name": {
    "FullName": "Alice Smith"
  "Icon": {}

The server will respond with 204 No Content and the updated Etag and Last-Modified time.

HTTP/1.1 204 No Content
Cache-Control: private, max-age=0
Etag: "9885cec382faf84b6de867371f39654f"
Expires: -1
Last-Modified: Mon, 10 Dec 2018 21:53:03 GMT
Vary: Origin
X-Create-Time: Mon, 10 Dec 2018 21:43:18 GMT
X-Id: Cq3OzybjHBxk2e
X-Trace: id=raAGHzt&t=2018-12-10T21%3A53%3A03Z&v=ebc04fe.20181210155010
Date: Mon, 10 Dec 2018 21:53:03 GMT

Deleting Objects

To delete an object make a DELETE request, which must include an If-Match header containing the current Etag of the object to be deleted.

DELETE /users/Cq3OzybjHBxk2e HTTP/1.1
Host: auth.groove.id
Authorization: Bearer gv6pvs264u5xyyocliasebyhsey3z2eztcme6nzogf5zrsftxcczzewl
If-Match: "9885cec382faf84b6de867371f39654f"
HTTP/1.1 204 No Content
Cache-Control: private, max-age=0
Expires: -1
Vary: Origin
X-Trace: id=htWuUv5&t=2018-12-10T21%3A57%3A22Z&v=ebc04fe.20181210155010
Date: Mon, 10 Dec 2018 21:57:22 GMT

Listing Objects

List objects by making a GET request to /:type, for example /users.

The response is of type application/x-json-lines in which each object is represented by a single line of JSON-encoded text.

You can use the order query parameter to specify the order that items are returned. If you prefix the field name with - then the fields will be sorted in reverse.


You can restrict which objects are listed with the filter parameter.

The form of each expression is field operator value. Operators are <, <=, =, >=, or >. Values must be encoded using JSON. For example to list all the items whose Name is “Alice”, filter would be Name="Alice".


The limit query parameter restricts the maximum number of rows that will be returned in a response. If not specified, then all rows are returned.

You can implement paging by index using the limit and offset query parameters. limit specifies the maximum number of rows to return, offset indicates the relative record number to start with. Using this method, if an object is created or modified while you are paging, it is not guaranteed to be included in the results.

When paging by index, you should specify an order field. Without an order field objects are returned in arbitrary order, offset has no meaning.

Paging by index may produce inconsistent results. If an object is created or modified while you are iterating such that it would appear in a page you’ve already fetched, it will change each object’s relative position in the resulting list. This means that your iteration may miss unrelated objects later in the list.

You can address the problems with paging by index using coherent iteration using start query parameter (or reverse coherent iteration using end). Each list response contains an X-Start-Cursor header which provides the cursor value for the first record returned. It will also contain an an X-End-Cursor header containing the cursor position immediately after the last record produced. Thus, the following pseudo code would give you coherent iteration:

cursor = ""
while true {
  response = get /users?limit=100&start=cursor
  for line in response.body.split("\n") {
    user = json parse line
    # do something with user
  cursor = response.trailers["X-End-Cursor"]

Note that the X-Start-Cursor and X-End-Cursor headers are only present when a limit is specified and that limit is less that 1000.