Other & Distribution-adjacent Endpoints
This page is a catch-all reference covering the remaining wallethero-api endpoints: workspace settings, SSO login, image-source helpers, engagement tracking, the embeddable distribution widget, distribution API tokens, template migration, internal passserver endpoints, and the test-only e2e seams.
Auth varies per endpoint — most are workspace-scoped Bearer-token calls (caller must be a member of the workspace), a few are public/token-authed (SSO, the embed widget), and the internal pass endpoints require the static admin token. Each endpoint states its own auth model below.
All paths include the /wallethero-api prefix. Base URL is https://api.wallethero.app.
Workspace Settings
Per-workspace defaults (currency, locale). The settings row is created on first read if it does not yet exist (defaults: USD / en).
Get workspace settings
GET /wallethero-api/workspace/:workspaceId/settings
Returns the workspace settings, creating the row with defaults if absent.
Auth: Bearer token — workspace member. Also accepts a mobile-app session via the x-mobile-token header (read-only).
Path Parameters
| Parameter | Type | Description |
|---|---|---|
workspaceId | string (UUID) | Workspace identifier |
Example Request
curl "https://api.wallethero.app/wallethero-api/workspace/WORKSPACE_ID/settings" \
-H "Authorization: Bearer YOUR_TOKEN"Response (200)
{
"data": {
"id": "11111111-1111-1111-1111-111111111111",
"workspace_id": "WORKSPACE_ID",
"default_currency": "USD",
"default_locale": "en",
"date_created": "2026-01-01T00:00:00.000Z",
"date_updated": "2026-01-01T00:00:00.000Z"
}
}SDK
const settings = await wh.workspaceSettings.getSettings(workspaceId);Update workspace settings
PATCH /wallethero-api/workspace/:workspaceId/settings
Updates the workspace defaults. The row is created first if it does not exist.
Auth: Bearer token — workspace member.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
workspaceId | string (UUID) | Workspace identifier |
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
default_currency | string | No | ISO 4217 currency code, exactly 3 letters, uppercased server-side (e.g. EUR) |
default_locale | string | No | Locale code, 2–10 chars (e.g. pl) |
Example Request
curl -X PATCH "https://api.wallethero.app/wallethero-api/workspace/WORKSPACE_ID/settings" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "default_currency": "EUR", "default_locale": "pl" }'Response (200)
{
"data": {
"id": "11111111-1111-1111-1111-111111111111",
"workspace_id": "WORKSPACE_ID",
"default_currency": "EUR",
"default_locale": "pl",
"date_created": "2026-01-01T00:00:00.000Z",
"date_updated": "2026-06-16T10:00:00.000Z"
}
}SDK
const settings = await wh.workspaceSettings.updateSettings(workspaceId, {
default_currency: "EUR",
default_locale: "pl",
});SSO
Single-sign-on login. Exchanges a third-party identity provider token for a Directus session. Runs before any workspace context exists.
SSO login
POST /wallethero-api/auth/sso
Verifies the provider token, finds or creates the matching Directus user, and issues a Directus session.
Auth: Public — no Bearer token. The request body carries the provider token.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
provider | string enum | Yes | Identity provider. Currently only google. |
token | string | Yes | The provider-issued token to verify (e.g. a Google ID token). |
Example Request
curl -X POST "https://api.wallethero.app/wallethero-api/auth/sso" \
-H "Content-Type: application/json" \
-d '{ "provider": "google", "token": "GOOGLE_ID_TOKEN" }'Response (200)
{
"access_token": "eyJhbGciOi...",
"refresh_token": "abc123...",
"user": {
"id": "22222222-2222-2222-2222-222222222222",
"email": "[email protected]",
"first_name": "Jane",
"last_name": "Doe"
}
}Note: this response is not wrapped in
{ "data": ... }.
SDK
No SDK method — call the REST endpoint directly.
Image Source
Classify and validate pass-template image fields, and invalidate cached external image URLs. Used by the template editor to determine whether a stored value is a Directus file UUID, an https:// URL, or a ${custom_fields.x} reference.
Preview an image source value
POST /wallethero-api/workspace/:workspaceId/image-source/preview
Classifies and lightly validates a single image-source value. Does not download external URLs.
Auth: Bearer token — workspace member.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
workspaceId | string (UUID) | Workspace identifier |
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
raw | string | Yes | The stored image-source value to classify (file UUID, https:// URL, or ${custom_fields.<name>}) |
Example Request
curl -X POST "https://api.wallethero.app/wallethero-api/workspace/WORKSPACE_ID/image-source/preview" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "raw": "https://cdn.example.com/logo.png" }'Response (200)
{
"data": {
"kind": "url",
"raw": "https://cdn.example.com/logo.png",
"url": "https://cdn.example.com/logo.png",
"previewUrl": "https://cdn.example.com/logo.png",
"cachedFileId": null,
"valid": true
}
}kind is one of uuid, url, custom_field, empty. When valid is false a reason string explains why.
SDK
const result = await wh.imageSource.preview(workspaceId, "https://cdn.example.com/logo.png");Refresh (invalidate) a cached image URL
POST /wallethero-api/workspace/:workspaceId/image-source/refresh
Drops the cached external URL → Directus file mapping for the given URL so the next pass generation re-downloads it. The lookup is scoped to the caller's workspace. No-op if no cache entry exists.
Auth: Bearer token — workspace member.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
workspaceId | string (UUID) | Workspace identifier |
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
sourceUrl | string | Yes | The external http:///https:// URL whose cache entry should be invalidated |
Example Request
curl -X POST "https://api.wallethero.app/wallethero-api/workspace/WORKSPACE_ID/image-source/refresh" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "sourceUrl": "https://cdn.example.com/logo.png" }'Response (200)
{ "success": true }SDK
await wh.imageSource.refresh(workspaceId, "https://cdn.example.com/logo.png");Engagement Tracking
Record customer engagement events (check-in, check-out, visit, scan). The event is persisted to client_events and dispatched to webhooks via the event bus.
Record an engagement event
POST /wallethero-api/workspace/:workspaceId/engagement
Persists an engagement event for a client (optionally tied to a pass) and returns the stored row.
Auth: Bearer token — workspace member.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
workspaceId | string (UUID) | Workspace identifier |
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
client_id | string (UUID) | Yes | Client the event belongs to. Must exist in this workspace. |
event_type | string enum | Yes | One of check_in, check_out, visit, scan. |
pass_id | string (UUID) | No | Pass associated with the event. If given, must exist in this workspace. |
event_timestamp | string (ISO 8601) | No | Event time. Defaults to the server's current time. |
source | string | No | Free-text source label. Defaults to api. |
metadata | object | No | Arbitrary key/value metadata. Defaults to {}. |
Example Request
curl -X POST "https://api.wallethero.app/wallethero-api/workspace/WORKSPACE_ID/engagement" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"client_id": "33333333-3333-3333-3333-333333333333",
"event_type": "check_in",
"source": "front-desk",
"metadata": { "location": "store-1" }
}'Response (201)
{
"data": {
"id": "44444444-4444-4444-4444-444444444444",
"workspace_id": "WORKSPACE_ID",
"client_id": "33333333-3333-3333-3333-333333333333",
"event_category": "engagement",
"event_type": "check_in",
"event_timestamp": "2026-06-16T10:00:00.000Z",
"pass_id": null,
"source": "front-desk",
"metadata": { "location": "store-1" }
}
}SDK
No SDK method — call the REST endpoint directly. (The SDK events service records engagement via the generic events API, not this endpoint.)
Embeddable Distribution Widget
Public endpoints reached from third-party origins (CORS-enabled). Authorized by the per-distribution token in the URL path — not a Directus user. Used by the embeddable enrollment widget / "Plain HTML or SDK snippet" from the distribution editor.
Get distribution embed config
GET /wallethero-api/embed/distribution/:token/config
Returns the public configuration the widget needs to render its form: status, delivery mode, form-field config, and basic workspace info.
Auth: Public — the :token path segment is the per-distribution API token (validated by the distribution-token middleware). CORS is applied for the distribution's allowed origins.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
token | string | The per-distribution API token. |
Example Request
curl "https://api.wallethero.app/wallethero-api/embed/distribution/DISTRIBUTION_TOKEN/config"Response (200)
{
"data": {
"id": "55555555-5555-5555-5555-555555555555",
"status": "active",
"delivery_mode": "email",
"form_fields_config": null,
"workspace": {
"id": "WORKSPACE_ID",
"name": "Acme Loyalty",
"slug": "acme"
}
},
"success": true
}SDK
// Browser widget helper (PublicDistributionService):
const config = await publicDistribution.getConfig();Submit distribution enrollment
POST /wallethero-api/embed/distribution/:token/submit
Enrolls a submitter into the distribution: creates/locates the client, issues a pass, and (per the distribution's delivery mode) emails it and/or returns inline pass URLs.
Auth: Public — the :token path segment is the per-distribution API token. CORS is applied for the distribution's allowed origins.
A bare
GETon this URL returns405 Method Not Allowedwith a hint to usePOST.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
token | string | The per-distribution API token. |
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
email | string (email) | Yes | Submitter's email address. |
first_name | string | Yes | Submitter's first name (min length 1). |
last_name | string | No | Submitter's last name. |
phone | string | No* | Submitter's phone number (trimmed, max 50 characters). Top-level field, not a custom field — it is what the server checks when form_fields_config marks phone required, and what it matches returning clients on. A phone sent only as custom_fields.phone is still accepted and promoted. |
marketing_consent | boolean | No* | Whether the submitter consented to marketing. Must be true when form_fields_config marks the consent checkbox required. |
custom_fields | object | No | Arbitrary key/value custom-field values for the client. |
referral_code | string | No | Referral code (trimmed, max 20 chars). |
Example Request
curl -X POST "https://api.wallethero.app/wallethero-api/embed/distribution/DISTRIBUTION_TOKEN/submit" \
-H "Content-Type: application/json" \
-d '{
"email": "[email protected]",
"first_name": "Jane",
"last_name": "Doe",
"phone": "+15551234567",
"marketing_consent": true
}'Response (200)
{
"data": {
"pass_id": "66666666-6666-6666-6666-666666666666",
"apple_pass_url": "https://api.wallethero.app/...",
"google_pass_url": "https://pay.google.com/...",
"client_token": "ct_abc123"
},
"isResend": false,
"emailSent": true,
"message": "Pass issued successfully.",
"success": true
}When a pass already exists for the email, isResend is true and the existing pass is returned.
SDK
// Browser widget helper (PublicDistributionService):
const result = await publicDistribution.enroll({
email: "[email protected]",
first_name: "Jane",
last_name: "Doe",
marketing_consent: true,
});Submit URL — GET guard
GET /wallethero-api/embed/distribution/:token/submit
A convenience guard so that opening the submit URL in a browser (which issues a GET) returns a friendly error instead of a bare Directus 404. The submission endpoint itself is POST only (see above).
Auth: Public — no token validation is performed on this GET; it always responds with 405.
Response (405)
{
"success": false,
"error": "This endpoint accepts POST only. Use the Plain HTML or SDK snippet from the distribution editor, or curl with -X POST and a JSON body."
}The response also sets the Allow: POST, OPTIONS header.
No SDK method — this is a browser-safety guard, not a callable endpoint.
Distribution API Tokens
Manage the per-distribution API token used by the embeddable widget.
Rotate a distribution's API token
POST /wallethero-api/workspace/:workspaceId/distributions/:distributionId/api-token/rotate
Generates a new API token for the distribution and returns the plaintext value exactly once — it is stored hashed and cannot be retrieved later. Verifies the distribution belongs to the workspace.
Auth: Bearer token — workspace member.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
workspaceId | string (UUID) | Workspace identifier |
distributionId | string (UUID) | Distribution identifier |
Example Request
curl -X POST "https://api.wallethero.app/wallethero-api/workspace/WORKSPACE_ID/distributions/DISTRIBUTION_ID/api-token/rotate" \
-H "Authorization: Bearer YOUR_TOKEN"Response (200)
{
"data": {
"distribution_id": "DISTRIBUTION_ID",
"token": "whd_live_abc123...",
"prefix": "whd_live"
},
"message": "API token rotated. Save the plaintext token now — it cannot be retrieved later."
}SDK
const { data } = await wh.distributions.rotateApiToken(workspaceId, distributionId);
// data.token is the plaintext token — store it now.Templates
Pass-template lifecycle helper. (Basic CRUD for pass_templates lives under /wallethero-api/pass-templates/...; the migration action below is documented here.)
Migrate passes and delete a template
POST /wallethero-api/templates/:templateId/migrate-and-delete
Moves all passes from the source template to target_template_id (also updating their project_id), then deletes the now-empty source template. Both templates must belong to the same workspace.
Auth: Bearer token — workspace member. The guard loads the source template and enforces membership against its workspace_id.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
templateId | string (UUID) | Source template to migrate from and delete. |
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
target_template_id | string (UUID) | Yes | Template to move passes to. Must differ from the source and belong to the same workspace. |
Example Request
curl -X POST "https://api.wallethero.app/wallethero-api/templates/SOURCE_TEMPLATE_ID/migrate-and-delete" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "target_template_id": "TARGET_TEMPLATE_ID" }'Response (200)
{
"data": {
"deleted_template_id": "SOURCE_TEMPLATE_ID",
"target_template_id": "TARGET_TEMPLATE_ID",
"migrated_passes": 42
}
}Returns 404 if the target template is not found, and 403 if the target belongs to a different workspace.
SDK
const { data } = await wh.passTemplates.migrateAndDelete(sourceTemplateId, targetTemplateId);Internal Pass Endpoints
Read-mostly endpoints consumed by the passserver on every wallet refresh. The /internal/* variants are gated to admins because the loyalty snapshot includes the client's portal_url (a long-lived capability link). A workspace-scoped sibling exists for the editor's pass preview.
Mark a pass as fetched
POST /wallethero-api/internal/passes/:passId/fetched
Marks queued notifications for the pass as received after a device (Apple) fetch or a successful Google Wallet object update. Idempotent and best-effort — never errors when the pass is unknown.
Auth: Admin only (static admin DIRECTUS_TOKEN). Consumed by the passserver.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
passId | string (UUID) | Pass identifier |
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
source | string enum | No | apple_fetch or google_update. |
fetched_at | string (ISO 8601) | No | Fetch timestamp. |
Example Request
curl -X POST "https://api.wallethero.app/wallethero-api/internal/passes/PASS_ID/fetched" \
-H "Authorization: Bearer ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "source": "apple_fetch" }'Response (200)
{ "data": { "updated": 1 } }SDK
No SDK method — call the REST endpoint directly.
Get loyalty render data (internal)
GET /wallethero-api/internal/passes/:passId/loyalty-render-data
Returns the live loyalty snapshot (points, tier, portal URL, tier benefits, active/available rewards, referral code) used to render the pass. Lazily provisions a referral code if the referral program is enabled.
Auth: Admin only (static admin DIRECTUS_TOKEN). Consumed by the passserver.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
passId | string (UUID) | Pass identifier |
Example Request
curl "https://api.wallethero.app/wallethero-api/internal/passes/PASS_ID/loyalty-render-data" \
-H "Authorization: Bearer ADMIN_TOKEN"Response (200)
{
"data": {
"points": 1200,
"tier": { "name": "Gold" },
"portal_url": "https://portal.wallethero.app/...",
"referral_code": "JANE-XYZ"
}
}Returns 404 { "errors": [{ "message": "Pass not found" }] } for an unknown pass, and an empty snapshot when the pass has no associated client.
SDK
No SDK method — call the REST endpoint directly (internal/admin only).
Get loyalty render data (workspace-scoped)
GET /wallethero-api/workspace/:workspaceId/passes/:passId/loyalty-render-data
Workspace-scoped sibling of the internal endpoint, used by the editor's pass preview so ${wallet.points} (and tier/etc) resolve to the same values the passserver renders. The pass must belong to the given workspace.
Auth: Bearer token — workspace member.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
workspaceId | string (UUID) | Workspace identifier |
passId | string (UUID) | Pass identifier |
Example Request
curl "https://api.wallethero.app/wallethero-api/workspace/WORKSPACE_ID/passes/PASS_ID/loyalty-render-data" \
-H "Authorization: Bearer YOUR_TOKEN"Response (200)
{
"data": {
"points": 1200,
"tier": { "name": "Gold" },
"portal_url": "https://portal.wallethero.app/...",
"referral_code": "JANE-XYZ"
}
}SDK
const { data } = await wh.passes.getLoyaltyRenderData(workspaceId, passId);Test-only endpoints (non-production)
These endpoints are NOT part of the public contract. They are mounted only when the
WALLETHERO_E2E_TEST_HOOKS=trueenvironment flag is set and are never enabled in production. They exist so the e2e suite can drive internal seams (simulate inbound IDPoS transactions, read back outbound IDPoS calls, trigger crons, seed mappings) without standing up the surrounding infrastructure. They do not use the workspace guard — they accept whatever the e2e harness sends as an authenticated admin. Listed for completeness only.
| Method & Path | Purpose |
|---|---|
POST /wallethero-api/__test__/idpos/ingest | Simulate an inbound IDPoS transaction: resolve the client via customer_id_mapping, dedupe by external_id, then create a transaction (fires the production hooks → automations → tier pipeline). Body: workspace_id, external_id, amount (required); idpos_customer_id / customer_id, currency, description (optional). |
GET /wallethero-api/__test__/idpos/calls | Read back the outbound calls the IdposClient would have made (recorded in-memory). |
POST /wallethero-api/__test__/idpos/calls/clear | Clear the recorded outbound IDPoS-call buffer. |
POST /wallethero-api/__test__/automations/run-time-based | Trigger the time-based automations cron path on demand (publishes test.run_time_based_automations on the domain event bus). |
POST /wallethero-api/__test__/passes/simulate-apple-install | Create an apple_registrations row so the registration hook fires a pass_lifecycle.pass_installed event. Body: workspace_id, pass_id (required); project_id, device_library_identifier, pass_type_identifier, serial_number, push_token (optional). |
POST /wallethero-api/__test__/notifications/run-delivery | Flush the notification delivery queue synchronously instead of waiting for the 30s cron tick. (No auth required.) |
POST /wallethero-api/__test__/clients/set-idpos-id | Seed the IDPoS↔WalletHero mapping: upsert the client identity and dual-write the legacy idpos_customer_id column. Body: client_id, idpos_customer_id. |
No SDK methods — test harness only.