Keystone Developers
Open Keystone
API reference

Console API

Promos

8 endpoints.

GET/api/v1/admin/promos/coupons

List coupons (platform admin)

Every coupon, newest first. `include_deleted` adds the ones already deleted at Stripe — their rows are kept so past redemptions still resolve to the terms that were granted.

Parameters

NameInTypeDescription
include_deletedquerybooleandefault false
authorizationheaderstring | null

Responses

200Successful Response
application/jsonSuccessResponse_CouponListData_
FieldTypeDescription
request_idrequiredstring
successtruedefault true
datarequiredCouponListData
CouponListData fields
FieldTypeDescription
couponsrequiredCouponData[]
CouponData[] fields

CouponData (not expanded)

metadataResponseMetadata | null
ResponseMetadata fields
FieldTypeDescription
paginationPaginationMetadata | null
PaginationMetadata fields
FieldTypeDescription
limitrequiredinteger
next_cursorstring | null
prev_cursorstring | null
has_morerequiredboolean
has_prevbooleandefault false
total_countinteger | null
status_countsobject | null
object | null fields

Map of string to integer

lifecycle_countsLifecycleCounts | null
LifecycleCounts fields
FieldTypeDescription
leadintegerdefault 0
prospectintegerdefault 0
customerintegerdefault 0
former_customerintegerdefault 0
classification_countsClassificationCounts | null
ClassificationCounts fields
FieldTypeDescription
signalobject
object fields

Nested object (not expanded)

touchobject
object fields

Nested object (not expanded)

stageobject
object fields

Nested object (not expanded)

metaResponseMeta | null
ResponseMeta fields
FieldTypeDescription
totalinteger | null
total_countinteger | null
pageinteger | null
per_pageinteger | null
  • 400Bad request
  • 401Unauthorized
  • 403Forbidden
  • 404Not found
  • 422Validation error
  • 500Internal server error
  • 503Service unavailable

Error bodies: ErrorResponse. See Errors.

POST/api/v1/admin/promos/coupons

Create a coupon (platform admin)

Create the discount's terms. The console names the plans it applies to (`applies_to_plan_names`, from `GET /promos/plans`); payments resolves each to the active plan's Stripe Product, which is why a name that is not an active priced plan comes back as a 400 rather than a silent unrestricted coupon. The restriction is immutable afterwards — a different set of plans is a different coupon.

`idempotency_key` is the console's, generated once per form submission and passed to Stripe verbatim, so a retry after a lost response returns the first coupon instead of minting a second.

Parameters

NameInTypeDescription
authorizationheaderstring | null

Request bodyrequired

application/jsonCouponCreateBody
FieldTypeDescription
namerequiredstringmin length 1 · max length 256
percent_offstring | null
amount_offinteger | null
currencystring | null
durationrequiredstringpattern ^(once|repeating|forever)$
duration_in_monthsinteger | null
applies_to_plan_namesstring[] | null
string[] | null fields
max_redemptionsinteger | null
redeem_bystring (date-time) | null
idempotency_keyrequiredstringmin length 1 · max length 128

Responses

200Successful Response
application/jsonSuccessResponse_CouponData_
FieldTypeDescription
request_idrequiredstring
successtruedefault true
datarequiredCouponData
CouponData fields
FieldTypeDescription
coupon_idrequiredstring (uuid)
namerequiredstring
percent_offstring | null
amount_offinteger | null
currencystring | null
durationrequiredstring
duration_in_monthsinteger | null
applies_to_plan_namesstring[] | null
string[] | null fields
max_redemptionsinteger | null
redeem_bystring (date-time) | null
validrequiredboolean
deleted_atstring (date-time) | null
created_bystring | null
times_redeemedintegerdefault 0
created_atstring (date-time) | null
metadataResponseMetadata | null
ResponseMetadata fields

ResponseMetadata, expanded above.

metaResponseMeta | null
ResponseMeta fields

ResponseMeta, expanded above.

  • 400Bad request
  • 401Unauthorized
  • 403Forbidden
  • 404Not found
  • 422Validation error
  • 500Internal server error
  • 503Service unavailable

Error bodies: ErrorResponse. See Errors.

GET/api/v1/admin/promos/coupons/{coupon_id}

One coupon with its codes (platform admin)

The detail screen: the coupon plus its code strings, each with its allowlist. A targeted code is issued once per business under the same string, so the entries **are** the allowlist; a public code has a single entry with no business. Every entry's business is enriched with its `company_name` here — payments stores only ids.

Parameters

NameInTypeDescription
coupon_idrequiredpathstring (uuid)
authorizationheaderstring | null

Responses

200Successful Response
application/jsonSuccessResponse_CouponDetailData_
FieldTypeDescription
request_idrequiredstring
successtruedefault true
datarequiredCouponDetailData
CouponDetailData fields
FieldTypeDescription
coupon_idrequiredstring (uuid)
namerequiredstring
percent_offstring | null
amount_offinteger | null
currencystring | null
durationrequiredstring
duration_in_monthsinteger | null
applies_to_plan_namesstring[] | null
string[] | null fields
max_redemptionsinteger | null
redeem_bystring (date-time) | null
validrequiredboolean
deleted_atstring (date-time) | null
created_bystring | null
times_redeemedintegerdefault 0
created_atstring (date-time) | null
codesCodeGroupData[]default []
CodeGroupData[] fields

CodeGroupData (not expanded)

metadataResponseMetadata | null
ResponseMetadata fields

ResponseMetadata, expanded above.

metaResponseMeta | null
ResponseMeta fields

ResponseMeta, expanded above.

  • 400Bad request
  • 401Unauthorized
  • 403Forbidden
  • 404Not found
  • 422Validation error
  • 500Internal server error
  • 503Service unavailable

Error bodies: ErrorResponse. See Errors.

DELETE/api/v1/admin/promos/coupons/{coupon_id}

Delete a coupon (platform admin)

Delete at Stripe. New redemptions stop and every code under it goes inactive, but **discounts already granted keep running to their duration** — Stripe copies the terms onto the subscription at redemption, so deleting the coupon does not claw anything back. The mirror row survives, flagged deleted, so past redemptions still name their terms.

Parameters

NameInTypeDescription
coupon_idrequiredpathstring (uuid)
authorizationheaderstring | null

Responses

200Successful Response
application/jsonSuccessResponse_CouponData_
FieldTypeDescription
request_idrequiredstring
successtruedefault true
datarequiredCouponData
CouponData fields

CouponData, expanded above.

metadataResponseMetadata | null
ResponseMetadata fields

ResponseMetadata, expanded above.

metaResponseMeta | null
ResponseMeta fields

ResponseMeta, expanded above.

  • 400Bad request
  • 401Unauthorized
  • 403Forbidden
  • 404Not found
  • 422Validation error
  • 500Internal server error
  • 503Service unavailable

Error bodies: ErrorResponse. See Errors.

POST/api/v1/admin/promos/coupons/{coupon_id}/promotion-codes

Issue a code, or add businesses to one (platform admin)

No `businesses` mints one public code; with them, one customer-restricted code **per business** sharing the string — which is also how a business is added to an existing code's allowlist.

Unknown businesses are refused here, before payments is called: payments would create a Stripe Customer for the id to restrict the code to, and a Customer minted for a typo is permanent. The `company_name` sor holds is passed along to label that Customer, because payments has no other source for it.

Parameters

NameInTypeDescription
coupon_idrequiredpathstring (uuid)
authorizationheaderstring | null

Request bodyrequired

application/jsonPromotionCodesCreateBody
FieldTypeDescription
coderequiredstringmin length 1 · max length 64
businessesBusinessRefBody[]default []
BusinessRefBody[] fields
FieldTypeDescription
business_idrequiredstring (uuid)
expires_atstring (date-time) | null
max_redemptionsinteger | null
first_time_transactionbooleandefault false
minimum_amountintegerdefault 0 · min 0
listed_for_auto_applyboolean | null
idempotency_keyrequiredstringmin length 1 · max length 128

Responses

200Successful Response
application/jsonSuccessResponse_PromotionCodesData_
FieldTypeDescription
request_idrequiredstring
successtruedefault true
datarequiredPromotionCodesData
PromotionCodesData fields
FieldTypeDescription
promotion_codesrequiredPromotionCodeData[]
PromotionCodeData[] fields

PromotionCodeData (not expanded)

metadataResponseMetadata | null
ResponseMetadata fields

ResponseMetadata, expanded above.

metaResponseMeta | null
ResponseMeta fields

ResponseMeta, expanded above.

  • 400Bad request
  • 401Unauthorized
  • 403Forbidden
  • 404Not found
  • 422Validation error
  • 500Internal server error
  • 503Service unavailable

Error bodies: ErrorResponse. See Errors.

GET/api/v1/admin/promos/plans

Plans a coupon can be restricted to (platform admin)

The plan picker on the coupon form. This is payments' catalogue **without** the free plan: a plan with no Stripe price has no Product to carry a discount, so offering it would only produce a 400 on submit. The names here are exactly what `applies_to_plan_names` takes.

Parameters

NameInTypeDescription
authorizationheaderstring | null

Responses

200Successful Response
application/jsonSuccessResponse_PromoPlansData_
FieldTypeDescription
request_idrequiredstring
successtruedefault true
datarequiredPromoPlansData
PromoPlansData fields
FieldTypeDescription
plansrequiredPromoPlanData[]
PromoPlanData[] fields

PromoPlanData (not expanded)

metadataResponseMetadata | null
ResponseMetadata fields

ResponseMetadata, expanded above.

metaResponseMeta | null
ResponseMeta fields

ResponseMeta, expanded above.

  • 400Bad request
  • 401Unauthorized
  • 403Forbidden
  • 404Not found
  • 422Validation error
  • 500Internal server error
  • 503Service unavailable

Error bodies: ErrorResponse. See Errors.

PATCH/api/v1/admin/promos/promotion-codes/{promotion_code_id}

Activate, deactivate, or list a promotion code (platform admin)

`active=false` is how a business leaves an allowlist — codes are never deleted at Stripe. `listed_for_auto_apply` controls whether the catalogue advertises the code in the customer's promo dropdown; an unlisted code still works when typed at Checkout.

Stripe refuses to reactivate a code that expired, hit its cap, or whose coupon was deleted, and that refusal reaches the admin verbatim rather than as a generic failure.

Parameters

NameInTypeDescription
promotion_code_idrequiredpathstring (uuid)
authorizationheaderstring | null

Request bodyrequired

application/jsonPromotionCodeUpdateBody
FieldTypeDescription
activeboolean | null
listed_for_auto_applyboolean | null

Responses

200Successful Response
application/jsonSuccessResponse_PromotionCodeData_
FieldTypeDescription
request_idrequiredstring
successtruedefault true
datarequiredPromotionCodeData
PromotionCodeData fields
FieldTypeDescription
promotion_code_idrequiredstring (uuid)
coupon_idrequiredstring (uuid)
coderequiredstring
activebooleandefault true
business_idstring (uuid) | null
company_namestring | null
expires_atstring (date-time) | null
max_redemptionsinteger | null
first_time_transactionbooleandefault false
minimum_amountintegerdefault 0
listed_for_auto_applybooleandefault false
created_atstring (date-time) | null
metadataResponseMetadata | null
ResponseMetadata fields

ResponseMetadata, expanded above.

metaResponseMeta | null
ResponseMeta fields

ResponseMeta, expanded above.

  • 400Bad request
  • 401Unauthorized
  • 403Forbidden
  • 404Not found
  • 422Validation error
  • 500Internal server error
  • 503Service unavailable

Error bodies: ErrorResponse. See Errors.

GET/api/v1/admin/promos/redemptions

Who redeemed what (platform admin)

Redemption reporting, newest first, filterable by business, coupon or code. A row is a Stripe Discount as payments mirrors it, so `ends_at` is null while the discount is still running. Each row's business is named here from sor's table.

Parameters

NameInTypeDescription
business_idquerystring (uuid) | null
coupon_idquerystring (uuid) | null
promotion_code_idquerystring (uuid) | null
limitqueryintegerdefault 200
authorizationheaderstring | null

Responses

200Successful Response
application/jsonSuccessResponse_RedemptionsData_
FieldTypeDescription
request_idrequiredstring
successtruedefault true
datarequiredRedemptionsData
RedemptionsData fields
FieldTypeDescription
redemptionsrequiredRedemptionData[]
RedemptionData[] fields

RedemptionData (not expanded)

metadataResponseMetadata | null
ResponseMetadata fields

ResponseMetadata, expanded above.

metaResponseMeta | null
ResponseMeta fields

ResponseMeta, expanded above.

  • 400Bad request
  • 401Unauthorized
  • 403Forbidden
  • 404Not found
  • 422Validation error
  • 500Internal server error
  • 503Service unavailable

Error bodies: ErrorResponse. See Errors.