Topolo Docs Topolo Platform
Open workspace

Record a code redemption — Campaigns

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.

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

FieldRequiredMeaning and validation
codeYesHuman code string from codes[].code; preserve exact spelling and keep it distinct from a record id. string; minLength=1
redeemedByNoOptional stored attribution string. Source does not verify user membership here; omission means a null attribution and no per-user-cap check. string
sourceNopay, signup, api or manual; omission defaults to api. It is attribution, not provider evidence. string; enum=["pay", "signup", "api", "manual"]
valueNoNumber 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
metadataNoStored 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.

  • 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.