shop.listing.published
Version 1 of the shop.listing.published webhook event.
The published shape of a Shop listing changed: the listing entered the published state, or an already-published listing was edited - including a scheduled discount change - and carries a new monotonic revision. The payload is the whole published shape rather than a delta, which is why no dedicated discount event exists. Draft-state edits publish nothing. The stock policy is disclosed; the code pool behind a code-pool policy is not.
| Event type | shop.listing.published |
| Schema version | 1 |
| Routing key | event.shop.listing.published.v1 |
| Produced by | Reward Management |
| Schema | urn:defendr:contract:shop:listing-published:1 |
data fields
| Field | Type | Required | Description |
|---|---|---|---|
listingKey | string | yes | |
revision | integer | yes | |
title | string | yes | |
category | string or null | yes | |
groupKey | string or null | yes | |
groupOrder | integer or null | yes | Presentational order within groupKey; null when the listing renders alone. |
price | array of object | yes | The listing's undiscounted price, one leg per accepted currency. |
price[].currencyKey | string | yes | |
price[].amount | string | yes | A whole amount of the tenant's published currency, as a decimal string. |
discount | object or null | yes | |
discount.kind | "percentage", "absolute" | yes | |
discount.percentage | string or null | yes | The percentage taken off, as a decimal string between 0 and 100; null for an absolute discount. |
discount.absolute | array or null | yes | The amount taken off per currency leg; null for a percentage discount. |
discount.absolute[].currencyKey | string | yes | |
discount.absolute[].amount | string | yes | A whole amount of the tenant's published currency, as a decimal string. |
discount.startsAt | string (date-time) | yes | |
discount.endsAt | string or null (date-time) | yes | The end of the discount window; null for an open-ended discount. |
fulfillment | object | yes | |
fulfillment.kind | "award", "code", "manual" | yes | |
fulfillment.awardKey | string or null | yes | |
stock | object | yes | |
stock.policy | "unlimited", "counted", "code-pool" | yes | |
stock.remaining | integer or null | yes | Units still buyable; for a code-pool policy this is the count of unclaimed codes. Null only for an unlimited policy. |
limit | object or null | yes | |
limit.maxPerPlayer | integer | yes | |
limit.window | "day", "week", "total" | yes | |
actorId | string | yes | |
publishedAt | string (date-time) | yes |
Some fields are required only in particular states. The JSON Schema below is authoritative.
Example delivery
Signed with the example secret whsec_placeholder_example_webhooks_signing_secret, so you can check your verifier against it. See Verifying signatures.
POST /your/webhook/endpoint HTTP/1.1
content-type: application/json
webhook-id: whd_00000000-0000-4000-8000-000000000027
webhook-timestamp: 1800000000
webhook-signature: v1,Zottz1KBjAGBJAi8WGTYYtEfKG9p/f/p7+6j3SgKNpA=
tanitar-event-type: shop.listing.published
tanitar-schema-version: 1
tanitar-delivery-attempt: 1
{"messageId":"evt_00000000-0000-4000-8000-000000000027","kind":"event","type":"shop.listing.published","schemaVersion":1,"occurredAt":"2027-01-15T10:00:00.000Z","producer":"reward-management-service","accountId":"acc_11111111-1111-4111-8111-111111111111","correlationId":"corr_00000000-0000-4000-8000-000000000027","data":{"listingKey":"founder-badge","revision":3,"title":"Founder badge","category":"cosmetics","groupKey":"founder","groupOrder":1,"price":[{"currencyKey":"red","amount":"500"}],"discount":{"kind":"percentage","percentage":"20","absolute":null,"startsAt":"2026-09-27T00:00:00.000Z","endsAt":"2026-10-04T00:00:00.000Z"},"fulfillment":{"kind":"award","awardKey":"founder"},"stock":{"policy":"unlimited","remaining":null},"limit":{"maxPerPlayer":1,"window":"total"},"actorId":"operator_demo","publishedAt":"2026-09-27T00:00:00.000Z"}}The same body, formatted:
{
"messageId": "evt_00000000-0000-4000-8000-000000000027",
"kind": "event",
"type": "shop.listing.published",
"schemaVersion": 1,
"occurredAt": "2027-01-15T10:00:00.000Z",
"producer": "reward-management-service",
"accountId": "acc_11111111-1111-4111-8111-111111111111",
"correlationId": "corr_00000000-0000-4000-8000-000000000027",
"data": {
"listingKey": "founder-badge",
"revision": 3,
"title": "Founder badge",
"category": "cosmetics",
"groupKey": "founder",
"groupOrder": 1,
"price": [
{
"currencyKey": "red",
"amount": "500"
}
],
"discount": {
"kind": "percentage",
"percentage": "20",
"absolute": null,
"startsAt": "2026-09-27T00:00:00.000Z",
"endsAt": "2026-10-04T00:00:00.000Z"
},
"fulfillment": {
"kind": "award",
"awardKey": "founder"
},
"stock": {
"policy": "unlimited",
"remaining": null
},
"limit": {
"maxPerPlayer": 1,
"window": "total"
},
"actorId": "operator_demo",
"publishedAt": "2026-09-27T00:00:00.000Z"
}
}JSON Schema
urn:defendr:contract:shop:listing-published:1
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "urn:defendr:contract:shop:listing-published:1",
"title": "Shop listing published",
"description": "The published shape of a Shop listing changed: the listing entered the `published` state, or an already-published listing was edited - including a scheduled discount change - and carries a new monotonic revision. The payload is the whole published shape rather than a delta, which is why no dedicated discount event exists. Draft-state edits publish nothing. The stock policy is disclosed; the code pool behind a `code-pool` policy is not.",
"type": "object",
"additionalProperties": false,
"required": [
"listingKey",
"revision",
"title",
"category",
"groupKey",
"groupOrder",
"price",
"discount",
"fulfillment",
"stock",
"limit",
"actorId",
"publishedAt"
],
"properties": {
"listingKey": {
"type": "string",
"pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$"
},
"revision": {
"type": "integer",
"minimum": 1
},
"title": {
"type": "string",
"minLength": 1
},
"category": {
"type": [
"string",
"null"
]
},
"groupKey": {
"type": [
"string",
"null"
],
"pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$"
},
"groupOrder": {
"type": [
"integer",
"null"
],
"minimum": 0,
"description": "Presentational order within `groupKey`; null when the listing renders alone."
},
"price": {
"type": "array",
"minItems": 1,
"description": "The listing's undiscounted price, one leg per accepted currency.",
"items": {
"type": "object",
"additionalProperties": false,
"required": [
"currencyKey",
"amount"
],
"properties": {
"currencyKey": {
"type": "string",
"pattern": "^[a-z][a-z0-9-]*$"
},
"amount": {
"type": "string",
"pattern": "^[0-9]+$",
"description": "A whole amount of the tenant's published currency, as a decimal string."
}
}
}
},
"discount": {
"type": [
"object",
"null"
],
"additionalProperties": false,
"required": [
"kind",
"percentage",
"absolute",
"startsAt",
"endsAt"
],
"properties": {
"kind": {
"enum": [
"percentage",
"absolute"
]
},
"percentage": {
"type": [
"string",
"null"
],
"pattern": "^(?:100(?:\\.0{1,4})?|[0-9]{1,2}(?:\\.[0-9]{1,4})?)$",
"description": "The percentage taken off, as a decimal string between 0 and 100; null for an absolute discount."
},
"absolute": {
"type": [
"array",
"null"
],
"description": "The amount taken off per currency leg; null for a percentage discount.",
"items": {
"type": "object",
"additionalProperties": false,
"required": [
"currencyKey",
"amount"
],
"properties": {
"currencyKey": {
"type": "string",
"pattern": "^[a-z][a-z0-9-]*$"
},
"amount": {
"type": "string",
"pattern": "^[0-9]+$",
"description": "A whole amount of the tenant's published currency, as a decimal string."
}
}
}
},
"startsAt": {
"type": "string",
"format": "date-time"
},
"endsAt": {
"type": [
"string",
"null"
],
"format": "date-time",
"description": "The end of the discount window; null for an open-ended discount."
}
},
"allOf": [
{
"if": {
"type": "object",
"required": [
"kind"
],
"properties": {
"kind": {
"const": "percentage"
}
}
},
"then": {
"type": "object",
"properties": {
"percentage": {
"type": "string"
},
"absolute": {
"type": "null"
}
}
}
},
{
"if": {
"type": "object",
"required": [
"kind"
],
"properties": {
"kind": {
"const": "absolute"
}
}
},
"then": {
"type": "object",
"properties": {
"percentage": {
"type": "null"
},
"absolute": {
"type": "array"
}
}
}
}
]
},
"fulfillment": {
"type": "object",
"additionalProperties": false,
"required": [
"kind",
"awardKey"
],
"properties": {
"kind": {
"enum": [
"award",
"code",
"manual"
]
},
"awardKey": {
"type": [
"string",
"null"
],
"pattern": "^[a-z0-9]+(?:-[a-z0-9]+)*$"
}
}
},
"stock": {
"type": "object",
"additionalProperties": false,
"required": [
"policy",
"remaining"
],
"properties": {
"policy": {
"enum": [
"unlimited",
"counted",
"code-pool"
]
},
"remaining": {
"type": [
"integer",
"null"
],
"minimum": 0,
"description": "Units still buyable; for a `code-pool` policy this is the count of unclaimed codes. Null only for an `unlimited` policy."
}
}
},
"limit": {
"type": [
"object",
"null"
],
"additionalProperties": false,
"required": [
"maxPerPlayer",
"window"
],
"properties": {
"maxPerPlayer": {
"type": "integer",
"minimum": 1
},
"window": {
"enum": [
"day",
"week",
"total"
]
}
}
},
"actorId": {
"type": "string",
"minLength": 1
},
"publishedAt": {
"type": "string",
"format": "date-time"
}
},
"allOf": [
{
"if": {
"type": "object",
"required": [
"fulfillment"
],
"properties": {
"fulfillment": {
"type": "object",
"required": [
"kind"
],
"properties": {
"kind": {
"const": "code"
}
}
}
}
},
"then": {
"type": "object",
"properties": {
"fulfillment": {
"type": "object",
"properties": {
"awardKey": {
"type": "null"
}
}
},
"stock": {
"type": "object",
"properties": {
"policy": {
"const": "code-pool"
}
}
}
}
}
},
{
"if": {
"type": "object",
"required": [
"fulfillment"
],
"properties": {
"fulfillment": {
"type": "object",
"required": [
"kind"
],
"properties": {
"kind": {
"const": "award"
}
}
}
}
},
"then": {
"type": "object",
"properties": {
"fulfillment": {
"type": "object",
"properties": {
"awardKey": {
"type": "string"
}
}
},
"stock": {
"type": "object",
"properties": {
"policy": {
"enum": [
"unlimited",
"counted"
]
}
}
}
}
}
},
{
"if": {
"type": "object",
"required": [
"fulfillment"
],
"properties": {
"fulfillment": {
"type": "object",
"required": [
"kind"
],
"properties": {
"kind": {
"const": "manual"
}
}
}
}
},
"then": {
"type": "object",
"properties": {
"fulfillment": {
"type": "object",
"properties": {
"awardKey": {
"type": "null"
}
}
},
"stock": {
"type": "object",
"properties": {
"policy": {
"enum": [
"unlimited",
"counted"
]
}
}
}
}
}
},
{
"if": {
"type": "object",
"required": [
"stock"
],
"properties": {
"stock": {
"type": "object",
"required": [
"policy"
],
"properties": {
"policy": {
"const": "unlimited"
}
}
}
}
},
"then": {
"type": "object",
"properties": {
"stock": {
"type": "object",
"properties": {
"remaining": {
"type": "null"
}
}
}
}
}
},
{
"if": {
"type": "object",
"required": [
"stock"
],
"properties": {
"stock": {
"type": "object",
"required": [
"policy"
],
"properties": {
"policy": {
"enum": [
"counted",
"code-pool"
]
}
}
}
}
},
"then": {
"type": "object",
"properties": {
"stock": {
"type": "object",
"properties": {
"remaining": {
"type": "integer"
}
}
}
}
}
}
]
}