Record a code redemption
Evaluates and records a redemption, consuming the stored overall/per-user limit where enforced. This is a write, not a validation read, discount delivery, payment charge or commission payout.
Outcome and boundaries
Evaluates and records a redemption, consuming the stored overall/per-user limit where enforced. This is a write, not a validation read, discount delivery, payment charge or commission payout.
A rejected evaluation can append a redemption-failure row and schedule a background failure notification. A success can schedule a redemption notification; neither response proves delivery.
Before you start
- Confirm the intended environment and current credential organization using whoami; discover the exact application/action before any call.
- Required application permission: app_topolo_campaigns.redemptions:redeem. Catalog discoverability is not permission to execute.
- Discover an authorized canonical workspace and bind resourceType workspace/resourceId separately from action input.
- All worked cases in this guide are hypothetical source-reviewed illustrations; no business action, live fixture, customer data or provider request was executed.
- Canonical workspace minimum access is read_write in addition to the application permission. After grant verification, the first request may create an app-local identity/recipient mirror even before the later app-permission check; this is not campaign-domain completion.
Discover the intended identifiers
- Read codes.list then codes.validate with the same code/workspace. A prior valid result does not reserve capacity. Qualify the intended redeemedBy attribution separately.
Action: app_topolo_campaigns.codes.redeem · POST /api/codes/{code}/redeem.
Input fields
| Field | Required | Meaning and validation |
|---|---|---|
code | Yes | Human code string from codes[].code; preserve exact spelling and keep it distinct from a record id. string; minLength=1 |
redeemedBy | No | Optional stored attribution string. Source does not verify user membership here; omission means a null attribution and no per-user-cap check. string |
source | No | pay, signup, api or manual; omission defaults to api. It is attribution, not provider evidence. string; enum=["pay", "signup", "api", "manual"] |
value | No | Number used only when no offer is linked; otherwise the offer’s stored value is used. No currency/minor-unit contract or charge is provided. number |
metadata | No | Stored JSON object. It is not an instruction, access grant or evidence of a provider outcome; avoid credentials or sensitive customer data. object |
Safe call sequence
Write the reviewed input to payload.json; fixture identifiers below are unusable examples. Validate before planning, bind the discovered resource separately, and execute only within actual authority.
topolo actions validate app_topolo_campaigns.codes.redeem --data @payload.json --json
topolo actions plan app_topolo_campaigns.codes.redeem --resource-type workspace --resource-id "$CAMPAIGNS_WORKSPACE_ID" --data @payload.json --json
The published action requires confirmation. Only after the exact mutation is authorized, use that same payload/context with --confirm on plan/call; no operation in this guide has been executed.
Worked cases
Every response below is synthetic and source-reviewed, not captured. HTTP status and body are explained independently; a structurally valid body alone never proves successful execution. Middleware may add a different error envelope.
Record an attributed redemption · HTTP 200
Input:
{
"code": "EXAMPLE-20",
"redeemedBy": "user_example",
"source": "manual",
"value": 15
}
Illustrative response:
{
"redeemed": true,
"redemption": {
"id": "redemption_example",
"codeId": "code_example",
"campaignId": "campaign_example",
"redeemedBy": "user_example",
"value": 15,
"commissionAccrued": 1.5,
"source": "manual",
"redeemedAt": "2026-10-20T00:00:00.000Z"
},
"effect": null
}
Hypothetical campaign has no offer and pct commission 10; no payment or commission payout is illustrated.
Unknown code creates a failure record · HTTP 409
Input:
{
"code": "EXAMPLE-ABSENT"
}
Illustrative response:
{
"redeemed": false,
"reason": "code_not_found",
"failure": {
"id": "redemption_failure_example",
"campaignId": null,
"reason": "code_not_found",
"occurredAt": "2026-10-20T00:00:00.000Z"
}
}
Per-user limit reached · HTTP 409
Input:
{
"code": "EXAMPLE-20",
"redeemedBy": "user_example"
}
Illustrative response:
{
"redeemed": false,
"reason": "per_user_limit_reached",
"failure": {
"id": "redemption_failure_example",
"campaignId": "campaign_example",
"reason": "per_user_limit_reached",
"occurredAt": "2026-10-20T00:00:00.000Z"
}
}
Verify the outcome
- Require redeemed:true and retain redemption.id; inspect redemptions.list for the campaign. redeemed:false/409 may include failure.id and is not evidence of zero writes.
Refusals and recovery
- HTTP 409
code_not_found: Correct the named input or retain the authoritative refusal in the same context. - HTTP 409
campaign_not_active: Correct the named input or retain the authoritative refusal in the same context. - HTTP 409
code_disabled: Correct the named input or retain the authoritative refusal in the same context. - HTTP 409
offer_not_started: Correct the named input or retain the authoritative refusal in the same context. - HTTP 409
offer_expired: Correct the named input or retain the authoritative refusal in the same context. - HTTP 409
max_redemptions_reached: Correct the named input or retain the authoritative refusal in the same context. - HTTP 409
per_user_limit_reached: Correct the named input or retain the authoritative refusal in the same context. - HTTP 403
forbidden: Raw handler permission refusal. SDKs can normalize it to permission_denied. Keep the same intended resource; do not widen access. - HTTP 403
workspace_forbidden: The canonical workspace grant refused the selected resource. Resolve it with the access owner, not a guessed workspace.
No deletion or exact undo is published for this record. Preserve the returned identifier and inspect the corresponding list before considering another separately authorized operation. A timeout does not prove that nothing was written.
Source and catalog limits
There is no idempotency key or reversal action in this contract. Repeating a successful request can add another redemption. Source permits omitted redeemedBy and applies maxPerUser only when it is supplied; do not promise anonymous per-user enforcement. An offer overrides input.value. Financial units are not declared here; do not reinterpret value as cents or a charge.
Exact schemas and provenance
Input schema:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"code": {
"type": "string",
"minLength": 1
},
"redeemedBy": {
"type": "string"
},
"source": {
"type": "string",
"enum": [
"pay",
"signup",
"api",
"manual"
]
},
"value": {
"type": "number"
},
"metadata": {
"type": "object",
"properties": {},
"additionalProperties": {}
}
},
"required": [
"code"
],
"additionalProperties": false
}
Output schema:
{
"type": "object",
"title": "Redeem a code response",
"description": "Operation result returned by codes.redeem.",
"properties": {
"success": {
"type": "boolean"
},
"id": {
"type": "string"
},
"data": {
"anyOf": [
{
"type": "object",
"title": "code record",
"properties": {
"id": {
"type": "string",
"description": "Stable identifier when the code exposes one."
}
},
"additionalProperties": true
},
{
"type": "object",
"additionalProperties": true
}
]
},
"result": {
"type": "object",
"additionalProperties": true
},
"message": {
"type": "string"
}
},
"additionalProperties": true,
"x-topolo-contract-source": "derived-action-metadata",
"x-topolo-contract-precision": "structural"
}
Reviewed immutable source: TopoloCampaigns@cacbb8b12b4e19a662cf82d03a927aef3c3a35e8, apps/api/src/campaigns-store.ts. Fresh production catalog was read on 2026-10-20. Its publication metadata is not proof that this source is loaded. Output examples are not widened or rewritten to force schema success; the accompanying validation record classifies exact success, refusal, no-body and structural-schema limits.
Related actions
- Check code eligibility — Campaigns: Evaluates a code and returns HTTP 200 with valid:true or valid:false and a reason. This read does not consume redemption capacity.
- List campaign redemptions — Campaigns: Reads the campaign redemption ledger and sums stored value and commissionAccrued. It does not redeem a code, charge a payment, pay commission or send a notification. A missing campaign returns HTTP 404 campaign_not_found.
Local input rejection example
This deliberately invalid input belongs to a pure schema check; it is never a planned or executed request. The closed action input schema must reject the extra field. It has no HTTP status or server response.
{
"code": "EXAMPLE-20",
"redeemedBy": "user_example",
"source": "manual",
"value": 15,
"__unsupported_field": true
}
The hidden companion labels this as ILLUSTRATIVE_LOCAL_SCHEMA_CHECK; its validation representation is not an action output.
Published output validation limits
Current published output qualification: the output schema accepts an empty object, a partial success marker, a partial data object and a refusal body. It therefore establishes only broad structure. Require the documented HTTP/domain result and source-shaped fields; missing fields or a schema-valid error are not success.
These observations come from pure checks of the exact published schema; they are not server responses or production captures.