# Custom Signing Endpoints

> Receive documents signed in the Document Viewer in a DS Server plugin endpoint: SignatureData, RedirectUrlAfterSignature, access token checks and PDF storage.

- **Product:** DS Server
- **Section:** Documentation
- **Kind:** Topic
- **Path:** DS Server > Plugins > Custom Signing Endpoints
- **4397 words**
- **Web URL:** https://docs.textcontrol.com/ds-server/plugins/signing/
- **LLMs URL:** https://docs.textcontrol.com/ds-server/plugins/signing/llms.txt
- **LLMs-Full URL:** https://docs.textcontrol.com/ds-server/plugins/signing/llms-full.txt

---

When a user signs a document in the Document Viewer, the signed document can be sent on to an endpoint of your choice, where your own code stores it, applies a certificate or starts a workflow. A plugin can provide that endpoint inside DS Server. Since DS Server 5.0, the request to the endpoint carries the current OAuth access token, so the plugin can accept signed documents only from clients with a valid token.

What follows covers how the signed document reaches the endpoint, a plugin that receives and stores it, and the Document Viewer settings that point to it. The plugin project is set up as in [Creating a Plugin](https://docs.textcontrol.com/ds-server/plugins/creating/llms-full.txt).

### How a Custom Signing Endpoint Works

The Document Viewer shows a signature bar when its signature settings enable it. The user signs the signature boxes of the document, and the signed document then takes this route:

```text
Browser                     DS Server                                    Plugin in DS Server

Document Viewer  --signed-->  Document Viewer controller
                              |
                              +--HTTP POST, SignatureData as JSON,------>  POST /plugin/signatures/sign
                                 with the current access token (5.0)       stores or processes the document
```

- **The redirect URL.** The property `RedirectUrlAfterSignature` of the signature settings names the endpoint. The Document Viewer controller sends an HTTP POST request to this URL with a `SignatureData` object as its body.
- **The access token.** Since version 5.0, the Document Viewer controller adds the access token of the current viewer to this request. An endpoint that uses the access token validation of DS Server then accepts the request, while other callers without a valid token are rejected.
- **The address.** DS Server sends the request itself, not the browser. Use an address under which DS Server reaches its own plugin, such as its public address or `http://localhost:5000` on the server.

#### The Signature Data

The JSON body holds the result of the signing process. The properties an endpoint typically reads, named as in the `SignatureData` class that Text Control's signature plugin sample deserializes:

| Property | Content |
|---|---|
| `SignedDocument.Document` | The signed document, encoded as Base64 |
| `SignedDocument.SignerName`, `SignedDocument.SignerInitials` | The name and initials of the signer |
| `SignedDocument.NumPages` | The number of pages of the signed document |
| `UniqueId` | A unique string rendered on the document, which can serve as the envelope ID when you store it |
| `DocumentName` | The name of the document |
| `SignatureBoxes` | The signature boxes, each with its `Name` and whether it is `Signed` |
| `FormFields` | The completed form fields, each with its `Name` and `Value` |

The model binding of ASP.NET Core matches the JSON names to the properties of a class without regard to case.

### Create the Signing Plugin

The plugin below receives the signed document, converts it to PDF with the [`IDocumentProcessingService`](https://docs.textcontrol.com/ds-server/plugins/services/#idocumentprocessingservice) of DS Server and saves the file in a folder of the plugin. It consists of four files.

*SigningSettings.cs* holds the folder for the signed documents:

```csharp
namespace SigningPlugin;

public class SigningSettings
{
    public string OutputFolder { get; set; } = "";
}
```

*SignatureRequest.cs* maps the part of the signature data the endpoint reads:

```csharp
namespace SigningPlugin;

// The parts of the signature data that the endpoint reads
public class SignatureRequest
{
    public string? UniqueId { get; set; }
    public SignedDocumentData? SignedDocument { get; set; }
}

public class SignedDocumentData
{
    public string? Document { get; set; }
    public string? SignerName { get; set; }
}
```

*SigningPlugin.cs* reads the folder from the configuration and creates it when the plugin starts:

```csharp
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using TXTextControl.DocumentServices.Plugin.Abstractions;

namespace SigningPlugin;

public class SigningPlugin : IPlugin
{
    public string Name => "SigningPlugin";
    public string Description => "Receives signed documents from the Document Viewer at /plugin/signatures/sign.";
    public string Version => "1.0.0";

    public void ConfigureServices(IServiceCollection services, PluginContext context)
    {
        // A relative folder is resolved against the plugin's own folder
        string pluginFolder = Path.GetDirectoryName(typeof(SigningPlugin).Assembly.Location)!;
        string outputFolder = context.Configuration["SigningPlugin:OutputFolder"] ?? "SignedDocuments";

        services.AddSingleton(new SigningSettings
        {
            OutputFolder = Path.Combine(pluginFolder, outputFolder)
        });
    }

    public void OnStart(IServiceProvider services, PluginContext context)
    {
        SigningSettings settings = services.GetRequiredService<SigningSettings>();
        Directory.CreateDirectory(settings.OutputFolder);

        services.GetService<ILogger<SigningPlugin>>()?
            .LogInformation("SigningPlugin stores signed documents in {Folder}", settings.OutputFolder);
    }
}
```

*SignaturesController.cs* is the endpoint:

```csharp
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using TXTextControl.DocumentServices.DocumentProcessing.Abstractions;
using TXTextControl.DocumentServices.DocumentProcessing.Models;
using TXTextControl.DocumentServices.Plugin.Abstractions;

namespace SigningPlugin;

[ApiController]
[Route("plugin/signatures")]
[Authorize(AuthenticationSchemes = PluginContext.AuthenticationScheme)]
public class SignaturesController : ControllerBase
{
    private readonly IDocumentProcessingService m_processing;
    private readonly SigningSettings m_settings;
    private readonly ILogger<SignaturesController> m_logger;

    public SignaturesController(IDocumentProcessingService processing,
        SigningSettings settings, ILogger<SignaturesController> logger)
    {
        m_processing = processing;
        m_settings = settings;
        m_logger = logger;
    }

    [HttpPost("sign")]
    public async Task<IActionResult> Sign([FromBody] SignatureRequest request)
    {
        if (string.IsNullOrEmpty(request.SignedDocument?.Document))
        {
            return BadRequest("The request contains no signed document.");
        }

        // The signed document arrives encoded as Base64
        byte[] document = Convert.FromBase64String(request.SignedDocument.Document);

        byte[] pdf = await m_processing.ConvertAsync(
            document, ReturnFormat.PDF, HttpContext.RequestAborted);

        string fileName = $"{Guid.NewGuid():N}.pdf";
        await System.IO.File.WriteAllBytesAsync(
            Path.Combine(m_settings.OutputFolder, fileName), pdf, HttpContext.RequestAborted);

        m_logger.LogInformation("Stored the document signed by {Signer} as {File}",
            request.SignedDocument.SignerName, fileName);

        return Ok(fileName);
    }
}
```

The code works as follows:

- `SigningPlugin` implements only the members it needs: `ConfigureServices` registers the settings and `OnStart` creates the folder. The folder is *SignedDocuments* in the plugin's subfolder unless the key `SigningPlugin:OutputFolder` of *appsettings.json* names another one.
- `[Authorize(AuthenticationSchemes = PluginContext.AuthenticationScheme)]` makes DS Server check the access token of every request to the controller, as described below.
- `Sign` decodes `signedDocument.document` and converts it with `ConvertAsync` into `ReturnFormat.PDF`. The document arrives in the internal format of TX Text Control, which `ConvertAsync` accepts like any other format.
- The file gets a new GUID as its name, so no value from the request becomes part of a path. The endpoint returns the file name.

Build the project, copy *SigningPlugin.dll* into *Plugins/SigningPlugin/* of DS Server and restart DS Server, as described in [Build and Deploy the Plugin](https://docs.textcontrol.com/ds-server/plugins/creating/#build-and-deploy-the-plugin).

### Protect the Endpoint

The endpoints of a plugin are not covered by the OAuth protection of the built-in services: without an `Authorize` attribute, DS Server passes every request to them, with or without a token. A signing endpoint receives documents, so protect it with the authentication scheme of DS Server, `TXTextControl`, as the controller above does.

With OAuth enabled in the admin portal, DS Server then answers a request without a valid access token with 401 Unauthorized and writes the reason to its log:

```text
[Information] TXTextControl was not authenticated. Failure message: OAuth authorization: No authorization header, access token query parameter or access token cookie found.
[Information] AuthenticationScheme: TXTextControl was challenged.
```

The request of the Document Viewer controller carries the access token of the viewer since version 5.0, so it passes the check. With OAuth disabled, the scheme accepts every request, as the built-in services do. [Enable OAuth](https://docs.textcontrol.com/ds-server/administration/oauth/llms-full.txt) describes the switch, and [Creating a Plugin](https://docs.textcontrol.com/ds-server/plugins/creating/#protect-the-endpoint-with-oauth) shows the results of the check in a table.

### Point the Document Viewer to the Endpoint

Set `RedirectUrlAfterSignature` in the signature settings of the Document Viewer. In an ASP.NET Core view with the package `TXTextControl.DocumentServices.DocumentViewer`:

```cshtml
@using TXTextControl.DocumentServices.DocumentViewer

@await Html.TXTextControl().DocumentViewer(settings => {
    settings.BasePath = "https://ds.example.com";
    settings.OAuthSettings.ClientId = "<client ID>";
    settings.OAuthSettings.ClientSecret = "<client secret>";
    settings.OAuthSettings.RedirectUri = "https://your.app/textcontrol/documentviewer/auth/callback";
    settings.DocumentData = Model.DocumentData;
    settings.SignatureSettings = new SignatureSettings {
        ShowSignatureBar = true,
        OwnerName = "Paul Paulsen",
        SignerName = "Tim Typer",
        SignerInitials = "TT",
        RedirectUrlAfterSignature = "https://ds.example.com/plugin/signatures/sign",
        SignatureBoxes = new SignatureBox[] {
            new SignatureBox("txsign") { SigningRequired = true }
        }
    };
}).RenderAsync()
```

- `BasePath` and `OAuthSettings` connect the viewer to DS Server, as described in [Client-Side Libraries: ASP.NET Core](https://docs.textcontrol.com/ds-server/client-libraries/aspnet-core/llms-full.txt).
- `DocumentData` is the Base64-encoded document with a signature box. `SignatureBox("txsign")` names the frame of the document that serves as the box.
- `RedirectUrlAfterSignature` is the address of the plugin endpoint on DS Server.

The Angular, React and JavaScript viewers take the same setting as `redirectUrlAfterSignature` in their [`SignatureSettings`](https://docs.textcontrol.com/ds-server/reference/javascript-document-viewer/signaturesettings/llms-full.txt) object: see [Angular](https://docs.textcontrol.com/ds-server/client-libraries/angular/llms-full.txt) and [React](https://docs.textcontrol.com/ds-server/client-libraries/react/llms-full.txt).

### Test the Endpoint Without the Viewer

You can call the endpoint with cURL to check the plugin and the token validation before you connect the viewer. Request an access token with the client credentials of a security profile, as described in [OAuth Grant Types](https://docs.textcontrol.com/ds-server/administration/oauth-grant-types/llms-full.txt), and send a body with a Base64-encoded document:

```bash
curl -X POST http://localhost:5000/plugin/signatures/sign \
  -H "Authorization: Bearer <access token>" \
  -H "Content-Type: application/json" \
  -d '{ "uniqueId": "abc", "signedDocument": { "document": "<Base64-encoded document>", "signerName": "Tim Typer" } }'
```

The endpoint answers with the name of the stored file, and the log shows the entry of the controller:

```text
[Information] Stored the document signed by Tim Typer as 94a1d9c9da63402b87cccda1388f909c.pdf
```

Without the `Authorization` header, the endpoint answers 401 Unauthorized when OAuth is enabled.

### Apply a Certificate

The plugin above stores the document as the Document Viewer signed it. To add a digital signature with your organization's certificate, Text Control's open source [TXTextControl.DSServer.SignaturePlugin](https://github.com/TextControl/TXTextControl.DSServer.SignaturePlugin) loads the signed document with TX Text Control .NET Server and signs each signature box with a *.pfx* certificate whose path and password it reads from *appsettings.json*. It also serves pages under `/signature-plugin`, linked from the **Plugins** page of the admin portal, that list the signed documents. The sample was written for DS Server 4.1: it targets .NET 8 and references version 4.1.0 of the plugin package and `TXTextControl.TextControl.Core.SDK` 33.0.3, and its signing controller has the `Authorize` attribute commented out. The blog article [Extending DS Server with Custom Digital Signature APIs](https://www.textcontrol.com/blog/2025/10/09/extending-ds-server-with-custom-digital-signature-apis/) walks through its code.

### Next Steps

- [Plugin Services](https://docs.textcontrol.com/ds-server/plugins/services/llms-full.txt): the other operations of `IDocumentProcessingService`, and the Document Editor sessions.
- [Creating Security Profiles](https://docs.textcontrol.com/ds-server/administration/security-profiles/llms-full.txt): create the client credentials the viewer and your tests use.
- [Document Viewer in ASP.NET Core](https://docs.textcontrol.com/ds-server/getting-started/document-viewer/aspnet-core/llms-full.txt): a first application with the Document Viewer.

---

## Nearby

- **Previous:** [Plugin Services](https://docs.textcontrol.com/ds-server/plugins/services/llms-full.txt)
- **Next:** [Troubleshooting](https://docs.textcontrol.com/ds-server/troubleshooting/llms-full.txt)
