# CRM · Promotions

Part of AppEngine API Documentation. Full index: https://appengine.appmint.io/documentation.md
## POST /crm/promotions/{name}/subscribe

**Subscribe to a promotion**

`operationId: PromotionController_subscribe`

Subscribes someone to a promotion or mailing list. Either `email` or `phone` identifies them, and `channel` decides how they are contacted.

**`consentText` records what the person actually agreed to.** Store the exact wording shown at the point of signup — it is the evidence of consent, and a generic value undermines it. The UTM fields capture where the signup came from.

#### Signature

```http
POST /crm/promotions/{name}/subscribe (name: string, body) -> The subscription
```

#### Access

Public — no credentials required.

#### Notes

- Depending on the promotion, a confirmation email may follow — see `GET /crm/promotions/confirm/{token}`.

#### Errors

Plus the standard platform errors: `429`, `500`.

#### See also

- `GET /crm/promotions/confirm/{token}`
- `POST /crm/promotions/{name}/unsubscribe`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `name` | path | string | yes | Promotion name. |

### Request body

Who is subscribing, and what they consented to.

```json
{
  "email": "ada@example.com",
  "name": "Ada Lovelace",
  "channel": "email",
  "source": "footer-form",
  "consentText": "I agree to receive marketing emails from Acme Retail. Unsubscribe any time."
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The subscription |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /crm/promotions/confirm/{token}

**Confirm a subscription**

`operationId: PromotionController_confirmSubscription`

Confirms a double opt-in subscription from the link in a confirmation email. The token is single-purpose and identifies the subscriber, so treat the link as a credential.

#### Signature

```http
GET /crm/promotions/confirm/{token} (token: string) -> The confirmation result
```

#### Access

Public — no credentials required.

#### Notes

- Public by necessity — the recipient clicks it from their inbox without signing in.

#### Errors

Plus the standard platform errors: `429`, `500`.

#### See also

- `POST /crm/promotions/{name}/subscribe`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes |  |
| `token` | path | string | yes | Confirmation token from the email. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The confirmation result |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /crm/promotions/{name}/unsubscribe

**Unsubscribe from a promotion**

`operationId: PromotionController_unsubscribe`

Removes someone from a promotion. Honour this promptly and completely — an unsubscribe that keeps sending is a compliance problem, not just a bug.

#### Signature

```http
POST /crm/promotions/{name}/unsubscribe (name: string, body) -> The unsubscribe result
```

#### Access

Public — no credentials required.

#### Errors

Plus the standard platform errors: `429`, `500`.

#### See also

- `GET /crm/promotions/preferences`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `name` | path | string | yes | Promotion name. |

### Request body

Who is unsubscribing.

```json
{
  "email": "ada@example.com"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The unsubscribe result |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /crm/promotions/preferences

**Get subscription preferences**

`operationId: PromotionController_getPreferences`

A subscriber's current preferences across promotions — what a preference centre reads to show which lists someone is on.

#### Signature

```http
GET /crm/promotions/preferences (email?: string, phone?: string) -> Subscription preferences
```

#### Access

Public — no credentials required.

#### Errors

Plus the standard platform errors: `429`, `500`.

#### See also

- `POST /crm/promotions/{name}/unsubscribe`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `email` | query | string | — | Subscriber email. |
| `phone` | query | string | — | Subscriber phone. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Subscription preferences |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /crm/promotions

**List promotions**

`operationId: PromotionController_listPromotions`

The promotions defined for the org.

#### Signature

```http
GET /crm/promotions (status?: string, type?: string) -> Promotions
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `GET /crm/promotions/{name}`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `status` | query | string | — | Filter by status. |
| `type` | query | string | — | Filter by type. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Promotions |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /crm/promotions

**Create a promotion**

`operationId: PromotionController_createPromotion`

Creates a promotion or mailing list that people can subscribe to.

#### Signature

```http
POST /crm/promotions (body) -> The created promotion
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `GET /crm/promotions`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |

### Request body

The promotion to create.

```json
{
  "name": "summer-newsletter",
  "title": "Summer Newsletter"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The created promotion |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /crm/promotions/{name}

**Get a promotion**

`operationId: PromotionController_getPromotion`

Fetches one promotion by name.

#### Signature

```http
GET /crm/promotions/{name} (name: string) -> The promotion
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | NOT_FOUND | Promotion not found | No promotion has that identifier. | Check the identifier against the corresponding list endpoint. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `PUT /crm/promotions/{name}`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `name` | path | string | yes | Promotion name. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The promotion |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `404` | Promotion not found — No promotion has that identifier. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## PUT /crm/promotions/{name}

**Update a promotion**

`operationId: PromotionController_updatePromotion`

Updates a promotion's details. Changing the consent wording here does not alter what existing subscribers agreed to — their recorded `consentText` stands.

#### Signature

```http
PUT /crm/promotions/{name} (name: string, body) -> The updated promotion
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | NOT_FOUND | Promotion not found | No promotion has that identifier. | Check the identifier against the corresponding list endpoint. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `DELETE /crm/promotions/{name}`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `name` | path | string | yes | Promotion name. |

### Request body

Fields to change.

```json
{
  "title": "Summer Newsletter 2026"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `200` | The updated promotion |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `404` | Promotion not found — No promotion has that identifier. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## DELETE /crm/promotions/{name}

**Delete a promotion**

`operationId: PromotionController_deletePromotion`

Deletes a promotion. Its subscriber list goes with it, including the consent records — export them first if you need to prove consent later.

#### Signature

```http
DELETE /crm/promotions/{name} (name: string) -> Deletion result
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Notes

- Consent evidence is deleted along with the subscribers.

#### Errors

| Status | Code | Message | When | What to do |
| --- | --- | --- | --- | --- |
| `404` | NOT_FOUND | Promotion not found | No promotion has that identifier. | Check the identifier against the corresponding list endpoint. |

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `GET /crm/promotions/{name}/subscribers`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `name` | path | string | yes | Promotion name. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Deletion result |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `404` | Promotion not found — No promotion has that identifier. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## GET /crm/promotions/{name}/subscribers

**List promotion subscribers**

`operationId: PromotionController_listSubscribers`

Everyone subscribed to a promotion, with their consent record and where they signed up from.

#### Signature

```http
GET /crm/promotions/{name}/subscribers (name: string, status?: string, page?: string, pageSize?: string) -> Subscribers
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Notes

- Contains personal data and consent evidence — restrict access accordingly.

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `POST /crm/promotions/{name}/subscribe`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `name` | path | string | yes | Promotion name. |
| `status` | query | string | — | Filter by status. |
| `page` | query | string | — | Page number (1-based). |
| `pageSize` | query | string | — | Rows per page. |

### Responses

| Status | Meaning |
| --- | --- |
| `200` | Subscribers |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

## POST /crm/promotions/{name}/incentive-claimed

**Record an incentive claim**

`operationId: PromotionController_markIncentiveClaimed`

Records that a subscriber claimed the promotion's incentive — the discount code or freebie offered for signing up — so it is not given twice.

#### Signature

```http
POST /crm/promotions/{name}/incentive-claimed (name: string, body) -> The claim result
```

#### Access

Requires a bearer JWT (`Authorization: Bearer <token>`).

#### Errors

Plus the standard platform errors: `401`, `403`, `429`, `500`.

#### See also

- `GET /crm/promotions/{name}/subscribers`

### Parameters

| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `orgid` | header | string | yes | Organization (tenant) identifier. Every request is scoped to this org; data from other orgs is never visible. Issued with your API credentials. |
| `name` | path | string | yes | Promotion name. |

### Request body

Who claimed it.

```json
{
  "email": "ada@example.com"
}
```

### Responses

| Status | Meaning |
| --- | --- |
| `201` | The claim result |
| `401` | Authentication failed: Invalid or expired token — The `Authorization` header is missing, malformed, or the JWT has expired. |
| `403` | You do not have permission to perform this action — The caller is authenticated but lacks the role required by the endpoint, or is acting on another org. |
| `429` | Too Many Requests — More than 100,000 requests from one IP within 5 minutes (configurable per deployment). CORS preflights and requests from inside the platform cluster are not counted. The limiter answers before the error filter, so the body is `{ statusCode, error, message }` with no `path`, `method` or `timeStamp`; the `RateLimit-*` response headers say when the window resets. |
| `500` | An unexpected error occurred. Our team has been notified. — An unhandled server-side failure. |

