Every API here is available over the APIs.io API and to AI agents over MCP.
openapi: 3.2.0
info:
title: Offer Decisioning Public API - v5 Public Offerings API
version: 5.0.0
description: 'The Offerings API lets you create, update, and list offerings for Offer Decisioning, and
list the personalization templates available for offering content. For lifecycle, rate limits,
idempotency, and error details, see the [Offerings Overview](/api/offerings/offerings-overview).
'
servers:
- url: https://api-{dc}.moengage.com
variables:
dc:
default: '01'
description: 'MoEngage data center identifier. Replace with your assigned DC number. Full example: https://api-01.moengage.com
'
tags:
- name: Public Offerings
paths:
/v5/offers:
get:
operationId: listPublicOfferings
summary: List Offerings
description: 'Fetch all offerings in the workspace. Returns a summary of each offering — ID, name,
status, tags, and creation metadata.
'
x-mint:
content: '#### Rate Limit
The rate limit is 150 requests per minute, 500 requests per hour, and 1000 requests per day per consumer.
'
x-moe-mcp:
expose:
external: true
internal: true
tool_description: "List all offer campaigns in the workspace. Use this as the first step to discover\nexisting offerings and their IDs before calling update_offering or inspecting details.\n\nReturns a paginated summary — ID, name, status, tags, created_at, created_by.\nUse the fields parameter to request only the fields you need (reduces token usage).\n\nTypical agent flow:\n 1. Call list_offerings (optionally filter by status_in=active or name_contains)\n 2. Pick the offering ID (e.g. 6a4f69ee490f66322f968b87) from the results\n 3. Call update_offering with that ID to modify the offering\n\nPagination: pass next_cursor from the pagination object on the next call.\nKeep paginating until has_more is false.\n"
param_overrides:
status_in:
description: 'Filter by lifecycle status. Use ''active'' to find offerings currently being
served to users. Use ''scheduled'' for upcoming ones. Omit to return all statuses
including drafts. Valid values: active, scheduled, expired, archived, draft.
Example: status_in=active,scheduled
'
cursor:
description: 'An opaque pagination token used to retrieve the next page of results.
* **Initial Request:** Omit this parameter entirely on the first call to fetch the beginning of the list.
* **Subsequent Pages:** Pass the exact string received from the `next_cursor` field of the immediate previous response.
* **Handling:** Pass this token completely verbatim. Do not decode, alter, or modify the string in any way.
'
fields:
description: 'Comma-separated list of fields to return. Omit for all six fields.
Use to minimise token usage. Valid values: `id`, `name`, `status`, `tags`, `created_at`, `created_by`.
Example: fields=id,name,status
'
limit:
description: 'The number of offerings to return per paginated response.
'
tags:
- Public Offerings
security:
- basicAuth: []
parameters:
- name: id
in: query
required: false
schema:
type: string
description: 'Look up a single offering by its exact prefixed ID (e.g. offer_6a4f69ee490f66322f968b87). Use this when you already know the offering ID and want to confirm it exists before updating it.
'
- name: name_contains
in: query
required: false
schema:
type: string
maxLength: 100
description: 'Find offerings whose name contains specific text (case-insensitive). For example, pass "summer" to find all summer-related offerings.
'
- name: status_in
in: query
required: false
schema:
type: string
description: 'Filter by lifecycle status. Valid values: active, scheduled, expired, archived, draft. Example: status_in=active,scheduled
'
- name: tags
in: query
required: false
schema:
type: array
items:
type: string
style: form
explode: true
description: 'Filter to offerings that have ALL of the specified tags. Repeat the parameter for multiple tags: tags=summer-sale&tags=vip-users.
'
- name: created_date_gte
in: query
required: false
schema:
type: string
format: date
description: 'List offerings created on or after this date. Format: YYYY-MM-DD.
'
- name: created_date_lte
in: query
required: false
schema:
type: string
format: date
description: 'List offerings created on or before this date. Format: YYYY-MM-DD.
'
- name: created_by
in: query
required: false
schema:
type: array
items:
type: string
style: form
explode: true
description: 'Filter offerings created by specific team members. Pass one or more email addresses. Example: created_by=john.doe@example.com,marketing-team@example.com
'
- name: fields
in: query
required: false
schema:
type: string
description: 'Request only the fields you need. Example: fields=id,name,status returns only those three fields per item.
'
- name: cursor
in: query
required: false
schema:
type: string
description: 'Pagination cursor returned as `next_cursor` in the previous response. Omit on the first request. When `next_cursor` is `null` in the response, you have reached the last page.
'
- name: limit
in: query
required: false
schema:
type: integer
default: 20
minimum: 1
maximum: 100
description: Items per page. Default 20, max 100.
- name: X-MOE-Request-Id
in: header
required: false
schema:
type: string
format: uuid
description: 'Client-supplied trace ID (UUID v4). Echoed back in the `X-MOE-Request-Id` response header.
'
responses:
'200':
description: Paginated list of offerings.
headers:
X-MOE-Request-Id:
schema:
type: string
description: Trace ID echoed from the gateway.
X-RateLimit-Limit:
schema:
type: integer
example: 150
description: Maximum requests allowed in the current window.
X-RateLimit-Remaining:
schema:
type: integer
description: Requests remaining in the current window before rate limiting applies.
X-RateLimit-Reset:
schema:
type: integer
description: UTC epoch second when the current rate-limit window resets.
content:
application/json:
schema:
type: object
required:
- response_id
- type
- data
- pagination
properties:
response_id:
type: string
description: 'Format: `resp_<X-MOE-Request-Id>` header value (auto-generated UUID if the `X-MOE-Request-Id` header was not part of the request body).
'
type:
type: string
enum:
- offer
data:
type: array
description: List of offering summaries matching the applied filters.
items:
$ref: '#/components/schemas/OfferingListItem'
pagination:
$ref: '#/components/schemas/CursorPagination'
example:
response_id: resp_d31d6d61-6f3b-40dc-a459-548f385a60cb
type: offer
data:
- id: offer_6a4f69ee490f66322f968b87
name: Summer_Sale_Promo_2026
status: active
tags:
- tag_<object-id>
created_at: '2026-06-23T12:29:41.225270281Z'
created_by: marketing-team@example.com
- id: offer_6a4f69ee490f66322f968b87
name: Homepage_Banner_AB_Test
status: scheduled
created_at: '2026-06-23T12:29:41.225270281Z'
created_by: growth-team@example.com
pagination:
next_cursor: b2ZmZXJzOjZhM2E3YzM0NTE4NjdkNGRjZmIxNzQ5Mw==
limit: 20
has_more: true
total_count: 42
'400':
description: 'Invalid query parameter. Possible error codes in `error.details[].code`:
- `OUT_OF_RANGE` — `limit` > 100, or `name_contains` > 100 chars.
- `INVALID_CURSOR` — `cursor` is present but malformed (not valid base64, or corrupted segment/id).
- `INVALID_ENUM_VALUE` — unrecognised value in `status_in`.
- `INVALID_FIELDS_PARAM` — unrecognised field name in `fields`.
- `INVALID_TYPE` — `created_date_gte` or `created_date_lte` not in YYYY-MM-DD format.
'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
example:
error:
code: VALIDATION_FAILED
message: One or more query parameters are invalid.
doc_url: https://www.moengage.com/docs/api/offerings/offerings-overview
details:
- code: INVALID_ENUM_VALUE
target: status_in
message: '''invalid_status'' is not a valid status. Allowed values: active, scheduled, expired, archived, draft.'
doc_url: https://www.moengage.com/docs/api/offerings/offerings-overview
response_id: resp_<trace-id>
'401':
$ref: '#/components/responses/GatewayAuthError'
'403':
$ref: '#/components/responses/GatewayAuthError'
'429':
description: 'The rate limit for this endpoint has been exceeded. Retry after the window
indicated in the `Retry-After` response header (in seconds).
'
headers:
Retry-After:
schema:
type: integer
description: Seconds to wait before retrying.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'503':
$ref: '#/components/responses/ServiceUnavailable'
post:
operationId: createPublicOffering
summary: Create Offering
description: 'Create a new Offering in the workspace.
'
x-mint:
content: '#### Rate Limit
The rate limit is 100 requests per minute, 300 requests per hour, and 600 requests per day per consumer.
'
x-moe-mcp:
expose:
external: true
internal: true
tool_description: "Create a new offering in the workspace.\n\nKey constraints:\n - start_datetime MUST be in the future and use UTC \"Z\" suffix (e.g. \"2026-07-01T00:00:00Z\").\n A past value returns 400 INVALID_SCHEDULING. Offset strings like \"+05:30\" are not accepted.\n - tags must be pre-existing tag IDs — unknown IDs return 400 INVALID_TAG_ID.\n - variation_meta CAN be updated via PATCH with status-based rules. See update_offering.\n - name must be alphanumeric + underscores only (no spaces or hyphens). 5–100 chars.\n\nAlways supply a unique Idempotency-Key (UUID v4) header to prevent duplicate offerings\non retries. The same key within 24 hours returns the original 201 response.\n\nFor a simple single-variant offering with no A/B test:\n - Set variation_meta.type = SMV, variantsPerLocale = [Variant_A], split = {Variant_A: 100}\n - Put content in offer_content.locales[\"0\"].variations.Variant_A\n - Locale key must be \"0\" for single-locale offerings (not \"default\" or \"en\")\n"
param_overrides:
variation_meta:
description: "A/B test configuration. For a simple offering (no A/B test), use:\n type: SMV, variantsPerLocale: [Variant_A], split: {Variant_A: 100}\nFor A/B test (SMV): split values must sum to exactly 100 inclusive of control group allocation(INVALID_SPLIT if not).\nFor AI optimization (DMV): provide equal initial splits; the server learns over time.\nStatus-based PATCH rules: SCHEDULED=free; ACTIVE+SMV=split% only; ACTIVE+DMV=control_group.percentage only.\n"
offer_content:
description: "Content payload. Use the locales[\"0\"] nested format for single-locale offerings:\n {\"locales\": {\"0\": {\"variations\": {\"Variant_A\": {\"content_1\": {\"type\": \"json\", \"value\": \"...\"}}}}}}\nThe locale key \"0\" is required for single-locale — do not use \"default\" or \"en\".\nVariation keys in offer_content must exactly match variantsPerLocale and split keys (SPLIT_KEY_MISMATCH if not).\n"
scheduling:
description: 'Schedule window. Both start_datetime and expiry_datetime are required and **MUST** use
UTC "Z" suffix (e.g. "2026-07-01T00:00:00Z"). Offset strings like "+05:30" return INVALID_TYPE.
start_datetime must be in the future.
'
segment_info:
description: 'Audience targeting. To target all users with no restrictions, send: {"filters": {}}
For attribute-based targeting, use filter_type: "user_attributes" with data_type required.
'
tags:
- Public Offerings
security:
- basicAuth: []
parameters:
- name: Idempotency-Key
in: header
required: true
schema:
type: string
format: uuid
description: 'UUID v4 idempotency token. Submitting the same key within 24 hours returns the original `201 response` without re-creating the Offering. A different body hash on the same key returns `409 IDEMPOTENCY_CONFLICT`. A concurrent in-progress request with the same key returns `409 DUPLICATE_IDEMPOTENCY_KEY`.
'
- name: X-MOE-Request-Id
in: header
required: false
schema:
type: string
format: uuid
description: 'Client-supplied trace ID (UUID v4). Echoed back in the X-MOE-Request-Id response header. Use this to correlate client requests with server-side logs.
'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ClientOfferCreateRequest'
examples:
jsonSingleVariantNoCapping:
summary: 'JSON: 1 variant, no control group, no capping'
value:
name: Summer_Sale
description: Summer sale discount offers
priority: 50
delivery: scheduled
created_by: john.doe@example.com
scheduling:
start_datetime: '2026-07-16T00:00:00Z'
expiry_datetime: '2026-07-16T23:59:00Z'
timezone: Asia/Kolkata
segment_info:
filters:
included_filters:
filter_operator: and
filters:
- filter_type: custom_segments
name: level_0_for_automation
id: 6a572827fad010a3318a1bc1
offer_content:
content_1:
type: json
value: '{"discount":"10%"}'
variation_meta:
type: SMV
variantsPerLocale:
- Variant_A
control_group:
is_enabled: false
percentage: 0
smv_distribution:
split:
Variant_A: 100
capping_rules: {}
is_global_control_enabled: false
imp_track_hours: 36
jsonMultiVariantWithCapping:
summary: 'JSON: 2 variants, control group enabled, overall + user-level capping (scheduled delivery)'
value:
name: Summer_Sale
description: Summer sale discount offers
priority: 50
delivery: scheduled
created_by: john.doe@example.com
scheduling:
start_datetime: '2026-07-16T00:00:00Z'
expiry_datetime: '2026-07-16T23:59:00Z'
timezone: Asia/Kolkata
segment_info:
filters:
included_filters:
filter_operator: and
filters:
- filter_type: custom_segments
name: level_0_for_automation
id: 6a572827fad010a3318a1bc1
offer_content:
locales:
'0':
variations:
Variant_A:
content_1:
type: json
value: '{"discount":"10%"}'
Variant_B:
content_1:
type: json
value: '{"discount":"20%"}'
variation_meta:
type: SMV
variantsPerLocale:
- Variant_A
- Variant_B
control_group:
is_enabled: true
percentage: 10
smv_distribution:
split:
Variant_A: 50
Variant_B: 50
capping_rules:
overall:
enabled: true
limit_value: 1
limit_schedule: DAILY
user_level:
enabled: true
limit_value: 15
limit_schedule: MONTHLY
is_global_control_enabled: true
imp_track_hours: 36
templateDmvWithCapping:
summary: 'Template: 2 variants (DMV), control group enabled, user_attributes segment, capping (scheduled delivery)'
value:
name: Summer_Sale
description: Summer sale discount offers
priority: 50
delivery: scheduled
created_by: john.doe@example.com
scheduling:
start_datetime: '2026-07-16T11:00:00Z'
expiry_datetime: '2026-07-16T23:59:00Z'
timezone: Asia/Kolkata
segment_info:
filters:
included_filters:
filter_operator: and
filters:
- filter_type: user_attributes
data_type: string
name: Name
value:
- David
case_sensitive: false
operator: in
negate: false
- filter_type: user_attributes
data_type: string
name: last_purchase_category
value:
- Electronics
case_sensitive: false
operator: in
negate: false
excluded_filters:
filter_operator: and
filters:
- filter_type: user_attributes
data_type: string
name: city
value:
- Detroit
case_sensitive: false
operator: in
negate: false
offer_content:
locales:
'0':
variations:
Variant_A:
content_1:
type: template
value:
id: card_notification_01
title: Welcome to Your Dashboard
description: This is a generic description field supporting standard text notification content.
ctaUrl: https://example.com/destination-pathv2
darkModeImageUrl: https://example.com/assets/dark-mode-featurev2.png
lightModeImageUrl: https://example.com/assets/light-mode-featurev2.png
darkModeBackgroundImageUrl: https://example.com/assets/dark-mode-bgv2.png
ariaLabel: Alternative text description for screen readers
meta:
templateId: feature_hero_slide
Variant_B:
content_1:
type: template
value:
id: card_notification_01
title: Welcome to Your Dashboard
description: This is a generic description field supporting standard text notification content.
ctaUrl: https://example.com/destination-path
darkModeImageUrl: https://example.com/assets/dark-mode-feature.png
lightModeImageUrl: https://example.com/assets/light-mode-feature.png
darkModeBackgroundImageUrl: https://example.com/assets/dark-mode-bg.png
ariaLabel: Alternative text description for screen readers
meta:
templateId: feature_hero_slide
variation_meta:
type: DMV
variantsPerLocale:
- Variant_A
- Variant_B
control_group:
is_enabled: true
percentage: 5
capping_rules:
overall:
enabled: true
limit_value: 1
limit_schedule: DAILY
user_level:
enabled: true
limit_value: 1
limit_schedule: WEEKLY
is_global_control_enabled: true
imp_track_hours: 36
responses:
'201':
description: Offering created successfully.
headers:
X-MOE-Request-Id:
schema:
type: string
description: Echoed trace ID (from request header or server-generated).
X-RateLimit-Limit:
schema:
type: integer
example: 100
description: Maximum requests allowed in the current window.
X-RateLimit-Remaining:
schema:
type: integer
description: Requests remaining in the current window before rate limiting applies.
X-RateLimit-Reset:
schema:
type: integer
description: UTC epoch second when the current rate-limit window resets.
content:
application/json:
schema:
$ref: '#/components/schemas/PublicOfferCreateResponse'
'400':
description: "Validation errors. All errors are collected before returning, so a single 400\nresponse may contain multiple `details` entries. Possible error codes:\n- `REQUIRED_FIELD_MISSING` — a required field is absent (e.g. missing `variation_meta`, `name`, `scheduling.start_datetime`). The `target` field identifies the missing field.\n- `INVALID_TYPE` — a field has the wrong type or format (e.g. `content_1.value` is not a JSON object when `type` is `template`; or `scheduling.start_datetime` is not a valid ISO 8601 datetime with UTC \"Z\" suffix).\n- `OUT_OF_RANGE` — numeric field outside its allowed range (e.g. `priority` outside 1–100, `control_group.percentage` outside 1–99).\n- `INVALID_ENUM_VALUE` — enum field has an unrecognised value.\n- `INVALID_OFFER_NAME` — `name` contains characters other than alphanumeric characters and underscores. Spaces and hyphens are not allowed.\n- `INVALID_SCHEDULING` — `expiry_datetime` is not after `start_datetime`; `start_datetime` is in the past; or `expiry_datetime` is less than 10 minutes in the future.\n- `INVALID_TIMEZONE` — `scheduling.timezone` is present but not a valid IANA timezone name (e.g. `Asia/Kolkata`, `America/New_York`, `UTC`).\n- `SPLIT_KEY_MISMATCH` — keys in `smv_distribution.split` do not match `variantsPerLocale`.\n- `INVALID_SPLIT` — values in `smv_distribution.split` do not sum to exactly 100. Adjust the percentages so the total equals 100.\n- `VARIANT_COUNT_MISMATCH` — count of variants in `offer_content` does not match `variantsPerLocale`.\n- `INVALID_TEMPLATE_ID` — `meta.templateId` refers to a template ID not registered in the workspace.\n- `MISSING_TEMPLATE_FIELD` — a required field (per the template's config) is missing or blank in the `value` map.\n The `target` field will be `offer_content.locales.<locale>.variations.<variant>.content_1.value.<fieldKey>`.\n- `DUPLICATE_OFFER_NAME` — `name` matches an existing non-archived offering in this workspace. Offering names must be unique. Choose a different name.\n- `INVALID_TAG_ID` — one or more tag IDs in `tags` do not exist in this workspace. Create the tags first via the dashboard before referencing them here.\n- `REVENUE_FIELDS_INCOMPLETE` — `conversion.primary.revenue_amount` or `conversion.primary.revenue_currency` is missing when `is_revenue_tracking` is true.\n- `INVALID_CURRENCY_CODE` — `conversion.primary.revenue_currency` is not a valid ISO 4217 code (e.g. use \"USD\", \"EUR\", \"INR\").\n- `MISSING_IDEMPOTENCY_KEY` — `Idempotency-Key` header is absent. Add the header with a UUID v4 value.\n- `INVALID_IDEMPOTENCY_KEY` — `Idempotency-Key` is present but not a valid UUID v4. Use a standard UUID v4 format (e.g. <uuid-v4>).\n"
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
example:
error:
code: VALIDATION_FAILED
message: Request validation failed.
doc_url: https://www.moengage.com/docs/api/offerings/offerings-overview
details:
- code: REQUIRED_FIELD_MISSING
target: variation_meta
message: variation_meta is required.
doc_url: https://www.moengage.com/docs/api/offerings/offerings-overview
- code: INVALID_SCHEDULING
target: scheduling
message: expiry_datetime must be after start_datetime.
doc_url: https://www.moengage.com/docs/api/offerings/offerings-overview
- code: INVALID_TEMPLATE_ID
target: offer_content.locales.0.variations.Variant_A.content_1.meta.templateId
message: Template 'app_large' is not registered in this workspace.
doc_url: https://www.moengage.com/docs/api/offerings/offerings-overview
- code: MISSING_TEMPLATE_FIELD
target: offer_content.locales.0.variations.Variant_A.content_1.value.cta_url
message: Field 'cta_url' is required by the template but is missing or blank.
doc_url: https://www.moengage.com/docs/api/offerings/offerings-overview
response_id: resp_<trace-id>
'401':
description: "Authentication failure. Two sources produce 401 on this endpoint:\n- **Gateway (APISIX)** — credentials missing or invalid. Returns `Content-Type: text/plain`\n with body `{\"code\":\"ER001\",\"target\":\"Authentication Invalid\",\"message\":\"...\"}`.\n- **Service** — credentials passed the gateway but the injected `X-MOE-Tenant-ID` header\n is missing (request bypassed gateway). Returns `Content-Type: application/json`.\n"
content:
text/plain:
schema:
type: string
description: Gateway (APISIX) authentication failure. Body is JSON-formatted but sent as text/plain.
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'403':
$ref: '#/components/responses/GatewayAuthError'
'409':
description: 'Idempotency conflict. Two distinct error codes are possible:
**DUPLICATE_IDEMPOTENCY_KEY** — a request with the same Idempotency-Key is already
in-flight (concurrent duplicate). Wait for the first request to complete before retrying.
**IDEMPOTENCY_CONFLICT** — the Idempotency-Key was already used within the 24-hour
window but the request body hash differs from the original request. Use a new
Idempotency-Key to submit a different payload.
'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'429':
description: 'The rate limit for this endpoint has been exceeded. Retry after the window
indicated in the `Retry-After` response header (in seconds).
'
headers:
Retry-After:
schema:
type: integer
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'503':
$ref: '#/components/responses/ServiceUnavailable'
/v5/offers/{offer_id}:
patch:
operationId: updatePublicOffering
summary: Update Offering
description: 'Modify an existing offering. Only the fields you include in the request body are
changed.
'
x-mint:
content: '#### Rate Limit
The rate limit is 100 requests per minute, 300 requests per hour, and 600 requests per day per consumer.
'
x-moe-mcp:
expose:
external: true
internal: true
tool_description: "Partially update an existing offering. Only the fields included in the request body\nare changed — all other fields retain their current values.\n\nUse list_offerings first to discover the offering ID if you don't already have it.\n\nCommon update patterns:\n - Extend expiry: include only scheduling.expiry_datetime with a later date (Z suffix required)\n - Rename: include only name (alphanumeric + underscores only; returns DUPLICATE_OFFER_NAME if taken)\n - Update content: include only offer_content\n - Replace tags: include tags as a full list of tag IDs (full replacement, not additive).\n Tag IDs from list_offerings are returned with \"tag_\" prefix (e.g. \"tag_<object-id>\")\n and can be passed directly — both \"tag_<id>\" and raw \"<id>\" formats are accepted.\n\nStatus-gated fields — return 400 IMMUTABLE_FIELD if violated:\n - scheduling.start_datetime: locked once status becomes 'active'\n - conversion: locked once status becomes 'active'\n - variation_meta.type: locked once status is 'active'\n - Adding/removing variants in variantsPerLocale: locked once 'active'\n - ACTIVE + SMV: only
# --- truncated at 32 KB (89 KB total) ---
# Full source: https://raw.githubusercontent.com/api-evangelist/moengage/refs/heads/main/openapi/moengage-public-offerings-api-openapi.yml