API reference
Promo Codes and Manual Grants API
Manage product promo codes and grant customer access without confusing a grant with a paid checkout.
Choose a discount or an access grant
A promo code changes the offer used at checkout. A manual grant creates product access for a named customer without collecting a checkout payment. These are different operations: a grant is not a refund, paid sale or recurring card mandate.
Use an account session for the effective developer workspace. Promo listing requires marketing.view; promo creation and changes require marketing.manage. Manual grants require entitlements.manage. Product ownership and team product assignments still apply.
Create a code and inspect its sync state
POST /api/developer/products/{productId}/promo-codes with code, discountType (percent or amount), discountValue and the intended purchaseType (one_time or subscription). Amount discounts are in dollars. New codes have a fixed $9.99 minimum spend; the paid product must also remain at least $9.99 after discounts, before tax.
List returns both product-specific codes and workspace-wide codes. global:true creates a workspace-wide code and requires all-products workspace access, even through this product route. Assigned-product teammates can still manage codes for their assigned products. Optional dates, usage limits, customerIds/customerEmails, sale-item discounts and automatic-offer stacking affect eligibility. A saved code can have a Stripe sync error; check stripeSyncError and the correct test/live provider IDs. Do not treat a successful save as proof that every provider offer is active.
SoundSync
Illustrative promo creation
curl --fail-with-body -sS -X POST "$SOUNDSYNC_API_BASE/api/developer/products/123/promo-codes" \
-H "Authorization: Bearer $DEVELOPER_TOKEN" \
-H "Content-Type: application/json" \
--data '{"code":"EXAMPLE10","discountType":"percent","discountValue":10,"purchaseType":"one_time"}'
Edit eligibility without rewriting the offer
PUT /api/developer/products/{productId}/promo-codes/{promoCodeId} is a sparse update. It changes supported limits, dates, active state, sale-item/stacking controls, customer eligibility and metadata. Omitted fields keep their values. code, discountType and discountValue are not changed by this handler; minimum spend and purchase type cannot be changed after creation.
PROMO_MINIMUM_IMMUTABLE and PROMOTION_TYPE_IMMUTABLE require a new or duplicated offer, not repeated retries. PROMOTION_LIMIT_BELOW_COMMITTED means the requested limit is below completed plus reserved uses. maxRedemptions:null removes the limit. Updating a saved workspace-wide code requires all-products access; setting global:false or supplying a productId does not make it product-only. A restricted write returns 403 developer_team_all_products_scope_required without updating the code or syncing Stripe.
Grant access with a reason
POST /api/developer/manual-grants requires productId, email and reason. email remains required when customerId is supplied. Choose access, new_serial or existing_serial through grantType; serial-only non-subscription products can use generated serial fulfillment. licenseType labels the grant, and internalNote records private context. Use sendEmail:false to suppress the normal customer email attempt.
Inspect created, skipped, reason, entitlements, serials and email. An existing active entitlement can return already_granted without a new purchase. Partial workflow errors need reconciliation before a retry; this endpoint does not promise whole-workflow atomicity. Keep returned customer identity, serials and account links private. A grant does not import payment methods or override an unsubscribe.
Subscription grants have an additional lifecycle
Subscription access grants require subscriptionGrantDuration: week, month, three_months, custom or lifetime. custom also needs subscriptionGrantEndDate as YYYY-MM-DD. A timed first-billing date must be at least two days and no more than two years away. Read the returned subscription-specific result and customer continuation rather than treating it as a normal one-time serial.
A timed manual keep-products grant immediately gives permanent ownership of the products included at the time of the grant, without payment. Those products remain owned after subscription expiry, cancellation or incomplete billing setup; an explicit developer access-removal action can revoke the grant. Products added later and future paid subscription benefits are not included in that original ownership snapshot.
Lifetime grants provide complimentary ongoing access rather than this timed-grant ownership. A paid invoice establishes ownership from its own paid product snapshot; an ordinary unpaid trial or failed invoice does not. Future billing still requires separate payment setup, preview and customer confirmation. Existing grant conflict, bundle readiness, provider setup and serial-fulfillment checks can reject a grant request.
SoundSync
Related workflows
SoundSync
Requests and responses
/api/developer/products/{productId}/promo-codes
List product and workspace-wide promo codes
- Auth
- Account Bearer JWT
- Scope
- marketing.view and assigned-product access; effective workspace product ownership.
- Returns
- HTTP 200: promoCodes.
- Notes
- Includes product-specific and workspace-wide codes. Private customer eligibility and provider sync metadata are management information, not a public checkout response.
Request, response and error reference
No request body fields are read by this operation.
HTTP 200 response (application/json)
{
"type": "object",
"properties": {
"promoCodes": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "integer",
"minimum": 1
},
"developerId": {
"type": "integer",
"minimum": 1
},
"productId": {
"type": [
"integer",
"null"
]
},
"code": {
"type": "string"
},
"purchaseType": {
"enum": [
"one_time",
"subscription"
]
},
"discountType": {
"enum": [
"percent",
"amount"
]
},
"discountValue": {
"type": "number"
},
"minimumOrderCents": {
"type": "integer"
},
"minimumOrderAmount": {
"type": "number"
},
"allowDiscountOnSaleItems": {
"type": "boolean"
},
"maxRedemptions": {
"type": [
"integer",
"null"
]
},
"redemptionCount": {
"type": "integer"
},
"startsAt": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"expiresAt": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"isActive": {
"type": "boolean"
},
"customerEligibilityMode": {
"type": "string"
},
"customerEligibility": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": true,
"description": "Operation-specific extension data; do not put credentials or unnecessary personal information here."
}
},
"customerEligibilityCount": {
"type": "integer"
},
"stripeTestCouponId": {
"type": [
"string",
"null"
]
},
"stripeTestPromotionCodeId": {
"type": [
"string",
"null"
]
},
"stripeLiveCouponId": {
"type": [
"string",
"null"
]
},
"stripeLivePromotionCodeId": {
"type": [
"string",
"null"
]
},
"stripeSyncError": {
"type": [
"string",
"null"
]
},
"stripeSyncedAt": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"metadata": {
"type": "object",
"additionalProperties": true,
"description": "Operation-specific extension data; do not put credentials or unnecessary personal information here."
},
"createdAt": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"updatedAt": {
"type": [
"string",
"null"
],
"format": "date-time"
}
},
"additionalProperties": true
}
}
},
"additionalProperties": true
}
Documented failures: 400, 401, 403, 404, 409, 500, 503.
/api/developer/products/{productId}/promo-codes
Create a product promo code
- Auth
- Account Bearer JWT
- Scope
- marketing.manage and assigned-product access; workspace-wide creation additionally requires all-products scope.
- Request fields
- maxRedemptions: integer,null
- startsAt: string,null
- expiresAt: string,null
- isActive: boolean
- allowDiscountOnSaleItems: boolean
- allowAutomaticOfferStacking: boolean
- metadata: object. Operation-specific extension data; do not put credentials or unnecessary personal information here.
- customerIds: array
- customerEmails: array
- code: string (required)
- discountType: percent | amount
- discountValue: number
- minimumOrderAmount: 9.99
- purchaseType: one_time | subscription
- global: boolean
- Returns
- HTTP 201: promoCode.
- Notes
- New minimum spend is fixed at $9.99. Percent discounts cannot exceed 100. Amount discounts are dollar amounts, not cents. Omit purchaseType for one_time; a subscription code must match its product. global:true creates a workspace-wide code and requires all-products access; selected-product teammates receive 403 developer_team_all_products_scope_required before creation or Stripe sync. Stripe mirroring can have side effects; inspect stripeSyncError and environment-specific IDs, not just HTTP 201. A saved code does not bypass the $9.99 discounted paid-product floor.
Request, response and error reference
Request (application/json)
{
"type": "object",
"properties": {
"maxRedemptions": {
"type": [
"integer",
"null"
],
"minimum": 1
},
"startsAt": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"expiresAt": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"isActive": {
"type": "boolean"
},
"allowDiscountOnSaleItems": {
"type": "boolean"
},
"allowAutomaticOfferStacking": {
"type": "boolean"
},
"metadata": {
"type": "object",
"additionalProperties": true,
"description": "Operation-specific extension data; do not put credentials or unnecessary personal information here."
},
"customerIds": {
"type": "array",
"items": {
"type": "integer",
"minimum": 1
}
},
"customerEmails": {
"type": "array",
"items": {
"type": "string",
"format": "email"
}
},
"code": {
"type": "string"
},
"discountType": {
"enum": [
"percent",
"amount"
]
},
"discountValue": {
"type": "number",
"minimum": 0
},
"minimumOrderAmount": {
"const": 9.99
},
"purchaseType": {
"enum": [
"one_time",
"subscription"
]
},
"global": {
"type": "boolean"
}
},
"required": [
"code"
],
"additionalProperties": true
}
HTTP 201 response (application/json)
{
"type": "object",
"properties": {
"promoCode": {
"type": "object",
"properties": {
"id": {
"type": "integer",
"minimum": 1
},
"developerId": {
"type": "integer",
"minimum": 1
},
"productId": {
"type": [
"integer",
"null"
]
},
"code": {
"type": "string"
},
"purchaseType": {
"enum": [
"one_time",
"subscription"
]
},
"discountType": {
"enum": [
"percent",
"amount"
]
},
"discountValue": {
"type": "number"
},
"minimumOrderCents": {
"type": "integer"
},
"minimumOrderAmount": {
"type": "number"
},
"allowDiscountOnSaleItems": {
"type": "boolean"
},
"maxRedemptions": {
"type": [
"integer",
"null"
]
},
"redemptionCount": {
"type": "integer"
},
"startsAt": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"expiresAt": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"isActive": {
"type": "boolean"
},
"customerEligibilityMode": {
"type": "string"
},
"customerEligibility": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": true,
"description": "Operation-specific extension data; do not put credentials or unnecessary personal information here."
}
},
"customerEligibilityCount": {
"type": "integer"
},
"stripeTestCouponId": {
"type": [
"string",
"null"
]
},
"stripeTestPromotionCodeId": {
"type": [
"string",
"null"
]
},
"stripeLiveCouponId": {
"type": [
"string",
"null"
]
},
"stripeLivePromotionCodeId": {
"type": [
"string",
"null"
]
},
"stripeSyncError": {
"type": [
"string",
"null"
]
},
"stripeSyncedAt": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"metadata": {
"type": "object",
"additionalProperties": true,
"description": "Operation-specific extension data; do not put credentials or unnecessary personal information here."
},
"createdAt": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"updatedAt": {
"type": [
"string",
"null"
],
"format": "date-time"
}
},
"additionalProperties": true
}
},
"additionalProperties": true
}
Documented failures: 400, 401, 403, 404, 409, 500, 503.
Operation errors: PROMO_MINIMUM_FIXED, PROMOTION_PRODUCT_TYPE_MISMATCH, PROMO_CODE_EXISTS, developer_team_all_products_scope_required.
/api/developer/products/{productId}/promo-codes/{promoCodeId}
Update limits, dates, eligibility and active state
- Auth
- Account Bearer JWT
- Scope
- marketing.manage and assigned-product access; editing an existing workspace-wide code additionally requires all-products scope.
- Request fields
- maxRedemptions: integer,null
- startsAt: string,null
- expiresAt: string,null
- isActive: boolean
- allowDiscountOnSaleItems: boolean
- allowAutomaticOfferStacking: boolean
- metadata: object. Operation-specific extension data; do not put credentials or unnecessary personal information here.
- customerIds: array
- customerEmails: array
- Returns
- HTTP 200: promoCode.
- Notes
- Sparse update despite PUT: omitted fields retain their saved values. Does not change code, discount type or discount value. Minimum spend and purchase type are immutable. maxRedemptions:null clears the limit; a lower limit than completed plus reserved uses is rejected. Customer eligibility and Stripe sync can be updated. The saved productId determines scope: a workspace-wide code shown under a product still requires all-products access, even if the body supplies global:false or another productId. Selected-product writes fail with 403 developer_team_all_products_scope_required before update or Stripe sync.
Request, response and error reference
Request (application/json)
{
"type": "object",
"properties": {
"maxRedemptions": {
"type": [
"integer",
"null"
],
"minimum": 1
},
"startsAt": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"expiresAt": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"isActive": {
"type": "boolean"
},
"allowDiscountOnSaleItems": {
"type": "boolean"
},
"allowAutomaticOfferStacking": {
"type": "boolean"
},
"metadata": {
"type": "object",
"additionalProperties": true,
"description": "Operation-specific extension data; do not put credentials or unnecessary personal information here."
},
"customerIds": {
"type": "array",
"items": {
"type": "integer",
"minimum": 1
}
},
"customerEmails": {
"type": "array",
"items": {
"type": "string",
"format": "email"
}
}
},
"additionalProperties": true
}
HTTP 200 response (application/json)
{
"type": "object",
"properties": {
"promoCode": {
"type": "object",
"properties": {
"id": {
"type": "integer",
"minimum": 1
},
"developerId": {
"type": "integer",
"minimum": 1
},
"productId": {
"type": [
"integer",
"null"
]
},
"code": {
"type": "string"
},
"purchaseType": {
"enum": [
"one_time",
"subscription"
]
},
"discountType": {
"enum": [
"percent",
"amount"
]
},
"discountValue": {
"type": "number"
},
"minimumOrderCents": {
"type": "integer"
},
"minimumOrderAmount": {
"type": "number"
},
"allowDiscountOnSaleItems": {
"type": "boolean"
},
"maxRedemptions": {
"type": [
"integer",
"null"
]
},
"redemptionCount": {
"type": "integer"
},
"startsAt": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"expiresAt": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"isActive": {
"type": "boolean"
},
"customerEligibilityMode": {
"type": "string"
},
"customerEligibility": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": true,
"description": "Operation-specific extension data; do not put credentials or unnecessary personal information here."
}
},
"customerEligibilityCount": {
"type": "integer"
},
"stripeTestCouponId": {
"type": [
"string",
"null"
]
},
"stripeTestPromotionCodeId": {
"type": [
"string",
"null"
]
},
"stripeLiveCouponId": {
"type": [
"string",
"null"
]
},
"stripeLivePromotionCodeId": {
"type": [
"string",
"null"
]
},
"stripeSyncError": {
"type": [
"string",
"null"
]
},
"stripeSyncedAt": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"metadata": {
"type": "object",
"additionalProperties": true,
"description": "Operation-specific extension data; do not put credentials or unnecessary personal information here."
},
"createdAt": {
"type": [
"string",
"null"
],
"format": "date-time"
},
"updatedAt": {
"type": [
"string",
"null"
],
"format": "date-time"
}
},
"additionalProperties": true
}
},
"additionalProperties": true
}
Documented failures: 400, 401, 403, 404, 409, 500, 503.
Operation errors: PROMO_CODE_NOT_FOUND, PROMO_MINIMUM_IMMUTABLE, PROMOTION_TYPE_IMMUTABLE, PROMOTION_LIMIT_BELOW_COMMITTED, developer_team_all_products_scope_required.
/api/developer/manual-grants
Grant access to an owned product with an audit reason
- Auth
- Account Bearer JWT
- Scope
- entitlements.manage and assigned-product access; product ownership and customer matching remain enforced.
- Request fields
- productId: integer (required)
- email: string (required)
- customerId: integer
- firstName: string
- lastName: string
- reason: string (required)
- internalNote: string
- licenseType: paid | free | nfr | beta | promo | reseller | manual | migration | subscription
- grantType: access | existing_serial | new_serial
- createSerial: boolean
- serialId: integer
- serialCode: string
- sendEmail: boolean
- grantRequestId: string
- subscriptionGrantDuration: week | month | three_months | custom | lifetime
- subscriptionGrantEndDate: string
- Returns
- HTTP 201: success, grantType, customer, accountLink, product, purchase, items, entitlements, serial, serials, email, created, skipped, reason, licenseType.
- Notes
- email is required even with customerId. Supports documented snake_case aliases. grantType defaults to access (or new_serial for a non-subscription serial-only product); createSerial:true is a compatibility shortcut. An existing active grant can return skipped:true/already_granted with no new purchase. Inspect created, email and entitlement outcomes. This writes customer/access/purchase records and normally attempts customer email; sendEmail:false disables that send. It is not a paid checkout, processor refund or migration of stored card details. Subscription grants require an explicit duration and use their own workflow. Timed manual keep-products grants immediately give permanent ownership of the products included when granted, without payment. Those products remain owned after subscription cancellation or expiry; future paid subscription benefits still require billing setup. Explicit developer access removal can revoke the grant. Lifetime grants provide complimentary ongoing access and do not count as paid ownership. An existing customer email is reused across developer workspaces without reassigning its legacy owner. No whole-workflow transaction or blanket replay guarantee is promised.
Request, response and error reference
Request (application/json)
{
"type": "object",
"properties": {
"productId": {
"type": "integer",
"minimum": 1
},
"email": {
"type": "string",
"format": "email"
},
"customerId": {
"type": "integer",
"minimum": 1
},
"firstName": {
"type": "string"
},
"lastName": {
"type": "string"
},
"reason": {
"type": "string"
},
"internalNote": {
"type": "string"
},
"licenseType": {
"enum": [
"paid",
"free",
"nfr",
"beta",
"promo",
"reseller",
"manual",
"migration",
"subscription"
]
},
"grantType": {
"enum": [
"access",
"existing_serial",
"new_serial"
]
},
"createSerial": {
"type": "boolean"
},
"serialId": {
"type": "integer",
"minimum": 1
},
"serialCode": {
"type": "string"
},
"sendEmail": {
"type": "boolean"
},
"grantRequestId": {
"type": "string"
},
"subscriptionGrantDuration": {
"enum": [
"week",
"month",
"three_months",
"custom",
"lifetime"
]
},
"subscriptionGrantEndDate": {
"type": "string",
"format": "date"
}
},
"required": [
"productId",
"email",
"reason"
],
"additionalProperties": true
}
HTTP 201 response (application/json)
{
"type": "object",
"properties": {
"success": {
"type": "boolean"
},
"grantType": {
"type": "string"
},
"customer": {
"type": "object",
"additionalProperties": true,
"description": "Operation-specific extension data; do not put credentials or unnecessary personal information here."
},
"accountLink": {
"type": "object",
"additionalProperties": true,
"description": "Operation-specific extension data; do not put credentials or unnecessary personal information here."
},
"product": {
"type": "object",
"additionalProperties": true,
"description": "Operation-specific extension data; do not put credentials or unnecessary personal information here."
},
"purchase": {
"type": [
"object",
"null"
]
},
"items": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": true,
"description": "Operation-specific extension data; do not put credentials or unnecessary personal information here."
}
},
"entitlements": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": true,
"description": "Operation-specific extension data; do not put credentials or unnecessary personal information here."
}
},
"serial": {
"type": [
"object",
"null"
]
},
"serials": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": true,
"description": "Operation-specific extension data; do not put credentials or unnecessary personal information here."
}
},
"email": {
"type": "object",
"additionalProperties": true,
"description": "Operation-specific extension data; do not put credentials or unnecessary personal information here."
},
"created": {
"type": "boolean"
},
"skipped": {
"type": "boolean"
},
"reason": {
"type": "string"
},
"licenseType": {
"type": "string"
}
},
"additionalProperties": true
}
Documented failures: 400, 401, 403, 404, 409, 500, 503.
Operation errors: PRODUCT_REQUIRED, REASON_REQUIRED, CUSTOMER_EMAIL_REQUIRED, INVALID_GRANT_TYPE, PRODUCT_SERIALS_DISABLED, SUBSCRIPTION_GRANT_DURATION_REQUIRED, CUSTOMER_IDENTITY_CHANGED.
