Skip to main content

License keys, codes & gift cards

Traide sells secret credentials as first-class catalog items: software license keys, redeemable codes, and gift cards. These are the per-unit fulfillment kinds — unlike a digital file, every unit sold is a distinct secret with its own lifecycle, its own entitlement, and its own audit trail.

note

The license key types, queries, and mutations on this page are not yet in the generated API reference; they will appear with the next schema regeneration. Until then, the SDL excerpts here are the authoritative shape.

enum FulfillmentKind {
SHIPMENT
DIGITAL_FILE
SERVICE
LICENSE_KEY
CODE
GIFT_CARD
}
  • LICENSE_KEY — a software license the buyer reveals and activates.
  • CODE — a generic redeemable secret (access codes, voucher codes for external systems).
  • GIFT_CARD — a sellable gift-card code. Selling only — see the boundary note below.
Marketplaces opt in

The key kinds are platform-available but disabled per marketplace by default. An operator enables them with marketplaceConfigurationUpdate on enabledFulfillmentKinds; until then, catalog writes using these kinds are rejected. Read the current allowlist from marketplaceConfiguration { enabledFulfillmentKinds }.

Gift cards: selling ≠ redemption

Stack C makes gift cards sellable — pool-backed codes with reveal and verification. Redemption as tender (applying a sold card's balance at checkout) is a separate, future rail: a sold gift-card code is not accepted by checkoutAddPromoCode. Say "sell gift cards" to your users; do not imply in-marketplace spendability.

Key pools — the seller's secret inventory​

A pool holds the keys a seller sells for one variant. Availability is always computed from key states — there is deliberately no quantity column to drift, because a code is a secret issued once, never a stock counter.

mutation {
licenseKeyPoolCreate(
input: {
variantId: "UHJvZHVjdFZhcmlhbnQ6MTIz"
mode: UPLOADED # or GENERATED
isActive: true
}
) {
licenseKeyPool { id mode isActive }
entitlementErrors { field code message }
}
}
  • UPLOADED pools take seller-supplied secrets via licenseKeysAdd(id: ID!, keys: [String!]!) — id is the pool's id; bulk, deduplicated per pool, minimum 8 characters per key (shorter secrets would leak most of themselves through the last-4 display). Requires the seller to be approved for digital sales.
  • GENERATED pools mint platform-generated keys (XXXX-XXXX-XXXX-XXXX) on demand at checkout — no upload, effectively unbounded inventory.
  • One active pool per variant. Deactivating a pool (licenseKeyPoolUpdate) stops sales for that variant without touching already-sold keys — the stock-zero analogue.
  • licenseKeyPools / licenseKeyPool(id) list and inspect pools (MANAGE_PRODUCTS; sellers see their own, operators see all), including per-state key counts (available / reserved / issued / revealed / void / total).
  • licenseKeyVoid(id) — id is the key's id — withdraws an unsold key from inventory. Sold keys are withdrawn by revoking their entitlement instead (below), so the customer-facing record and the credential can never disagree.

Every key's secret is encrypted at rest, deduplicated by hash, and surfaces to staff only as secretLast4. The plaintext exists in exactly one API response: the buyer's reveal.

The key lifecycle​

enum LicenseKeyStatus {
AVAILABLE # in inventory, sellable
RESERVED # held by a live checkout
ISSUED # sold; bound to an entitlement
REVEALED # the buyer has seen the secret
VOID # withdrawn
}

The machine only moves forward (AVAILABLE → RESERVED → ISSUED → REVEALED, any state → VOID), with one sanctioned reverse edge: a reservation whose payment never completes returns to AVAILABLE — eagerly when the order is canceled, or by a sweep after 24 hours.

Checkout holds keys during completion — before payment is processed, so a shortage fails the checkout cleanly rather than after money moved. Adding a per-unit item to a cart also gets an early availability check. Two boundaries to design around:

  • Draft and quote orders refuse per-unit kinds (including CSV line uploads). Reservations exist only in the checkout flow; a draft would sell keys it can never issue.
  • A payment retry re-enters reservation idempotently — holds top up or trim to match the line, never double-hold.

Issuance happens on full payment: each reserved key flips to ISSUED and is bound to its own entitlement — a quantity-3 line produces three entitlement rows (quantity: 1 each), one per key, unlike DIGITAL_FILE's one-entitlement-per-line. Entitlement.licenseKey and LicenseKey.entitlement link the pair.

Reveal — how the buyer gets the secret​

Delivery is reveal-on-demand: the secret is never emailed and never appears in any listing. The buyer (and only the buyer — the mutation resolves strictly through the requesting customer's own library) calls:

mutation {
entitlementRevealCode(id: "RW50aXRsZW1lbnQ6NDI=") {
code # the plaintext secret — this response only
entitlement { id status }
entitlementErrors { field code message }
}
}

The first successful call is the audited event: the key flips ISSUED → REVEALED, a REVEAL redemption is recorded with request forensics, and the unit's refund eligibility flips (below). Every later call returns the same secret with no new state — an idempotent read. A revoked entitlement or voided key refuses with INVALID; guest purchases must be claimed to an account before reveal (until then, reveal is support-mediated). App tokens are refused — reveal belongs to the purchasing customer.

Refunds respect the reveal​

A revealed secret cannot be returned — the buyer keeps the knowledge. So a refund whose scope covers any REVEALED key is refused by default, and all three refund mutations accept an explicit override:

extend type Mutation {
refundLinesAdd(refundId: ID!, lineItems: [RefundLineInput!]!, allowRevealed: Boolean! = false): RefundLinesAdd!
refundLinesUpdate(refundId: ID!, lineItems: [RefundLineUpdateInput!]!, allowRevealed: Boolean! = false): RefundLinesUpdate!
refundsChangeStatus(ids: [ID!]!, status: RefundStatusEnum!, allowRevealed: Boolean! = false): RefundsChangeStatus!
}

The check runs where refund lines attach and again at approval — a buyer can reveal between the two, and approval is the gate that commits money. Note that a whole-order refund scope includes its key lines even when you mean to refund something else (shipping, say); allowRevealed: true is the escape hatch, under the same MANAGE_REFUNDS permission. Unrevealed (ISSUED) units refund freely, and when a refund completes, the revoked entitlements' keys are voided with them.

Verification API — for the seller's own software​

Sellers integrating license checks into their product call a public REST endpoint — no API token; the key itself is the credential:

curl -X POST https://<your-api-host>/tenant/<tenant_id>/licenses/verify/ \
-H "Content-Type: application/json" \
-d '{
"license_key": "ABCD-EF12-3456-7890",
"product_id": "UHJvZHVjdDox",
"increment_uses_count": true,
"idempotency_key": "device-e8a2"
}'

product_id, increment_uses_count, and idempotency_key are all optional — a body with just license_key is a pure status check. <tenant_id> identifies the marketplace: your marketplace operator provides it, and it is the same tenant segment that appears in the marketplace's digital-download URLs.

A resolvable key answers 200 with lifecycle states and a counter, never a boolean — "revoked" and "expired" are different facts your policy can act on. The REST endpoint reports the enum values lower-cased ("revealed", not "REVEALED"):

{
"key_status": "revealed",
"entitlement_status": "active",
"revoked_reason": null,
"uses": 3,
"product": { "name": "Pro License", "sku": "PRO-1" }
}

Semantics worth designing around:

  • Unknown key, malformed body, or product mismatch all return the same 404 ({"detail": "License key not found."}) — the endpoint leaks nothing to enumeration.
  • increment_uses_count records an ACTIVATION redemption and bumps uses. With an idempotency_key (≤ 255 chars), replays count once; without one, every call increments — send a stable per-install key for exactly-once semantics.
  • A revoked or expired entitlement (and any voided key) still reports its states, but the counter stops moving for it.
  • uses is a signal for your policy, not a platform-enforced lock. If your integration hard-fails past N activations, an attacker who obtains a leaked key can exhaust a legitimate buyer's allowance — prefer soft handling.
  • Requests are rate-limited per key and per IP (HTTP 429). The ceilings are operator-configurable; if your backend verifies many licenses from one IP and hits limits, ask the marketplace operator to raise them.

Webhooks​

Integrators can subscribe to the key lifecycle alongside the entitlement events:

  • LICENSE_KEY_ISSUED — a sold key bound to its entitlement.
  • LICENSE_KEY_REVEALED — the buyer's first reveal.
  • LICENSE_KEY_VOIDED — the key was withdrawn (inventory void, or revocation on refund).

Payloads identify the key by id, status, timestamps, and secretLast4 — never the secret or its hash.

See also​

Was this page helpful?