Saves the filled form data as a new HTML document or a new version of an existing document at the specified path, using an infoRouter template document and XML content data to populate the template fields.
When the target path does not exist, a new document is created. When it already exists, a new version of that document is created using the template.
This API is the save step in the three-API form workflow:
UseFormTemplate / EditFilledForm → user fills in the form → SaveFilledForm
/srv.asmx/SaveFilledForm
/srv.asmx/SaveFilledForm?authenticationTicket=...&path=...&templatePath=...&xmlContent=.../srv.asmx/SaveFilledForm (form data)http://tempuri.org/SaveFilledForm| Parameter | Type | Required | Description |
|---|---|---|---|
authenticationTicket |
string | Yes | Authentication ticket obtained from AuthenticateUser. |
path |
string | Yes | Full infoRouter path for the document to create or update (e.g. /MyLibrary/Reports/Summary.htm). If the path does not yet exist, a new document is created. If it already exists, a new version is created. |
templatePath |
string | Yes | Full infoRouter path of the existing HTML template document to use (e.g. /Templates/ReportTemplate.htm), or ~D<id> short form (e.g. ~D42). Pass "999" to use a blank/empty template. |
xmlContent |
string | Yes | XML-formatted data used to populate the template fields. Must use the <FORMDATA> structure described below. Pass an empty string if the template has no fields. |
<response success="true" error="" DocumentID="42" DocumentName="Summary.htm" />
| Attribute | Description |
|---|---|
success |
true if the document was created successfully. |
DocumentID |
The integer ID of the newly created document. |
DocumentName |
The name of the created file (.htm extension is added automatically if the path does not end with .html or .htm). |
<root success="true" />
<response success="false" error="Error message" />
path).path. If the document is already checked out by another user, the call fails.The xmlContent parameter must use the <FORMDATA> structure. Each template field is represented as a <Prompt> element whose Name attribute is the field name and whose text content is the field value:
<FORMDATA>
<Prompt Name="title">Q1 2026</Prompt>
<Prompt Name="author">Jane Smith</Prompt>
<Prompt Name="textcontent">Body text of the document...</Prompt>
</FORMDATA>
Pass an empty string for xmlContent only when the template has no user-defined fields.
When calling this API after presenting the form to the user via UseFormTemplate, the rendered HTML contains a hidden input named InfoRouter_Fields that lists all template fields. Its value is a comma-separated array of 4-token groups:
'IR_title','CHAR','N','N','IR_author','CHAR','Y','N','IR_duedate','DATE','Y','N'
Each group of 4 tokens describes one field:
| Token (0-based position in group) | Meaning |
|---|---|
0 — 'IR_{fieldname}' |
Field name with 'IR_ prefix and trailing '. Strip those to get the HTML input name. |
1 — 'CHAR' | 'DATE' | 'NUMBER' | 'BOOLEAN' |
Data type of the field. |
2 — 'Y' | 'N' |
Whether the field is required. |
3 — 'N' |
Reserved, always 'N'. |
To extract the field name from token 0: remove the leading 'IR_ (4 characters) and the trailing '.
JavaScript example — parse InfoRouter_Fields and build xmlContent:
function parseInfoRouterFields(iframeDoc, form) {
const fieldsInput = iframeDoc.getElementById('InfoRouter_Fields');
if (!fieldsInput || !fieldsInput.value.trim()) return [];
const tokens = fieldsInput.value.split(',');
const fields = [];
for (let i = 0; i + 3 < tokens.length; i += 4) {
const raw = tokens[i].trim(); // e.g. "'IR_title'"
const name = raw.slice(4, raw.length - 1); // strip 'IR_ prefix and trailing '
const dataType = tokens[i + 1].trim().replace(/'/g, ''); // CHAR | DATE | NUMBER | BOOLEAN
const required = tokens[i + 2].trim() === "'Y'";
const el = form.elements[name];
const value = el ? el.value : '';
fields.push({ name, value, dataType, required });
}
return fields;
}
function buildXmlContent(fields) {
if (!fields.length) return '';
return '<FORMDATA>' +
fields.map(f => `<Prompt Name="${f.name}">${escapeXml(f.value)}</Prompt>`).join('') +
'</FORMDATA>';
}
GET /srv.asmx/SaveFilledForm
?authenticationTicket=3f2504e0-4f89-11d3-9a0c-0305e82c3301
&path=/MyLibrary/Reports/Q1Summary.htm
&templatePath=/Templates/QuarterlyReport.htm
&xmlContent=%3CFORMDATA%3E%3CPrompt+Name%3D%22title%22%3EQ1+2026%3C%2FPrompt%3E%3C%2FFORMDATA%3E
HTTP/1.1
POST /srv.asmx/SaveFilledForm HTTP/1.1
Content-Type: application/x-www-form-urlencoded
authenticationTicket=3f2504e0-4f89-11d3-9a0c-0305e82c3301
&path=/MyLibrary/Reports/Q1Summary.htm
&templatePath=/Templates/QuarterlyReport.htm
&xmlContent=<FORMDATA><Prompt Name="title">Q1 2026</Prompt><Prompt Name="author">Jane Smith</Prompt></FORMDATA>
POST /srv.asmx/SaveFilledForm HTTP/1.1
Content-Type: application/x-www-form-urlencoded
authenticationTicket=3f2504e0-4f89-11d3-9a0c-0305e82c3301
&path=/MyLibrary/Reports/Q1Summary.htm
&templatePath=999
&xmlContent=<FORMDATA><Prompt Name="title">Q1 2026 (revised)</Prompt></FORMDATA>
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"
xmlns:tns="http://tempuri.org/">
<soap:Body>
<tns:SaveFilledForm>
<tns:authenticationTicket>3f2504e0-4f89-11d3-9a0c-0305e82c3301</tns:authenticationTicket>
<tns:path>/MyLibrary/Reports/Q1Summary.htm</tns:path>
<tns:templatePath>/Templates/QuarterlyReport.htm</tns:templatePath>
<tns:xmlContent><FORMDATA><Prompt Name="title">Q1 2026</Prompt></FORMDATA></tns:xmlContent>
</tns:SaveFilledForm>
</soap:Body>
</soap:Envelope>
.html / .htm). If the document name in path does not end with .html or .htm, the extension .htm is automatically appended to the created file name.path) must already exist. It is not created automatically.path is checked out by a different user, the call fails with an error.templatePath = "999" to generate the document content from a blank template rather than an existing template file.<response> when creating a new document, <root> when creating a new version.| Error | Description |
|---|---|
[900] Authentication failed |
Invalid or missing authentication ticket. |
[901] Session expired or Invalid ticket |
The ticket has expired or does not exist. |
Folder not found |
The destination folder (parent of path) does not exist or is not accessible. |
Document not found |
The templatePath does not refer to an existing document. |
This document has been checked out by another user. |
The document at path is checked out by a different user; a new version cannot be created. |
Production-ready patterns derived from the reference demo at IRWebCore/wwwRoot/form-template-demo.html. This API is always called after UseFormTemplate (create flow) or EditFilledForm (update flow) has collected the user’s form data via an iframe.
function escapeXml(s) {
return String(s ?? '')
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
.replace(/"/g, '"').replace(/'/g, ''');
}
// Parse the InfoRouter_Fields hidden input inside the rendered iframe form.
// Returns an array of { name, value, dataType, required }.
// Token format: 'IR_field1','CHAR','N','N','IR_field2','DATE','Y','N',...
function parseInfoRouterFields(iframeDoc, form) {
const input = iframeDoc.getElementById('InfoRouter_Fields');
if (!input || !input.value.trim()) return [];
const tokens = input.value.split(',');
const fields = [];
for (let i = 0; i + 3 < tokens.length; i += 4) {
const raw = tokens[i].trim();
if (raw.length < 5) continue;
const name = raw.slice(4, raw.length - 1); // strip 'IR_ prefix + trailing '
const dataType = tokens[i + 1].trim().replace(/'/g, ''); // CHAR | DATE | NUMBER | BOOLEAN
const required = tokens[i + 2].trim() === "'Y'";
const el = form.elements[name];
const value = el ? el.value : '';
fields.push({ name, value, dataType, required });
}
return fields;
}
// Build the <FORMDATA> XML payload for the xmlContent parameter
function buildXmlContent(fields) {
if (!fields || fields.length === 0) return '';
return '<FORMDATA>' +
fields.map(f => `<Prompt Name="${f.name}">${escapeXml(f.value)}</Prompt>`).join('') +
'</FORMDATA>';
}
Called after UseFormTemplate intercepts the iframe submit:
async function createDocument({ apiBase, ticket, targetFolderPath, docName, templatePath, fields }) {
const path = targetFolderPath.replace(/\/+$/, '') + '/' + docName;
const xmlContent = buildXmlContent(fields);
const body = new URLSearchParams({
authenticationTicket: ticket,
path, // New path → creates document
templatePath, // Full path, ~D<id>, or '999' for blank template
xmlContent,
});
const res = await fetch(`${apiBase}/SaveFilledForm`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: body.toString(),
});
const doc = new DOMParser().parseFromString(await res.text(), 'text/xml');
const root = doc.querySelector('response') ?? doc.querySelector('root');
if (root?.getAttribute('success') !== 'true') {
throw new Error(root?.getAttribute('error') ?? 'Create failed');
}
return {
documentId: root.getAttribute('DocumentID'), // numeric string
documentName: root.getAttribute('DocumentName'), // filename, .htm extension added automatically
path,
};
}
Called after EditFilledForm intercepts the iframe submit:
async function updateDocument({ apiBase, ticket, existingDocPath, templatePath, fields }) {
const xmlContent = buildXmlContent(fields);
const body = new URLSearchParams({
authenticationTicket: ticket,
path: existingDocPath, // Existing path → creates new version, checks document back in
templatePath, // ~D{id} read from the InfoRouter_TemplateID hidden field
xmlContent,
});
const res = await fetch(`${apiBase}/SaveFilledForm`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: body.toString(),
});
const doc = new DOMParser().parseFromString(await res.text(), 'text/xml');
const root = doc.querySelector('root') ?? doc.querySelector('response');
if (root?.getAttribute('success') !== 'true') {
throw new Error(root?.getAttribute('error') ?? 'Update failed');
}
// No DocumentID or DocumentName on success — new version response is <root success="true"/>
}
| Scenario | Root element | DocumentID |
DocumentName |
|---|---|---|---|
| New document created | <response> |
Present — numeric string | Present — filename with .htm |
| New version created | <root> |
Absent | Absent |
Always query with a fallback (doc.querySelector('response') ?? doc.querySelector('root')) because the element name differs between the two modes.
── Create new document ──────────────────────────────────────────────────
1. AuthenticateUser → ticket
2. UseFormTemplate(targetFolderPath, templatePath, submitUrl='')
→ renderedHtml (HTML wrapped in CDATA — use r.el.textContent)
3. Render in <iframe srcDoc={renderedHtml} onLoad={handleLoad} sandbox="allow-scripts allow-forms allow-same-origin">
4. In onLoad: inject InfoRouter_Ticket; intercept form submit with e.preventDefault()
5. On submit: parseInfoRouterFields(iframeDoc, form) → fields[]
buildXmlContent(fields) → xmlContent
6. SaveFilledForm(path=newPath, templatePath=templatePath, xmlContent)
→ <response success="true" DocumentID="42" DocumentName="file.htm"/>
── Update existing document ─────────────────────────────────────────────
1. AuthenticateUser → ticket
2. EditFilledForm(documentPath, submitUrl='')
→ renderedHtml (pre-filled HTML; document checked out)
3. Render in <iframe srcDoc={renderedHtml} onLoad={handleLoad} sandbox="allow-scripts allow-forms allow-same-origin">
4. In onLoad: inject InfoRouter_Ticket; intercept form submit with e.preventDefault()
read InfoRouter_TemplateID → templatePath = '~D{id}'
5. On submit: parseInfoRouterFields(iframeDoc, form) → fields[]
buildXmlContent(fields) → xmlContent
6. SaveFilledForm(path=existingDocPath, templatePath='~D{id}', xmlContent)
→ <root success="true"/> (document checked back in; no DocumentID returned)