You can define REST endpoints in ScriptRunner, for example to:

Adding a REST Endpoint

Navigate to Admin → REST Endpoints.

Click a heading to add a handler. Choose Custom endpoint to add your own endpoint.

REST endpoints are configured programatically. There is currently one sample defined (click Expand Examples), which we’ll discuss in-depth:

import groovy.json.JsonBuilder
import groovy.transform.BaseScript


@BaseScript CustomEndpointDelegate delegate (1)

doSomething( (2)
    httpMethod: "GET", groups: ["confluence-administrators"] (3)
) { MultivaluedMap queryParams, String body -> (4)
    return Response.ok(new JsonBuilder([abc: 42]).toString()).build() (5)
1 - this line makes methods in your script recognisable as endpoints, and is required
2 - the name of the REST endpoint, which forms part of the URL, in this case doSomething
3 - configuration of the endpoint, in this case which HTTP verb to handle, and what groups to allow
4 - parameters which are provided to your method body
5 - the body of your method, where you will return a object

Once this is added to the list of configured endpoints, either as an inline script, or by copying it to a file and adding as a script file, you should be able to test the endpoint by visiting in your browser:


Alternatively using a command line utility:

>curl -u admin:admin http://localhost:8080/confluence/rest/scriptrunner/latest/custom/doSomething

If you are using a file, you can try changing the response, though you may need to hit the "Scan" button on the REST Endpoints page before calls to the endpoint return the new response. See the section on script root scanning, below.

admin:admin corresponds to a username and password.


The general format of a method defining a REST endpoint is:

methodName (Map configuration, Closure closure)

For the configuration only the following options are supported:




One or more groups. If the requesting user is in any of these groups the request will be allowed

Note that either or both of these can be omitted. If you omit the groups attribute, the endpoint will be available to unauthenticated users.

The closure can take these parameters:

MultivaluedMap queryParams

corresponds to the URL parameters

String content

The body of the request, for POST and PUT etc

HttpServletRequest request

The request object. You can use this to get the requesting user for instance.

You can use any of these forms for your closure:

something() { MultivaluedMap queryParams ->
something() { MultivaluedMap queryParams, String body ->
something() { MultivaluedMap queryParams, String body, HttpServletRequest request ->

depending on what you need access to.

Access request URL

Sometimes you may need to use the URL path after your method name, for instance in the following call, you want to retrieve the /foo/bar:


To get this, use the 3-param form of the closure definition, and call the getAdditionalPath method from the base class:

doSomething() { MultivaluedMap queryParams, String body, HttpServletRequest request ->

    def extraPath = getAdditionalPath(request)
    // extraPath will contain /foo/bar when called as above
In previous versions, an extraPath variable was injected into the scripts, but this is not thread-safe - use the above method instead.


Create User

This example demonstrates plugging a gap in the official REST API. As at the time of writing, there is no way to create a new User.

import bucket.user.UserAccessor
import com.atlassian.sal.api.component.ComponentLocator
import com.atlassian.user.GroupManager
import com.atlassian.user.impl.DefaultUser
import groovy.json.JsonBuilder
import groovy.transform.BaseScript

import javax.servlet.http.HttpServletRequest

import static

@BaseScript CustomEndpointDelegate delegate

def userAccessor = ComponentLocator.getComponent(UserAccessor)
def groupManager = ComponentLocator.getComponent(GroupManager)

        httpMethod: "POST", groups: ["confluence-administrators"]
) { MultivaluedMap queryParams, String body ->

    def mapper = new ObjectMapper()
    def user = mapper.readValue(body, Map)
    assert user.username // must provide username
    assert user.fullname // must provide fullname
    assert // must provide email
    assert // must provide group
    assert user.password // must provide group

    try {
        def newUser = new DefaultUser(user.username, user.fullname,
        // password ought to be encrypted but this is just an example
        userAccessor.createUser(newUser, unencrypted(user.password as String))
        userAccessor.addMembership(, user.username)

    } catch (e) {
        return Response.serverError().entity([error: e.message]).build()

    return Response.created(new URI("/$user.username")).build()

Most of this code is involved with validating the JSON and query params that are passed to it, that is ensuring that all required fields for a user are present. We use the appropriate method on the Response class to send the right status code, eg 500 (server error) if the user already exists, and 201 (created) if we create the user.

To test it you could use

curl -v -X POST -H "Content-type: text/json" -u admin:admin --data "@user.json" \

where user.json is a text file containing:

  "username": "newuser",
  "fullname": "New User",
  "email": "",
  "group": "confluence-users",
  "password": "newuser"

Also note that you can have multiple methods with the same name in the same file, which is useful to do simple CRUD REST APIs, eg:

POST /user - creates a user
PUT /user - updates a user
DELETE /user - deletes a user
GET /user - gets a user

Get A User

In the previous example we created a user. This example demonstrates how we can retrieve that user.

        httpMethod: "GET", groups: ["confluence-administrators"]
) { MultivaluedMap queryParams, String body, HttpServletRequest request ->

    // validate we have username as a url parameter
    def extraPath = getAdditionalPath(request)
    assert extraPath =~ "^/[a-zA-Z]+"
    def username = extraPath.split("/").last()

    def user = userAccessor.getUser(username)
    // user must exist in Confluence
    if (!user) {
        return Response.serverError().entity([error: "User $username does not exist"]).build()

    def userResponse = [
            fullname: user.fullName,
            groups: groupManager.getGroups(user)*.name

    return Response.ok(new JsonBuilder(userResponse).toString()).build()

Note that when we created the previous user we got the following header in the response

Location: <confluence_base_url>/rest/scriptrunner/latest/custom/user/newuser

We can then use the GET user endpoint in the same script, using that location to retrieve the user from.

To retrieve the user you could use the following command

curl -X GET -u admin:admin <confluence_base_url>/rest/scriptrunner/latest/custom/user/newuser

As before we use the appropriate method on the Response class to send the right status code, eg 500 (server error) if the user does not exist, and 200 (ok) if we retrieve the user.

For how-to questions please ask on Atlassian Answers where there is a very active community. Adaptavist staff are also likely to respond there.

Ask a question about ScriptRunner for JIRA, for for Bitbucket Server, or for Confluence.