Digylearn Page SDK — API Reference
The Digylearn Page SDK is a small vanilla-JavaScript client auto-injected into every published page. It lets your site read collections, submit records, and handle visitor login — all same-origin, no build step.
The SDK is exposed as window.PageSDK. All methods return a Promise that resolves with parsed JSON on a 2xx response and rejects on any non-2xx (the rejection value is the parsed JSON body, or an { error } object).
Availability across render modes.
window.PageSDKis injected on both sanitized-mode and code-mode pages. However, in sanitized mode (digy_update_page) all<script>tags are stripped on render, so you cannot call the SDK from JavaScript. To use the SDK from client-side code, author the page withdigy_write_page_code(code mode) — your JavaScript runs as-is behind a strict Content-Security-Policy whoseconnect-src 'self'permits same-origin SDK calls. See the build guide for render-mode details.
Configuration
On load the SDK reads window.PAGE_SDK_CONFIG (injected by the server) into PageSDK.config:
window.PAGE_SDK_CONFIG = { siteSlug: "my-portfolio" };
PageSDK.config = { siteSlug: "my-portfolio" };
All request URLs are built from the site slug: base = /api/public/sites/${siteSlug}. Requests are relative (same-origin) and always send X-Requested-With: XMLHttpRequest and Accept: application/json.
PageSDK.records.list(collectionSlug, [options])
Reads records from a collection. Anonymous visitors can read public collections; authenticated collections require a logged-in visitor; private collections are never readable via the public API.
const { collection, count, records } = await PageSDK.records.list("testimonials", {
filter: { approved: true }, // optional: exact-match field filters
limit: 20, // optional, max 200, default 50
});
// records: [{ id, data, created_at }, ...]
filter— an object of{ field: value }exact-match conditions.limit— capped at 200, defaults to 50. Records are newest-first.- Resolves:
{ collection, count, records: [{ id, data, created_at }] }.
PageSDK.records.create(collectionSlug, data)
Submits a new record. Requires the collection write rule to permit the caller (public = anyone, authenticated = logged-in visitor, private = never via public API). Respects the site's plan total-entries limit.
const { success, record } = await PageSDK.records.create("contact-submissions", {
name: "Ada",
email: "ada@example.com",
message: "Hello!",
});
// record: { id, data, created_at }
data— an object (or array) of fields to store.- Resolves (HTTP 201):
{ success: true, record: { id, data, created_at } }.
PageSDK.records.update(collectionSlug, recordId, data, [options])
Updates an existing record by id. Requires the collection write rule to permit the caller (same tiers as create). Supports two modes:
// Merge (default): shallow-merge the given fields over the existing data.
const { success, record } = await PageSDK.records.update("contact-submissions", 42, {
status: "read",
read_at: "2026-08-20",
});
// Replace: overwrite the whole data object with the given one.
await PageSDK.records.update("contact-submissions", 42,
{ status: "archived", note: "moved" }, { mode: "replace" });
// record: { id, data, created_at, updated_at }
data— object of fields to store. Inmergemode, keys present here overwrite existing keys and any keys not mentioned are preserved; inreplacemode the wholedataobject is overwritten.options.mode—"merge"(default) or"replace".- Resolves (HTTP 200):
{ success: true, record: { id, data, created_at, updated_at } }.updated_atadvances on every change.
PageSDK.records.delete(collectionSlug, recordId)
Deletes a record by id (hard delete — there is no trash/undo via the public API). Requires the collection write rule to permit the caller.
const { success, deleted } = await PageSDK.records.delete("contact-submissions", 42);
// deleted: { record_id, collection }
- Resolves (HTTP 200):
{ success: true, deleted: { record_id, collection } }. - Deleting an already-deleted id returns 404
not_found(not idempotent via the public API).
PageSDK.auth.me()
Returns the current visitor's login state (resolved from the httpOnly session cookie — the token itself is never readable by JS).
const { authenticated, end_user } = await PageSDK.auth.me();
if (authenticated) {
// end_user: { id, email, username, role, active }
}
- Resolves:
{ authenticated: false }when anonymous, or{ authenticated: true, end_user: { id, email, username, role } }when logged in. - The SDK refreshes its cached login flag by calling this on load.
PageSDK.auth.login(identifier, password)
Logs a visitor in. Only works when the site has visitor auth enabled (configure_site_auth). identifier is matched against the enabled auth methods (email and/or username). Passwords are verified server-side with Hash::check.
const { success, end_user } = await PageSDK.auth.login("ada@example.com", "secret");
// end_user: { id, email, username, role, active }
- Resolves:
{ success: true, end_user: { id, email, username, role, active } }. - Sets the
digy_visitorhttpOnly,SameSite=Laxsession cookie (30-day expiry). The browser never sees the token. The cookie uses path/, so the visitor session persists across every page on the same site subdomain without re-logging in. - Rejects (403) when auth is disabled, or when the account is deactivated (
code: "account_disabled"); rejects (401/422) on bad credentials.
PageSDK.auth.register(payload)
Registers a new visitor account from the published site. Only works when the site has self-registration enabled (configure_site_auth → registration.enabled). Which identifiers may be registered is set by the site's registration.methods (email and/or username).
// Approval mode (default): the account is created but stays inactive until an
// admin activates it. The response is pending and no session is set.
const { success, pending, end_user } = await PageSDK.auth.register({
email: "ada@example.com",
password: "secret123", // min 8 characters
});
// end_user: { id, email, username, role, active }
// Open mode: the account is active immediately and the visitor is logged in.
const { success, authenticated, end_user } = await PageSDK.auth.register({
email: "ada@example.com",
password: "secret123",
});
payload—{ email?, username?, password }. Send only the identifiers enabled for registration; at least one is required.- Resolves (approval):
{ success: true, pending: true, end_user: { id, email, username, role, active } }(HTTP 201,active: false, no cookie). - Resolves (open):
{ success: true, authenticated: true, end_user: { id, email, username, role, active } }(HTTP 200,active: true, session cookie set — same aslogin). - Rejects (403) when registration is disabled (
code: "registration_disabled"), the plan's end-users feature is off, or the site entries limit is reached (code: "permission_denied"). - Rejects (422) when an identifier is already taken (
code: "identifier_taken"), the password is too short, or an identifier is missing. - Self-registration can never grant the
adminrole; the default role is set by the site (registration.default_role, defaultmember).
PageSDK.auth.logout()
Ends the current visitor session (deletes the server session row and clears the cookie).
const { success } = await PageSDK.auth.logout(); // { success: true }
PageSDK.auth.isLoggedIn()
Synchronous cached flag, kept in sync by me(). Useful for quick UI checks without awaiting a request.
if (PageSDK.auth.isLoggedIn()) { /* show logged-in UI */ }
PageSDK.admin.listEndUsers()
Lists the site's end users. Requires the current visitor to be an active site admin (an end_user with role=admin for this site — never a platform user). Use this to build a custom admin dashboard on the published site to approve/activate members.
const { end_users } = await PageSDK.admin.listEndUsers();
// end_users: [{ id, email, username, role, active, created_at }, ...]
- Resolves:
{ end_users: [{ id, email, username, role, active, created_at }] }. - Rejects (403) when the visitor is not an admin for this site.
PageSDK.admin.updateEndUser(id, changes)
Updates an end user's access and/or role. Requires an active site admin. Disabling a user (active: false) revokes their sessions immediately.
// Approve/activate a self-registered member:
await PageSDK.admin.updateEndUser(42, { active: true });
// Promote to admin:
await PageSDK.admin.updateEndUser(42, { role: "admin" });
// Deactivate:
await PageSDK.admin.updateEndUser(42, { active: false });
changes—{ active?, role? }whereroleis"visitor" | "member" | "admin".- Resolves:
{ success: true, end_user: { id, email, username, role, active } }. - Guards: an admin cannot deactivate or demote themselves, and cannot demote the last remaining active admin (→ 422
validation_failed). - Rejects (403) when the visitor is not an admin for this site.
Page access levels
Every published page has an access level (write_page_code / update_page access arg, or the dashboard page editor):
| Level | Who can view the published page |
|---|---|
public |
Anyone (default) |
authenticated |
Any active logged-in end_user of the same site (visitor, member, or admin) |
admin |
Active end_user with role=admin for the site |
Access is enforced server-side on every page render (subdomain and custom domain). When access is denied:
- Anonymous visitors see a themed "Log in required" page (403).
- Logged-in visitors who lack permission (e.g. a member on an
adminpage) see a themed "Access denied" page (403).
The owner builds the actual sign-in / registration UX on a code-mode page (using
PageSDK.auth.login/register). The themed denied-pages only inform the visitor.
Access-controlled pages are never served from the full-page file cache: they are composed fresh per request and sent with Cache-Control: private, no-store and an ETag that varies by the visitor's access bucket (anon / auth / admin), so members-only HTML can never leak to anonymous visitors through a shared cache.
Access-rule behavior
Each collection has access_rules with read and write keys, each one of public | authenticated | private:
| Value | read | write |
|---|---|---|
public |
Anonymous visitors OK | Anyone can submit |
authenticated |
Only logged-in visitors of the same site | Only logged-in visitors of the same site |
private |
Never via public API (owner-only, MCP/dashboard) | Never via public API (owner-only, MCP/dashboard) |
Default (when a collection has no rules): { read: "public", write: "private" }. Rules are enforced server-side on every request, so they cannot be bypassed from the browser.
Error format
Non-2xx responses reject the returned Promise. The rejection value is the parsed JSON body when available, otherwise { error: "<status text>" }. The body always carries an error string (read this for a human message) plus stable machine-readable siblings:
success— alwaysfalseon errors.error— human-readable string (backward compatible; keep reading this).code— machine-readable error code (see table below).fields— present only on validation errors (code: "validation_failed"); an object of{ field: [messages] }.
| Status | code |
Meaning |
|---|---|---|
| 403 | permission_denied |
Access rule denied, feature disabled, or site plan limit reached |
| 403 | registration_disabled |
Self-registration (or visitor login) is not enabled for this site |
| 403 | account_disabled |
Login attempted with an account that an admin has deactivated |
| 404 | not_found |
Site not published, or collection/record slug or id not found |
| 419 | — | Mutating request (POST/PATCH/DELETE) missing the X-Requested-With header |
| 422 | validation_failed |
Request input invalid (fields lists the offending fields) |
| 422 | identifier_taken |
Registration: the email or username already exists on this site |
| 422 | too_large |
Record data payload exceeds the 256 KB limit |
| 429 | rate_limited |
Rate limit exceeded (login / register / record create/update/delete are throttled) |
| 401 | — | Invalid login credentials |
try {
await PageSDK.records.create("private-col", { a: 1 });
} catch (err) {
console.error(err.error); // e.g. "This collection does not allow public writes."
if (err.code === 'validation_failed' && err.fields) {
console.warn(err.fields); // { data: ["The data field is required."] }
}
}
Reserved page slugs
The following slugs are reserved by the platform and cannot be used as a page (or site) slug — they route to platform URLs (/api/…, /dashboard, etc.) and would collide with your content:
api admin dashboard assets storage login register logout mcp
If you want a page that conveys one of these words, pick an alternative such as sign-in (instead of login), media (instead of assets), control-panel (instead of admin), or data-api. Page creation rejects reserved slugs with a validation error.
Combining the SDK with custom JavaScript
On a code-mode page (digy_write_page_code) your own JavaScript runs alongside window.PageSDK. A common pattern is to keep UI state in memory and persist it via the SDK. For example, a simple order form that reads a menu collection and submits an order:
// 1. Load menu items from a public collection
const { records } = await PageSDK.records.list('menu', { limit: 50 });
// 2. Build cart state in memory
const cart = {};
function add(id) { cart[id] = (cart[id] || 0) + 1; render(); }
// 3. Submit the finished order to a public-write collection
async function checkout(customer) {
await PageSDK.records.create('orders', {
customer,
items: cart,
total: Object.entries(cart).reduce((s, [id, q]) => s + priceOf(id) * q, 0),
});
// Then hand off to WhatsApp via navigation (CSP-allowed):
window.location.href = 'https://wa.me/60123456789?text=' + encodeURIComponent(JSON.stringify(cart));
}
Both the SDK call (connect-src 'self') and the WhatsApp navigation succeed under the code-mode CSP. See the build guide for the full list of CSP constraints.
Security notes
- The session token lives only in an httpOnly cookie — JavaScript (and the SDK) cannot read it.
- Mutating endpoints require the
X-Requested-Withheader as a CSRF defense (the SDK sends it automatically; plain cross-site forms/images cannot). tenant_id/site_idare never trusted from the client — they are always derived from the request host and the slug.