ReportingCloud

Tracked Changes

Topic

Contracts and offers often come back from reviewers with every edit marked: text that was inserted or deleted while change tracking was turned on, each change with its author and time. The review endpoints of the Web API read these tracked changes from a document and accept or reject them one at a time. Your application can apply the decisions a reviewer made in a web page, or a rule such as "accept the changes of the legal department", and keep a document without markup.

How Tracked Changes Work

A tracked change stays marked in the document until it is accepted, which keeps the change, or rejected, which undoes it: a rejected insertion is removed and a rejected deletion is restored. Two endpoints work with them:

EndpointRequestResponse
POST /v1/processing/review/trackedchangesThe document as a Base64 string in a JSON stringAn array of TrackedChange objects
POST /v1/processing/review/removetrackedchangeThe same body, and the query parameters id and acceptA ModifiedDocument object with the changed document and removed

Both endpoints work as follows:

  • Formats. The document can be RTF, DOC, DOCX or TX, the TX Text Control format. It is sent with each request; the endpoints don't work with the template storage. A document in another format isn't refused: trackedchanges returns an empty array for it, as for a document without tracked changes.
  • One change per request. removetrackedchange accepts the change with the number id when accept is true and rejects it when accept is false. It returns the whole document with this one change removed.
  • The result is TX. The returned document is a Base64 string in the TX format. Send it as it is with the next request, or convert it to DOCX or PDF with POST /v1/document/convert, as Converting Documents shows.
  • Quota. removetrackedchange counts against the document quota, one document per successful request, also when it removes nothing. trackedchanges counts against no quota.

Every request carries an Authorization header, as Authentication describes.

List the Tracked Changes of a Document

The following commands send contract.docx to trackedchanges:

# Wrap the Base64 string of the document in a JSON string
base64 -w 0 contract.docx | jq -Rs . > contract.json

curl -X POST "https://api.reporting.cloud/v1/processing/review/trackedchanges" \
  -H "Authorization: ReportingCloud-APIKey $REPORTINGCLOUD_API_KEY" \
  -H "Content-Type: application/json" \
  --data-binary @contract.json

The response has one object per tracked change. The first object of the reference's sample:

[
  {
    "changeKind": 4096,
    "changeTime": "2019-01-01T00:00:00Z",
    "defaultHighlightColor": "60, 255, 0, 0",
    "highlightColor": "60, 87, 174, 168",
    "highlightMode": 3,
    "length": 13,
    "start": 3,
    "id": 0,
    "text": "trackedchange",
    "userName": "ReportingCloudUser"
  }
]

A TrackedChange object holds:

PropertyContent
idThe number of the change, which removetrackedchange takes as id. The ids start at 0, in the order of the array
changeKind4096 for inserted text, 8192 for deleted text. Format changes are not listed, and moved text is listed as a deletion and an insertion
textThe inserted or deleted text
start, lengthThe one-based index of the first character of the change, counted in the main text, the header or the footer that holds it, and the length of the change. A paragraph end counts as one character
userNameThe name of the author, empty if the document names none
changeTimeThe time of the change, in UTC
highlightModeThe highlight mode of the change: 1 (Never), 2 (Activated) or 3 (Always)
highlightColor, defaultHighlightColorThe highlight color of this change and the default highlight color of tracked changes, as strings of four comma-separated numbers such as "60, 255, 0, 0"

Accept or Reject a Change

The following command accepts the change with the number 1 and saves the response:

curl -X POST "https://api.reporting.cloud/v1/processing/review/removetrackedchange?id=1&accept=true" \
  -H "Authorization: ReportingCloud-APIKey $REPORTINGCLOUD_API_KEY" \
  -H "Content-Type: application/json" \
  --data-binary @contract.json \
  --output modified.json

# true when the change was removed
jq .removed modified.json

# The document without the change, in the TX format
jq -r .document modified.json | base64 -d > contract.tx

The response is a JSON object such as {"document":"CAcBAA4AAAAAAAAAAAAAAAIAAABsAQAAAQ...==","removed":true}:

  • document is the modified document as a Base64 string in the TX format.
  • removed says whether the change was removed. It is false, and document is null, if the document has no change with the given id. Set accept=false to reject the change instead.

To decide on the next change, send the modified document: jq .document modified.json > contract.json writes it as the JSON string the next request takes.

Warning

Change Ids Shift. Every request removes one change and returns a new document, in which the ids of the changes that followed the removed one are lower by 1, so read the remaining changes and their numbers from the modified document with trackedchanges before you remove the next one.

Accept the Changes of One Author With .NET

The NuGet package TXTextControl.ReportingCloud has both endpoints in rc.Processing.Review. The following console application accepts the changes of one author, rejects all others and saves the result as DOCX:

using TXTextControl.ReportingCloud;

var rc = new ReportingCloud(Environment.GetEnvironmentVariable("REPORTINGCLOUD_API_KEY"));

string trustedAuthor = "Alice Johnson";
byte[] document = File.ReadAllBytes("contract.docx");

List<TrackedChange> changes = rc.Processing.Review.GetTrackedChanges(document);

while (changes.Count > 0)
{
    TrackedChange change = changes[0];
    bool accept = change.UserName == trustedAuthor;

    // Accept or reject the change; the result is the document in the TX format
    TrackedChangeModifiedDocument result = rc.Processing.Review.RemoveTrackedChange(document, change.Id, accept);

    if (!result.Removed)
    {
        Console.WriteLine($"Change {change.Id} was not removed.");
        break;
    }

    Console.WriteLine($"{(accept ? "Accepted" : "Rejected")} {change.ChangeKind} by {change.UserName}: {change.Text}");

    // Read the remaining changes from the modified document
    document = result.Document;
    changes = rc.Processing.Review.GetTrackedChanges(document);
}

// Convert the result from the TX format to DOCX
byte[] docx = rc.ConvertDocument(document, ReturnFormat.DOCX);
File.WriteAllBytes("contract-reviewed.docx", docx);

The code works as follows:

  • GetTrackedChanges takes the document as a byte array and returns a list of TrackedChange objects with Id, ChangeKind, Text, Start, Length, UserName, ChangeTime and the highlight properties. ChangeKind is an enumeration with the values InsertedText and DeletedText.
  • RemoveTrackedChange takes the document, the number of the change and true to accept or false to reject. It returns a TrackedChangeModifiedDocument whose Document holds the modified document as a byte array and whose Removed says whether the change was removed. When no change is removed, though, the response's document is null and the method throws an ArgumentNullException, so a removal that fails ends the loop with an exception, not at the check of Removed.
  • The loop removes the first change of each list, then lists the changes of the modified document again, so that every number it sends belongs to the document it sends.
  • ConvertDocument converts the TX result with POST /v1/document/convert, a request that counts against the document quota of your plan. Each change itself takes two requests, one to removetrackedchange, which counts against the document quota as well, and one to trackedchanges, which doesn't.

The PHP SDK has the methods getTrackedChanges($documentFilename) and removeTrackedChange($documentFilename, $id, $accept). They read the document from a file, and removeTrackedChange returns the modified document decoded, under the key document: write it to a .tx file to pass it on. PHP SDK 4.0.5 can't read a time stamp with fractions of a second, such as 2019-08-06T12:34:13.9188379Z: getTrackedChanges returns such a change with a change_time of 0.

Next Steps