{
  "openapi": "3.0.3",
  "info": {
    "title": "WalletMe Developer API",
    "version": "1.0.0",
    "description": "Integrator surface for this company: customers, custom fields, and card field values.\n\n**Auth:** `Authorization: Bearer wh_live_…` (company API key). Creating a customer with an API key requires `Idempotency-Key`.\n\n**Cards:** Updating a customer value for a token placed on the template refreshes that person's wallet card. Use `POST /v1/customers/{customerId}/sync` from the dashboard session when you want to push an update email later."
  },
  "servers": [
    { "url": "https://sandbox-api.walletmeinc.com", "description": "UAT" },
    { "url": "https://api.walletmeinc.com", "description": "Production" },
    { "url": "https://sandbox-api.walletmelocal.com", "description": "Local Compose" }
  ],
  "tags": [
    {
      "name": "Customers",
      "description": "People on your cards. Create opens a wallet. List returns groups and company custom-field defs for forms."
    },
    {
      "name": "Customer custom fields",
      "description": "Company-wide named fields shared by every customer. Their keys can be placed as tokens on Templates."
    },
    {
      "name": "Card fields",
      "description": "Read the card template and apply design changes (place or remove tokens). Per-person values stay on the customer."
    }
  ],
  "paths": {
    "/v1/customers": {
      "get": {
        "tags": ["Customers"],
        "operationId": "listCustomers",
        "summary": "List customers",
        "description": "Returns one page of customers for this company, plus the groups you can assign and the company custom-field definitions. Use `cursor` from a previous response to fetch the next page.",
        "security": [{ "ApiKeyAuth": [] }],
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size. Defaults to 50. Maximum 100.",
            "schema": { "type": "integer", "minimum": 1, "maximum": 100, "default": 50 }
          },
          {
            "name": "cursor",
            "in": "query",
            "required": false,
            "description": "Opaque cursor from the previous page. Omit on the first request. Company-scoped — do not reuse across companies.",
            "schema": { "type": "string" }
          }
        ],
        "responses": {
          "200": {
            "description": "Page of customers, groups, and customerCustomFields.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/CustomerList" },
                "examples": {
                  "page": {
                    "summary": "First page",
                    "value": {
                      "company": { "companyId": "11111111-1111-1111-1111-111111111111", "name": "Demo Gym" },
                      "groups": [{ "groupId": "22222222-2222-2222-2222-222222222222", "name": "Main" }],
                      "customerCustomFields": [{ "id": "33333333-3333-3333-3333-333333333333", "name": "Tier", "key": "tier", "required": false }],
                      "customers": [
                        {
                          "customerId": "44444444-4444-4444-4444-444444444444",
                          "displayName": "Ada Lovelace",
                          "email": "ada@example.com",
                          "balance": "$12.00",
                          "balanceCents": 1200
                        }
                      ],
                      "cursor": null
                    }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "source": "curl -sS \"$API/v1/customers?limit=50\" \\\n  -H \"Authorization: Bearer $API_KEY\""
          },
          {
            "lang": "JavaScript",
            "source": "const res = await fetch(`${api}/v1/customers?limit=50`, {\n  headers: { Authorization: `Bearer ${apiKey}` },\n});\nconst page = await res.json();"
          },
          {
            "lang": "Python",
            "source": "import requests\nr = requests.get(f\"{api}/v1/customers\", headers={\"Authorization\": f\"Bearer {api_key}\"}, params={\"limit\": 50})\npage = r.json()"
          }
        ]
      },
      "post": {
        "tags": ["Customers"],
        "operationId": "createCustomer",
        "summary": "Add a customer",
        "description": "Creates a customer in a group and opens their wallet card. With an API key, `Idempotency-Key` is required: the same key and body replays the first create; a different body with the same key is `idempotency_conflict`.\n\n`sendWelcomeEmail` is optional (dashboard). Integrator creates typically omit it or set false unless you intend to queue the pass email.",
        "security": [{ "ApiKeyAuth": [] }],
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": true,
            "schema": { "type": "string", "minLength": 8 },
            "description": "Required for company API keys. Unique per create attempt."
          }
        ],
        "requestBody": {
          "required": true,
          "description": "Choose a body shape for your use case. Minimum required is name, email, and groupId.",
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/CreateCustomer" },
              "examples": {
                "minimum": {
                  "summary": "Minimum required",
                  "description": "Only the fields required to create the person and open a $0 wallet.",
                  "value": {
                    "displayName": "Ada Lovelace",
                    "email": "ada@example.com",
                    "groupId": "22222222-2222-2222-2222-222222222222"
                  }
                },
                "withBalance": {
                  "summary": "With starting balance",
                  "description": "Store money in cents on create.",
                  "value": {
                    "displayName": "Ada Lovelace",
                    "email": "ada@example.com",
                    "groupId": "22222222-2222-2222-2222-222222222222",
                    "balanceCents": 2500
                  }
                },
                "withExternalIds": {
                  "summary": "CRM / HighLevel link",
                  "description": "Attach integrator external ids for webhooks and sync.",
                  "value": {
                    "displayName": "Ada Lovelace",
                    "email": "ada@example.com",
                    "groupId": "22222222-2222-2222-2222-222222222222",
                    "externalIds": { "Go high level": "contact_123" }
                  }
                },
                "withCardTokens": {
                  "summary": "Card token values",
                  "description": "Set values for tokens already placed on the template.",
                  "value": {
                    "displayName": "Ada Lovelace",
                    "email": "ada@example.com",
                    "groupId": "22222222-2222-2222-2222-222222222222",
                    "tokenValues": { "memberName": "Ada", "tier": "Gold" },
                    "customFieldValues": { "33333333-3333-3333-3333-333333333333": "Gold" }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Created (or idempotent replay). Wallet card is opened.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Customer" },
                "examples": {
                  "created": {
                    "summary": "New customer",
                    "value": {
                      "customerId": "44444444-4444-4444-4444-444444444444",
                      "displayName": "Ada Lovelace",
                      "email": "ada@example.com",
                      "balanceCents": 0,
                      "balance": "$0.00",
                      "wallet": { "cardId": "55555555-5555-5555-5555-555555555555", "contentVersion": 1 }
                    }
                  }
                }
              }
            }
          },
          "400": { "$ref": "#/components/responses/ValidationFailed" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "409": { "$ref": "#/components/responses/Conflict" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "source": "curl -sS -X POST \"$API/v1/customers\" \\\n  -H \"Authorization: Bearer $API_KEY\" \\\n  -H \"Idempotency-Key: create-ada-1\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"displayName\":\"Ada Lovelace\",\"email\":\"ada@example.com\",\"groupId\":\"GROUP_ID\"}'"
          },
          {
            "lang": "JavaScript",
            "source": "const res = await fetch(`${api}/v1/customers`, {\n  method: 'POST',\n  headers: {\n    Authorization: `Bearer ${apiKey}`,\n    'Idempotency-Key': 'create-ada-1',\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({\n    displayName: 'Ada Lovelace',\n    email: 'ada@example.com',\n    groupId,\n  }),\n});"
          },
          {
            "lang": "Python",
            "source": "import requests, uuid\nr = requests.post(\n  f\"{api}/v1/customers\",\n  headers={\n    \"Authorization\": f\"Bearer {api_key}\",\n    \"Idempotency-Key\": str(uuid.uuid4()),\n    \"Content-Type\": \"application/json\",\n  },\n  json={\"displayName\": \"Ada Lovelace\", \"email\": \"ada@example.com\", \"groupId\": group_id},\n)"
          }
        ]
      }
    },
    "/v1/customers/{customerId}": {
      "get": {
        "tags": ["Customers"],
        "operationId": "getCustomer",
        "summary": "Get one customer",
        "description": "Full customer record plus company custom-field defs and card tokens from the current template (excluding balance).",
        "security": [{ "ApiKeyAuth": [] }],
        "parameters": [{ "$ref": "#/components/parameters/customerId" }],
        "responses": {
          "200": {
            "description": "Customer detail.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/CustomerDetail" }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "source": "curl -sS \"$API/v1/customers/$CUSTOMER_ID\" \\\n  -H \"Authorization: Bearer $API_KEY\""
          },
          {
            "lang": "JavaScript",
            "source": "const res = await fetch(`${api}/v1/customers/${customerId}`, {\n  headers: { Authorization: `Bearer ${apiKey}` },\n});"
          }
        ]
      },
      "patch": {
        "tags": ["Customers"],
        "operationId": "updateCustomer",
        "summary": "Edit customer fields",
        "description": "Partial update. Changing balance, customFieldValues, tokenValues, or group refreshes wallet content versions. Does not queue an update email by itself — use sync from the dashboard when you want to notify.",
        "security": [{ "ApiKeyAuth": [] }],
        "parameters": [{ "$ref": "#/components/parameters/customerId" }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/UpdateCustomer" },
              "examples": {
                "balanceOnly": {
                  "summary": "Balance only",
                  "value": { "balanceCents": 5000 }
                },
                "nameEmail": {
                  "summary": "Name and email",
                  "value": { "displayName": "Ada L.", "email": "ada.l@example.com" }
                },
                "cardTokens": {
                  "summary": "Update card token values",
                  "value": { "tokenValues": { "tier": "Platinum" } }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated customer.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Customer" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/ValidationFailed" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "409": { "$ref": "#/components/responses/Conflict" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "source": "curl -sS -X PATCH \"$API/v1/customers/$CUSTOMER_ID\" \\\n  -H \"Authorization: Bearer $API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"balanceCents\":5000}'"
          },
          {
            "lang": "JavaScript",
            "source": "await fetch(`${api}/v1/customers/${customerId}`, {\n  method: 'PATCH',\n  headers: {\n    Authorization: `Bearer ${apiKey}`,\n    'Content-Type': 'application/json',\n  },\n  body: JSON.stringify({ balanceCents: 5000 }),\n});"
          }
        ]
      },
      "delete": {
        "tags": ["Customers"],
        "operationId": "deleteCustomer",
        "summary": "Remove a customer",
        "description": "Deletes the customer and their wallet card from this company. Irreversible.",
        "security": [{ "ApiKeyAuth": [] }],
        "parameters": [{ "$ref": "#/components/parameters/customerId" }],
        "responses": {
          "200": {
            "description": "Removed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": ["ok", "customerId"],
                  "properties": {
                    "ok": { "type": "boolean", "description": "Always true on success." },
                    "customerId": { "type": "string", "format": "uuid", "description": "The removed customer id." }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "source": "curl -sS -X DELETE \"$API/v1/customers/$CUSTOMER_ID\" \\\n  -H \"Authorization: Bearer $API_KEY\""
          }
        ]
      }
    },
    "/v1/customer-custom-fields": {
      "get": {
        "tags": ["Customer custom fields"],
        "operationId": "listCustomerCustomFields",
        "summary": "List custom field definitions",
        "description": "Company-wide field defs. Use `id` as the key in `customFieldValues` on create/update customer. `key` is the token-friendly slug for Templates.",
        "security": [{ "ApiKeyAuth": [] }],
        "responses": {
          "200": {
            "description": "Field list.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "companyId": { "type": "string", "format": "uuid" },
                    "fields": {
                      "type": "array",
                      "items": { "$ref": "#/components/schemas/CustomerCustomField" }
                    }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "source": "curl -sS \"$API/v1/customer-custom-fields\" -H \"Authorization: Bearer $API_KEY\""
          }
        ]
      },
      "post": {
        "tags": ["Customer custom fields"],
        "operationId": "createCustomerCustomField",
        "summary": "Add a custom field",
        "description": "Adds a named field for every customer. Optional `required` marks it required on customer forms.",
        "security": [{ "ApiKeyAuth": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["name"],
                "properties": {
                  "name": {
                    "type": "string",
                    "minLength": 1,
                    "description": "Human label, e.g. Membership tier. Required.",
                    "example": "Membership tier"
                  },
                  "required": {
                    "type": "boolean",
                    "default": false,
                    "description": "When true, customer create/update forms should require a value."
                  }
                }
              },
              "examples": {
                "optional": {
                  "summary": "Optional field",
                  "value": { "name": "Membership tier" }
                },
                "requiredField": {
                  "summary": "Required field",
                  "value": { "name": "Member ID", "required": true }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Created field.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/CustomerCustomField" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/ValidationFailed" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "409": { "$ref": "#/components/responses/Conflict" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "source": "curl -sS -X POST \"$API/v1/customer-custom-fields\" \\\n  -H \"Authorization: Bearer $API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"name\":\"Membership tier\"}'"
          }
        ]
      }
    },
    "/v1/customer-custom-fields/{fieldId}": {
      "patch": {
        "tags": ["Customer custom fields"],
        "operationId": "updateCustomerCustomField",
        "summary": "Edit a custom field",
        "description": "Rename the field or change whether it is required. Does not change existing customer values.",
        "security": [{ "ApiKeyAuth": [] }],
        "parameters": [{ "$ref": "#/components/parameters/fieldId" }],
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": { "type": "string", "description": "New label. Optional." },
                  "required": { "type": "boolean", "description": "Whether forms require a value. Optional." }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated field.",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/CustomerCustomField" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/ValidationFailed" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      },
      "delete": {
        "tags": ["Customer custom fields"],
        "operationId": "deleteCustomerCustomField",
        "summary": "Remove a custom field",
        "description": "Deletes the definition and clears that field's values from every customer.",
        "security": [{ "ApiKeyAuth": [] }],
        "parameters": [{ "$ref": "#/components/parameters/fieldId" }],
        "responses": {
          "200": {
            "description": "Removed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": { "type": "boolean" },
                    "id": { "type": "string", "format": "uuid" }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "404": { "$ref": "#/components/responses/NotFound" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        }
      }
    },
    "/v1/card-studio": {
      "get": {
        "tags": ["Card fields"],
        "operationId": "getCardStudio",
        "summary": "Read the card template",
        "description": "Returns the current card design document (tokens, field placements, colors) and customerFields available to place. Session login can edit; API keys may read depending on key mode.",
        "security": [{ "ApiKeyAuth": [] }],
        "parameters": [
          {
            "name": "facilityId",
            "in": "query",
            "required": false,
            "schema": { "type": "string", "format": "uuid" },
            "description": "Facility template. Omit for the company-wide template."
          }
        ],
        "responses": {
          "200": {
            "description": "Studio payload.",
            "content": {
              "application/json": {
                "schema": { "type": "object", "description": "Includes document.tokens, document.fields, customerFields, version." },
                "examples": {
                  "company": {
                    "summary": "Company template",
                    "value": {
                      "document": {
                        "name": "Membership",
                        "tokens": [{ "key": "balance", "label": "Balance", "type": "number" }],
                        "fields": [{ "slot": "primary", "tokenKey": "balance" }]
                      },
                      "version": 3
                    }
                  }
                }
              }
            }
          },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "source": "curl -sS \"$API/v1/card-studio\" -H \"Authorization: Bearer $API_KEY\""
          }
        ]
      }
    },
    "/v1/card-studio/apply": {
      "post": {
        "tags": ["Card fields"],
        "operationId": "applyCardStudio",
        "summary": "Apply or remove card fields",
        "description": "Saves a new design version. Send the full document with tokens and field placements. Per-customer values are not set here — use PATCH customer `tokenValues` / `customFieldValues`.",
        "security": [{ "ApiKeyAuth": [] }],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": ["document"],
                "properties": {
                  "facilityId": {
                    "type": "string",
                    "format": "uuid",
                    "nullable": true,
                    "description": "Facility scope, or null/omit for company."
                  },
                  "note": { "type": "string", "description": "Optional version note." },
                  "document": {
                    "type": "object",
                    "description": "Full card document: name, colors, tokens[], fields[]. Required."
                  }
                }
              },
              "examples": {
                "placeTier": {
                  "summary": "Place a tier token",
                  "value": {
                    "note": "Add tier",
                    "document": {
                      "name": "Membership",
                      "tokens": [
                        { "key": "balance", "label": "Balance", "type": "number", "defaultValue": "0" },
                        { "key": "tier", "label": "Tier", "type": "string", "defaultValue": "Member" }
                      ],
                      "fields": [
                        { "slot": "primary", "tokenKey": "balance" },
                        { "slot": "secondary", "tokenKey": "tier" }
                      ]
                    }
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Saved design version.",
            "content": {
              "application/json": {
                "schema": { "type": "object" }
              }
            }
          },
          "400": { "$ref": "#/components/responses/ValidationFailed" },
          "401": { "$ref": "#/components/responses/Unauthorized" },
          "403": { "$ref": "#/components/responses/Forbidden" },
          "429": { "$ref": "#/components/responses/RateLimited" }
        },
        "x-codeSamples": [
          {
            "lang": "curl",
            "source": "curl -sS -X POST \"$API/v1/card-studio/apply\" \\\n  -H \"Authorization: Bearer $API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d @document.json"
          }
        ]
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ApiKeyAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "wh_live_",
        "description": "Company API key from Developer → API key management. Header: `Authorization: Bearer wh_live_…`."
      }
    },
    "parameters": {
      "customerId": {
        "name": "customerId",
        "in": "path",
        "required": true,
        "description": "Customer.id (UUID). Same as customerId on list/detail.",
        "schema": { "type": "string", "format": "uuid" }
      },
      "fieldId": {
        "name": "fieldId",
        "in": "path",
        "required": true,
        "description": "CustomerCustomField.id (UUID).",
        "schema": { "type": "string", "format": "uuid" }
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Missing or invalid API key / session.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/ApiError" },
            "example": { "statusCode": 401, "code": "unauthorized", "message": "Unauthorized" }
          }
        }
      },
      "Forbidden": {
        "description": "Key or login cannot perform this action (pending company, wrong role, Origin with API key, etc.).",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/ApiError" },
            "example": { "statusCode": 403, "code": "forbidden", "message": "This account cannot perform the action." }
          }
        }
      },
      "NotFound": {
        "description": "No such customer or field in this company.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/ApiError" },
            "example": { "statusCode": 404, "code": "not_found", "message": "Customer not found" }
          }
        }
      },
      "ValidationFailed": {
        "description": "Request body or query failed validation.",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/ApiError" },
            "example": { "statusCode": 400, "code": "validation_failed", "message": "email must be an email" }
          }
        }
      },
      "Conflict": {
        "description": "Duplicate email/name, or Idempotency-Key reused with a different body (`idempotency_conflict`).",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/ApiError" },
            "example": { "statusCode": 409, "code": "idempotency_conflict", "message": "Idempotency-Key was reused with a different body" }
          }
        }
      },
      "RateLimited": {
        "description": "API key per-minute ceiling exceeded. Respect Retry-After.",
        "headers": {
          "Retry-After": {
            "description": "Seconds until the window resets.",
            "schema": { "type": "integer" }
          }
        },
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/ApiError" },
            "example": { "statusCode": 429, "code": "rate_limited", "message": "Too many requests" }
          }
        }
      }
    },
    "schemas": {
      "ApiError": {
        "type": "object",
        "required": ["statusCode", "message"],
        "properties": {
          "statusCode": { "type": "integer", "description": "HTTP status." },
          "code": { "type": "string", "description": "Machine code when present (unauthorized, forbidden, not_found, …)." },
          "message": { "type": "string", "description": "Human-readable error." }
        }
      },
      "CreateCustomer": {
        "type": "object",
        "required": ["displayName", "email", "groupId"],
        "properties": {
          "displayName": {
            "type": "string",
            "minLength": 1,
            "description": "Display name on the card and in the directory. Required."
          },
          "email": {
            "type": "string",
            "format": "email",
            "description": "Unique per company. Used for pass email when welcome is sent. Required."
          },
          "groupId": {
            "type": "string",
            "format": "uuid",
            "description": "Target group from listCustomers.groups. Required."
          },
          "balanceCents": {
            "type": "integer",
            "minimum": 0,
            "description": "Starting balance in cents. Optional; defaults to 0."
          },
          "sendWelcomeEmail": {
            "type": "boolean",
            "description": "When true, queues the wallet welcome email. Optional; dashboard default is off."
          },
          "externalIds": {
            "type": "object",
            "additionalProperties": { "type": "string" },
            "description": "Optional integrator key/value map (e.g. CRM contact id)."
          },
          "customFieldValues": {
            "type": "object",
            "additionalProperties": { "type": "string" },
            "description": "Optional map of CustomerCustomField.id → value."
          },
          "tokenValues": {
            "type": "object",
            "additionalProperties": { "type": "string" },
            "description": "Optional map of card token key → value for tokens on the template."
          }
        }
      },
      "UpdateCustomer": {
        "type": "object",
        "description": "All properties optional; send only what changes.",
        "properties": {
          "displayName": { "type": "string", "minLength": 1, "description": "New display name. Optional." },
          "email": { "type": "string", "format": "email", "description": "New email. Optional." },
          "groupId": { "type": "string", "format": "uuid", "description": "Move to another group. Optional." },
          "balanceCents": { "type": "integer", "minimum": 0, "description": "New balance in cents. Optional." },
          "externalIds": {
            "type": "object",
            "additionalProperties": { "type": "string" },
            "description": "Replace external id map. Optional."
          },
          "customFieldValues": {
            "type": "object",
            "additionalProperties": { "type": "string" },
            "description": "Replace custom field values by field id. Optional."
          },
          "tokenValues": {
            "type": "object",
            "additionalProperties": { "type": "string" },
            "description": "Replace card token values by token key. Optional."
          }
        }
      },
      "Customer": {
        "type": "object",
        "properties": {
          "customerId": { "type": "string", "format": "uuid", "description": "Public customer id." },
          "displayName": { "type": "string" },
          "email": { "type": "string", "nullable": true },
          "balanceCents": { "type": "integer", "description": "Balance in cents." },
          "balance": { "type": "string", "description": "Formatted money, e.g. $12.00." },
          "externalIds": { "type": "object", "additionalProperties": { "type": "string" } },
          "customFieldValues": { "type": "object", "additionalProperties": { "type": "string" } },
          "tokenValues": { "type": "object", "additionalProperties": { "type": "string" } },
          "group": {
            "type": "object",
            "description": "Assigned group.",
            "properties": {
              "groupId": { "type": "string", "format": "uuid" },
              "name": { "type": "string" }
            }
          },
          "wallet": {
            "type": "object",
            "description": "Wallet card pointers when present.",
            "properties": {
              "cardId": { "type": "string", "format": "uuid", "nullable": true },
              "contentVersion": { "type": "integer", "nullable": true },
              "amount": { "type": "string", "nullable": true }
            }
          }
        }
      },
      "CustomerList": {
        "type": "object",
        "properties": {
          "company": { "type": "object", "description": "Company summary." },
          "groups": { "type": "array", "items": { "type": "object" }, "description": "Groups for create/update forms." },
          "customerCustomFields": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/CustomerCustomField" }
          },
          "customers": { "type": "array", "items": { "$ref": "#/components/schemas/Customer" } },
          "cursor": { "type": "string", "nullable": true, "description": "Next page cursor, or null on the last page." }
        }
      },
      "CustomerDetail": {
        "type": "object",
        "properties": {
          "companyId": { "type": "string", "format": "uuid" },
          "customer": { "$ref": "#/components/schemas/Customer" },
          "customerCustomFields": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/CustomerCustomField" }
          },
          "cardTokens": {
            "type": "array",
            "description": "Non-balance tokens on the current template.",
            "items": {
              "type": "object",
              "properties": {
                "key": { "type": "string" },
                "label": { "type": "string" },
                "type": { "type": "string" },
                "defaultValue": { "type": "string" }
              }
            }
          }
        }
      },
      "CustomerCustomField": {
        "type": "object",
        "properties": {
          "id": { "type": "string", "format": "uuid", "description": "Use as key in customFieldValues." },
          "name": { "type": "string", "description": "Human label." },
          "key": { "type": "string", "description": "Slug for templates/tokens." },
          "required": { "type": "boolean", "description": "Whether forms require a value." },
          "sortOrder": { "type": "integer" }
        }
      }
    }
  }
}
