Power Pages One Step Submit: Building the Record with Web API
- Sofia Ng
- Aug 26
- 3 min read
In the first post I showed how to take a clunky two-step process in Power Pages (submit first, upload later) and replace it with one action. Users get a new upload box with a file picker, remove buttons, and a single Submit button that creates the record, uploads files, and binds them together.
That was the front half. In this post we’ll look at how the record creation actually works under the hood.

The moving parts
There are three things we need when creating a record through the Web API in Power Pages:
Authentication – using the portal’s built-in anti-forgery token.
Payload – the fields from your form that become columns in Dataverse.
Lookups – linking to related tables with @odata.bind.
Tokens and fetch
The portal already gives us a token we can pass with each request. It’s either available through shell.getTokenDeferred() or in the hidden __RequestVerificationToken input. We wrap that in a helper so every fetch call includes the right headers.
async function getPortalToken() {
if (window.shell?.getTokenDeferred) return await shell.getTokenDeferred();
return document.querySelector("input[name='__RequestVerificationToken']")?.value;
}
async function apiFetch(url, opts={}) {
const t = await getPortalToken();
const h = new Headers(opts.headers || {});
h.set("__RequestVerificationToken", t);
h.set("OData-Version", "4.0");
h.set("OData-MaxVersion", "4.0");
return fetch(url, { ...opts, headers:h, credentials:"same-origin" });
}Building the payload
Now we need to collect values from the form. In this example we’re working with the orders table, so we map the form’s input IDs into a JSON payload.
function buildParentPayload(){
const name = document.getElementById("order_name")?.value?.trim();
const email = document.getElementById("order_contactemail")?.value?.trim();
const desc = document.getElementById("order_description")?.value?.trim();
return {
"order_name": name || ("Web submit " + new Date().toISOString()),
"order_contactemail": email || null,
"order_description": desc || null
};
}That object goes straight into the body of our Web API POST.
Handling lookups
What if your parent record needs to reference another table — say customers or contracts? That’s where @odata.bind comes in.
function buildLookupBinds(){
const customerId = document.getElementById("order_customerid")?.value;
const binds = {};
if (customerId) {
binds["order_customerid@odata.bind"] = `/customers(${customerId})`;
}
return binds;
}When we create the parent, we merge the scalars and the binds.
async function createParent(){
const payload = { ...buildParentPayload(), ...buildLookupBinds() };
const r = await apiFetch(`/_api/orders`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
const id = (r.headers.get("entityid")||"").replace(/[{}]/g,"");
if (!id) throw new Error("No entityid returned");
return id;
}
A fallback for resilience
Lookups can be fussy — the attribute name must be cased exactly right, and your portal user must have the correct Append/AppendTo permissions. A safe pattern is:
Try create with the binds.
If it fails, retry without them and log a warning.
This avoids blocking the user if one lookup isn’t available.
What we’ve got so far
At this point, our custom Submit button:
Gathers the form fields.
Posts them to Dataverse with the token.
Creates the parent record and returns the new ID.
That parent ID is what we’ll use in the next step when we start attaching files.
What’s next
In the third and final post we’ll dive into attachments at scale:
Creating child rows for each file.
PATCHing the binary into the File column.
Linking each child back to the parent.
Progress counters, concurrency limits, and error handling.
A few production guardrails like file allow-lists and friendly messages.
We help organisations simplify Power Platform. If you’re ready to save your users time and reduce friction, reach out to us to see how we can support your team.

Comments