General Structure
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:
| Endpoint | Method | Purpose |
|---|---|---|
/token | POST | Issue a JWT that authenticates all subsequent form requests |
/loadform | POST | Load the form definition and initial field data |
/runevent | POST | Handle all form events (button clicks, field changes, table interactions) |
/uploadfiles | POST | Receive and store file uploads from file-uploader widgets |
/deletefile | POST | Remove a file previously linked to a record |
/downloadfile | POST | Stream a stored file to the browser |
/getpresignedurl | POST | Generate a time-limited URL for direct file access |
/getlinkedfilebychunks | POST | Stream large files (PDFs, videos) using HTTP range requests |
/getlinkedfilemeta | POST | Return metadata (name, size, MIME type) for a stored file |
/getlinkedfile | POST | Return 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:
| Endpoint | DTO |
|---|---|
/runevent | PluginContextEventRequestDto |
/loadform | PluginContextEventRequestDto |
/deletefile, /downloadfile | FileRequestDto |
/getpresignedurl, /getlinkedfile, /getlinkedfilemeta, /getlinkedfilebychunks | LinkedFileRequestDto |
/uploadfiles | multipart form data (no JSON body) |
/token | no 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:
| Field | Type | Description |
|---|---|---|
WidgetName | string | Name of the widget that triggered the event (as set in the Form Builder) |
WidgetEvent | string | The event type — e.g. onClick, onChange, onTableLoadData |
WidgetValue | string? | The new value for onChange events; the search term for autocomplete |
FormData | Dictionary<string, object> | Current values of all form fields at the moment the event fired |
Guid | string | Record identifier — "new" for a new record, a UUID for an existing one |
FormCode | string | The form that is currently open |
PluginCode | string | Used 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());
| Constant | JSON key | Description |
|---|---|---|
FormData | formData | Updated field values to render in the form |
FieldAllowedValues | fieldAllowedValues | Options for select/dropdown widgets |
WidgetsState | widgetsState | Visibility and read-only states for widgets |
FECommand | feCommand | Frontend commands to execute (show message, open modal, close form, etc.) |
FormDefinition | definition | Updated form schema (rarely needed at runtime) |
WidgetData | widgetData | Row data for table widgets |
WidgetRelatedData | widgetRelatedData | Additional widget context (e.g. selected row IDs) |
Layout | layout | Record layout override |
UiTranslations | uiTranslations | Translated UI strings |
Next step
See Required API Endpoints for the complete C# implementation of each endpoint — request shapes, response fields, and full method bodies.