Skip to main content

General Structure

Language-agnostic API

Buildocs UI Engine communicates with your backend over plain HTTP — there is no SDK or library requirement on the server side. You can implement the backend in any language or framework: Node.js, Python, PHP, Java, Go, Ruby, or anything else that can receive a POST request and return JSON.

This guide explains the implementation in .NET (ASP.NET Core) because that is the language used in the reference example. The endpoint contracts, request shapes, and response structures are identical regardless of language.

A Buildocs backend integration follows a single-controller pattern. All form traffic enters through one controller that exposes a fixed set of HTTP endpoints. The code below is based on the Workforce Management example on GitHub.


Required endpoints

Every Buildocs backend must expose these ten endpoints:

EndpointMethodPurpose
/tokenPOSTIssue a JWT that authenticates all subsequent form requests
/loadformPOSTLoad the form definition and initial field data
/runeventPOSTHandle all form events (button clicks, field changes, table interactions)
/uploadfilesPOSTReceive and store file uploads from file-uploader widgets
/deletefilePOSTRemove a file previously linked to a record
/downloadfilePOSTStream a stored file to the browser
/getpresignedurlPOSTGenerate a time-limited URL for direct file access
/getlinkedfilebychunksPOSTStream large files (PDFs, videos) using HTTP range requests
/getlinkedfilemetaPOSTReturn metadata (name, size, MIME type) for a stored file
/getlinkedfilePOSTReturn the base64-encoded content of a stored file

Controller skeleton

[ApiController]
[Route("api/[controller]")]
public class DemoController : ControllerBase
{
private readonly IFileStorageProvider _fileStorage;
private readonly IConfiguration _configuration;
private readonly IAmazonDynamoDB _dynamoClient;
private readonly IFormLocalizer _localizer;
private readonly string _formRecordsTable;

private readonly RequestContext _requestContext = new();

public DemoController(IFileStorageProvider fileStorage, IConfiguration configuration,
IAmazonDynamoDB dynamoClient, IFormLocalizer localizer)
{
_fileStorage = fileStorage;
_configuration = configuration;
_dynamoClient = dynamoClient;
_localizer = localizer;
_formRecordsTable = configuration["AWS:DynamoDB:FormRecordsTable"] ?? "FormRecords";
}

private PluginHelper CreatePluginHelper(PluginContextEventRequestDto request)
=> new PluginHelper(request, _requestContext, _dynamoClient, _formRecordsTable, _localizer);

[HttpPost("token")] public IActionResult GenerateToken() { ... }
[HttpPost("loadform")] public async Task<IActionResult> LoadForm([FromBody] PluginContextEventRequestDto request) { ... }
[HttpPost("runevent")] public async Task<IActionResult> RunEvent([FromBody] PluginContextEventRequestDto request) { ... }
[HttpPost("uploadfiles")] public async Task<IActionResult> UploadFiles() { ... }
[HttpPost("deletefile")] public async Task<IActionResult> DeleteFile([FromBody] FileRequestDto request) { ... }
[HttpPost("downloadfile")] public async Task<IActionResult> DownloadFile([FromBody] FileRequestDto request) { ... }
[HttpPost("getpresignedurl")] public async Task<IActionResult> GetPresignedUrl([FromBody] LinkedFileRequestDto request) { ... }
[HttpPost("getlinkedfilebychunks")] public async Task<IActionResult> GetLinkedFileByChunks([FromBody] LinkedFileRequestDto request) { ... }
[HttpPost("getlinkedfilemeta")] public async Task<IActionResult> GetLinkedFileMeta([FromBody] LinkedFileRequestDto request) { ... }
[HttpPost("getlinkedfile")] public async Task<IActionResult> GetLinkedFile([FromBody] LinkedFileRequestDto request) { ... }
}

Request DTOs

Each endpoint uses its own request DTO depending on its purpose:

EndpointDTO
/runeventPluginContextEventRequestDto
/loadformPluginContextEventRequestDto
/deletefile, /downloadfileFileRequestDto
/getpresignedurl, /getlinkedfile, /getlinkedfilemeta, /getlinkedfilebychunksLinkedFileRequestDto
/uploadfilesmultipart form data (no JSON body)
/tokenno body

The full definition of each DTO is documented under its endpoint in Required API Endpoints.


/runevent request

Every time something happens on screen the SDK POSTs a PluginContextEventRequestDto to /runevent. The most important fields your handler reads are:

FieldTypeDescription
WidgetNamestringName of the widget that triggered the event (as set in the Form Builder)
WidgetEventstringThe event type — e.g. onClick, onChange, onTableLoadData
WidgetValuestring?The new value for onChange events; the search term for autocomplete
FormDataDictionary<string, object>Current values of all form fields at the moment the event fired
GuidstringRecord identifier — "new" for a new record, a UUID for an existing one
FormCodestringThe form that is currently open
PluginCodestringUsed to resolve the correct handler class

Example payload for a button click:

{
"pluginCode": "INTRA",
"formCode": "EMPLTBL",
"guid": "a1b2c3d4-...",
"widgetName": "savebtn",
"widgetEvent": "onClick",
"widgetValue": null,
"formData": {
"firstName": "Jane",
"lastName": "Smith",
"department": "3",
"status": "1"
}
}

Example payload for a field change:

{
"pluginCode": "INTRA",
"formCode": "EMPLTBL",
"guid": "new",
"widgetName": "department",
"widgetEvent": "onChange",
"widgetValue": "3",
"formData": {
"firstName": "Jane",
"lastName": "Smith",
"department": "3"
}
}

Example payload for a table load:

{
"pluginCode": "INTRA",
"formCode": "EMPLTBL",
"guid": "a1b2c3d4-...",
"widgetName": "employeetbl",
"widgetEvent": "onTableLoadData",
"widgetValue": null,
"formData": {}
}

/runevent response structure

/runevent is the central endpoint of every Buildocs integration. It is called automatically by the SDK every time anything happens on the screen — a button is clicked, a field value changes, a select refreshes, a table loads its rows, a row action fires, and so on. Your handler inspects the incoming widgetName and widgetEvent to decide what to do, then returns a response that tells the SDK what to update in the UI.

The response is built using HandlerResponse, a thin wrapper around Dictionary<string, object?>. Set only the fields relevant to the current event — the SDK ignores keys that are null.

var response = new HandlerResponse();
response.Set(HandlerResponse.FormData, formData);
response.Set(HandlerResponse.FieldAllowedValues, allowedValues);
response.Set(HandlerResponse.FECommand, commands);
return Ok(response.Get());
ConstantJSON keyDescription
FormDataformDataUpdated field values to render in the form
FieldAllowedValuesfieldAllowedValuesOptions for select/dropdown widgets
WidgetsStatewidgetsStateVisibility and read-only states for widgets
FECommandfeCommandFrontend commands to execute (show message, open modal, close form, etc.)
FormDefinitiondefinitionUpdated form schema (rarely needed at runtime)
WidgetDatawidgetDataRow data for table widgets
WidgetRelatedDatawidgetRelatedDataAdditional widget context (e.g. selected row IDs)
LayoutlayoutRecord layout override
UiTranslationsuiTranslationsTranslated UI strings

Next step

See Required API Endpoints for the complete C# implementation of each endpoint — request shapes, response fields, and full method bodies.