Space SDK
Space SDK is a library that lets you work with the JetBrains Space HTTP API and simplifies developing Space applications.
Add the following dependencies to the project's build.gradle
/build.gradle.kts
file:
// the example is given for build.gradle.kts
repositories {
// here go other project repos ...
maven("https://maven.pkg.jetbrains.space/public/p/space/maven")
}
dependencies {
// here go other dependencies ...
// We use SDK v.159302, but when you read this guide, a newer version may be available.
// To find out which SDK version is the latest available: open API Playground,
// in the "Code" section on the right, choose "Kotlin SDK", and click "Set up dependency..."
implementation("org.jetbrains:space-sdk-jvm:159302-beta")
// We also need a Ktor client
implementation("io.ktor:ktor-client-cio-jvm:2.0.3")
}
To make any calls, your application must first authorize in Space. Normally, you should register your application in Space and use the received client ID and secret for authorization (Client Credentials OAuth 2.0 flow). But for testing purposes, it is faster and easier to create a personal token:
When creating the token, specify permissions required by your application (e.g., to send messages to Space users, grant Send direct messages in the global context). Note that a production application must use other ways to obtain the required permissions depending on its authorization flow.
Space SDK provides a client that simplifies making calls to Space:
import space.jetbrains.api.runtime.SpaceAuth
import space.jetbrains.api.runtime.SpaceClient
import space.jetbrains.api.runtime.ktorClientForSpace
val spaceHttpClient = ktorClientForSpace()
// serverUrl - URL of your Space instance
// token - access token created in the previous step
val spaceClient = SpaceClient(ktorClient = spaceHttpClient,
serverUrl = "https://mycompany.jetbrains.space",
token = "here-goes-access-token")
Use the Space client to make calls to your Space instance:
spaceClient.chats.messages.sendMessage(
channel = ChannelIdentifier.Profile(ProfileIdentifier.Username("John.Doe")),
content = message {
section { text("Hello from my app!") }
}
)
Add the Space SDK client to your project. For example, with the dotnet
command-line tool:
dotnet add package JetBrains.Space.Client --version 1.0.0-beta.v2022.2.0-DEV.113510.9115
We use SDK 1.0.0-beta.v2022.2.0-DEV.113510.9115, but when you read this guide, a newer version may be available.
To make any calls, your application must first authorize in Space. Normally, you should register your application in Space and use the received client ID and secret for authorization (Client Credentials OAuth 2.0 flow). But for testing purposes, it is faster and easier to create a personal token:
When creating the token, specify permissions required by your application (e.g., to send messages to Space users, grant Send direct messages in the global context). Note that a production application must use other ways to obtain the required permissions depending on its authorization flow.
using JetBrains.Space.Client;
using JetBrains.Space.Common;
// url of your Space instance
var url = new Uri("https://mycompany.jetbrains.space");
// access token created in the previous step
var token = "here-goes-access-token";
var authTokens = new AuthenticationTokens(token);
var connection = new BearerTokenConnection(url, authTokens);
Create the required client and make calls to your Space instance:
// create a client that will send chat messages
var chatClient = new ChatClient(connection);
await chatClient.Messages.SendMessageAsync(
recipient: MessageRecipient.Member(ProfileIdentifier.Username("John.Doe")),
content: new ChatMessageText("Hello from my app!"));
Space SDK provides:
Space HTTP API client – The client provides access to Space endpoints and lets you
Processing requests from Space – A set of classes that helps you process payload received in Space requests.
Various helper utils that let you reduce boilerplate code, for example, formatting chat messages, processing Space payload, and so on.
The SDK comes in two versions:
Space SDK for Kotlin available at the public JetBrains Space repository:
org.jetbrains:space-sdk-jvm:{version}
– Space HTTP API client that can be used on the Java Virtual Machine (JVM).org.jetbrains:space-sdk-js:{version}
– Space HTTP API client that can be used with Kotlin/JS.
Space SDK for .NET available at nuget.org:
JetBrains.Space.Client
— The generated client code to work with the Space HTTP API.JetBrains.Space.AspNetCore
— Helpers for using JetBrains.Space with ASP.NET Core.JetBrains.Space.AspNetCore.Authentication
— Authentication provider that integrates with ASP.NET Core.
note
Though both versions have much in common, some implementation details may differ. This documentation is written for the 'Space SDK for Kotlin'. For the details on 'Space SDK for .NET', refer to its README.md.
The important part of Space SDK is the Space HTTP API client. Among many other things, the client provides direct access to Space endpoints. The corresponding properties and methods of SpaceHttpClient
are structured in the same way as in API Playground. You can always use the playground as interactive help and even client code generator:

You can use Space endpoints to get data from Space. For example, this is how you can get a user profile from the Team Directory
val appInstance = AppInstance(clientId, clientSecret, spaceServerUrl)
// requires the ViewProfile permission
SpaceClient(appInstance, SpaceAuth.ClientCredentials()).use {
it.teamDirectory.profiles.getProfile(ProfileIdentifier.Username("John.Doe"))
}
The memberProfile
has properties of the profile such as id
, username
, and about
.
Space endpoints let you perform many operations in Space, e.g. send a chat message, create a blog post, add a project member, close a code review, and so on. For example, this is how you can publish a blog post:
val client = SpaceHttpClient(HttpClient())
.withServiceAccountTokenSource(id, secret, url)
// requires the PublishArticles right
client.blog.publishBlogPost(
title = "My First Blog Post",
content = "Hello World!"
)
To perform get or set requests to Space endpoints, your application must have corresponding permissions. Learn how to request permissions.
You can find out what permissions are required to perform a certain call in API Playground.

All Space responses contain a JSON object with the requested data. By default, if you don't specify the fields you want to get, this JSON object will contain all top-level properties.
HTTP Request:
|
Response:
|
You can do the same request with the Space SDK:
val memberProfile = spaceClient.teamDirectory.profiles
.getProfile(ProfileIdentifier.Username("John.Doe"))
For most requests, you can shape the results you want to retrieve from Space. For example, to retrieve only the user id
and the about
description, you can set the $fields
parameter in an API request to $fields=id,about
.
Being able to retrieve just the information our application requires, helps to reduce the payload size and results in better overall performance.
HTTP Request:
|
Response:
|
To perform such a request with the Space SDK, you should use the so-called partial methods (id()
and about()
in our case):
val memberProfile = spaceClient.teamDirectory.profiles
.getProfile(ProfileIdentifier.Username("John.Doe")
) {
id()
about()
}
If you try to access the memberProfile.name
property, which is not in the response, the Space HTTP client will throw an IllegalStateException
with additional information.
try {
// This will fail...
println("${memberProfile.name.firstName} ${memberProfile.name.lastName}")
} catch (e: IllegalStateException) {
// ...and you'll get a pointer about why it fails:
// Property 'name' was not requested. Reference chain: getAllProfiles->data->[0]->name
println("The Space API client tells us which partial query should be added to access the property:");
println(e.message)
}
Worth noting that fields can be not only of a value type (integer, string, boolean, and so on), but of a complex type as well. As an example, a user profile has the name
field of the TD_ProfileName
, which in turn has the firstName
and lastName
fields. To request this hierarchy, you need to query $fields=name(firstName,lastName)
If you perform a request like that:
val memberProfile = spaceClient.teamDirectory.profiles
.getProfile(ProfileIdentifier.Username("John.Doe"))
Space will return an instance of the TD_MemberProfile
type which includes a lot of top-level properties: id
, username
, about
, and others. Among them there is the managers
property which is a collection of nested TD_MemberProfile
instances. Such a property is not retrieved by default. You must do this explicitly.
For example, if you want to retrieve managers
for a user profile including manager names, you should request these properties by extending the default partial result:
val memberProfile = spaceClient.teamDirectory.profiles
.getProfile(ProfileIdentifier.Username("John.Doe")) {
defaultPartial() // with all top level fields
managers { // include managers
id() // with their id
username() // and their username
name { // and their name
firstName() // with firstName
lastName() // and firstName
}
}
}
Here:
defaultPartial()
: a special partial method that adds all fields at the current level (*
field definition).
The example with memberProfile.managers
is quite interesting because every manager can have its own manager (who can also have a manager, who ...). As you don't know how many managers are in this tree, you can request a recursive response. It will return the entire managers tree.
The functions that represent such tree structures have special overloads for retrieving recursive responses. For example in case of managers
, it's managers(recursiveAs: TD_MemberProfilePartial)
. This is how you can use it:
val memberProfile = spaceClient.teamDirectory.profiles
.getProfile(ProfileIdentifier.Username("John.Doe")) {
defaultPartial() // with all top level fields
managers(this) // and the same fields for all managers
}
There are several Space endpoints that return subclasses (polymorphic responses).
One such example is spaceClient.projects.planning.issues.getAllIssues()
, where the createdBy
property can be a subclass of CPrincipalDetails
:
CAutomationTaskPrincipalDetails
, if the issue was created by an automation task.CBuiltInServicePrincipalDetails
, if the issue was created by Space itself.CExternalServicePrincipalDetails
, if the issue was created by an external service.CUserWithEmailPrincipalDetails
, if the issue was created by a user that has an e-mail address.CUserPrincipalDetails
, if the issue was created by a user.
The partial builder contains properties for all of these classes.
Here's an example retrieving issues from a project. For the createdBy
property, you are defining that the response should contain:
CUserPrincipalDetails
with theuser.id
property.CUserWithEmailPrincipalDetails
with thename
andemail
properties.
val issueStatuses = spaceClient.projects.planning.issues.statuses
.getAllIssueStatuses(ProjectIdentifier.Key("CRL")).map { it.id }
val issues = spaceClient.projects.planning.issues
.getAllIssues(ProjectIdentifier.Key("CRL"),
sorting = IssuesSorting.UPDATED,
descending = true,
statuses = issueStatuses) {
defaultPartial()
creationTime()
createdBy {
details {
// Available on CUserPrincipalDetails
user {
id()
}
// Available on CUserWithEmailPrincipalDetails,
// CAutomationTaskPrincipalDetails, CBuiltInServicePrincipalDetails
name()
email()
}
}
status()
}.data.forEach { issue ->
when (issue.createdBy.details) {
is CUserPrincipalDetails -> {
// ...
}
is CUserWithEmailPrincipalDetails -> {
// ...
}
}
}
You can cast these types, use when
expressions on them, and so on.
There are a lot of requests that can return a collection of results. To increase performance, these responses will be paginated, and can be retrieved in batches.
You must specify the properties of the data type you need. For example, let's retrieve the user's To-Do items for some date including their id
and content
:
// Get all To-Do
var todoBatchInfo = BatchInfo("0", 100)
do {
val todoBatch = spaceClient.todoItems
.getAllTodoItems(from = LocalDate(2020, 01, 01), batchInfo = todoBatchInfo) {
id()
content()
}
todoBatch.data.forEach { todo ->
// ...
}
todoBatchInfo = BatchInfo(todoBatch.next, 100)
} while (todoBatch.hasNext())
Here:
hasNext()
: an extension method that lets you to determine whether more results need to be retrieved:fun Batch<*>.hasNext() = !data.isEmpty()
The resulting batch will contain one page of results. To retrieve more To-Do items, you should make additional API calls.
Thanks for your feedback!