You already have an HTML form on a Quickbase code page that adds new records, maybe one that people outside your app can reach through Everyone on the Internet access. Now you want a second version of that form that opens an existing record, shows its current values, and lets someone change them and save.
That question came up in this Quickbase Community thread. The short answer: put the record's Record ID# in the link to the code page, load the record with API_GetRecordInfo, and save it with API_EditRecord instead of API_AddRecord. This guide walks through the whole thing with a complete page you can copy, plus the field-type details and security settings that trip people up.
I built and tested everything below in a sandbox app with a small Service Requests table (Requester Name, Description, Status, Due Date, Urgent, Estimated Hours) and records for people like Sarah Lee, Mike Chen and Priya Patel.
What we're building
- A button on each record that opens your code page with
&rid=and that record's Record ID# on the end of the URL. - The code page reads
ridfrom its own address. - It calls API_GetRecordInfo for that record and drops each field's value into the matching input.
- The person edits the form.
- Save calls API_EditRecord with the same rid and shows a status message: saved, failed, or "someone else changed this record first".
No server, no extra tools. The page talks to the Quickbase XML API directly, the same way your add form already does.
Step 1: Add the code page and find its page ID
If you already have the add form, make a copy of it. In your app go to Settings > Pages, create a new code page, and paste in the full page from Step 3. Save it and open it once. The address bar shows something like:
https://yourrealm.quickbase.com/db/APP_DBID?a=dbpage&pageID=2
That pageID number is what you'll use in the link. In my sandbox it was 2. Use whatever yours is.
Step 2: Build the link that passes the rid
On the table you want to edit, add a Formula - URL field (call it something like Edit on Form) with this formula, changing 2 to your page ID:
URLRoot() & "db/" & AppID() & "?a=dbpage&pageID=2&rid=" & [Record ID#]
URLRoot() gives you https://yourrealm.quickbase.com/, and AppID() is the app's dbid. Code pages live at the app level, so this is the app dbid, not the table's. On record 1 the link comes out as:
https://yourrealm.quickbase.com/db/APP_DBID?a=dbpage&pageID=2&rid=1
In the field properties, set it to display as a button and pick whether it opens in the same window or a new one. Add the field to your form or report, and you have an Edit button on every record.
If people outside your app get to the page some other way, for example from a link in a notification or on your website, it's the same URL. Just make sure it ends with &rid= and the record number.
Step 3: The complete page
Here is the whole code page. It's plain HTML and JavaScript with no libraries. Change the two settings at the top of the script, TABLE_DBID and APP_TOKEN, and the field IDs in the form to match your table.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Edit Service Request</title>
<style>
body { font-family: Arial, sans-serif; max-width: 560px; margin: 32px auto; padding: 0 16px; color: #212529; }
label { display: block; margin-top: 14px; font-weight: bold; }
input[type=text], input[type=number], input[type=date], select, textarea {
width: 100%; padding: 8px; margin-top: 4px; box-sizing: border-box; font-size: 15px; }
textarea { height: 90px; }
.check { font-weight: normal; }
button { margin-top: 20px; padding: 10px 22px; font-size: 15px; cursor: pointer; }
#status { margin-top: 16px; padding: 10px; border-radius: 6px; display: none; }
#status.ok { display: block; background: #d1e7dd; color: #0f5132; }
#status.err { display: block; background: #f8d7da; color: #842029; }
#status.info { display: block; background: #e7f1ff; color: #084298; }
</style>
</head>
<body>
<h2>Edit Service Request <span id="ridLabel"></span></h2>
<form id="editForm">
<label for="fid_6">Requester Name</label>
<input type="text" id="fid_6" readonly>
<label for="fid_7">Description</label>
<textarea id="fid_7"></textarea>
<label for="fid_8">Status</label>
<select id="fid_8">
<option value="New">New</option>
<option value="In Progress">In Progress</option>
<option value="Done">Done</option>
</select>
<label for="fid_9">Due Date</label>
<input type="date" id="fid_9">
<label class="check"><input type="checkbox" id="fid_10"> Urgent</label>
<label for="fid_11">Estimated Hours</label>
<input type="number" step="0.25" id="fid_11">
<button type="submit" id="saveBtn" disabled>Save changes</button>
</form>
<div id="status"></div>
<script>
// ---- settings: change these for your app ----
const TABLE_DBID = 'YOUR_TABLE_DBID'; // the table's dbid, not the app's
const APP_TOKEN = ''; // leave blank if the app doesn't require app tokens
// ----------------------------------------------
const form = document.getElementById('editForm');
const saveBtn = document.getElementById('saveBtn');
const statusBox = document.getElementById('status');
const rid = new URLSearchParams(location.search).get('rid');
let updateId = ''; // Quickbase's version stamp for the record we loaded
function showStatus(text, kind) {
statusBox.textContent = text;
statusBox.className = kind;
}
// Every input whose id looks like fid_<number>
// (read-only ones are filled in but never sent back)
function fieldInputs() {
return Array.from(form.querySelectorAll('[id^="fid_"]')).filter(el => !el.readOnly);
}
// Quickbase dates come back as milliseconds since 1970 (UTC midnight).
// A date input wants YYYY-MM-DD.
function msToDateInput(ms) {
if (!ms) return '';
return new Date(Number(ms)).toISOString().slice(0, 10);
}
// Send a call to the XML API and hand back the parsed response.
// Throws if Quickbase reports an error (errcode other than 0).
async function qbCall(params) {
if (APP_TOKEN) params.set('apptoken', APP_TOKEN);
const res = await fetch('/db/' + TABLE_DBID, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: params.toString(),
credentials: 'same-origin'
});
// Quickbase returns XML even when the HTTP status is 400 or 404
const doc = new DOMParser().parseFromString(await res.text(), 'text/xml');
const code = doc.querySelector('errcode');
if (!code) throw new Error('Unexpected response from Quickbase (HTTP ' + res.status + ')');
if (code.textContent !== '0') {
const detail = doc.querySelector('errdetail') || doc.querySelector('errtext');
const err = new Error(detail ? detail.textContent : 'Quickbase error ' + code.textContent);
err.code = code.textContent;
throw err;
}
return doc;
}
async function loadRecord() {
if (!rid || !/^\d+$/.test(rid)) {
showStatus('No record number in the link. Open this page from a record.', 'err');
return;
}
document.getElementById('ridLabel').textContent = '#' + rid;
showStatus('Loading...', 'info');
try {
const doc = await qbCall(new URLSearchParams({ a: 'API_GetRecordInfo', rid: rid }));
updateId = doc.querySelector('qdbapi > update_id').textContent;
doc.querySelectorAll('field').forEach(f => {
const el = document.getElementById('fid_' + f.querySelector('fid').textContent);
if (!el) return; // field isn't on this form
const value = f.querySelector('value').textContent;
if (el.tagName === 'SELECT' && ![...el.options].some(o => o.value === value)) {
el.add(new Option(value, value)); // keep values the list doesn't have yet
}
if (el.type === 'checkbox') el.checked = (value === '1');
else if (el.type === 'date') el.value = msToDateInput(value);
else el.value = value;
});
saveBtn.disabled = false;
statusBox.className = '';
} catch (e) {
showStatus('Could not load record ' + rid + ': ' + e.message, 'err');
}
}
form.addEventListener('submit', async (event) => {
event.preventDefault();
saveBtn.disabled = true;
showStatus('Saving...', 'info');
const params = new URLSearchParams({ a: 'API_EditRecord', rid: rid, update_id: updateId });
fieldInputs().forEach(el => {
const fid = el.id.slice(4);
params.set('_fid_' + fid, el.type === 'checkbox' ? (el.checked ? '1' : '0') : el.value);
});
try {
const doc = await qbCall(params);
updateId = doc.querySelector('update_id').textContent;
const changed = doc.querySelector('num_fields_changed').textContent;
showStatus('Saved. ' + changed + ' field(s) changed.', 'ok');
} catch (e) {
if (e.code === '60') {
showStatus('Someone else changed this record after you opened it. Reload the page to see their changes, then edit again.', 'err');
} else {
showStatus('Save failed: ' + e.message, 'err');
}
} finally {
saveBtn.disabled = false;
}
});
loadRecord();
</script>
</body>
</html>
A few things worth pointing out:
- TABLE_DBID is the table, not the app. Record calls like
API_GetRecordInfoandAPI_EditRecordgo to/db/<table dbid>. Open the table in Quickbase and you'll see its dbid in the URL. Pointing these calls at the app dbid is the most common reason they fail. - The URL is relative (
/db/...). The code page is served from your realm, so the call goes to the same site and needs no CORS setup. - The calls use POST with form-encoded parameters.
URLSearchParamshandles the encoding, so ampersands, quotes and long descriptions arrive in one piece. A GET with the same parameters works too, and that's fine for loading. For saving, POST keeps long text out of the URL. - Quickbase still sends XML when something goes wrong. A missing record comes back as HTTP 404, and a bad app token or bad value as HTTP 400, but the body is still a normal
<qdbapi>response witherrcodeanderrtext. That's whyqbCallreads the XML and checkserrcodeinstead of trusting the HTTP status. update_idstops overwrites.API_GetRecordInforeturns the record'supdate_id. Sending it back withAPI_EditRecordtells Quickbase to save only if nobody has changed the record since it was loaded. If someone has, you get error 60 ("Update conflict detected") and the page asks the person to reload. Leaveupdate_idout if you'd rather have the last save win.
How the field IDs map to the inputs
The whole trick is a naming rule: every input's id is fid_ followed by the Quickbase field ID. Requester Name is field 6, so its input is id="fid_6". Status is field 8, so it's id="fid_8".
To find field IDs, open the table's Fields settings and turn on the field ID column, or open a field's properties and look at the end of the URL.
Here's what API_GetRecordInfo actually returned for Sarah Lee's request in my sandbox (trimmed):
<qdbapi>
<action>API_GetRecordInfo</action>
<errcode>0</errcode>
<errtext>No error</errtext>
<rid>1</rid>
<num_fields>11</num_fields>
<update_id>1793509694233</update_id>
<field>
<fid>6</fid>
<name>Requester Name</name>
<type>Text</type>
<value>Sarah Lee</value>
</field>
<field>
<fid>8</fid>
<name>Status</name>
<type>Text</type>
<value>New</value>
</field>
<field>
<fid>9</fid>
<name>Due Date</name>
<type>Date</type>
<value>1793923200000</value>
<printable>11-06-2026</printable>
</field>
<field>
<fid>10</fid>
<name>Urgent</name>
<type>Checkbox</type>
<value>1</value>
<printable>yes</printable>
</field>
<field>
<fid>11</fid>
<name>Estimated Hours</name>
<type>Numeric</type>
<value>2.5</value>
</field>
...
</qdbapi>
Every field comes back as a <field> with a <fid> and a <value>. The loader loops over them, looks for an element with id="fid_" + fid, and fills it in. Fields that aren't on your form, like Date Created or Record Owner, have no matching input, so they're skipped.
Saving goes the other way. The page collects every fid_ input and sends it as _fid_6=..., _fid_8=... and so on. That's the same _fid_N parameter format API_AddRecord uses, so if your add form already builds those, you can reuse that code. You only swap the action and add rid.
When Mike Chen changes Sarah's request from New to In Progress and moves the due date, the save request carries these parameters:
a=API_EditRecord
rid=1
update_id=1793509694233
_fid_7=Replace the badge reader at the side entrance
_fid_8=In Progress
_fid_9=2026-11-09
_fid_10=1
_fid_11=3
Quickbase replies with num_fields_changed (3 here, since only Status, Due Date and Estimated Hours actually changed) and a new update_id, which the page keeps for the next save.
Handling different field types
The raw <value> isn't always what an HTML input wants. Here's what I saw in testing and how the page handles each type.
Text and multi-line text. The value is plain text and goes straight into el.value. Ampersands and other XML characters come back decoded (& turns into &) because textContent handles that.
Multiple-choice text (Status). It comes back as <type>Text</type> with the chosen value. Use a <select> whose option values match your choice list exactly. If a record holds a value your dropdown doesn't list, setting el.value quietly leaves the dropdown blank, and the next save would blank the field. The page avoids that by adding the missing option on the fly. I tested this with a record whose status was "On Hold", which wasn't on the form, and it loaded and saved without losing the value.
On saving, Quickbase checks the choice list. With Allow users to create new choices turned off, a value outside the list fails with error 2: "You must provide a valid choice for the field named Status." With it turned on, the API quietly adds your value as a new choice. That's worth knowing if your page lets people type into that field.
Date. The <value> is milliseconds since 1970 at UTC midnight (Nov 6, 2026 is 1793923200000). There's also a <printable> in the app's date format (11-06-2026). A date input needs YYYY-MM-DD, so the page converts with new Date(ms).toISOString().slice(0, 10). Use the UTC conversion. If you build the date from local getDate() / getMonth(), anyone west of UTC sees the day before. For saving, API_EditRecord accepted both 2026-11-09 (what a date input produces) and the app-format 11-09-2026 in my tests, so the date input's value can be sent as is.
Checkbox. The value is 1 or 0 (printable yes / no). Set el.checked = value === '1' and send back 1 or 0. Note that an unchecked box still has to be sent as 0. If you leave it out, the field doesn't change.
Numeric. A plain number like 2.5, which works directly in <input type="number">.
Date / Time (like Date Modified). Also milliseconds. If you want it editable, convert it for a datetime-local input and mind time zones. Usually it's easier to show it read-only using the <printable> text.
User fields. The <value> is the user's email address and <printable> is their name. Remember that if you're thinking about who can see this response (more on that below).
Formula, lookup, summary and Record ID# fields can't be written. If you want to show one, give its input the readonly attribute. The page fills it in but leaves it out of the save. In the example, Requester Name is read-only so a person editing the request can't change who asked for it.
File attachments need a different approach (base64 content in an XML body) and aren't covered here.
To clear a field, empty its input. An empty _fid_N value clears the field in Quickbase.
Do you need an app token?
It depends on one app setting. In App settings > App properties, look at Require Application Tokens.
- If it's on, every XML API call from the page has to include a valid app token. Without one, Quickbase answers
errcode 24, Invalid Application Token, and nothing loads. My sandbox app has this turned on, and that's exactly what came back. Create a token under the app's Manage Application Tokens settings and paste it intoAPP_TOKEN. The page adds it to every call. - If it's off, leave
APP_TOKENas an empty string and the page won't send one.
Your existing add form already works, so look at how it calls API_AddRecord. If it sends an apptoken, use the same one here.
Signed-in users don't need anything extra. The browser sends their Quickbase session with the request, so the calls run as that person with their role's permissions. Everyone on the Internet visitors have no session, so their calls run as the Everyone on the Internet role.
Security: what Everyone on the Internet can really do
This is the part to slow down on. The rule to remember: a code page is not a security boundary. The role is.
Anyone can call the API the page calls. Everything in the page, including the table dbid and the app token, is visible with View Source. Anyone who can open the page can make the same API_GetRecordInfo or API_EditRecord call themselves, with any rid they like. Record IDs are just 1, 2, 3, so guessing them is easy. If the Everyone on the Internet role can view a record, assume anyone can read it.
An app token is not a password. It tells Quickbase the call comes from code that's allowed to use this app, which helps against random scripts hitting your app. It doesn't identify a person, it doesn't limit which records can be read, and it's sitting in the page source. Treat it as public.
What actually protects you is the role's permissions. For the Everyone on the Internet role on this table:
- View: Custom rule, not All. Limit view to the records that really need public editing, for example Status is not Done, or a checkbox like Open for Public Edit that your team ticks when they send a link out. Anything outside the rule returns no data, even with a valid rid.
- Modify: Custom rule too, with the same or a tighter condition. Turn off Add if this page only edits, and turn off Delete.
- Field-level permissions.
API_GetRecordInforeturns every field the role can view, not just the ones on your form. Record Owner and Last Modified By return email addresses. Set fields the public doesn't need to No access for this role, and set fields they can see but shouldn't change to View only. - Don't count on hiding the rid. Swapping the rid for a random key and using
API_DoQuerymakes links harder to guess, but anyone who can view the table can still run their own query. It's a nice extra, never the main control.
If the records hold anything you wouldn't put on a public web page, don't use Everyone on the Internet for editing. Make people sign in and give their role a rule like Record Owner is the current user. The same page works unchanged for signed-in users.
Testing it
- Open the button on a real record. The fields should fill in within a second and the Save button should turn on. If the form stays empty with no message, open the browser's developer tools (F12) and check the Console and Network tabs.
- Change one field and save. You should see Saved. 1 field(s) changed. Open the record in Quickbase and confirm the change, and that Date Modified moved.
- Test the conflict check. Open the page, change the same record in Quickbase in another tab, then save on the page. You should get the "someone else changed this record" message, not a silent overwrite.
- Try a bad rid. Change
rid=in the address bar to a number that doesn't exist. The page should show "Could not load record 999: Sorry, there is no record in this application table with the record identifier (rid) of 999." Then removeridcompletely and check that it says the link has no record number. - Test as the public. Open the page in a private window where you aren't signed in to Quickbase. This is the real Everyone on the Internet test. Then try a rid that the role's view rule should hide and make sure nothing loads.
Troubleshooting
Error 24, Invalid Application Token. The app requires app tokens and the page either didn't send one or sent one that isn't assigned to this app. Check APP_TOKEN and the app's token list.
Error 30, No such record. The rid doesn't exist in that table. Check that TABLE_DBID is the table you meant, not another table or the app.
Errors 3, 22, 34 or 83, for public visitors only. These are the permission family: Insufficient permissions, Sign-in required, You cannot change the value of this field and Access denied. The Everyone on the Internet role lacks view or modify permission on the table or field, the record falls outside its custom rule, or Everyone on the Internet isn't turned on for the app. Signed-in testing won't show any of this, so always do the private-window test.
Error 60, Update conflict detected. Someone saved the record after the page loaded it. Reload and try again, or remove update_id from the save if last-save-wins is fine for you.
Error 2, Invalid input. A value doesn't fit the field: a choice not on the list, text in a numeric field, or a date Quickbase can't read. The errdetail tells you which field.
The dropdown or checkbox doesn't show the saved value. Check the input id matches the field ID exactly (fid_8, not fid8 or fid_08), and that dropdown option values match the choice text exactly, including capitals and spaces.
Dates are a day off. You're converting the millisecond value with local time. Use toISOString() as the page does.
Nothing happens at all. If rid is missing from the URL, check that the Formula - URL field ends with & [Record ID#] and that whatever opens the page keeps the full query string.
For the full list of parameters and responses, see Quickbase's API_GetRecordInfo and API_EditRecord reference pages.