Everything you need to create, upload, answer, and verify quizzes on the Personalized Pool platform.
curl -X POST https://api.personalizedpool.com/quiz/{quizId} \
-H "Authorization: Bearer <ADMIN_JWT>" \
-H "Content-Type: application/json" \
--data-binary @schema.jsoncurl -X PATCH https://api.personalizedpool.com/quiz/{quizId} \
-H "Authorization: Bearer <ADMIN_JWT>" \
-H "Content-Type: application/json" \
--data-binary @schema.jsoncurl -H "Authorization: Bearer <USER_JWT>" \
https://api.personalizedpool.com/quiz/{quizId}curl -X GET https://api.personalizedpool.com/quizList \
-H "Authorization: Bearer <USER_JWT>"curl -X POST https://api.personalizedpool.com/answers/{quizId} \
-H "Authorization: Bearer <USER_JWT>" \
-H "Content-Type: application/json" \
--data-binary @answers.jsoncurl -X GET https://api.personalizedpool.com/answers/{quizId} \
-H "Authorization: Bearer <USER_JWT>"🔒 Only admins can upload or delete quizzes.
curl -X POST http://api.personalizedpool.com/register \
-H "Content-Type: application/json" \
-d '{"email": "user@example.com", "password": "yourPassword123"}'Response:
{ "token": "JWT_TOKEN" }curl -X POST http://api.personalizedpool.com/sendEmailConfirmationCode \
-H "Authorization: Bearer JWT_TOKEN"curl -X POST http://api.personalizedpool.com/confirmEmail \
-H "Authorization: Bearer JWT_TOKEN" \
-H "Content-Type: text/plain" \
-d "123456"curl -X GET http://api.personalizedpool.com/me \
-H "Authorization: Bearer JWT_TOKEN"Expected:
{
"email": "user@example.com",
"isEmailVerified": true
}Changing your password requires receiving and confirming a code. A password should be at least 8 characters long.
curl -X POST http://api.personalizedpool.com/sendEmailConfirmationCodeForPassword \
-H "Content-Type: text/plain" \
-d "user@example.com"curl -X POST http://api.personalizedpool.com/checkEmailConfirmationCodeForPassword/user@example.com \
-H "Content-Type: text/plain" \
-d "123456"Response on success:
{
"message": "Code is valid. You can proceed with password change."
}If the code is invalid, it is deleted and the response is
400 Bad Request.
curl -X POST http://api.personalizedpool.com/changePassword \
-H "Content-Type: application/json" \
-d '{
"code": "123456",
"newPassword": "NewPass123!",
"email": "user@example.com"
}'curl -X POST http://api.personalizedpool.com/login \
-H "Content-Type: application/json" \
-d '{"email": "user@example.com", "password": "NewPass123!"}'curl -X POST http://api.personalizedpool.com/login \
-H "Content-Type: application/json" \
-d '{"email": "user@example.com", "password": "yourPassword123"}'curl -X POST http://api.personalizedpool.com/logout \
-H "Authorization: Bearer JWT_TOKEN"curl -X GET http://api.personalizedpool.com/me \
-H "Authorization: Bearer JWT_TOKEN"Using social login is an alternative way to verify user’s email. After any social login is done successfully the email is marked verified
curl -X POST http://api.personalizedpool.com/login/google \
-H "Content-Type: text/plain" \
-d "SOCIAL_ACCESS_TOKEN"(Replace google with facebook,
apple, or microsoft)
https://api.personalizedpool.com/__microsoft
https://api.personalizedpool.com/__google
https://api.personalizedpool.com/__facebook
https://api.personalizedpool.com/__apple
A quiz schema is a JSON object where each key is a question ID and each value defines how that question behaves.
The quiz always starts at the question with ID
"root".
Each question must define:
type — how the user interacts with itquestion — the actual prompt textnext, nextTrue,
nextFalse, etc.You can use {someId} in the question text to insert
prior answers.
| Type | Description | Navigation Keys |
|---|---|---|
text |
Short text input with optional validation regex and
maxLength. |
next |
volume |
Pool size input in "45000l" or "12000g"
format. |
next |
season |
Seasonal range using "M/W-M/W" format
(e.g. "5/1-9/1" → May week 1 to Sept week 1). |
next |
bool |
Yes/No toggle. Branches to two different questions. | nextTrue, nextFalse |
radio |
Choose one option. Each option may route to a different question. | Per-option next |
radioWithOther |
Like radio, but includes "Other" with free
text and a separate nextOther route. |
Per-option next, nextOther |
checkbox |
Choose multiple answers. All answers follow the same path. | next |
checkboxWithSkip |
Like checkbox, but allows skipping the question
(null value). |
next, nextSkip |
optionalEnd |
Offers the user to either continue with optional questions or end. | nextContinue, nextSkip |
email |
Final step to enter the user’s email address. | next (usually null) |
error |
Displays a stopping message (e.g. unsupported configuration) and ends the quiz. | next: null |
Answers is a key-value JSON, where key is the question ID inside quiz
schema. Each answer in the quiz must match the type of the
question.
| Type | Example Answer | Notes |
|---|---|---|
text |
"Alice" |
Can be validated via regex |
volume |
"45000l" or "12000g" |
Must include unit suffix (l or g) |
season |
"5/1-9/1" |
Format: "M/W-M/W" where M = month, W = week in
month |
bool |
true or false |
Branches conditionally |
radio |
"Chlorine" |
Option must exist in schema |
radioWithOther |
{ "value": "Other", "other": "Ionization" } |
other is Required if value is "Other" |
checkbox |
["Option A", "Option B"] |
Select multiple |
checkboxWithSkip |
null or ["Option A"] |
Use null to skip question |
optionalEnd |
"continue" or "skip" |
Branches to next or exits |
email |
"user@example.com" |
Required to receive results |
error |
(no answer — this ends the quiz) |
{
"root": "Alice",
"poolVolume": "45000l",
"seasonLength": "My pool is seasonal",
"seasonalPool": "1/1-6/1",
"isYourPoolGetClosed": true,
"primarySanitizer": "Chlorine",
"chlorineType": ["Liquid chlorine"],
"typeOfPool": "Vinyl Liner",
"inGroundOrAboveGround": "In-ground",
"automaticCover": true,
"hasHotSystem": false,
"genericQuizPassed": "continue",
"hasSecondarySanitizer": "Yes",
"secondarySanitizer": ["UV", "Ozone"],
"typeOfFilter": "Sand/crushed glass",
"filterBrand": { "value": "Other", "other": "BlueWaterPro" },
"typeOfPump": "Variable-speed",
"pumpBrand": { "value": "Pentair", "other": null },
"circulateHours": "16 or more hours",
"automation": "Yes",
"automationType": ["Full automation (phone app)"],
"automationBrand": { "value": "Intellicenter® (Pentair)", "other": null },
"dogsInPool": false,
"email": "alice@example.com"
}| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /quiz/{quizId} |
Confirmed email | Fetch quiz schema |
| POST | /quiz/{quizId} |
Admin only | Upload new quiz |
| PATCH | /quiz/{quizId} |
Admin only | Update existing quiz |
| DELETE | /quiz/{quizId} |
Admin only | Delete quiz |
| GET | /quizList |
Confirmed email | Get available quiz list |
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /answers/{quizId} |
Confirmed email | Fetch user’s answers and the suggested kit |
| POST | /answers/{quizId} |
Confirmed email | Submit new answers |
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /register |
None | Register a new user |
| POST | /login |
None | Login with email and password |
| POST | /logout |
JWT required | Logout and invalidate current session |
| GET | /me |
JWT required | Get current user info |
| POST | /sendEmailConfirmationCode |
JWT required | Send email confirmation code |
| POST | /confirmEmail |
JWT required | Submit confirmation code |
| POST | /sendEmailConfirmationCodeForPassword |
None | Send password reset code |
| POST | /checkEmailConfirmationCodeForPassword/{email} |
None | ✅ Check if reset code is valid |
| POST | /changePassword |
None | Change password with email and code |
| POST | /login/{provider} |
None | Social login (google, facebook, etc.) |
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /kit/matrix |
Admin | Export current kit matrix as CSV |
| POST | /kit/matrix |
Admin | Upload new kit matrix CSV |
Example:
curl -X GET https://api.personalizedpool.com/kit/matrix \
-H "Authorization: Bearer <ADMIN_JWT>"Response: CSV file containing the kit matrix.
curl -X POST https://api.personalizedpool.com/kit/matrix \
-H "Authorization: Bearer <ADMIN_JWT>" \
-H "Content-Type: text/plain" \
--data-binary @matrix.csvYou can submit a quiz without a user account by authenticating with a short-lived admin key.
Get an IV Request a fresh IV (valid for
10 seconds) and pass it back in the
x-admin-iv header.
curl -X GET https://api.personalizedpool.com/ivResponse (example):
{ "iv": "16-byte-iv-string" }Create the admin key (client side)
Take the current Unix timestamp in milliseconds
(e.g., Date.now()).
Encrypt that timestamp (as a UTF-8 string) with AES/CBC/PKCS5Padding using:
/ivBase64-encode the ciphertext.
Send it as x-admin-key and send the raw IV from step
1 as x-admin-iv.
POST /answers/{quizId} endpoint. When
x-admin-iv and x-admin-key are present and
valid, the submission is treated as anonymous and
accepted without a user JWT.⏱️ Important: The IV from
/ivexpires after 10 seconds. Generate and send the key immediately before the request.
x-admin-key// Node.js (built-in 'crypto')
const crypto = require("crypto");
/**
* Create x-admin-key by encrypting the current Unix timestamp in ms
* with AES/CBC/PKCS5Padding using the provided secret and IV.
*
* @param {string} ivUtf8 - IV received from GET /iv (must be 16 bytes when UTF-8 encoded)
* @param {string|Buffer} secret - Shared secret (16 or 32 bytes)
* @returns {{ adminKey: string, timestamp: string }}
*/
function makeAdminKey(ivUtf8, secret) {
const timestamp = String(Date.now()); // Unix ms as string
const iv = Buffer.from(ivUtf8, "utf8");
// Allow 16-byte (AES-128) or 32-byte (AES-256) secrets
const key = Buffer.isBuffer(secret) ? secret : Buffer.from(secret, "utf8");
const algo = key.length === 16 ? "aes-128-cbc"
: key.length === 32 ? "aes-256-cbc"
: (() => { throw new Error("Secret must be 16 or 32 bytes"); })();
const cipher = crypto.createCipheriv(algo, key, iv);
cipher.setAutoPadding(true);
const encrypted = Buffer.concat([
cipher.update(Buffer.from(timestamp, "utf8")),
cipher.final(),
]);
return {
adminKey: encrypted.toString("base64"),
timestamp, // optional: useful for logging/debug
};
}
// Example usage:
// 1) Fetch iv from GET /iv
// 2) Call makeAdminKey(iv, SECRET)
// 3) Send { x-admin-iv: iv, x-admin-key: adminKey } as headers# 1) Get IV (valid for 10s)
IV=$(curl -s https://api.personalizedpool.com/iv | jq -r .iv)
# 2) Generate x-admin-key with your app using IV + SECRET (done in JS above)
# Suppose your app prints just the base64 key:
ADMIN_KEY=$(node genAdminKey.js "$IV") # your script wraps makeAdminKey
# 3) Submit answers anonymously
curl -X POST https://api.personalizedpool.com/answers/{quizId} \
-H "x-admin-iv: $IV" \
-H "x-admin-key: $ADMIN_KEY" \
-H "Content-Type: application/json" \
--data-binary @answers.jsonNotes
iv you place in x-admin-iv is
exactly what /iv returned (UTF-8
string).answers.json format is identical
to normal submissions. If your schema contains an email
question and you truly want to stay anonymous, design the schema to make
that question optional or route around it."root" forward — no
skipping./confirmEmail is called./checkEmailConfirmationCodeForPassword/{email} endpoint to
validate codes before changing passwords.