Member extra data and custom fields
Use grouped operations for group and field names you define. Use custom-fields operations for the reserved custom_fields group. Both use the same member/resource extra-data table. They do not read/write the dynamic user.extra relationship object or Firestore automatically. Values use string storage; choose a stable group/key convention in your integration.
Member and storage context
Use the resolved member and their existing relationship to the selected resource. User ACL and shared resource, session and account restrictions apply. Send the Wallkit session in token and resource public key in resource. Firebase-enabled members also supply the matching ID token in firebase-token; omit that header for an ordinary integration.
Examples use synthetic member 42 in resource 1001. The extra-data row definition describes storage and input limits; sample runtimes describe the clients. Read stored fields after a change to detect skipped inputs.
Operations
| Task | Method / path |
|---|---|
| Read grouped member data | GET /api/v1/user/extra/data |
| Upsert grouped member fields | PUT /api/v1/user/extra/data |
| Delete selected grouped fields | DELETE /api/v1/user/extra/data |
| Read custom fields | GET /api/v1/user/extra/data/custom-fields |
| Upsert custom fields | PUT /api/v1/user/extra/data/custom-fields |
| Delete one custom field | DELETE /api/v1/user/extra/data/custom-fields/{field_key} |
Read grouped member data
GET /api/v1/user/extra/data
Read extra-data rows grouped by group_key for this member/resource. This table is separate from user.extra stored on the resource relationship; it can include custom_fields as one group.
Before you call
Existing member/resource relationship and resolved user credentials. See member and storage context for shared checks and credential transport.
Request
No request body.
| Name | Location | Type | Requirement / default | Meaning and constraints |
|---|---|---|---|---|
| resource | header | string | required | Public key for the intended resource. |
| token | header | string | required member context | Existing Wallkit session token. |
| firebase-token | header | string | Firebase-enabled member only | Matching Firebase ID token; omit for ordinary resource. |
Result
HTTP 200.
| Field / projection | Type / presence | Meaning |
|---|---|---|
| items | group-key map / empty array | Named groups map to arrays of extra-data rows. No records returns []. No paginator or order guarantee. |
Example
Read music_style=techno from the preferences group. See member and storage context for the synthetic member and configuration.
cURL
curl -X GET "${WALLKIT_API_BASE}/api/v1/user/extra/data" \
-H "resource: ${RESOURCE_KEY}" \
-H "token: ${USER_TOKEN}" \
-H "firebase-token: ${FIREBASE_ID_TOKEN}"
JavaScript
// Node.js 18+; built-in fetch.
async function main() {
const url = new URL("/api/v1/user/extra/data", process.env.WALLKIT_API_BASE);
const response = await fetch(url, {
method: "GET",
headers: {
"resource": process.env.RESOURCE_KEY,
"token": process.env.USER_TOKEN,
"firebase-token": process.env.FIREBASE_ID_TOKEN
}
});
console.log(response.status, await response.json());
}
main().catch(console.error);
Python
# Python 3; standard library only.
import json
import os
from urllib.error import HTTPError
from urllib.parse import urljoin
from urllib.request import Request, urlopen
url = urljoin(os.environ["WALLKIT_API_BASE"], '/api/v1/user/extra/data')
request = Request(url, headers={'resource': os.environ['RESOURCE_KEY'], 'token': os.environ['USER_TOKEN'], 'firebase-token': os.environ['FIREBASE_ID_TOKEN']}, method='GET')
try:
with urlopen(request) as response:
print(response.status, json.load(response))
except HTTPError as error:
print(error.code, json.load(error))
HTTP 200 response excerpt:
{
"items": {
"preferences": [
{
"id": 9001,
"user_id": 42,
"resource_id": 1001,
"group_key": "preferences",
"field_key": "music_style",
"field_value": "techno",
"created_at": "2030-01-01 00:00:00",
"updated_at": "2030-01-01 00:00:00"
}
]
}
}
Alternate result
No stored rows returns items as an empty array, rather than an empty object. HTTP 200 excerpt:
{
"items": []
}
Recovery
| HTTP status | API code / shape | Cause | Next action |
|---|---|---|---|
| 409 | user_not_exists | No user/resource relationship. | Establish the intended member relationship before storing data. |
| 409 | error_get_user_extra_data | Caught read orchestration exception. | Contact support if expected fields are missing. |
Shared errors cover credential/context checks.
Next task
Upsert grouped member fields
PUT /api/v1/user/extra/data
Create/update only supplied valid group/field pairs and dispatch a per-field event. This writes sequentially; no transaction/atomic batch guarantee. Invalid empty/overlength group/field keys are skipped silently, and untouched pairs remain. Returned items read all current groups, not just accepted inputs.
Before you call
Existing member/resource relationship and resolved user credentials. See member and storage context for shared checks and credential transport.
Request
JSON body.
| Name | Location | Type | Requirement / default | Meaning and constraints |
|---|---|---|---|---|
| resource | header | string | required | Public key for the intended resource. |
| token | header | string | required member context | Existing Wallkit session token. |
| firebase-token | header | string | Firebase-enabled member only | Matching Firebase ID token; omit for ordinary resource. |
| extra_groups | body | array of objects | required; nonempty | Each object identifies one group/field pair; omitted existing pairs remain untouched. |
| extra_groups[].group_key | body | string | nonempty; max64 after trim | Dynamic group name; invalid/overlength keys skip this item silently. |
| extra_groups[].field_key | body | string | nonempty; max128 after trim | Dynamic field name within group; invalid/overlength keys skip silently. |
| extra_groups[].field_value | body | string | for each upserted field | String-filtered and trimmed; no action-level value-length validation or JSON-object preservation. Send a string rather than relying on missing/null coercion. |
Result
HTTP 200.
| Field / projection | Type / presence | Meaning |
|---|---|---|
| items | group-key map / empty array | Named groups map to arrays of extra-data rows. No records returns []. No paginator or order guarantee. |
Example
Upsert music_style=techno in the preferences group. See member and storage context for the synthetic member and configuration.
cURL
curl -X PUT "${WALLKIT_API_BASE}/api/v1/user/extra/data" \
-H "resource: ${RESOURCE_KEY}" \
-H "token: ${USER_TOKEN}" \
-H "firebase-token: ${FIREBASE_ID_TOKEN}" \
-H "Content-Type: application/json" \
--data '{"extra_groups":[{"group_key":"preferences","field_key":"music_style","field_value":"techno"}]}'
JavaScript
// Node.js 18+; built-in fetch.
async function main() {
const url = new URL("/api/v1/user/extra/data", process.env.WALLKIT_API_BASE);
const response = await fetch(url, {
method: "PUT",
headers: {
"resource": process.env.RESOURCE_KEY,
"token": process.env.USER_TOKEN,
"firebase-token": process.env.FIREBASE_ID_TOKEN,
"Content-Type": "application/json"
},
body: JSON.stringify({"extra_groups":[{"group_key":"preferences","field_key":"music_style","field_value":"techno"}]})
});
console.log(response.status, await response.json());
}
main().catch(console.error);
Python
# Python 3; standard library only.
import json
import os
from urllib.error import HTTPError
from urllib.parse import urljoin
from urllib.request import Request, urlopen
url = urljoin(os.environ["WALLKIT_API_BASE"], '/api/v1/user/extra/data')
body = {'extra_groups': [{'group_key': 'preferences', 'field_key': 'music_style', 'field_value': 'techno'}]}
request = Request(url, data=json.dumps(body, ensure_ascii=False).encode("utf-8"), headers={'resource': os.environ['RESOURCE_KEY'], 'token': os.environ['USER_TOKEN'], 'firebase-token': os.environ['FIREBASE_ID_TOKEN'], "Content-Type": "application/json"}, method='PUT')
try:
with urlopen(request) as response:
print(response.status, json.load(response))
except HTTPError as error:
print(error.code, json.load(error))
HTTP 200 response excerpt:
{
"items": {
"preferences": [
{
"id": 9001,
"user_id": 42,
"resource_id": 1001,
"group_key": "preferences",
"field_key": "music_style",
"field_value": "techno",
"created_at": "2030-01-01 00:00:00",
"updated_at": "2030-01-01 00:00:00"
}
]
}
}
Alternate result
An empty extra_groups array is rejected; it does not clear existing data. HTTP 409 excerpt:
{
"error": "incorrect_data",
"error_description": "Empty user extra groups data"
}
Recovery
| HTTP status | API code / shape | Cause | Next action |
|---|---|---|---|
| 409 | user_not_exists | No user/resource relationship. | Establish the intended member relationship before storing data. |
| 409 | incorrect_data | Missing JSON/nonempty outer collection. | Send the documented nonempty array in JSON. |
| 409 | error_update_user_extra_data | Caught upsert/readback orchestration exception. | Contact support; read current fields before repeating upserts because earlier items may already have changed. |
Shared errors cover credential/context checks.
Next task
Read grouped fields to display stored values and detect skipped items.
Delete selected grouped fields
DELETE /api/v1/user/extra/data
Delete each selected user/resource/group/field row and dispatch per-field events. Only selected pairs are deleted; invalid/overlength keys are silently skipped. Multiple deletions are sequential, so partial changes are possible. Missing targets have no idempotent-success guarantee.
Before you call
Existing member/resource relationship and resolved user credentials. See member and storage context for shared checks and credential transport.
Request
JSON body, including the grouped DELETE request.
| Name | Location | Type | Requirement / default | Meaning and constraints |
|---|---|---|---|---|
| resource | header | string | required | Public key for the intended resource. |
| token | header | string | required member context | Existing Wallkit session token. |
| firebase-token | header | string | Firebase-enabled member only | Matching Firebase ID token; omit for ordinary resource. |
| extra_groups | body | array of objects | required; nonempty | Each object identifies one group/field pair; omitted existing pairs remain untouched. |
| extra_groups[].group_key | body | string | nonempty; max64 after trim | Dynamic group name; invalid/overlength keys skip this item silently. |
| extra_groups[].field_key | body | string | nonempty; max128 after trim | Dynamic field name within group; invalid/overlength keys skip silently. |
Result
HTTP 200.
| Field / projection | Type / presence | Meaning |
|---|---|---|
| response | ordinary empty JSON response | No action-defined result/success field; shared response metadata may still appear. This does not report deleted counts or skipped pairs. |
Example
Delete the existing preferences/music_style pair. See member and storage context for the synthetic member and configuration.
cURL
curl -X DELETE "${WALLKIT_API_BASE}/api/v1/user/extra/data" \
-H "resource: ${RESOURCE_KEY}" \
-H "token: ${USER_TOKEN}" \
-H "firebase-token: ${FIREBASE_ID_TOKEN}" \
-H "Content-Type: application/json" \
--data '{"extra_groups":[{"group_key":"preferences","field_key":"music_style"}]}'
JavaScript
// Node.js 18+; built-in fetch.
async function main() {
const url = new URL("/api/v1/user/extra/data", process.env.WALLKIT_API_BASE);
const response = await fetch(url, {
method: "DELETE",
headers: {
"resource": process.env.RESOURCE_KEY,
"token": process.env.USER_TOKEN,
"firebase-token": process.env.FIREBASE_ID_TOKEN,
"Content-Type": "application/json"
},
body: JSON.stringify({"extra_groups":[{"group_key":"preferences","field_key":"music_style"}]})
});
console.log(response.status, await response.json());
}
main().catch(console.error);
Python
# Python 3; standard library only.
import json
import os
from urllib.error import HTTPError
from urllib.parse import urljoin
from urllib.request import Request, urlopen
url = urljoin(os.environ["WALLKIT_API_BASE"], '/api/v1/user/extra/data')
body = {'extra_groups': [{'group_key': 'preferences', 'field_key': 'music_style'}]}
request = Request(url, data=json.dumps(body, ensure_ascii=False).encode("utf-8"), headers={'resource': os.environ['RESOURCE_KEY'], 'token': os.environ['USER_TOKEN'], 'firebase-token': os.environ['FIREBASE_ID_TOKEN'], "Content-Type": "application/json"}, method='DELETE')
try:
with urlopen(request) as response:
print(response.status, json.load(response))
except HTTPError as error:
print(error.code, json.load(error))
HTTP 200 response excerpt:
{}
Alternate result
An empty extra_groups list is rejected rather than requesting deletion of every group. HTTP 409 excerpt:
{
"error": "incorrect_data",
"error_description": "Empty user extra groups data"
}
Recovery
| HTTP status | API code / shape | Cause | Next action |
|---|---|---|---|
| 409 | user_not_exists | No user/resource relationship. | Establish the intended member relationship before storing data. |
| 409 | incorrect_data | Missing JSON/nonempty outer collection. | Send the documented nonempty array in JSON. |
| 409 | error_delete_user_extra_data | Caught read/write/delete orchestration exception. | Contact support; inspect current fields before repeating writes/deletes because earlier items may already have changed. |
Missing delete targets can fail in the helper; no successful no-op or one final missing-target status is promised. Shared errors cover credential/context checks.
Next task
Read grouped fields to inspect what remains; an empty response is not a deletion receipt.
Read custom fields
GET /api/v1/user/extra/data/custom-fields
Read the same extra-data table limited to group custom_fields. This is a fixed group name with dynamic field keys, not a separate fixed customer schema.
Before you call
Existing member/resource relationship and resolved user credentials. See member and storage context for shared checks and credential transport.
Request
No request body.
| Name | Location | Type | Requirement / default | Meaning and constraints |
|---|---|---|---|---|
| resource | header | string | required | Public key for the intended resource. |
| token | header | string | required member context | Existing Wallkit session token. |
| firebase-token | header | string | Firebase-enabled member only | Matching Firebase ID token; omit for ordinary resource. |
Result
HTTP 200.
| Field / projection | Type / presence | Meaning |
|---|---|---|
| items | array | Extra-data rows limited to group custom_fields. Empty [] when none. No paginator/order guarantee. |
Example
Read music_style from the reserved custom_fields group. See member and storage context for the synthetic member and configuration.
cURL
curl -X GET "${WALLKIT_API_BASE}/api/v1/user/extra/data/custom-fields" \
-H "resource: ${RESOURCE_KEY}" \
-H "token: ${USER_TOKEN}" \
-H "firebase-token: ${FIREBASE_ID_TOKEN}"
JavaScript
// Node.js 18+; built-in fetch.
async function main() {
const url = new URL("/api/v1/user/extra/data/custom-fields", process.env.WALLKIT_API_BASE);
const response = await fetch(url, {
method: "GET",
headers: {
"resource": process.env.RESOURCE_KEY,
"token": process.env.USER_TOKEN,
"firebase-token": process.env.FIREBASE_ID_TOKEN
}
});
console.log(response.status, await response.json());
}
main().catch(console.error);
Python
# Python 3; standard library only.
import json
import os
from urllib.error import HTTPError
from urllib.parse import urljoin
from urllib.request import Request, urlopen
url = urljoin(os.environ["WALLKIT_API_BASE"], '/api/v1/user/extra/data/custom-fields')
request = Request(url, headers={'resource': os.environ['RESOURCE_KEY'], 'token': os.environ['USER_TOKEN'], 'firebase-token': os.environ['FIREBASE_ID_TOKEN']}, method='GET')
try:
with urlopen(request) as response:
print(response.status, json.load(response))
except HTTPError as error:
print(error.code, json.load(error))
HTTP 200 response excerpt:
{
"items": [
{
"id": 9001,
"user_id": 42,
"resource_id": 1001,
"group_key": "custom_fields",
"field_key": "music_style",
"field_value": "techno",
"created_at": "2030-01-01 00:00:00",
"updated_at": "2030-01-01 00:00:00"
}
]
}
Alternate result
No custom_fields rows returns an empty array. HTTP 200 excerpt:
{
"items": []
}
Recovery
| HTTP status | API code / shape | Cause | Next action |
|---|---|---|---|
| 409 | user_not_exists | No user/resource relationship. | Establish the intended member relationship before storing data. |
| 409 | error_get_user_extra_data_custom_fields | Caught read orchestration exception. | Contact support if expected fields are missing. |
Shared errors cover credential/context checks.
Next task
Upsert custom fields
PUT /api/v1/user/extra/data/custom-fields
Create/update valid supplied keys in custom_fields and dispatch per-field events. Writes are sequential; empty/overlength keys skip silently. Existing omitted fields remain. The response reads all custom_fields rows.
Before you call
Existing member/resource relationship and resolved user credentials. See member and storage context for shared checks and credential transport.
Request
JSON body.
| Name | Location | Type | Requirement / default | Meaning and constraints |
|---|---|---|---|---|
| resource | header | string | required | Public key for the intended resource. |
| token | header | string | required member context | Existing Wallkit session token. |
| firebase-token | header | string | Firebase-enabled member only | Matching Firebase ID token; omit for ordinary resource. |
| items | body | array of objects | required; nonempty | Objects identify custom_fields keys to upsert; untouched fields remain. |
| items[].field_key | body | string | nonempty; max128 after trim | Dynamic name; empty/overlength keys skip silently. |
| items[].field_value | body | string | for each upserted item | Filtered/trimmed string; no action-level length check. Send a string; no typed JSON-value preservation guarantee. |
Result
HTTP 200.
| Field / projection | Type / presence | Meaning |
|---|---|---|
| items | array | Extra-data rows limited to group custom_fields. Empty [] when none. No paginator/order guarantee. |
Example
Upsert music_style=techno in custom_fields. See member and storage context for the synthetic member and configuration.
cURL
curl -X PUT "${WALLKIT_API_BASE}/api/v1/user/extra/data/custom-fields" \
-H "resource: ${RESOURCE_KEY}" \
-H "token: ${USER_TOKEN}" \
-H "firebase-token: ${FIREBASE_ID_TOKEN}" \
-H "Content-Type: application/json" \
--data '{"items":[{"field_key":"music_style","field_value":"techno"}]}'
JavaScript
// Node.js 18+; built-in fetch.
async function main() {
const url = new URL("/api/v1/user/extra/data/custom-fields", process.env.WALLKIT_API_BASE);
const response = await fetch(url, {
method: "PUT",
headers: {
"resource": process.env.RESOURCE_KEY,
"token": process.env.USER_TOKEN,
"firebase-token": process.env.FIREBASE_ID_TOKEN,
"Content-Type": "application/json"
},
body: JSON.stringify({"items":[{"field_key":"music_style","field_value":"techno"}]})
});
console.log(response.status, await response.json());
}
main().catch(console.error);
Python
# Python 3; standard library only.
import json
import os
from urllib.error import HTTPError
from urllib.parse import urljoin
from urllib.request import Request, urlopen
url = urljoin(os.environ["WALLKIT_API_BASE"], '/api/v1/user/extra/data/custom-fields')
body = {'items': [{'field_key': 'music_style', 'field_value': 'techno'}]}
request = Request(url, data=json.dumps(body, ensure_ascii=False).encode("utf-8"), headers={'resource': os.environ['RESOURCE_KEY'], 'token': os.environ['USER_TOKEN'], 'firebase-token': os.environ['FIREBASE_ID_TOKEN'], "Content-Type": "application/json"}, method='PUT')
try:
with urlopen(request) as response:
print(response.status, json.load(response))
except HTTPError as error:
print(error.code, json.load(error))
HTTP 200 response excerpt:
{
"items": [
{
"id": 9001,
"user_id": 42,
"resource_id": 1001,
"group_key": "custom_fields",
"field_key": "music_style",
"field_value": "techno",
"created_at": "2030-01-01 00:00:00",
"updated_at": "2030-01-01 00:00:00"
}
]
}
Alternate result
Missing, non-array or empty items is rejected; it does not clear custom fields. HTTP 409 excerpt:
{
"error": "field_items_required",
"error_description": "Field items is required"
}
Recovery
| HTTP status | API code / shape | Cause | Next action |
|---|---|---|---|
| 409 | user_not_exists | No user/resource relationship. | Establish the intended member relationship before storing data. |
| 409 | incorrect_data / field_items_required | Missing JSON or nonempty items array. | Send the documented nonempty items array. |
| 409 | error_update_user_extra_data_custom_fields | Caught upsert/readback orchestration exception. | Contact support; read current fields before repeating upserts because earlier items may already have changed. |
Shared errors cover credential/context checks.
Next task
Read custom fields and verify the displayed keys, rather than assuming every input was accepted.
Delete one custom field
DELETE /api/v1/user/extra/data/custom-fields/{field_key}
Delete the matching custom_fields row for this member/resource and dispatch its deletion event. This deletes stored profile data. It has no guaranteed successful no-op for an absent key. Other fields and groups remain.
Before you call
Existing member/resource relationship and resolved user credentials. See member and storage context for shared checks and credential transport.
Request
No request body.
| Name | Location | Type | Requirement / default | Meaning and constraints |
|---|---|---|---|---|
| resource | header | string | required | Public key for the intended resource. |
| token | header | string | required member context | Existing Wallkit session token. |
| firebase-token | header | string | Firebase-enabled member only | Matching Firebase ID token; omit for ordinary resource. |
| field_key | path | URL-encoded string | required; nonempty max128 after trim | Existing custom_fields key; source validates the route value, not a body field. |
Result
HTTP 200.
| Field / projection | Type / presence | Meaning |
|---|---|---|
| success | boolean | true after the helper/event path returns; no independent delete-result/count verification. |
Example
Delete the existing custom_fields/music_style row. See member and storage context for the synthetic member and configuration.
cURL
curl -X DELETE "${WALLKIT_API_BASE}/api/v1/user/extra/data/custom-fields/music_style" \
-H "resource: ${RESOURCE_KEY}" \
-H "token: ${USER_TOKEN}" \
-H "firebase-token: ${FIREBASE_ID_TOKEN}"
JavaScript
// Node.js 18+; built-in fetch.
async function main() {
const url = new URL("/api/v1/user/extra/data/custom-fields/music_style", process.env.WALLKIT_API_BASE);
const response = await fetch(url, {
method: "DELETE",
headers: {
"resource": process.env.RESOURCE_KEY,
"token": process.env.USER_TOKEN,
"firebase-token": process.env.FIREBASE_ID_TOKEN
}
});
console.log(response.status, await response.json());
}
main().catch(console.error);
Python
# Python 3; standard library only.
import json
import os
from urllib.error import HTTPError
from urllib.parse import urljoin
from urllib.request import Request, urlopen
url = urljoin(os.environ["WALLKIT_API_BASE"], '/api/v1/user/extra/data/custom-fields/music_style')
request = Request(url, headers={'resource': os.environ['RESOURCE_KEY'], 'token': os.environ['USER_TOKEN'], 'firebase-token': os.environ['FIREBASE_ID_TOKEN']}, method='DELETE')
try:
with urlopen(request) as response:
print(response.status, json.load(response))
except HTTPError as error:
print(error.code, json.load(error))
HTTP 200 response excerpt:
{
"success": true
}
Alternate result
An empty or over 128-character key after trimming is rejected; use an existing valid field key. HTTP 409 excerpt:
{
"error": "incorrect_field_key",
"error_description": "Incorrect field key"
}
Recovery
| HTTP status | API code / shape | Cause | Next action |
|---|---|---|---|
| 409 | user_not_exists | No user/resource relationship. | Establish the intended member relationship before storing data. |
| 409 | incorrect_field_key | Invalid empty/overlength path key. | Use a valid encoded custom field name. |
| 409 | error_delete_user_extra_data | Caught read/write/delete orchestration exception. | Contact support; inspect current fields before repeating writes/deletes because earlier items may already have changed. |
Missing delete targets can fail in the helper; no successful no-op or one final missing-target status is promised. Shared errors cover credential/context checks.