Integrations
Integrations connect a workspace to an external provider (e.g. IDPoS, KosmetologAPI) for inbound webhooks, outbound sync, and historical imports. Everything is provider-agnostic and keyed by a :provider slug — the set of valid slugs is decided server-side by the provider registry. Each provider declares which capabilities it supports (inbound, outbound, import); endpoints that depend on a capability the provider lacks return an error.
Auth: Every endpoint is workspace-scoped — pass a Bearer token for a user who is a member of the workspace in the path. Access is enforced by createWorkspaceGuard (path source) plus an explicit checkWorkspaceAccess call in each handler.
All paths are mounted under the /wallethero-api prefix. Base URL: https://api.wallethero.app.
Secrets are never returned. Only redacted secrets_meta (e.g. { "api_key": { "last4": "9f02", "rotated_at": "..." } }) appears in responses.
List Integrations
GET /wallethero-api/workspace/:workspaceId/integrations
Returns every configured integration for the workspace plus the list of provider slugs the server supports.
Auth: Bearer token — workspace member.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
workspaceId | string (UUID) | Workspace identifier |
Example Request
curl "https://api.wallethero.app/wallethero-api/workspace/WORKSPACE_ID/integrations" \
-H "Authorization: Bearer YOUR_TOKEN"Response (200)
{
"data": [
{
"id": "c1...",
"workspace_id": "WORKSPACE_ID",
"provider": "idpos",
"enabled_inbound": true,
"enabled_outbound": false,
"inbox_id": "ibx_123",
"inbox_ingest_url": "https://webhook.wallethero.app/ingest/TOKEN",
"config": { "base_url": "https://...", "location_ids": [1, 2] },
"secrets_meta": { "api_key": { "last4": "9f02", "rotated_at": "2026-06-01T10:00:00.000Z" } },
"last_synced_at": null,
"last_error": null,
"date_created": "2026-06-01T10:00:00.000Z",
"date_updated": "2026-06-01T10:00:00.000Z"
}
],
"available_providers": ["idpos", "kosmetolog"]
}SDK
const { data, available_providers } = await wh.integrations.list(workspaceId);Get Integration
GET /wallethero-api/workspace/:workspaceId/integrations/:provider
Returns the saved config for one provider. Responds 404 ({ "error": { "code": "not_found" } }) when the provider isn't configured for the workspace.
Auth: Bearer token — workspace member.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
workspaceId | string (UUID) | Workspace identifier |
provider | string | Provider slug (e.g. idpos). Must be a registered provider |
Example Request
curl "https://api.wallethero.app/wallethero-api/workspace/WORKSPACE_ID/integrations/idpos" \
-H "Authorization: Bearer YOUR_TOKEN"Response (200)
{
"data": {
"id": "c1...",
"provider": "idpos",
"enabled_inbound": true,
"enabled_outbound": false,
"inbox_id": "ibx_123",
"inbox_ingest_url": "https://webhook.wallethero.app/ingest/TOKEN",
"config": { "base_url": "https://...", "location_ids": [1, 2] },
"secrets_meta": { "api_key": { "last4": "9f02" } },
"last_synced_at": null,
"last_error": null
}
}SDK
const result = await wh.integrations.get(workspaceId, "idpos");
// result is null when not configuredCreate or Update Integration
PUT /wallethero-api/workspace/:workspaceId/integrations/:provider
Creates or updates (upsert) the integration config for a provider. config is shallow-merged and validated against the provider's schema; secrets are validated against the provider's known secret keys. Enabling inbound auto-provisions a webhook inbox (for webhook-receiving providers) and returns the inbox token. Enabling outbound is gated — if the provider's prerequisites (e.g. tier mappings) aren't satisfied, the request fails with the missing reason.
Auth: Bearer token — workspace member.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
workspaceId | string (UUID) | Workspace identifier |
provider | string | Provider slug. Must be a registered provider |
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
enabled_inbound | boolean | No | Turn inbound (webhook receive / import) on or off |
enabled_outbound | boolean | No | Turn outbound sync on or off (gated by provider prerequisites) |
config | object | No | Provider-specific config. Validated against the provider's configSchema. For IDPoS this includes base_url, location_ids, default_location_id, tier_mappings, customer_id_mapping, etc. |
secrets | Record<string, string | null> | No | Per-key secret rotation. A string sets/rotates the value, null clears it, omitting a key leaves it unchanged. Unknown secret keys are rejected |
Example Request
curl -X PUT "https://api.wallethero.app/wallethero-api/workspace/WORKSPACE_ID/integrations/idpos" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"enabled_inbound": true,
"config": { "base_url": "https://gateway.idpos.example", "location_ids": [1], "default_location_id": 1 },
"secrets": { "api_key": "SECRET_VALUE" }
}'Response (200)
{
"data": {
"id": "c1...",
"provider": "idpos",
"enabled_inbound": true,
"inbox_ingest_url": "https://webhook.wallethero.app/ingest/TOKEN",
"config": { "base_url": "https://gateway.idpos.example", "location_ids": [1] },
"secrets_meta": { "api_key": { "last4": "alue" } }
},
"token": "INBOX_INGEST_TOKEN"
}token is present only when inbound was just auto-provisioned. A save_warning string is included when the save succeeded but a provider post-save hook (e.g. IDPoS webhook reconcile) failed.
SDK
const { data, token } = await wh.integrations.upsert(workspaceId, "idpos", {
enabled_inbound: true,
config: { base_url: "https://gateway.idpos.example", location_ids: [1], default_location_id: 1 },
secrets: { api_key: "SECRET_VALUE" },
});Delete Integration
DELETE /wallethero-api/workspace/:workspaceId/integrations/:provider
Deletes the integration config and best-effort revokes its webhook inbox. Responds 204 with no body.
Auth: Bearer token — workspace member.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
workspaceId | string (UUID) | Workspace identifier |
provider | string | Provider slug |
Example Request
curl -X DELETE "https://api.wallethero.app/wallethero-api/workspace/WORKSPACE_ID/integrations/idpos" \
-H "Authorization: Bearer YOUR_TOKEN"Response (204)
No content.
SDK
await wh.integrations.delete(workspaceId, "idpos");Provision Inbox
POST /wallethero-api/workspace/:workspaceId/integrations/:provider/provision-inbox
Idempotently creates a webhook inbox for inbound events and returns the ingest URL + token. If an inbox already exists upstream the token is not regenerated (token is null) — use rotate-inbox-token to mint a new one. Only valid for providers that receive webhooks.
Auth: Bearer token — workspace member.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
workspaceId | string (UUID) | Workspace identifier |
provider | string | Provider slug |
Example Request
curl -X POST "https://api.wallethero.app/wallethero-api/workspace/WORKSPACE_ID/integrations/idpos/provision-inbox" \
-H "Authorization: Bearer YOUR_TOKEN"Response (200)
{
"data": {
"id": "c1...",
"provider": "idpos",
"inbox_id": "ibx_123",
"inbox_ingest_url": "https://webhook.wallethero.app/ingest/TOKEN"
},
"token": "INBOX_INGEST_TOKEN"
}SDK
const { data, token } = await wh.integrations.provisionInbox(workspaceId, "idpos");Rotate Inbox Token
POST /wallethero-api/workspace/:workspaceId/integrations/:provider/rotate-inbox-token
Rotates the inbound inbox token, invalidating the old ingest URL. Requires an already-provisioned inbox.
Auth: Bearer token — workspace member.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
workspaceId | string (UUID) | Workspace identifier |
provider | string | Provider slug |
Example Request
curl -X POST "https://api.wallethero.app/wallethero-api/workspace/WORKSPACE_ID/integrations/idpos/rotate-inbox-token" \
-H "Authorization: Bearer YOUR_TOKEN"Response (200)
{
"data": {
"id": "c1...",
"inbox_ingest_url": "https://webhook.wallethero.app/ingest/NEW_TOKEN"
},
"token": "NEW_INBOX_INGEST_TOKEN"
}SDK
const { data, token } = await wh.integrations.rotateInboxToken(workspaceId, "idpos");Integration Health
GET /wallethero-api/workspace/:workspaceId/integrations/:provider/health
Returns the saved config plus live webhook-inbox queue stats (pending/leased/processed/failed counts, events processed in the last 24h, and the most recent failed event). When the recorded inbox no longer exists upstream the stale reference is cleared and inbox_missing: true is returned (the integration self-heals by re-provisioning if inbound is on).
Auth: Bearer token — workspace member.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
workspaceId | string (UUID) | Workspace identifier |
provider | string | Provider slug |
Example Request
curl "https://api.wallethero.app/wallethero-api/workspace/WORKSPACE_ID/integrations/idpos/health" \
-H "Authorization: Bearer YOUR_TOKEN"Response (200)
{
"data": {
"config": { "id": "c1...", "provider": "idpos", "enabled_inbound": true },
"inbox": {
"counts": { "pending": 0, "leased": 0, "processed": 124, "failed": 2 },
"processed_24h": 30,
"last_failed": {
"id": "evt_999",
"received_at": "2026-06-02T08:00:00.000Z",
"last_error": "signature mismatch",
"attempts": 3
}
}
}
}inbox is null when no inbox is provisioned (or stats are temporarily unavailable). data.inbox_missing: true appears when the upstream inbox was lost.
SDK
const { data } = await wh.integrations.health(workspaceId, "idpos");Test Connection
POST /wallethero-api/workspace/:workspaceId/integrations/:provider/test-connection
Tests connectivity to the provider. You may pass credentials in the body to validate creds before saving them; omit the body to test the stored credentials. Providers without a tester return { ok: true, note: "no test for this provider" }. For IDPoS a successful test also returns the location catalog so a setup wizard can render a picker.
Auth: Bearer token — workspace member.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
workspaceId | string (UUID) | Workspace identifier |
provider | string | Provider slug |
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
credentials | object | No | Provider-specific credentials to test (IDPoS: client_id/api_key; KosmetologAPI: email/password). Omit to test stored credentials |
Example Request
curl -X POST "https://api.wallethero.app/wallethero-api/workspace/WORKSPACE_ID/integrations/idpos/test-connection" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "credentials": { "client_id": "...", "api_key": "..." } }'Response (200)
{
"data": {
"ok": true,
"status": 200,
"locations": [{ "id": 1, "name": "Main store" }]
}
}SDK
const { data } = await wh.integrations.testConnection(workspaceId, "idpos", {
credentials: { client_id: "...", api_key: "..." },
});Start Historical Import
POST /wallethero-api/workspace/:workspaceId/integrations/:provider/import-transactions
Kicks off a historical transaction import for a date range. The request is validated by the provider module, a job row is inserted (status: "pending"), and a background worker performs the fetch+ingest. Returns immediately with the job so you can poll for progress. Only available for providers with the import capability.
Auth: Bearer token — workspace member.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
workspaceId | string (UUID) | Workspace identifier |
provider | string | Provider slug (must support import) |
Request Body
Provider-specific and validated by the provider module. For IDPoS:
| Field | Type | Required | Description |
|---|---|---|---|
date_from | string | Yes | Start of range |
date_to | string | Yes | End of range |
location_ids | number[] | Yes | Locations to import (subset of the configured locations) |
Example Request
curl -X POST "https://api.wallethero.app/wallethero-api/workspace/WORKSPACE_ID/integrations/idpos/import-transactions" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{ "date_from": "2026-05-01", "date_to": "2026-05-31", "location_ids": [1] }'Response (200)
{
"data": {
"id": "job_123",
"workspace_id": "WORKSPACE_ID",
"provider": "idpos",
"status": "pending",
"date_from": "2026-05-01",
"date_to": "2026-05-31",
"location_ids": [1],
"fetch_total": 31,
"fetch_done": 0,
"processed_count": 0,
"imported_count": 0,
"date_created": "2026-06-02T10:00:00.000Z"
}
}SDK
const { data } = await wh.integrations.startImport(workspaceId, "idpos", {
date_from: "2026-05-01",
date_to: "2026-05-31",
location_ids: [1],
});List Import Jobs
GET /wallethero-api/workspace/:workspaceId/integrations/:provider/import-jobs
Lists recent import jobs for the provider, most recent first. Only available for import-capable providers.
Auth: Bearer token — workspace member.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
workspaceId | string (UUID) | Workspace identifier |
provider | string | Provider slug (must support import) |
Query Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
limit | number | No | 20 | Max jobs to return |
Example Request
curl "https://api.wallethero.app/wallethero-api/workspace/WORKSPACE_ID/integrations/idpos/import-jobs?limit=20" \
-H "Authorization: Bearer YOUR_TOKEN"Response (200)
{
"data": [
{
"id": "job_123",
"status": "running",
"phase": "fetching",
"fetch_total": 31,
"fetch_done": 12,
"imported_count": 340,
"date_created": "2026-06-02T10:00:00.000Z"
}
]
}SDK
const { data } = await wh.integrations.listImportJobs(workspaceId, "idpos", 20);Get Import Job
GET /wallethero-api/workspace/:workspaceId/integrations/:provider/import-jobs/:jobId
Fetches a single job's status, counters, and error log. Poll this while a job is pending/running. Responds 404 when the job doesn't exist.
Auth: Bearer token — workspace member.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
workspaceId | string (UUID) | Workspace identifier |
provider | string | Provider slug (must support import) |
jobId | string (UUID) | Import job identifier |
Example Request
curl "https://api.wallethero.app/wallethero-api/workspace/WORKSPACE_ID/integrations/idpos/import-jobs/JOB_ID" \
-H "Authorization: Bearer YOUR_TOKEN"Response (200)
{
"data": {
"id": "JOB_ID",
"status": "completed",
"phase": null,
"fetch_total": 31,
"fetch_done": 31,
"total_fetched": 1200,
"processed_count": 1200,
"imported_count": 1180,
"skipped_count": 20,
"failed_count": 0,
"error_log": [],
"last_error": null,
"completed_at": "2026-06-02T10:20:00.000Z"
}
}SDK
const result = await wh.integrations.getImportJob(workspaceId, "idpos", jobId);
// result is null when the job is not foundCancel Import Job
POST /wallethero-api/workspace/:workspaceId/integrations/:provider/import-jobs/:jobId/cancel
Requests cancellation. A pending job is cancelled immediately; a running job's cancel_requested flag is set and the worker stops after the in-flight row, preserving partial counters. Responds 404 when the job doesn't exist.
Auth: Bearer token — workspace member.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
workspaceId | string (UUID) | Workspace identifier |
provider | string | Provider slug (must support import) |
jobId | string (UUID) | Import job identifier |
Example Request
curl -X POST "https://api.wallethero.app/wallethero-api/workspace/WORKSPACE_ID/integrations/idpos/import-jobs/JOB_ID/cancel" \
-H "Authorization: Bearer YOUR_TOKEN"Response (200)
{
"data": {
"id": "JOB_ID",
"status": "cancelled",
"cancel_requested": true
}
}SDK
const { data } = await wh.integrations.cancelImportJob(workspaceId, "idpos", jobId);Provider-specific endpoints
Beyond the generic surface above, each provider module mounts its own routes under /wallethero-api/workspace/:workspaceId/integrations/<slug>/…. For IDPoS these include the location/discount/customer-group catalogs, customer-id backfill, and upstream webhook register/unregister/status, exposed via dedicated SDK methods (wh.integrations.listIdposLocations, listIdposDiscounts, listIdposCustomerGroups, backfillIdposCustomerIds, getIdposWebhookStatus, registerIdposWebhook, unregisterIdposWebhook). See the IDPoS provider reference for details.
IDPoS Provider Endpoints
These routes are mounted per-provider under the idpos slug (prefix /wallethero-api/workspace/:workspaceId/integrations/idpos) and require the IDPoS integration to be configured for the workspace. All run behind the workspace-member guard.
Backfill Customer IDs
POST /wallethero-api/workspace/:workspaceId/integrations/idpos/backfill-customer-ids
Stamps a random N-digit ID onto the configured client custom field for every client in the workspace whose field is currently empty. Only valid when the IDPoS customer_id_mapping is set to custom_field with auto_generate.enabled = true; otherwise responds 400. Iterates clients in batches and reports per-client failures.
Auth: Bearer token — workspace member.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
workspaceId | string (UUID) | Workspace identifier |
Example Request
curl -X POST "https://api.wallethero.app/wallethero-api/workspace/WORKSPACE_ID/integrations/idpos/backfill-customer-ids" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json"Response (200)
{
"data": {
"processed": 120,
"generated": 118,
"skipped_already_set": 0,
"errors": [
{ "client_id": "5d2b…", "error": "Failed to generate unique id" }
]
}
}SDK
const { data } = await wh.integrations.backfillIdposCustomerIds(workspaceId);List Locations
GET /wallethero-api/workspace/:workspaceId/integrations/idpos/locations
Fetches the live IDPoS Location catalog so the config page can resolve names for the saved location_ids and offer additions without re-running a connection test.
Auth: Bearer token — workspace member.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
workspaceId | string (UUID) | Workspace identifier |
Example Request
curl "https://api.wallethero.app/wallethero-api/workspace/WORKSPACE_ID/integrations/idpos/locations" \
-H "Authorization: Bearer YOUR_TOKEN"Response (200)
{
"data": [
{ "id": 1, "name": "Main Store" },
{ "id": 2, "name": "Airport Kiosk" }
]
}SDK
const { data } = await wh.integrations.listIdposLocations(workspaceId);List Discounts
GET /wallethero-api/workspace/:workspaceId/integrations/idpos/discounts
Fetches the live IDPoS discount catalog, used to render the discount IDs that live on each CustomerGroup so operators can pick the right group per tier.
Auth: Bearer token — workspace member.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
workspaceId | string (UUID) | Workspace identifier |
Example Request
curl "https://api.wallethero.app/wallethero-api/workspace/WORKSPACE_ID/integrations/idpos/discounts" \
-H "Authorization: Bearer YOUR_TOKEN"Response (200)
{
"data": [
{
"id": 10,
"name": "VIP 10%",
"value": 10,
"discountType": "Percentage",
"isActive": true
}
]
}SDK
const { data } = await wh.integrations.listIdposDiscounts(workspaceId);List Customer Groups
GET /wallethero-api/workspace/:workspaceId/integrations/idpos/customer-groups
Fetches the live IDPoS CustomerGroup catalog so operators can map each WalletHero tier to a group. The group's discounts array is what drives the customer's rebates in IDPoS after a tier change.
Auth: Bearer token — workspace member.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
workspaceId | string (UUID) | Workspace identifier |
Example Request
curl "https://api.wallethero.app/wallethero-api/workspace/WORKSPACE_ID/integrations/idpos/customer-groups" \
-H "Authorization: Bearer YOUR_TOKEN"Response (200)
{
"data": [
{ "id": 7, "name": "Gold", "discounts": [10, 11] }
]
}SDK
const { data } = await wh.integrations.listIdposCustomerGroups(workspaceId);Webhook Status
GET /wallethero-api/workspace/:workspaceId/integrations/idpos/webhook/status
Reports per-location webhook registration status. For each entry in location_ids it indicates whether a local webhook id is stored and whether it's still alive upstream; stale entries (id stored but IDPoS returned 404) are pruned from the stored map. registered is true only when every configured location has a verified upstream webhook.
Auth: Bearer token — workspace member.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
workspaceId | string (UUID) | Workspace identifier |
Example Request
curl "https://api.wallethero.app/wallethero-api/workspace/WORKSPACE_ID/integrations/idpos/webhook/status" \
-H "Authorization: Bearer YOUR_TOKEN"Response (200)
{
"data": {
"registered": true,
"has_any": true,
"locations": [
{ "location_id": 1, "webhook_id": 42, "registered": true, "stale": false },
{ "location_id": 2, "webhook_id": null, "registered": false, "stale": false }
]
}
}SDK
const { data } = await wh.integrations.getIdposWebhookStatus(workspaceId);Register Webhooks
POST /wallethero-api/workspace/:workspaceId/integrations/idpos/webhook/register
Reconciles per-location webhook registrations in IDPoS to match location_ids. Idempotent — re-running after a config change creates webhooks for new locations and removes them for unselected ones. Requires inbound to be provisioned first. Persists the resulting id map and returns it.
Auth: Bearer token — workspace member.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
workspaceId | string (UUID) | Workspace identifier |
Example Request
curl -X POST "https://api.wallethero.app/wallethero-api/workspace/WORKSPACE_ID/integrations/idpos/webhook/register" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json"Response (200)
{
"data": {
"registered": true,
"idpos_webhook_ids": { "1": 42, "2": 43 }
}
}SDK
const { data } = await wh.integrations.registerIdposWebhook(workspaceId);Unregister Webhooks
POST /wallethero-api/workspace/:workspaceId/integrations/idpos/webhook/unregister
Removes all per-location webhooks from IDPoS and clears the locally-stored id map. Idempotent — missing upstream entries (404) are tolerated.
Auth: Bearer token — workspace member.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
workspaceId | string (UUID) | Workspace identifier |
Example Request
curl -X POST "https://api.wallethero.app/wallethero-api/workspace/WORKSPACE_ID/integrations/idpos/webhook/unregister" \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json"Response (200)
{
"data": { "registered": false }
}SDK
const { data } = await wh.integrations.unregisterIdposWebhook(workspaceId);