Sitecore Authoring API: A COMPLETE Example

In Sitecore 10.3 (and in some release of XM Cloud), Sitecore released two new API layers for interacting with the Content Management role of a Sitecore implementation – the Authoring API and the Management API. They have some documentation for these APIs, but I would not call them…*ahem*…robust. That being said, here’s where to start:

For this article, I’ll be focused on XM/XP, but the principles and methods should remain the same, if not the endpoints.

Enabling requests to the Authoring and Management APIs

There are essentially 4 steps mentioned in the docs to start with for the most basic of queries:

  1. Enable the GraphQL Playground
  2. Request an access token for authorizing the requests
  3. Include the access token in the header of your requests
  4. PROFIT! Execute queries! πŸ˜€

The documents listed cover steps 1, 3, and 4 pretty well (I mean, they are the simplest of the 4 steps), but the examples for step 2 are, well, outdated. The example given in this document involves creating a v2.1 .net core app. .Net core 2.1 was EOL’d, like, A LOT of years ago. So, I’ll give you a couple of examples on how to obtain this token with…more modern technologies.

In Powershell, you could do something like this:

$Uri = 'https://<your-id-server>/connect/token'
 
$ContentType = 'application/x-www-form-urlencoded'
 
$Body = @{ 
    grant_type='password'
    username='sitecore\admin'
    password='b'
    scope='openid sitecore.profile sitecore.profile.api'
    client_id='<your-id-server-client-id>'
    client_secret='<your-id-server-client-secret>'
}
 
$Response = Invoke-WebRequest -Uri $Uri -Method POST -ContentType $ContentType -Body $Body
$JsonResponse = ConvertFrom-Json -InputObject $Response.Content
Write-Host $JsonResponse.access_token

Using curl, it might look something like this:

curl --location --request POST 'https://<your-id-server>/connect/token' --header 'Content-Type: application/x-www-form-urlencoded' --data 'grant_type=password&username=<sitecore-username>&password=<sitecore-password>&client_id=<your-id-server-client-id>&client_secret=<your-id-server-client-secret>&scope=openid+sitecore.profile+sitecore.profile.api' --ssl-no-revoke

Or in .netcore v<more-modern-than-2.1>:

public string GetToken()
{
    var token = string.Empty;

    using (var httpClient = new HttpClient())
    {
        httpClient.BaseAddress = new Uri("https://<your-id-server?");

        var passwordRequest = new PasswordTokenRequest()
        {
            Address = "/connect/token",
            ClientId = "<your-id-server-client-id>",
            ClientSecret = "<your-id-server-client-secret>",
            GrantType = IdentityModel.OidcConstants.GrantTypes.Password,
            Scope = "openid sitecore.profile sitecore.profile.api",
            UserName = @"sitecore\admin",
            Password = "b"
        };

        var tokenResult = httpClient.RequestPasswordTokenAsync(passwordRequest).Result;
        token = tokenResult?.AccessToken ?? string.Empty;
    }

    return token;
}

All three of these options will end up giving you a really long string (~1160 characters) as your access token. Once you get that token, you can proceed to “Authorize the GraphQL IDE” section of the Sitecore docs above and start getting results like this:

**NOTE: The examples above use what is called the Password Credential Flow from Identity Server. This is a great way to get started easily when using the Authoring API locally, but this shouldn’t be used for production situations. Kristoffer Kjeldby has a great explanation in this post about the different credential flows available through Identity Server.

But this is the “Authoring API”, right?

You may be thinking to yourself, “I can do all of this querying of data through the normal GraphQL endpoint that is used by the Layout Service. That just uses the API Key from system/settings/services/api keys. Why all this extra rigmarole, just to query stuff??

Well, through the Authoring API, you can also do other stuff (called mutations), like createItem, deleteItem, updateItem, etc…

For example, you can create a new item with a GraphQL query like this:

mutation {
  createItem(
    input: {
      name: "taco-child-page"
      templateId: "{B4BE8618-2F26-45C9-80AD-6E4874F16B1E}"
      parent: "{91559D18-C7A4-4BE6-A5F6-EB31CC82F645}"
      language: "en"
      fields: [
        { name: "title", value: "Taco Child Page" }
        { name: "navigationtitle", value: "Taco Child Page" }
      ]
    }
  ) {
    item {
      itemId
      name
      path
      fields(ownFields: true, excludeStandardFields: true) {
        nodes {
          name
          value
        }
      }
    }
  }
}

Or, if you want to update an existing item, you could use updateItem like this:

mutation UpdateItem {
  updateItem(
    input:{
      path:"/sitecore/content/taco-demo/authoring-api/home/taco-child-page"
      fields:[
        {name:"Title", value:"This is a child page created by LonghornTaco"}
      ]  
    }
    
  ) {
    item {
      name
    }
  }
}

You can even search for items using the search method, like this:

query {
  search(
    query: {
      searchStatement: {
        criteria: [
          { criteriaType: SEARCH, field: "_Name", value: "taco-child-page" }
        ]
      }
    }
  )
 {
    results {
      innerItem {
        path
        field(name: "Title") {
          value
        }
      }
    }
  }
}

But why? Why do we need this API?

Back in the good ol’ days, if you wanted to perform a custom action when something happened on the Content Management role, you could very easily create your own event handler and patch it in to any of the myriad of events that fire on the CM at various times. For example, if you want to perform an action whenever an item was saved, you could patch in your custom event handler item:saved event. This ability, along with the other thousands of ways you could customize Sitecore, is what makes Sitecore so wonderful and flexible! But it’s often the reason that Sitecore was so difficult to upgrade.

But now that we’re in the SaaS world with XM Cloud, where all of your traditional Sitecore roles are managed, maintained, AND UPGRADED by Sitecore on a regular basis, we need a different way of customizing the platform as we have done in the past. That’s where Webhooks come in. If you pair Webhooks with the Authoring API, you can do almost all that you did before, without the need to deploy C# code to the Sitecore roles.

How about an example…

Let’s look at a simple example. When a Page item is saved and the Title field was changed, I want to automatically update the NavigationTitle field to match.

I’ll start by spinning up a ASP.NET Core Web Api project and add this controller, just to get the connection established:

[ApiController]
[Route("api/webhooks")]
public class WebhookController : ControllerBase
{
    [HttpPost("updatenavigationtitle")]
    public IActionResult UpdateNavigationTitle([FromBody] ItemSavedRequest request)
    {
        return Ok();
    }
}

Now, in Sitecore, navigate to /System/Webhooks and add a new Webhook Event Handler:

Note, since I’m running this in docker, in the Url field, I use the container name as the hostname. Also, to keep this example simple, I’m not adding any authorization for this API, but you can configure different authorization types in /sitecore/system/Settings/Webhooks/Authorizations, then select the newly created Authorization item in the Authorization field.

Now, when a change to a Page item is saved, this webhook will get called.

If we inspect the request, we can see the fields that changed:

Now that the webhook connection is working, let’s write some code to interact with the Authoring API.

First, we need a method to get a token from Identity Server:

private string GetToken()
{
    var token = string.Empty;

    using (var httpClient = new HttpClient())
    {
        httpClient.BaseAddress = new Uri("https://<your-id-server?");

        var passwordRequest = new PasswordTokenRequest()
        {
            Address = "/connect/token",
            ClientId = "<your-id-server-client-id>",
            ClientSecret = "<your-id-server-client-secret>",
            GrantType = IdentityModel.OidcConstants.GrantTypes.Password,
            Scope = "openid sitecore.profile sitecore.profile.api",
            UserName = @"sitecore\admin",
            Password = "b"
        };

        var tokenResult = httpClient.RequestPasswordTokenAsync(passwordRequest).Result;
        token = tokenResult?.AccessToken ?? string.Empty;
    }

    return token;
}

Now, a method to execute a GraphQL query that we may pass it:

public string ExecuteQuery(string query)
{
    var token = GetToken();

    using (var client = new HttpClient())
    {
        client.DefaultRequestHeaders.Add("Authorization", $"Bearer {token}");

        var requestPayload = new 
        { 
            query = query 
        };

        var jsonPayload = JObject.FromObject(requestPayload).ToString();
        var content = new StringContent(jsonPayload, Encoding.UTF8, "application/json");
        var response = client.PostAsync(graphqlEndpoint, content).Result;
        var responseContent = response.Content.ReadAsStringAsync().Result;

        if (!response.IsSuccessStatusCode)
        {
            throw new Exception($"Error: {response.StatusCode}\n{responseContent}");
        }
        return responseContent;
    }
}

I wrapped these methods in a class called SitecoreAuthoringApi that implements the IAuthoringApi interface, then registered that with the container. Now I can inject that dependency into my controller.

Below, you can see my WebhookController and UpdateNavigationTitle controller action that includes the UpdateItem mutation to send an update to Sitecore through our IAuthoringApi.ExecuteQuery method.

[ApiController]
[Route("api/webhooks")]
public class WebhookController : ControllerBase
{
    private readonly IAuthoringApi _authoringApi;

    public WebhookController(IAuthoringApi authoringApi)
    {
        _authoringApi = authoringApi ?? throw new ArgumentNullException(nameof(authoringApi));
    }

    [HttpPost("updatenavigationtitle")]
    public IActionResult UpdateNavigationTitle([FromBody] ItemSavedRequest request)
    {
        const string titleFieldId = "0efa10f5-63cb-4204-960b-538025e9757b";
        var fieldChange = request.Changes?.FieldChanges?.FirstOrDefault(x => x.FieldId == titleFieldId);
        if (fieldChange != null)
        {
            var query = @"
                mutation UpdateItem {
                    updateItem(
                    input:{
                        itemId:""" + request.Item.Id + @"""
                        fields:[
                           {name:""NavigationTitle"", value:""" + fieldChange.Value + @"""}
                        ]  
                      }
                    ) {
                    item {
                        name
                    }
                 }
              }
            ";
            var result = _authoringApi.ExecuteQuery(query);

            return Ok(result);
        }

        return Ok("The Title field was not updated");
    }
}

Now, with all of this in place, when you make a change to a Page item in Sitecore, it will call this webhook. And in the webhook, if the Title field is one of the fields that was changed, it will call the Authoring API to update the NavigationTitle field to match.

Endpoints

Something to make note of is the difference between endpoints between 10.3 and 10.4. The actual authoring endpoint, to which you submit your requests, didn’t change between versions. It is:

https://<your-cm-here>/sitecore/api/authoring/graphql/v1

However, the URL for the UI or the “Playground” changed:

10.3:

https://<your-cm-here>/sitecore/api/authoring/graphql/playground/

10.4:

https://<your-cm-here>/sitecore/api/authoring/graphql/ide/

In 10.4, the 10.3 url will redirect to graphql/ide, but I just wanted to point out that they are different.

Authoring API Queuing of Requests

Another thing to understand about these APIs is that, when executed, they are queued up on the CM for execution. If, for some reason, the CM goes down before it actually executes your request, that request is lost.

Management API

While my focus in this post has been mostly directed towards the Authoring API, the Management API is just the same, just with a different endpoint and different tasks or operations that you can perform. You can see the official documentation

Query examples for management operations

Conclusion

Hopefully, this post serves to stitch together information from various sources, with updated examples, to get you started with the Authoring API!

A complete repo, containing all the code from this post, can be found in this repository on GitHub.

Happy Sitecore trails, my friend!

  One thought on “Sitecore Authoring API: A COMPLETE Example

  1. michaellwest's avatar
    michaellwest
    November 4, 2024 at 12:47 pm

    Thanks for sharing!

    Like

Leave a comment