Custom Connector API Spec

You should implement the following endpoints to build a custom connector. See Create your own connector to learn how to generate boilerplate code. Opal also provides an example Datadog custom connector you can reference to help build your own implementation.

Endpoints

GET /status

Checks the status of the connector, it's mainly used for successfully creating the app in Opal, and verifying that the connector is properly set up.

Query params

paramtypedescription
app_idstringThe Opal app ID to list resources for. This allows your service to distinguish apps if it’s supporting multiple apps via the same set of endpoints

Response params (200)

No body is required as part of this endpoint for status code 200.

Response params (Error)

paramtypedescription
messagestringAn error message that will be exposed in the Opal UI.
codeintegerThe error code that describes the error. See Error Codes for details.

GET /resources

Returns a list of all resources for the app in question.

Query params

paramtypedescription
app_idstringThe Opal app ID to list resources for. This allows your service to distinguish apps if it’s supporting multiple apps via the same set of endpoints
cursorstringFor pagination. Empty string on the first call, then set to the value provided via next_cursor
parent_idstringOptional: This parameter is used for nested resources. If your connector supports this feature, it will receive the ID of the parent resource and return its immediate children. See Create your own connector for more information.

Response params (200)

paramtypedescription
resourcesarray of objectsList of the resource objects. See below for what each resource should include.
next_cursorstringThe cursor that should be used for the next call. If cursor is an empty string, it is assumed that all resources have been fetched.

Response params (Error)

paramtypedescription
messagestringAn error message that will be exposed in the Opal UI.
codeintegerThe error code that describes the error. See Error Codes for details.

Resource object

fieldtypedescription
idstringThe id of the resource that uniquely identifies it in your system.
namestringThe name of the resource
descriptionstringThe description of the resource

Example response

{
  "next_cursor": "gjroieapghnfagjfdgpadshfasd",
  "resources": [
    {
      "id": "1",
      "name": "Gooli Metadata",
      "description": "Access to Gooli customer metadata"
    },
    {
      "id": "2",
      "name": "Eviato Metadata",
      "description": "Access to Eviato customer metadata",
      "can_have_usage_data": true
    },
    {
      "id": "3",
      "name": "Moolybib Metadata",
      "description": "Access to Moolybib customer metadata"
    }
  ]
}

GET /resources/{resource_id}

Get a specific resource by its id.

Path params

paramtypedescription
resource_idstringThe id of the resource

Query params

paramtypedescription
app_idstringThe Opal app ID to list resources for. This allows your service to distinguish apps if it’s supporting multiple apps via the same set of endpoints

Response params (200)

fieldtypedescription
resourceobjectResource object

Resource object

fieldtypedescription
idstringThe id of the resource that uniquely identifies it in your system. It should match the ID (<resource_id>) of the request.
namestringThe name of the resource
descriptionstringThe description of the resource
can_have_usage_databooleanOptional: This property is used to enable ingesting usage events for a resource to enable LPPM for custom connections. See here for more information. false if unspecified.

Response params (Error)

paramtypedescription
messagestringAn error message that will be exposed in the Opal UI.
codeintegerThe error code that describes the error. See Error Codes for details.

Example response

# GET /resources/1?app_id=app_unique_id
{
  "resource": {
      "id": "1",
      "name": "Gooli Metadata",
      "description": "Access to Gooli customer metadata"
   }
}

GET /resources/{resource_id}/access_levels

Returns all the available access levels for a resource, paginated.

Path params

paramtypedescription
resource_idstringThe id of the resource you’re trying to retrieve the access levels of

Query params

paramtypedescription
app_idstringThe Opal app ID to list resources for. This allows your service to distinguish apps if it’s supporting multiple apps via the same set of endpoints
cursorstringFor pagination. Empty string on the first call, then set to the value provided via next_cursor

Response params (200)

paramtypedescription
access_levelsarray of objectsList of the access levels objects. See below for what each object should include.
Hot tip: if your resource doesn't require any access level, it's allowed to return []here.
next_cursorstringThe cursor that should be used for the next call. If cursor is an empty string, it is assumed that all resources have been fetched.

Response params (Error)

paramtypedescription
messagestringAn error message that will be exposed in the Opal UI.
codeintegerThe error code that describes the error. See Error Codes for details.

Access level object

fieldtypedescription
idstringID of the access level to grant to this user.
namestringThe user-facing name of the access level.

Example response

{
  "next_cursor": "gjroieapghnfagjfdgpadshfasd",
  "access_levels": [
    {
      "id": "1",
      "name": "Admin"
    },
    {
      "id": "2",
      "name": "Read-Only Admin"
    },
    {
      "id": "3",
      "name": "Guest"
    }
  ]
}

📘

Hot tip: if your resource doesn't require any access level, it's allowed to return "access_levels": []here.

GET /resources/{resource_id}/users

Returns the users that currently have access to the provided resource.

Path params

paramtypedescription
resource_idstringThe id of the resource you’re trying to list the details of, as returned as id in the /resources/<resource_id> endpoint.

Query params

paramtypedescription
app_idstringThe app id specified by the end user during connection process. This can be used in your end system to distinguish apps if you are supporting multiple apps via the same set of endpoints
cursorstringFor pagination. Empty string on the first call, then passed values provided via next_cursor

Response params (200)

paramtypedescription
usersarray of objectsList of the resource user objects. See below for what each resource should include
next_cursorstringThe cursor that should be used for the next call. If cursor is an empty string, it is assumed that all resources have been fetched.

Response params (Error)

paramtypedescription
messagestringAn error message to expose in the Opal UI.
codeintegerThe error code that describes the error. See Error Codes for details.

ResourceUser object

fieldtypedescription
user_idstringThe user identifier used in the remote system, this will be provided back to you when making access changes.
emailstringThe email of the user. This will be used on our end to correlate it with Opal users. Although not required at the moment, we'd like implementers to include this value as well.
access_levelobjectThe access level granted to the user. If omitted, the default access level remote ID value (empty string) is used.

Example response

{
  "next_cursor": "bpaubmkeospb",
  "users": [
    {
      "email": "bill@example.com",
      "user_id": "1",
      "access_level": {
        "id": "1",
        "name": "Admin"
      }
    },
    {
      "email": "andrea@example.com",
      "user_id": "2",
      "access_level": {
        "id": "2",
        "name": "Read-Only Admin"
      }
    }
  ]
}

POST /resources/{resource_id}/users

Adds a user to the access list of the specified resource.

Path params

paramtypedescription
resource_idstringThe id of the resource you’re trying to add the user to

Body params (JSON encoded)

paramtypedescription
app_idstringThe app id specified by the end user during connection process. This can be used in your end system to distinguish apps if you are supporting multiple apps via the same set of endpoints
user_idstringThe id of the user to add to the resource
access_level_idstring (optional)The ID of the access level to assign to the user.

Example request

{
	"app_id": "datadog-production",
  "user_id": "568d0cd1-284d-4e82-a6c7-9c98b5e51a65",
  "access_level_id": "12f5ef35-5de6-4224-a006-7fe4c20db5c6"
}

Response params (200)

No body is required as part of this endpoint for status code 200.

Response params (Error)

paramtypedescription
messagestringAn error message that will be exposed in the Opal UI.
codeintegerThe error code that describes the error. See Error Codes for details.

DELETE /resources/{resource_id}/users/{user_id}

Remove a user from the access list of the specified resource.

Path params

paramtypedescription
resource_idstringThe id of the resource to be edited.
user_idstringThe id of the user to be removed.

Query params

paramtypedescription
app_idstringThe app id specified by the end user during connection process. This can be used in your end system to distinguish apps if you are supporting multiple apps via the same set of endpoints
access_level_idstring (optional)The ID of the access level associated to the user for this resource.

Response params (200)

No body is required as part of this endpoint for status code 200.

Response params (Error)

paramtypedescription
messagestringAn error message that will be exposed in the Opal UI.
codeintegerThe error code that describes the error. See Error Codes for details.

GET /groups

Returns a list of all groups for the app in question.

Query params

paramtypedescription
app_idstringThe Opal app ID to list groups for. This allows your service to distinguish apps if it’s supporting multiple apps via the same set of endpoints
cursorstringFor pagination. Empty string on the first call, then set to the value provided via next_cursor

Response params (200)

paramtypedescription
groupsarray of objectsList of the group objects. See below for what each group should include
next_cursorstringThe cursor that should be used for the next call. If cursor is an empty string, it is assumed that all groups have been fetched.

Response params (Error)

paramtypedescription
messagestringAn error message that will be exposed in the Opal UI.
codeintegerThe error code that describes the error. See Error Codes for details.

Group object

fieldtypedescription
idstringThe id of the group that uniquely identifies it in your system.
namestringThe name of the group
descriptionstringThe description of the group

Example response

{
  "next_cursor": "gjroieapghnfagjfdgpadshfasd",
  "groups": [
    {
      "id": "1",
      "name": "Eng team",
      "description": "Eng team users"
    },
    {
      "id": "2",
      "name": "Finance team (North America)",
      "description": "Users in the finance team in NA"
    }
  ]
}

GET /groups/{group_id}

Get a specific group by its id.

Path params

paramtypedescription
group_idstringThe id of the resource

Query params

paramtypedescription
app_idstringThe Opal app ID to list resources for. This allows your service to distinguish apps if it’s supporting multiple apps via the same set of endpoints

Response params (200)

fieldtypedescription
groupobjectA Group object.

Group object

fieldtypedescription
idstringThe id of the resource that uniquely identifies it in your system.
namestringThe name of the resource
descriptionstringThe description of the resource

Response params (Error)

paramtypedescription
messagestringAn error message that will be exposed in the Opal UI.
codeintegerThe error code that describes the error. See Error Codes for details.

Example response

# GET /groups/1?app_id=app_unique_id
{
  "group": {
      "id": "1",
      "name": "Eng team",
      "description": "Your friendly eng team."
   }
}

GET /groups/{group_id}/users

Returns the users that currently belong to the provided group.

Path params

paramtypedescription
group_idstringThe id of the group you’re trying to list the details of

Query params

paramtypedescription
app_idstringThe app id specified by the end user during connection process. This can be used in your end system to distinguish apps if you are supporting multiple apps via the same set of endpoints
cursorstringFor pagination. Empty string on the first call, then passed values provided via next_cursor

Response params (200)

paramtypedescription
usersarray of objectsList of the group user objects. See below for what each group should include
next_cursorstringThe cursor that should be used for the next call. If cursor is an empty string, it is assumed that all resources have been fetched.

Response params (Error)

paramtypedescription
messagestringAn error message to expose in the Opal UI.
codeintegerThe error code that describes the error. See Error Codes for details.

GroupUser object

fieldtypedescription
user_idstringThe user identifier used in the remote system, this will be provided back to you when making access changes.
emailstringThe email of the user. This will be used on our end to correlate it with Opal users. Although not required at the moment, we'd like implementers to include this value as well.

Example response

{
  "next_cursor": "bpaubmkeospb",
  "users": [
    {
      "email": "bill@example.com",
      "user_id": "1",
    },
    {
      "email": "andrea@example.com",
      "user_id": "2",
    }
  ]
}

POST /groups/{group_id}/users

Adds a user to the access list of the specified group.

Path params

paramtypedescription
group_idstringThe id of the group you’re trying to add the user to

Body params (JSON encoded)

paramtypedescription
app_idstringThe app id specified by the end user during connection process. This can be used in your end system to distinguish apps if you are supporting multiple apps via the same set of endpoints
user_idstringThe id of the user to add to the group

Example request

{
	"app_id": "datadog-production",
  "user_id": "568d0cd1-284d-4e82-a6c7-9c98b5e51a65",
}

Response params (200)

No body is required as part of this endpoint for status code 200.

Response params (Error)

paramtypedescription
messagestringAn error message that will be exposed in the Opal UI.
codeintegerThe error code that describes the error. See Error Codes for details.

DELETE /groups/{group_id}/users/{user_id}

Remove a user from the access list of the specified group.

Path params

paramtypedescription
group_idstringThe id of the group to be edited.
user_idstringThe id of the user to be removed.

Query params

paramtypedescription
app_idstringThe app id specified by the end user during connection process. This can be used in your end system to distinguish apps if you are supporting multiple apps via the same set of endpoints

Response params (200)

No body is required as part of this endpoint for status code 200.

Response params (Error)

paramtypedescription
messagestringAn error message that will be exposed in the Opal UI.
codeintegerThe error code that describes the error. See Error Codes for details.

GET /groups/{group_id}/resources

Returns the resources that currently belong to the provided group.

Path params

paramtypedescription
group_idstringThe id of the group you’re trying to list the details of

Query params

paramtypedescription
app_idstringThe app id specified by the end user during connection process. This can be used in your end system to distinguish apps if you are supporting multiple apps via the same set of endpoints
cursorstringFor pagination. Empty string on the first call, then passed values provided via next_cursor

Response params (200)

paramtypedescription
resourcesarray of objectsList of the group resource objects. See below for what each group should include
next_cursorstringThe cursor that should be used for the next call. If cursor is an empty string, it is assumed that all resources have been fetched.

Response params (Error)

paramtypedescription
messagestringAn error message to expose in the Opal UI.
codeintegerThe error code that describes the error. See Error Codes for details.

GroupResource object

fieldtypedescription
resource_idstringThe resource identifier used in the remote system, this will be provided back to you when making access changes.
access_levelobjectThe access level the group has access to the resource with.

Example response

{
  "next_cursor": "bpaubmkeospb",
  "resources": [
    {
      "resource_id": "1",
      "access_level": {
        "id": "1",
        "name": "Admin"
      }
    },
    {
      "resource_id": "2",
      "access_level": {
        "id": "2",
        "name": "Read-Only Admin"
      }
    }
  ]
}

POST /groups/{group_id}/resources

Adds a resource to the access list of the specified group.

Path params

paramtypedescription
group_idstringThe id of the group you’re trying to add the resource to

Body params (JSON encoded)

paramtypedescription
app_idstringThe app id specified by the end user during connection process. This can be used in your end system to distinguish apps if you are supporting multiple apps via the same set of endpoints
resource_idstringThe id of the resource to add to the group
access_level_idstring (optional)The ID of the access level to assign to the group to the resource.

Example request

{
	"app_id": "datadog-production",
  "resource_id": "568d0cd1-284d-4e82-a6c7-9c98b5e51a65",
  "access_level_id": "12f5ef35-5de6-4224-a006-7fe4c20db5c6"
}

Response params (200)

No body is required as part of this endpoint for status code 200.

Response params (Error)

paramtypedescription
messagestringAn error message that will be exposed in the Opal UI.
codeintegerThe error code that describes the error. See Error Codes for details.

DELETE /groups/{group_id}/resources/{resource_id}

Remove a resource from the access list of the specified group.

Path params

paramtypedescription
group_idstringThe id of the group to be edited.
resource_idstringThe id of the resource to be removed.

Query params

paramtypedescription
app_idstringThe app id specified by the end user during connection process. This can be used in your end system to distinguish apps if you are supporting multiple apps via the same set of endpoints
access_level_idstring (optional)The ID of the access level associated to the group for this resource.

Response params (200)

No body is required as part of this endpoint for status code 200.

Response params (Error)

paramtypedescription
messagestringAn error message that will be exposed in the Opal UI.
codeintegerThe error code that describes the error. See Error Codes for details.

GET /users

Returns a list of users for your custom connector app.

Query params

paramtypedescription
app_idstringThe Opal app ID to list users for. This allows your service to distinguish apps if it’s supporting multiple apps via the same set of endpoints
cursorstringFor pagination. Empty string on the first call, then set to the value provided via next_cursor

Response params (200)

paramtypedescription
usersarray of objectsList of the user objects. See below for what each user should include
next_cursorstringThe cursor that should be used for the next call. If cursor is an empty string, it is assumed that all resources have been fetched.

Response params (Error)

paramtypedescription
messagestringAn error message that will be exposed in the Opal UI.
codeintegerThe error code that describes the error. See Error Codes for details.

User object

fieldtypedescription
idstringThe id of the user that uniquely identifies it in your system.
emailstringThe email of the user, this is required to associate the user in Opal.

Example

{
  "next_cursor": "kjhiufewnoi",
  "users": [
    {
      "email": "jan@company.com",
      "id": "6bac7de4-08d7-4437-8397-75f44b6f76ae"
    },
    {
      "email": "tim@company.com",
      "id": "c1623a9c-cfb8-4c50-bd89-ea05bf597d60"
    },
    {
      "email": "josh@company.com",
      "id": "968bf191-ba81-4fa0-a5a9-aaff71655fbd"
    },
    {
      "email": "stephen@company.com",
      "id": "d67d4184-6cdf-4c43-9d65-698997bd0c52"
    }
  ]
}

GET /events

Query params

paramtypedescription
app_idstringThe Opal app ID to list events for. This allows your service to distinguish apps if it’s supporting multiple apps via the same set of endpoints
cursorstringFor pagination. Empty string on the first call, then set to the value provided via next_cursor

Response params (200)

paramtypedescription
eventsarray of objectsList of the event objects. See below for what each event should include
next_cursorstringThe cursor that should be used for the next call. If cursor is an empty string, it is assumed that all evets have been fetched.

Response params (Error)

paramtypedescription
messagestringAn error message that will be exposed in the Opal UI.
codeintegerThe error code that describes the error. See Error Codes for details.

Event object

fieldtypedescription
event_idstringThe id of the event that uniquely identifies it in your system.
event_typeOneOf: login.successThe type of the event.
occurred_atRFC3339 datetime stringThe date and time at which the event occurred. Return as a string using the following format: {year}-{month}-{day}T{hour}:{minute}:{second}.{microsecond}%z.
actor_user_identifierActorUserIdentifier objectAn ActorUserIdentifier object uniquely identifying the user that performed this event. See below for what this object should include.
event_contentOneOf: EventContentLoginSuccess objectThe content of this event, which is specific to the event type. See below for what each event content should include.

ActorUserIdentifier object

fieldtypedescription
user_idstringThe id of the user that uniquely identifies it in your system.
user_emailstringThe email of the user, this is required to associate the user in Opal.

EventContentLoginSuccess object

fieldtypedescription
resource_idstringThe id of the resource that the user logged in to.

Example

{
  "next_cursor": "kjhiufewnoi",
  "events": [
    {
        "event_id": "1",
        "event_type": "login.success",
        "occurred_at": "2025-01-15T00:58:33.967628+0000",
        "actor_user_identifier": {
            "user_id": "6bac7de4-08d7-4437-8397-75f44b6f76ae",
            "user_email": "jan@company.com",
        },
        "event_content": {"resource_id": "568d0cd1-284d-4e82-a6c7-9c98b5e51a65"},
    },
    {
        "event_id": "2",
        "event_type": "login.success",
        "occurred_at": "2025-01-15T00:58:33.967628+0000",
        "actor_user_identifier": {
            "user_id": "tim@company.com",
            "user_email": "c1623a9c-cfb8-4c50-bd89-ea05bf597d60",
        },
        "event_content": {"resource_id": "568d0cd1-284d-4e82-a6c7-9c98b5e51a65"},
    },
    {
        "event_id": "3",
        "event_type": "login.success",
        "occurred_at": "2025-01-15T00:58:33.967628+0000",
        "actor_user_identifier": {
            "user_id": "d67d4184-6cdf-4c43-9d65-698997bd0c52",
            "user_email": "stephen@company.com",
        },
        "event_content": {"resource_id": "568d0cd1-284d-4e82-a6c7-9c98b5e51a65"},
    },
  ]
}

Error codes

All error codes need to implement the following object:

paramtypedescription
messagestringAn error message to expose in the Opal UI.
codeintegerThe error code that describes the error. See Error Codes for details.

In addition, you can distinguish the type of error and meaning by following these codes

Status codeMeaning
200Response successful.
401Invalid request signature error.
404Entity not found (eg. resource, user, access level).
500Unexpected error.

Signature

To ensure that the API calls originate from Opal, we provide a header in each request that represents the encrypted request payload with a secret that is generated when creating the app connector in Opal. See Setup connector app in Opal for more details.

On each HTTP request that Opal sends, we add an X-Opal-Signature HTTP header. The signature is created by combining the signing secret with the body of the request we're sending using a standard HMAC-SHA256 keyed hash.

Here is an example with Node to compute the signature using your signing secret. You may compare it against the value retrieved from the X-Opal-Signature header:

const timestamp = request.header('X-Opal-Request-Timestamp')
const signingSecret = 'SIGNING_SECRET'
// In Typescript/JS, request.body is always an empty object
const sigBaseString = 'v0:' + timestamp + ':' + JSON.stringify(request.body)
const hmac = crypto.createHmac('sha256', signingSecret);
hmac.write(sigBaseString)
console.log(hmac.digest('hex'))
func generateSignature(
	signingSecret string,
	timestamp string,
	serializedBlob []byte,
) (string, error) {
	// Concatenate base string
	sigBaseString := "v0:" + timestamp + ":" + string(serializedBlob)

	// Hash base string to get signature
	hash := hmac.New(sha256.New, []byte(signingSecret))
	_, err := hash.Write([]byte(sigBaseString))
	if err != nil {
		return "", errors.Wrap(err, "error writing hash")
	}

	return hex.EncodeToString(hash.Sum(nil)), nil
}

func validateOpalSignature(signingSecret string) gin.HandlerFunc {
	return func(c *gin.Context) {
		opalSignature := c.GetHeader("X-Opal-Signature")
		if opalSignature == "" {
			c.AbortWithStatusJSON(http.StatusUnauthorized, &Error{
				Code:    http.StatusUnauthorized,
				Message: "X-Opal-Signature header is missing",
			})
			return
		}
		opalRequestTimestamp := c.GetHeader("X-Opal-Request-Timestamp")
		if opalRequestTimestamp == "" {
			c.AbortWithStatusJSON(http.StatusUnauthorized, &Error{
				Code:    http.StatusUnauthorized,
				Message: "X-Opal-Request-Timestamp header is missing",
			})
			return
		}

		var bodyStr string
		// Read request body, once the request body is read, it cannot be read again
		// so we need to save it in a variable and then reassign it to the Request.Body
		var bodyBytes []byte
		var err error
		if c.Request.Body != nil {
			bodyBytes, err = ioutil.ReadAll(c.Request.Body)
			if err != nil {
				c.AbortWithStatusJSON(http.StatusInternalServerError, &Error{
					Code:    http.StatusInternalServerError,
					Message: "Unable to read request body",
				})
				return
			}
			c.Request.Body = ioutil.NopCloser(bytes.NewBuffer(bodyBytes))
			bodyStr = strings.TrimSpace(string(bodyBytes))
		}
		if bodyStr == "" {
			bodyStr = "{}"
		}

		signature, err := GenerateSignature(signingSecret, opalRequestTimestamp, []byte(bodyStr))
		if signature != opalSignature || err != nil {
			c.AbortWithStatusJSON(http.StatusUnauthorized, &Error{
				Code:    http.StatusUnauthorized,
				Message: "Invalid signature",
			})
			return
		}

		c.Next()
	}
}

Note: when the body is empty, do coalesce the empty/null stringified body to {}. See the link below for more examples.

Check Create your own connector for signature examples.