update_coupon

Change one or more fields of a coupon code. Partial by design — anything omitted keeps its stored value. Emits coupon.updated.

Purpose

Change an existing coupon without restating it. Only the arguments present in the call are written, the way the REST PATCH behaves, so extending an expiry is one field, not a re-creation. Every field is checked the way create_coupon checks it, and cross-field rules are checked against the coupon's effective values: a percentage cap is judged against the type the coupon will have after the call, an end date against the start it will have.

Synchronous — the row after the change comes back in the response, and coupon.updated emits once with the fields that moved. A call that carries only coupon_id returns the current row and emits nothing.

Omit to keep, send null to clear

Leaving a field out leaves it alone. To remove a limit or a date, send the key with null: max_redemptions (unlimited), minimum_amount (no minimum), starts_at (usable now) and expires_at (no end date) all accept it. max_redemptions_per_user does not — it is always at least 1.

`plan_ids: []` widens the code to every plan

Omitting plan_ids leaves the restriction as it is. Sending an empty list deliberately covers every plan in the project, including ones added later. To narrow a code to "the Premium plan", pass that plan's UUID; resolve it with list_plans first.

Requires the Coupons Addon or a Growth plan on the project owner. When the tier has lapsed the call fails with TEAM_TIER_REQUIRED and nothing is written. The discount still applies to the first payment only; nothing here changes a subscriber's renewal price.

Switching discount_type has a side effect worth knowing: moving to fixed needs a currency_id unless the coupon already stores one, and moving to percentage clears the stored currency.

Required ability

project-coupon:update

Input schema

{
  "type": "object",
  "required": ["coupon_id"],
  "properties": {
    "coupon_id": {
      "type": "string",
      "description": "UUID of the coupon to change."
    },
    "code": {
      "type": "string",
      "description": "New code subscribers type at checkout. Letters, numbers and dashes only, 3 to 64 characters. Stored uppercase; must not already exist in the project."
    },
    "name": {
      "type": "string",
      "description": "Internal label, 3 to 255 characters."
    },
    "discount_type": {
      "type": "string",
      "description": "Either \"percentage\" or \"fixed\". Switching to fixed needs a currency_id unless the code already has one."
    },
    "discount_value": {
      "type": "number",
      "description": "For percentage, 1..100. For fixed, an amount greater than zero in the coupon's currency."
    },
    "currency_id": {
      "type": "string",
      "description": "Currency UUID for a fixed discount. Ignored and cleared for a percentage."
    },
    "max_redemptions": {
      "type": "integer",
      "minimum": 1,
      "description": "Total uses allowed across everyone. Pass null for unlimited."
    },
    "max_redemptions_per_user": {
      "type": "integer",
      "minimum": 1,
      "description": "How many times one subscriber may use the code."
    },
    "minimum_amount": {
      "type": "number",
      "description": "Only apply the code when the plan costs at least this much. Pass null for no minimum."
    },
    "starts_at": {
      "type": "string",
      "description": "ISO 8601 instant the code becomes usable. Pass null to start immediately."
    },
    "expires_at": {
      "type": "string",
      "description": "ISO 8601 instant the code stops working. Pass null for no end date. Must fall after the start."
    },
    "active": {
      "type": "boolean",
      "description": "Whether the code is usable."
    },
    "plan_ids": {
      "type": "array",
      "items": { "type": "string" },
      "description": "Plan UUIDs to restrict the code to. An empty list covers every plan in the project, including ones added later."
    }
  }
}

Output shape

{
  "data": {
    "id": "4b9d3e08-a1f6-4275-9c83-7e01d6a2f594",
    "project_id": "7f3d1c92-8b45-4e6a-9d21-5c8e0a4b6f13",
    "code": "BLACKFRIDAY",
    "name": "Black Friday 2026 (extended)",
    "discount_type": "percentage",
    "discount_value": "25.0000",
    "currency": null,
    "duration": "once",
    "max_redemptions": 750,
    "max_redemptions_per_user": 1,
    "minimum_amount": null,
    "redemptions": {
      "count": 137,
      "remaining": 613,
      "exhausted": false
    },
    "starts_at": "2026-11-27T00:00:00+00:00",
    "expires_at": "2026-12-08T00:00:00+00:00",
    "plan_ids": [],
    "active": true,
    "redeemable": true,
    "created_at": "2026-11-20T09:14:02+00:00"
  }
}

The whole row comes back, not only the fields sent, so there is no need for a follow-up get_coupon. code is uppercase whatever was sent; discount_value is a decimal string. redemptions.remaining is recomputed against the new cap.

Example prompts

"Extend coupon 4b9d3e08-a1f6-4275-9c83-7e01d6a2f594 to 8 December and raise the cap to 750 uses."

"Restrict the WELCOME10 code to the Starter plan only — everything else about it stays."

"Remove the expiry from the launch code so it runs indefinitely."

Failure modes

  • TEAM_TIER_REQUIRED — the project owner's tier no longer includes coupons. error.context.reason says what lapsed.
  • VALIDATION_FAILED — one entry per offending field in error.context: code outside 3–64 characters or containing anything but letters, numbers and dashes, or already taken in the project (compared uppercase); name outside 3–255 characters; discount_type not percentage or fixed; discount_value zero, negative, or above 100 on a percentage; currency_id missing or unknown when the effective type is fixed; max_redemptions below 1; max_redemptions_per_user below 1 or null; minimum_amount zero or negative; starts_at / expires_at not parseable, or an end that does not fall after the effective start; plan_ids not a list, or naming a plan that belongs to another project. A coupon entry means the caller's role lacks the team's coupon-update permission.
  • RESOURCE_NOT_FOUND — unknown coupon_id, a coupon on another team's project, one outside the token's scope:project: allow-list, or a coupon that has been deleted.
  • AUTHENTICATION_REQUIRED — no authenticated user on the request.
  • TOKEN_MISSING_ABILITY — token lacks project-coupon:update.

How is this guide?

On this page

Subscriby is a product designed by you — for you.

No boardroom full of executives deciding what we ships next. Our roadmap always shaped by you with your feedback.

Share feedback or a request