> For the complete documentation index, see [llms.txt](https://docs.thewalletcrew.io/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.thewalletcrew.io/api-reference/advanced/device-profiles.md).

# Device Profiles

## GET /api/{tenantId}/deviceProfiles

> List device profiles

```json
{"openapi":"3.1.1","info":{"title":"Neostore internal API","version":"v1"},"tags":[{"name":"DeviceProfiles"}],"servers":[{"url":"https://app.neostore.cloud","description":"Production Server"},{"url":"https://app-qa.neostore.cloud","description":"Staging Server"}],"security":[{"admin-bearer":["tenant.device:read"]},{"apiKey":[]}],"components":{"securitySchemes":{"admin-bearer":{"type":"oauth2","flows":{"implicit":{"authorizationUrl":"https://auth.neostore.cloud/authorize?audience=https://app.neostore.cloud/api/","scopes":{}}}},"apiKey":{"type":"apiKey","name":"X-API-KEY","in":"header"}},"schemas":{"DeviceProfile":{"type":"object","properties":{"id":{"type":"string","description":"Stable identifier, chosen on create."},"type":{"description":"Device type; only a device of that type can run it.","$ref":"#/components/schemas/DeviceType"},"name":{"type":"string","description":"Name shown to staff on the device."},"description":{"type":["null","string"],"description":"Free text for operators; null if none."},"emoji":{"type":["null","string"],"description":"Icon shown next to the name; null if none."},"color":{"type":["null","string"],"description":"Hex colour `#rrggbb`; null if none."},"validFrom":{"type":["null","string"],"description":"First usable instant, UTC; null for no start.","format":"date-time"},"validUntil":{"type":["null","string"],"description":"Instant it stops being usable, UTC; null for no end.","format":"date-time"},"schedule":{"type":"array","items":{"description":"Opening window: opens at each occurrence of Cron and stays open for DurationMinutes.","$ref":"#/components/schemas/DeviceProfileWindow"},"description":"Opening windows; empty means always open within the validity dates."},"settings":{"oneOf":[{"description":"What a Crew Check scanner does while running the profile.","$ref":"#/components/schemas/CrewCheckProfileSettings"}],"description":"Type-specific settings."},"warnings":{"type":"array","items":{"description":"Non-blocking issue an operator should know about.","$ref":"#/components/schemas/DeviceProfileWarning"},"description":"What an operator should fix; empty if nothing."}},"additionalProperties":false,"description":"Reusable scan context a device can run (Breakfast, Gate F, Cloakroom): what it accepts, and when."},"DeviceType":{"enum":["crewCheck"],"type":"string","description":"Device kind: decides its settings, profiles and API key permissions."},"DeviceProfileWindow":{"type":"object","properties":{"cron":{"type":"string","description":"Cron expression with seconds and no year, as in automations (`0 0 7 * * *` is daily at 07:00). Wall-clock time in the device's timezone; unparsable returns 400."},"durationMinutes":{"type":"integer","description":"Minutes the window stays open, 1 to 1440 (otherwise 400).","format":"int32"}},"additionalProperties":false,"description":"Opening window: opens at each occurrence of Cron and stays open for DurationMinutes."},"CrewCheckProfileSettings":{"required":["$type"],"type":"object","allOf":[{"description":"Type-specific settings. The JSON carries a `$type` discriminator (`crewCheck`) that must match the profile's `type`.","$ref":"#/components/schemas/DeviceProfileSettingsBase"}],"properties":{"scanFilter":{"description":"Benefits the device accepts; null or empty accepts all.","$ref":"#/components/schemas/ScanFilter"},"allowStamps":{"type":"boolean","description":"Whether staff can add stamps while the profile runs. Off, scanning a stamp card only shows it."},"redeemOnScan":{"description":"Redemption behaviour on scan; null applies defaults (5-second repeat-scan block).","$ref":"#/components/schemas/RedeemOnScanSettings"},"$type":{"enum":["crewCheck"],"type":"string","description":"Discriminator. Always `crewCheck` for this shape."}},"additionalProperties":false,"description":"What a Crew Check scanner does while running the profile."},"DeviceProfileSettingsBase":{"type":"object","description":"Type-specific settings. The JSON carries a `$type` discriminator (`crewCheck`) that must match the profile's `type`."},"ScanFilter":{"type":"object","properties":{"tags":{"type":"array","items":{"type":"string"},"description":"Benefit template tags to accept. At most 50, each non-blank, at most 100 characters; trimmed and deduplicated on save."},"benefitTemplateIds":{"type":"array","items":{"type":"string"},"description":"Benefit templates to accept. At most 200, none blank; trimmed and deduplicated on save. An unknown id is dropped with a `benefitTemplateMissing` warning, not refused."}},"additionalProperties":false,"description":"A benefit is accepted if it carries one of the tags or comes from one of the benefit templates. Both empty accepts every benefit."},"RedeemOnScanSettings":{"type":"object","properties":{"repeatScanBlockSeconds":{"type":"integer","description":"Seconds the device ignores a repeat scan of the same pass, 0 to 3600; 0 disables the block.","format":"int32"}},"additionalProperties":false,"description":"Redemption behaviour on scan."},"DeviceProfileWarning":{"type":"object","properties":{"code":{"type":"string","description":"`benefitTemplateMissing` or `filterAcceptsAll`."},"message":{"type":"string","description":"Human-readable explanation."},"benefitTemplateId":{"type":["null","string"],"description":"Benefit template concerned, for `benefitTemplateMissing`."}},"additionalProperties":false,"description":"Non-blocking issue an operator should know about."},"ProblemDetails":{"type":"object","properties":{"type":{"type":["null","string"]},"title":{"type":["null","string"]},"status":{"type":["null","integer"],"format":"int32"},"detail":{"type":["null","string"]},"instance":{"type":["null","string"]}},"additionalProperties":{}},"HttpValidationProblemDetails":{"type":"object","allOf":[{"$ref":"#/components/schemas/ProblemDetails"}],"properties":{"errors":{"type":"object","additionalProperties":{"type":"array","items":{"type":"string"}}}},"additionalProperties":{}}}},"paths":{"/api/{tenantId}/deviceProfiles":{"get":{"tags":["DeviceProfiles"],"summary":"List device profiles","parameters":[{"name":"tenantId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Profiles, each with its warnings.","content":{"text/plain":{"schema":{"type":"array","items":{"description":"Reusable scan context a device can run (Breakfast, Gate F, Cloakroom): what it accepts, and when.","$ref":"#/components/schemas/DeviceProfile"}}},"application/json":{"schema":{"type":"array","items":{"description":"Reusable scan context a device can run (Breakfast, Gate F, Cloakroom): what it accepts, and when.","$ref":"#/components/schemas/DeviceProfile"}}},"text/json":{"schema":{"type":"array","items":{"description":"Reusable scan context a device can run (Breakfast, Gate F, Cloakroom): what it accepts, and when.","$ref":"#/components/schemas/DeviceProfile"}}}}},"401":{"description":"The caller is not authenticated.","content":{"application/problem+json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ProblemDetails"},{"$ref":"#/components/schemas/HttpValidationProblemDetails"}]}}}},"403":{"description":"The caller lacks the <c>tenant.device:read</c> scope, or the endpoint is not enabled for the tenant.","content":{"application/problem+json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ProblemDetails"},{"$ref":"#/components/schemas/HttpValidationProblemDetails"}]}}}},"429":{"description":"Over 1200 requests per minute to this endpoint for the tenant (default limit); retry after <c>Retry-After</c>.","headers":{"Retry-After":{"description":"Seconds to wait before retrying. Sent when the limiter knows it.","schema":{"type":"integer"}}},"content":{"application/problem+json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ProblemDetails"},{"$ref":"#/components/schemas/HttpValidationProblemDetails"}]}}}}}}}}}
```

## Create a device profile

> \`id\` is required and unique within the tenant. Tags and benefit template ids are trimmed and deduplicated; an unknown template id is dropped with a warning, not refused.

```json
{"openapi":"3.1.1","info":{"title":"Neostore internal API","version":"v1"},"tags":[{"name":"DeviceProfiles"}],"servers":[{"url":"https://app.neostore.cloud","description":"Production Server"},{"url":"https://app-qa.neostore.cloud","description":"Staging Server"}],"security":[{"admin-bearer":["tenant.device:write"]},{"apiKey":[]}],"components":{"securitySchemes":{"admin-bearer":{"type":"oauth2","flows":{"implicit":{"authorizationUrl":"https://auth.neostore.cloud/authorize?audience=https://app.neostore.cloud/api/","scopes":{}}}},"apiKey":{"type":"apiKey","name":"X-API-KEY","in":"header"}},"schemas":{"UpsertDeviceProfile":{"type":"object","properties":{"type":{"description":"Device type. Changing it on update returns 400.","$ref":"#/components/schemas/DeviceType"},"name":{"type":"string","description":"Name shown to staff: required, at most 200 characters, trimmed."},"description":{"type":["null","string"],"description":"Free text, at most 500 characters; blank is stored as none."},"emoji":{"type":["null","string"],"description":"Icon, at most 16 characters; blank is stored as none."},"color":{"type":["null","string"],"description":"Hex `#rrggbb` (other formats return 400); blank is stored as none."},"validFrom":{"type":["null","string"],"description":"First usable instant, stored in UTC; null for no start.","format":"date-time"},"validUntil":{"type":["null","string"],"description":"Instant it stops being usable, UTC; must be after ValidFrom (error on `validFrom`); null for no end.","format":"date-time"},"schedule":{"type":["null","array"],"items":{"description":"Opening window: opens at each occurrence of Cron and stays open for DurationMinutes.","$ref":"#/components/schemas/DeviceProfileWindow"},"description":"Opening windows, at most 20; null or empty means always open within the validity dates."},"settings":{"oneOf":[{"$ref":"#/components/schemas/CrewCheckProfileSettings"}],"description":"Type-specific settings; the type must match Type."},"id":{"type":["null","string"],"description":"Required on create, ignored on update (the route names the profile). Letters, digits, `_` and `-`, starting with a letter or digit, at most 128 characters."}},"additionalProperties":false,"description":"Payload to create or update a profile."},"DeviceType":{"enum":["crewCheck"],"type":"string","description":"Device kind: decides its settings, profiles and API key permissions."},"DeviceProfileWindow":{"type":"object","properties":{"cron":{"type":"string","description":"Cron expression with seconds and no year, as in automations (`0 0 7 * * *` is daily at 07:00). Wall-clock time in the device's timezone; unparsable returns 400."},"durationMinutes":{"type":"integer","description":"Minutes the window stays open, 1 to 1440 (otherwise 400).","format":"int32"}},"additionalProperties":false,"description":"Opening window: opens at each occurrence of Cron and stays open for DurationMinutes."},"CrewCheckProfileSettings":{"required":["$type"],"type":"object","allOf":[{"description":"Type-specific settings. The JSON carries a `$type` discriminator (`crewCheck`) that must match the profile's `type`.","$ref":"#/components/schemas/DeviceProfileSettingsBase"}],"properties":{"scanFilter":{"description":"Benefits the device accepts; null or empty accepts all.","$ref":"#/components/schemas/ScanFilter"},"allowStamps":{"type":"boolean","description":"Whether staff can add stamps while the profile runs. Off, scanning a stamp card only shows it."},"redeemOnScan":{"description":"Redemption behaviour on scan; null applies defaults (5-second repeat-scan block).","$ref":"#/components/schemas/RedeemOnScanSettings"},"$type":{"enum":["crewCheck"],"type":"string","description":"Discriminator. Always `crewCheck` for this shape."}},"additionalProperties":false,"description":"What a Crew Check scanner does while running the profile."},"DeviceProfileSettingsBase":{"type":"object","description":"Type-specific settings. The JSON carries a `$type` discriminator (`crewCheck`) that must match the profile's `type`."},"ScanFilter":{"type":"object","properties":{"tags":{"type":"array","items":{"type":"string"},"description":"Benefit template tags to accept. At most 50, each non-blank, at most 100 characters; trimmed and deduplicated on save."},"benefitTemplateIds":{"type":"array","items":{"type":"string"},"description":"Benefit templates to accept. At most 200, none blank; trimmed and deduplicated on save. An unknown id is dropped with a `benefitTemplateMissing` warning, not refused."}},"additionalProperties":false,"description":"A benefit is accepted if it carries one of the tags or comes from one of the benefit templates. Both empty accepts every benefit."},"RedeemOnScanSettings":{"type":"object","properties":{"repeatScanBlockSeconds":{"type":"integer","description":"Seconds the device ignores a repeat scan of the same pass, 0 to 3600; 0 disables the block.","format":"int32"}},"additionalProperties":false,"description":"Redemption behaviour on scan."},"DeviceProfile":{"type":"object","properties":{"id":{"type":"string","description":"Stable identifier, chosen on create."},"type":{"description":"Device type; only a device of that type can run it.","$ref":"#/components/schemas/DeviceType"},"name":{"type":"string","description":"Name shown to staff on the device."},"description":{"type":["null","string"],"description":"Free text for operators; null if none."},"emoji":{"type":["null","string"],"description":"Icon shown next to the name; null if none."},"color":{"type":["null","string"],"description":"Hex colour `#rrggbb`; null if none."},"validFrom":{"type":["null","string"],"description":"First usable instant, UTC; null for no start.","format":"date-time"},"validUntil":{"type":["null","string"],"description":"Instant it stops being usable, UTC; null for no end.","format":"date-time"},"schedule":{"type":"array","items":{"description":"Opening window: opens at each occurrence of Cron and stays open for DurationMinutes.","$ref":"#/components/schemas/DeviceProfileWindow"},"description":"Opening windows; empty means always open within the validity dates."},"settings":{"oneOf":[{"description":"What a Crew Check scanner does while running the profile.","$ref":"#/components/schemas/CrewCheckProfileSettings"}],"description":"Type-specific settings."},"warnings":{"type":"array","items":{"description":"Non-blocking issue an operator should know about.","$ref":"#/components/schemas/DeviceProfileWarning"},"description":"What an operator should fix; empty if nothing."}},"additionalProperties":false,"description":"Reusable scan context a device can run (Breakfast, Gate F, Cloakroom): what it accepts, and when."},"DeviceProfileWarning":{"type":"object","properties":{"code":{"type":"string","description":"`benefitTemplateMissing` or `filterAcceptsAll`."},"message":{"type":"string","description":"Human-readable explanation."},"benefitTemplateId":{"type":["null","string"],"description":"Benefit template concerned, for `benefitTemplateMissing`."}},"additionalProperties":false,"description":"Non-blocking issue an operator should know about."},"ProblemDetails":{"type":"object","properties":{"type":{"type":["null","string"]},"title":{"type":["null","string"]},"status":{"type":["null","integer"],"format":"int32"},"detail":{"type":["null","string"]},"instance":{"type":["null","string"]}},"additionalProperties":{}},"HttpValidationProblemDetails":{"type":"object","allOf":[{"$ref":"#/components/schemas/ProblemDetails"}],"properties":{"errors":{"type":"object","additionalProperties":{"type":"array","items":{"type":"string"}}}},"additionalProperties":{}}}},"paths":{"/api/{tenantId}/deviceProfiles":{"post":{"tags":["DeviceProfiles"],"summary":"Create a device profile","description":"`id` is required and unique within the tenant. Tags and benefit template ids are trimmed and deduplicated; an unknown template id is dropped with a warning, not refused.","parameters":[{"name":"tenantId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Profile payload.","content":{"application/json":{"schema":{"description":"Payload to create or update a profile.","$ref":"#/components/schemas/UpsertDeviceProfile"}},"text/json":{"schema":{"description":"Payload to create or update a profile.","$ref":"#/components/schemas/UpsertDeviceProfile"}},"application/*+json":{"schema":{"description":"Payload to create or update a profile.","$ref":"#/components/schemas/UpsertDeviceProfile"}}}},"responses":{"201":{"description":"Profile created; warnings list what was dropped.","content":{"text/plain":{"schema":{"description":"Reusable scan context a device can run (Breakfast, Gate F, Cloakroom): what it accepts, and when.","$ref":"#/components/schemas/DeviceProfile"}},"application/json":{"schema":{"description":"Reusable scan context a device can run (Breakfast, Gate F, Cloakroom): what it accepts, and when.","$ref":"#/components/schemas/DeviceProfile"}},"text/json":{"schema":{"description":"Reusable scan context a device can run (Breakfast, Gate F, Cloakroom): what it accepts, and when.","$ref":"#/components/schemas/DeviceProfile"}}}},"400":{"description":"A payload rule is broken, settings do not match the type, or `id` is missing or malformed; every broken rule is reported by JSON path.","content":{"text/plain":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ProblemDetails"},{"$ref":"#/components/schemas/HttpValidationProblemDetails"}]}},"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ProblemDetails"},{"$ref":"#/components/schemas/HttpValidationProblemDetails"}]}},"text/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ProblemDetails"},{"$ref":"#/components/schemas/HttpValidationProblemDetails"}]}}}},"401":{"description":"The caller is not authenticated.","content":{"application/problem+json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ProblemDetails"},{"$ref":"#/components/schemas/HttpValidationProblemDetails"}]}}}},"403":{"description":"The caller lacks the <c>tenant.device:write</c> scope, or the endpoint is not enabled for the tenant.","content":{"application/problem+json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ProblemDetails"},{"$ref":"#/components/schemas/HttpValidationProblemDetails"}]}}}},"409":{"description":"Id already exists.","content":{"text/plain":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ProblemDetails"},{"$ref":"#/components/schemas/HttpValidationProblemDetails"}]}},"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ProblemDetails"},{"$ref":"#/components/schemas/HttpValidationProblemDetails"}]}},"text/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ProblemDetails"},{"$ref":"#/components/schemas/HttpValidationProblemDetails"}]}}}},"429":{"description":"Over 1200 requests per minute to this endpoint for the tenant (default limit); retry after <c>Retry-After</c>.","headers":{"Retry-After":{"description":"Seconds to wait before retrying. Sent when the limiter knows it.","schema":{"type":"integer"}}},"content":{"application/problem+json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ProblemDetails"},{"$ref":"#/components/schemas/HttpValidationProblemDetails"}]}}}}}}}}}
```

## GET /api/{tenantId}/deviceProfiles/{profileId}

> Get a device profile

```json
{"openapi":"3.1.1","info":{"title":"Neostore internal API","version":"v1"},"tags":[{"name":"DeviceProfiles"}],"servers":[{"url":"https://app.neostore.cloud","description":"Production Server"},{"url":"https://app-qa.neostore.cloud","description":"Staging Server"}],"security":[{"admin-bearer":["tenant.device:read"]},{"apiKey":[]}],"components":{"securitySchemes":{"admin-bearer":{"type":"oauth2","flows":{"implicit":{"authorizationUrl":"https://auth.neostore.cloud/authorize?audience=https://app.neostore.cloud/api/","scopes":{}}}},"apiKey":{"type":"apiKey","name":"X-API-KEY","in":"header"}},"schemas":{"DeviceProfile":{"type":"object","properties":{"id":{"type":"string","description":"Stable identifier, chosen on create."},"type":{"description":"Device type; only a device of that type can run it.","$ref":"#/components/schemas/DeviceType"},"name":{"type":"string","description":"Name shown to staff on the device."},"description":{"type":["null","string"],"description":"Free text for operators; null if none."},"emoji":{"type":["null","string"],"description":"Icon shown next to the name; null if none."},"color":{"type":["null","string"],"description":"Hex colour `#rrggbb`; null if none."},"validFrom":{"type":["null","string"],"description":"First usable instant, UTC; null for no start.","format":"date-time"},"validUntil":{"type":["null","string"],"description":"Instant it stops being usable, UTC; null for no end.","format":"date-time"},"schedule":{"type":"array","items":{"description":"Opening window: opens at each occurrence of Cron and stays open for DurationMinutes.","$ref":"#/components/schemas/DeviceProfileWindow"},"description":"Opening windows; empty means always open within the validity dates."},"settings":{"oneOf":[{"description":"What a Crew Check scanner does while running the profile.","$ref":"#/components/schemas/CrewCheckProfileSettings"}],"description":"Type-specific settings."},"warnings":{"type":"array","items":{"description":"Non-blocking issue an operator should know about.","$ref":"#/components/schemas/DeviceProfileWarning"},"description":"What an operator should fix; empty if nothing."}},"additionalProperties":false,"description":"Reusable scan context a device can run (Breakfast, Gate F, Cloakroom): what it accepts, and when."},"DeviceType":{"enum":["crewCheck"],"type":"string","description":"Device kind: decides its settings, profiles and API key permissions."},"DeviceProfileWindow":{"type":"object","properties":{"cron":{"type":"string","description":"Cron expression with seconds and no year, as in automations (`0 0 7 * * *` is daily at 07:00). Wall-clock time in the device's timezone; unparsable returns 400."},"durationMinutes":{"type":"integer","description":"Minutes the window stays open, 1 to 1440 (otherwise 400).","format":"int32"}},"additionalProperties":false,"description":"Opening window: opens at each occurrence of Cron and stays open for DurationMinutes."},"CrewCheckProfileSettings":{"required":["$type"],"type":"object","allOf":[{"description":"Type-specific settings. The JSON carries a `$type` discriminator (`crewCheck`) that must match the profile's `type`.","$ref":"#/components/schemas/DeviceProfileSettingsBase"}],"properties":{"scanFilter":{"description":"Benefits the device accepts; null or empty accepts all.","$ref":"#/components/schemas/ScanFilter"},"allowStamps":{"type":"boolean","description":"Whether staff can add stamps while the profile runs. Off, scanning a stamp card only shows it."},"redeemOnScan":{"description":"Redemption behaviour on scan; null applies defaults (5-second repeat-scan block).","$ref":"#/components/schemas/RedeemOnScanSettings"},"$type":{"enum":["crewCheck"],"type":"string","description":"Discriminator. Always `crewCheck` for this shape."}},"additionalProperties":false,"description":"What a Crew Check scanner does while running the profile."},"DeviceProfileSettingsBase":{"type":"object","description":"Type-specific settings. The JSON carries a `$type` discriminator (`crewCheck`) that must match the profile's `type`."},"ScanFilter":{"type":"object","properties":{"tags":{"type":"array","items":{"type":"string"},"description":"Benefit template tags to accept. At most 50, each non-blank, at most 100 characters; trimmed and deduplicated on save."},"benefitTemplateIds":{"type":"array","items":{"type":"string"},"description":"Benefit templates to accept. At most 200, none blank; trimmed and deduplicated on save. An unknown id is dropped with a `benefitTemplateMissing` warning, not refused."}},"additionalProperties":false,"description":"A benefit is accepted if it carries one of the tags or comes from one of the benefit templates. Both empty accepts every benefit."},"RedeemOnScanSettings":{"type":"object","properties":{"repeatScanBlockSeconds":{"type":"integer","description":"Seconds the device ignores a repeat scan of the same pass, 0 to 3600; 0 disables the block.","format":"int32"}},"additionalProperties":false,"description":"Redemption behaviour on scan."},"DeviceProfileWarning":{"type":"object","properties":{"code":{"type":"string","description":"`benefitTemplateMissing` or `filterAcceptsAll`."},"message":{"type":"string","description":"Human-readable explanation."},"benefitTemplateId":{"type":["null","string"],"description":"Benefit template concerned, for `benefitTemplateMissing`."}},"additionalProperties":false,"description":"Non-blocking issue an operator should know about."},"ProblemDetails":{"type":"object","properties":{"type":{"type":["null","string"]},"title":{"type":["null","string"]},"status":{"type":["null","integer"],"format":"int32"},"detail":{"type":["null","string"]},"instance":{"type":["null","string"]}},"additionalProperties":{}},"HttpValidationProblemDetails":{"type":"object","allOf":[{"$ref":"#/components/schemas/ProblemDetails"}],"properties":{"errors":{"type":"object","additionalProperties":{"type":"array","items":{"type":"string"}}}},"additionalProperties":{}}}},"paths":{"/api/{tenantId}/deviceProfiles/{profileId}":{"get":{"tags":["DeviceProfiles"],"summary":"Get a device profile","parameters":[{"name":"profileId","in":"path","description":"Profile id.","required":true,"schema":{"type":"string"}},{"name":"tenantId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"The profile, with its warnings.","content":{"text/plain":{"schema":{"description":"Reusable scan context a device can run (Breakfast, Gate F, Cloakroom): what it accepts, and when.","$ref":"#/components/schemas/DeviceProfile"}},"application/json":{"schema":{"description":"Reusable scan context a device can run (Breakfast, Gate F, Cloakroom): what it accepts, and when.","$ref":"#/components/schemas/DeviceProfile"}},"text/json":{"schema":{"description":"Reusable scan context a device can run (Breakfast, Gate F, Cloakroom): what it accepts, and when.","$ref":"#/components/schemas/DeviceProfile"}}}},"401":{"description":"The caller is not authenticated.","content":{"application/problem+json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ProblemDetails"},{"$ref":"#/components/schemas/HttpValidationProblemDetails"}]}}}},"403":{"description":"The caller lacks the <c>tenant.device:read</c> scope, or the endpoint is not enabled for the tenant.","content":{"application/problem+json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ProblemDetails"},{"$ref":"#/components/schemas/HttpValidationProblemDetails"}]}}}},"404":{"description":"Profile not found.","content":{"text/plain":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ProblemDetails"},{"$ref":"#/components/schemas/HttpValidationProblemDetails"}]}},"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ProblemDetails"},{"$ref":"#/components/schemas/HttpValidationProblemDetails"}]}},"text/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ProblemDetails"},{"$ref":"#/components/schemas/HttpValidationProblemDetails"}]}}}},"429":{"description":"Over 1200 requests per minute to this endpoint for the tenant (default limit); retry after <c>Retry-After</c>.","headers":{"Retry-After":{"description":"Seconds to wait before retrying. Sent when the limiter knows it.","schema":{"type":"integer"}}},"content":{"application/problem+json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ProblemDetails"},{"$ref":"#/components/schemas/HttpValidationProblemDetails"}]}}}}}}}}}
```

## Update a device profile

> Replaces the whole profile. Unknown benefit template ids are removed with warnings. The device type cannot change. Every device listing the profile is pushed \`settingsChanged\`, so it updates without reconnecting.

```json
{"openapi":"3.1.1","info":{"title":"Neostore internal API","version":"v1"},"tags":[{"name":"DeviceProfiles"}],"servers":[{"url":"https://app.neostore.cloud","description":"Production Server"},{"url":"https://app-qa.neostore.cloud","description":"Staging Server"}],"security":[{"admin-bearer":["tenant.device:write"]},{"apiKey":[]}],"components":{"securitySchemes":{"admin-bearer":{"type":"oauth2","flows":{"implicit":{"authorizationUrl":"https://auth.neostore.cloud/authorize?audience=https://app.neostore.cloud/api/","scopes":{}}}},"apiKey":{"type":"apiKey","name":"X-API-KEY","in":"header"}},"schemas":{"UpsertDeviceProfile":{"type":"object","properties":{"type":{"description":"Device type. Changing it on update returns 400.","$ref":"#/components/schemas/DeviceType"},"name":{"type":"string","description":"Name shown to staff: required, at most 200 characters, trimmed."},"description":{"type":["null","string"],"description":"Free text, at most 500 characters; blank is stored as none."},"emoji":{"type":["null","string"],"description":"Icon, at most 16 characters; blank is stored as none."},"color":{"type":["null","string"],"description":"Hex `#rrggbb` (other formats return 400); blank is stored as none."},"validFrom":{"type":["null","string"],"description":"First usable instant, stored in UTC; null for no start.","format":"date-time"},"validUntil":{"type":["null","string"],"description":"Instant it stops being usable, UTC; must be after ValidFrom (error on `validFrom`); null for no end.","format":"date-time"},"schedule":{"type":["null","array"],"items":{"description":"Opening window: opens at each occurrence of Cron and stays open for DurationMinutes.","$ref":"#/components/schemas/DeviceProfileWindow"},"description":"Opening windows, at most 20; null or empty means always open within the validity dates."},"settings":{"oneOf":[{"$ref":"#/components/schemas/CrewCheckProfileSettings"}],"description":"Type-specific settings; the type must match Type."},"id":{"type":["null","string"],"description":"Required on create, ignored on update (the route names the profile). Letters, digits, `_` and `-`, starting with a letter or digit, at most 128 characters."}},"additionalProperties":false,"description":"Payload to create or update a profile."},"DeviceType":{"enum":["crewCheck"],"type":"string","description":"Device kind: decides its settings, profiles and API key permissions."},"DeviceProfileWindow":{"type":"object","properties":{"cron":{"type":"string","description":"Cron expression with seconds and no year, as in automations (`0 0 7 * * *` is daily at 07:00). Wall-clock time in the device's timezone; unparsable returns 400."},"durationMinutes":{"type":"integer","description":"Minutes the window stays open, 1 to 1440 (otherwise 400).","format":"int32"}},"additionalProperties":false,"description":"Opening window: opens at each occurrence of Cron and stays open for DurationMinutes."},"CrewCheckProfileSettings":{"required":["$type"],"type":"object","allOf":[{"description":"Type-specific settings. The JSON carries a `$type` discriminator (`crewCheck`) that must match the profile's `type`.","$ref":"#/components/schemas/DeviceProfileSettingsBase"}],"properties":{"scanFilter":{"description":"Benefits the device accepts; null or empty accepts all.","$ref":"#/components/schemas/ScanFilter"},"allowStamps":{"type":"boolean","description":"Whether staff can add stamps while the profile runs. Off, scanning a stamp card only shows it."},"redeemOnScan":{"description":"Redemption behaviour on scan; null applies defaults (5-second repeat-scan block).","$ref":"#/components/schemas/RedeemOnScanSettings"},"$type":{"enum":["crewCheck"],"type":"string","description":"Discriminator. Always `crewCheck` for this shape."}},"additionalProperties":false,"description":"What a Crew Check scanner does while running the profile."},"DeviceProfileSettingsBase":{"type":"object","description":"Type-specific settings. The JSON carries a `$type` discriminator (`crewCheck`) that must match the profile's `type`."},"ScanFilter":{"type":"object","properties":{"tags":{"type":"array","items":{"type":"string"},"description":"Benefit template tags to accept. At most 50, each non-blank, at most 100 characters; trimmed and deduplicated on save."},"benefitTemplateIds":{"type":"array","items":{"type":"string"},"description":"Benefit templates to accept. At most 200, none blank; trimmed and deduplicated on save. An unknown id is dropped with a `benefitTemplateMissing` warning, not refused."}},"additionalProperties":false,"description":"A benefit is accepted if it carries one of the tags or comes from one of the benefit templates. Both empty accepts every benefit."},"RedeemOnScanSettings":{"type":"object","properties":{"repeatScanBlockSeconds":{"type":"integer","description":"Seconds the device ignores a repeat scan of the same pass, 0 to 3600; 0 disables the block.","format":"int32"}},"additionalProperties":false,"description":"Redemption behaviour on scan."},"DeviceProfile":{"type":"object","properties":{"id":{"type":"string","description":"Stable identifier, chosen on create."},"type":{"description":"Device type; only a device of that type can run it.","$ref":"#/components/schemas/DeviceType"},"name":{"type":"string","description":"Name shown to staff on the device."},"description":{"type":["null","string"],"description":"Free text for operators; null if none."},"emoji":{"type":["null","string"],"description":"Icon shown next to the name; null if none."},"color":{"type":["null","string"],"description":"Hex colour `#rrggbb`; null if none."},"validFrom":{"type":["null","string"],"description":"First usable instant, UTC; null for no start.","format":"date-time"},"validUntil":{"type":["null","string"],"description":"Instant it stops being usable, UTC; null for no end.","format":"date-time"},"schedule":{"type":"array","items":{"description":"Opening window: opens at each occurrence of Cron and stays open for DurationMinutes.","$ref":"#/components/schemas/DeviceProfileWindow"},"description":"Opening windows; empty means always open within the validity dates."},"settings":{"oneOf":[{"description":"What a Crew Check scanner does while running the profile.","$ref":"#/components/schemas/CrewCheckProfileSettings"}],"description":"Type-specific settings."},"warnings":{"type":"array","items":{"description":"Non-blocking issue an operator should know about.","$ref":"#/components/schemas/DeviceProfileWarning"},"description":"What an operator should fix; empty if nothing."}},"additionalProperties":false,"description":"Reusable scan context a device can run (Breakfast, Gate F, Cloakroom): what it accepts, and when."},"DeviceProfileWarning":{"type":"object","properties":{"code":{"type":"string","description":"`benefitTemplateMissing` or `filterAcceptsAll`."},"message":{"type":"string","description":"Human-readable explanation."},"benefitTemplateId":{"type":["null","string"],"description":"Benefit template concerned, for `benefitTemplateMissing`."}},"additionalProperties":false,"description":"Non-blocking issue an operator should know about."},"ProblemDetails":{"type":"object","properties":{"type":{"type":["null","string"]},"title":{"type":["null","string"]},"status":{"type":["null","integer"],"format":"int32"},"detail":{"type":["null","string"]},"instance":{"type":["null","string"]}},"additionalProperties":{}},"HttpValidationProblemDetails":{"type":"object","allOf":[{"$ref":"#/components/schemas/ProblemDetails"}],"properties":{"errors":{"type":"object","additionalProperties":{"type":"array","items":{"type":"string"}}}},"additionalProperties":{}}}},"paths":{"/api/{tenantId}/deviceProfiles/{profileId}":{"put":{"tags":["DeviceProfiles"],"summary":"Update a device profile","description":"Replaces the whole profile. Unknown benefit template ids are removed with warnings. The device type cannot change. Every device listing the profile is pushed `settingsChanged`, so it updates without reconnecting.","parameters":[{"name":"profileId","in":"path","description":"Profile id.","required":true,"schema":{"type":"string"}},{"name":"tenantId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"description":"Profile payload.","content":{"application/json":{"schema":{"description":"Payload to create or update a profile.","$ref":"#/components/schemas/UpsertDeviceProfile"}},"text/json":{"schema":{"description":"Payload to create or update a profile.","$ref":"#/components/schemas/UpsertDeviceProfile"}},"application/*+json":{"schema":{"description":"Payload to create or update a profile.","$ref":"#/components/schemas/UpsertDeviceProfile"}}}},"responses":{"200":{"description":"Profile updated; warnings list what was removed.","content":{"text/plain":{"schema":{"description":"Reusable scan context a device can run (Breakfast, Gate F, Cloakroom): what it accepts, and when.","$ref":"#/components/schemas/DeviceProfile"}},"application/json":{"schema":{"description":"Reusable scan context a device can run (Breakfast, Gate F, Cloakroom): what it accepts, and when.","$ref":"#/components/schemas/DeviceProfile"}},"text/json":{"schema":{"description":"Reusable scan context a device can run (Breakfast, Gate F, Cloakroom): what it accepts, and when.","$ref":"#/components/schemas/DeviceProfile"}}}},"400":{"description":"A payload rule is broken, settings do not match the type, or the device type changes; every broken rule is reported by JSON path.","content":{"text/plain":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ProblemDetails"},{"$ref":"#/components/schemas/HttpValidationProblemDetails"}]}},"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ProblemDetails"},{"$ref":"#/components/schemas/HttpValidationProblemDetails"}]}},"text/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ProblemDetails"},{"$ref":"#/components/schemas/HttpValidationProblemDetails"}]}}}},"401":{"description":"The caller is not authenticated.","content":{"application/problem+json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ProblemDetails"},{"$ref":"#/components/schemas/HttpValidationProblemDetails"}]}}}},"403":{"description":"The caller lacks the <c>tenant.device:write</c> scope, or the endpoint is not enabled for the tenant.","content":{"application/problem+json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ProblemDetails"},{"$ref":"#/components/schemas/HttpValidationProblemDetails"}]}}}},"404":{"description":"Profile not found.","content":{"text/plain":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ProblemDetails"},{"$ref":"#/components/schemas/HttpValidationProblemDetails"}]}},"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ProblemDetails"},{"$ref":"#/components/schemas/HttpValidationProblemDetails"}]}},"text/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ProblemDetails"},{"$ref":"#/components/schemas/HttpValidationProblemDetails"}]}}}},"429":{"description":"Over 1200 requests per minute to this endpoint for the tenant (default limit); retry after <c>Retry-After</c>.","headers":{"Retry-After":{"description":"Seconds to wait before retrying. Sent when the limiter knows it.","schema":{"type":"integer"}}},"content":{"application/problem+json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ProblemDetails"},{"$ref":"#/components/schemas/HttpValidationProblemDetails"}]}}}}}}}}}
```

## Delete a device profile

> Refused while any device lists the profile.

```json
{"openapi":"3.1.1","info":{"title":"Neostore internal API","version":"v1"},"tags":[{"name":"DeviceProfiles"}],"servers":[{"url":"https://app.neostore.cloud","description":"Production Server"},{"url":"https://app-qa.neostore.cloud","description":"Staging Server"}],"security":[{"admin-bearer":["tenant.device:write"]},{"apiKey":[]}],"components":{"securitySchemes":{"admin-bearer":{"type":"oauth2","flows":{"implicit":{"authorizationUrl":"https://auth.neostore.cloud/authorize?audience=https://app.neostore.cloud/api/","scopes":{}}}},"apiKey":{"type":"apiKey","name":"X-API-KEY","in":"header"}},"schemas":{"ProblemDetails":{"type":"object","properties":{"type":{"type":["null","string"]},"title":{"type":["null","string"]},"status":{"type":["null","integer"],"format":"int32"},"detail":{"type":["null","string"]},"instance":{"type":["null","string"]}},"additionalProperties":{}},"HttpValidationProblemDetails":{"type":"object","allOf":[{"$ref":"#/components/schemas/ProblemDetails"}],"properties":{"errors":{"type":"object","additionalProperties":{"type":"array","items":{"type":"string"}}}},"additionalProperties":{}}}},"paths":{"/api/{tenantId}/deviceProfiles/{profileId}":{"delete":{"tags":["DeviceProfiles"],"summary":"Delete a device profile","description":"Refused while any device lists the profile.","parameters":[{"name":"profileId","in":"path","description":"Profile id.","required":true,"schema":{"type":"string"}},{"name":"tenantId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Profile deleted."},"401":{"description":"The caller is not authenticated.","content":{"application/problem+json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ProblemDetails"},{"$ref":"#/components/schemas/HttpValidationProblemDetails"}]}}}},"403":{"description":"The caller lacks the <c>tenant.device:write</c> scope, or the endpoint is not enabled for the tenant.","content":{"application/problem+json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ProblemDetails"},{"$ref":"#/components/schemas/HttpValidationProblemDetails"}]}}}},"404":{"description":"Profile not found.","content":{"text/plain":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ProblemDetails"},{"$ref":"#/components/schemas/HttpValidationProblemDetails"}]}},"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ProblemDetails"},{"$ref":"#/components/schemas/HttpValidationProblemDetails"}]}},"text/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ProblemDetails"},{"$ref":"#/components/schemas/HttpValidationProblemDetails"}]}}}},"409":{"description":"Devices still list the profile (problem `Device profile in use`); `deviceIds` lists them.","content":{"text/plain":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ProblemDetails"},{"$ref":"#/components/schemas/HttpValidationProblemDetails"}]}},"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ProblemDetails"},{"$ref":"#/components/schemas/HttpValidationProblemDetails"}]}},"text/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ProblemDetails"},{"$ref":"#/components/schemas/HttpValidationProblemDetails"}]}}}},"429":{"description":"Over 1200 requests per minute to this endpoint for the tenant (default limit); retry after <c>Retry-After</c>.","headers":{"Retry-After":{"description":"Seconds to wait before retrying. Sent when the limiter knows it.","schema":{"type":"integer"}}},"content":{"application/problem+json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ProblemDetails"},{"$ref":"#/components/schemas/HttpValidationProblemDetails"}]}}}}}}}}}
```


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.thewalletcrew.io/api-reference/advanced/device-profiles.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
