Offer Bundles API
The POST /api/m2m/offer/bundles endpoint returns the bundles (and their artifacts, payment options, and pricing metadata) that a specific customer can currently purchase from a storefront.
Use this endpoint from machine-to-machine (M2M) services such as the portal backend or automation workflows that need to render bundle pickers or validate purchases.
Endpoint
| Method | Path | Auth scope |
|---|---|---|
POST | /api/m2m/offer/bundles | Service/M2M token with offer.read |
- Consumes:
application/json - Produces:
application/json - Rate limit hint: cache by
customerUrn+storefrontUrnwhile theoffer.expiresAtwindow is valid.
Request Body
The endpoint expects a context object that mirrors what production storefronts send today.
| Field | Type | Required | Description |
|---|---|---|---|
customerUrn | string | ✅ | Customer identifier sb.xx.xxx. Determines personalization and owning status. |
customerIpAddress | string | ✅ | IPv4/IPv6 address used for risk and region inference. |
promotionCode | string | ⛔ | Optional promotion code for campaign/promo activation. |
interactionType | number | ✅ | Enumerated action (example: 10 = Purchase). |
channel | number | ✅ | Sales channel. 101 currently represents Azotte storefront. |
Example Requests
- HTTP
- cURL
- JavaScript
- Java
- .NET
POST /api/m2m/offer/bundles HTTP/1.1
Host: api.moneti.local
Authorization: Bearer <service-token>
Content-Type: application/json
{
"context": {
"customerUrn": "sb.00.060",
"customerIpAddress": "203.0.113.24",
"interactionType": 10,
"channel": 101
}
}
curl -X POST https://api.moneti.local/api/m2m/offer/bundles \
-H "Authorization: Bearer <service-token>" \
-H "Content-Type: application/json" \
-d '{
"context": {
"customerUrn": "sb.00.060",
"customerIpAddress": "203.0.113.24",
"interactionType": 10,
"channel": 101
}
}'
const serviceToken = process.env.SERVICE_TOKEN;
const response = await fetch('https://api.moneti.local/api/m2m/offer/bundles', {
method: 'POST',
headers: {
Authorization: `Bearer ${serviceToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
context: {
customerUrn: 'sb.00.060',
customerIpAddress: '203.0.113.24',
interactionType: 10,
channel: 101
}
})
});
const data = await response.json();
console.log(data);
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
var serviceToken = System.getenv("SERVICE_TOKEN");
var payload = """
{
\"context\": {
\"customerUrn\": \"sb.00.060\",
\"customerIpAddress\": \"203.0.113.24\",
\"interactionType\": 10,
\"channel\": 101
}
}
""";
var client = HttpClient.newHttpClient();
var request = HttpRequest.newBuilder()
.uri(URI.create("https://api.moneti.local/api/m2m/offer/bundles"))
.header("Authorization", "Bearer " + serviceToken)
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(payload))
.build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
var serviceToken = Environment.GetEnvironmentVariable("SERVICE_TOKEN");
var httpClient = new HttpClient
{
BaseAddress = new Uri("https://api.moneti.local")
};
var requestBody = new
{
context = new
{
customerUrn = "sb.00.060",
customerIpAddress = "203.0.113.24",
interactionType = 10,
channel = 101
}
};
using var request = new HttpRequestMessage(HttpMethod.Post, "/api/m2m/offer/bundles")
{
Content = new StringContent(
JsonSerializer.Serialize(requestBody),
Encoding.UTF8,
"application/json"
)
};
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", serviceToken);
var response = await httpClient.SendAsync(request);
var content = await response.Content.ReadAsStringAsync();
Console.WriteLine(content);
Response Shape
Below is the structure of the response. Replace the placeholder block with your actual JSON later.
{
"diagnostics": {
"correlationId": null,
"effectiveRegionCode": null,
"createdAt": "2025-11-18T12:21:53.762051Z",
"nodeId": "DESKTOP-79T4RE5"
},
"offer": {
"offerIdentifier": "ofr.sb.00.060.101.111825122153.5958",
"expiresAt": "2025-11-20T12:21:53.5968343Z",
"offerType": "RecurringSubscription",
"channel": "Azotte",
"interactionType": "Purchase",
"tenantId": 0,
"customerId": 165,
"customerUrn": "sb.00.060",
"storefrontUrn": "st.00.001",
"customTags": null,
"campaign": null,
"steps": [
{
"stepIndex": 0,
"stepIdentifier": "stp_F51d5",
"informationContent": null,
"artifactGroups": [
{
"groupId": 4,
"groupUrn": "bg.00.002",
"groupName": "Foundation",
"artifacts": [
{
"itemIdentifier": "itm_fA817",
"artifactName": "Starter Core",
"pricingGroup": "RecurringSubscription",
"hierarchy": "Parent",
"maxQuantity": 1,
"bundleType": "Base",
"subscriptionChannel": "Azotte",
"groupUrn": "bg.00.002",
"tierLevel": 1,
"orderIndex": 0,
"artifactSku": "AZT.0000..0.10.2510151827",
"artifactUrn": "bd.00.001",
"recurring": true,
"price": {
"currency": {
"symbol": "€",
"symbolNative": "€",
"code": "EUR",
"namePlural": "euros"
},
"quantity": 1,
"pricingGroup": "RecurringSubscription",
"unitPriceTaxExclusive": 126.04,
"unitPriceTaxInclusive": 149.99,
"initialLineTotalTaxExclusive": 126.04,
"initialLineTotalTaxInclusive": 149.99,
"discountsBreakdown": [],
"totalDiscountAmount": 0.0,
"allowanceBreakdown": [],
"totalAllowance": 0.0,
"lineTotalTaxExclusive": 126.04,
"taxBreakdown": [
{
"taxSchemeCode": null,
"taxSchemeName": null,
"ratePercent": 19.0,
"taxableAmount": 126.04,
"taxAmount": 23.95,
"taxCalculationType": "Percentage"
}
],
"lineTotalTaxAmount": 23.95,
"lineTotalTaxInclusive": 149.99
},
"externalProductId": null,
"priceCardContents": {
"artifactName": "Starter Core",
"displayedPrice": "€ 149.99",
"displayedPriceCycle": "/month",
"body": "<p id=\"isPasted\">Core Orchestration</p><p>Customer Data Storage</p>",
"footer": "cancel anytime",
"header": "Azotte",
"badge": "Best Value",
"button": "Get Started",
"featuresText": "Features",
"description": "The essential tools to kickstart your journey with Azotte."
},
"priceCardTemplateIdentifier": "TMPL1",
"stepIdentifier": "stp_F51d5",
"offerIdentifier": "ofr.sb.00.060.101.111825122153.5958",
"owningStatus": {
"isOwned": true,
"startDate": "2025-11-15T12:00:20.664372Z",
"endDate": "2025-12-15T12:00:20.664403Z",
"purchasedDate": "2025-11-15T12:00:19.544621Z",
"willRenew": true,
"orderIdentifier": "mnt.sp.000.0060.1.251115130019",
"paidAmount": 149.9900,
"campaignName": null,
"currencyCode": "EUR",
"currencyName": null,
"subscriptionChannel": "Azotte"
},
"purchaseOption": {
"canPurchase": false,
"reason": "AlreadyOwnedOnSameChannel",
"hideOnPriceCard": false
}
},
],
"orderIndex": 0,
"tierLevel": 1,
"priceCardContents": {
"title": "Simple and Affordable Starter Plans",
"description": "Get started with our Starter Pack, designed for small businesses and individuals looking for essential features at an unbeatable price. "
}
}
]
}
],
"options": {
"paymentOptions": [
{
"paymentChannelType": "CreditCard",
"psp": [
{
"providerName": "CraftGate",
"providerIdentifier": "80b2bcf4-6fa1-47f9-904f-54c5dfa11e40"
}
]
}
],
"storedMethods": [
{
"methodType": "CreditCard",
"pspName": "Global PsP",
"cardType": "VISA",
"tokenizedCardNumber": "4111 11** **** 1111",
"thirdPartyTokenID": "d9ded314-be4f-40a3-9e8a-7e409391987b|d3ea78a6-62cf-41b4-b2c3-2189ae091718"
},
]
}
},
"customer": {
"externalId": "ext-728",
"urn": "sb.00.060"
},
"status": {
"severity": "SUCCESS",
"messageCode": "MNT-000-00",
"source": "Moneti",
"message": "Success",
"notes": null,
"ttl": null,
"warning": [],
"success": true,
"details": null,
"traceId": null
},
"statusCode": "MNT-000-00"
}
Key Sections
| Section | PresentationType |
|---|---|
diagnostics | Trace/debug metadata. Useful for log correlation. |
offer | Core pricing/purchase payload including steps, artifacts, and payment options. |
offer.steps[].artifactGroups[] | Each purchasable artifact/bundle entry. |
offer.options | Payment channels and stored payment methods available to the customer. |
status | Moneti status envelope (MNT-000-00 = success). |
Artifact Object Contract
| Field | Description |
|---|---|
artifactName / artifactSku | Display name and unique SKU. |
bundleType | Identifies Base, Addon, etc. |
price | Full pricing calculation with tax-inclusive/exclusive values. |
owningStatus | Shows if customer already owns the bundle (for gating UIs). |
purchaseOption | Determines if purchase is allowed; includes reason text. |
Usage Tips
- Respect
offer.expiresAt— pricing and eligibility are valid only until this timestamp. - Promotion codes: include
promotionCodeonly when user provides one. - Ordering: use
stepIndexandorderIndexto preserve storefront rendering order. - Payments: only show stored payment methods returned by the backend.
Troubleshooting
| Scenario | Recommendation |
|---|---|
status.success = false | Inspect status.messageCode and retry only after fixing root cause. |
Empty artifactGroups | Possibly already owned items or misconfigured channel. |
canPurchase = false | Respect gating and surface reason (e.g., AlreadyOwnedOnSameChannel). |