> For clean Markdown of any page, append .md to the page URL. > For a complete documentation index, see https://docs.getkard.com/2024-10-01/api/integration-guides/placements/llms.txt. > For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.getkard.com/_mcp/server. # Placements ## Overview Kard's placements, content strategy, and marketing delivery functionality give you control over where and when offers appear, so the right users see the right loyalty opportunities at the right times. Every placement is configurable: you choose the medium it delivers on, the schedule it follows, and the offers it surfaces by linking a **content strategy**. By pairing configurable **placements** with reusable **content strategies**, you can personalize the offer experience across in-app, push, and email from a single setup. This guide walks you through how placements work, the mediums they support, how content strategies select offers, and how to send offer notifications on a recurring **cadence** or **on demand** for one-time sends. Setting up follows three steps: 1. **Create a content strategy:** define which offers to show and how to rank them. 2. **Create a placement:** choose where and how offers are delivered, and link the content strategy to it. 3. **Send notifications:** reach users directly with a recurring push or a one-time on-demand push or email. ## Content Strategies A **content strategy** defines *which* offers to show and *how* to order them — independent of medium or timing, so one strategy can be reused everywhere. You create and manage strategies through the [Content Strategies API](/2024-10-01/api/organizations/content-strategies/create). | Field | Purpose | | -------------------- | ------------------------------------------------------------------------------------------------------------------- | | `name` | Name of the strategy (unique within the organization). | | `sort` | How selected offers are ordered: `NEWLY_LIVE`, `EXPIRING_SOON`, `HIGHEST_CASHBACK`, or `PERSONALIZED`. At most one. | | `categories` | Merchant categories to include. | | `categoryExclusions` | Merchant categories to exclude. | | `merchantExclusions` | Merchant IDs to exclude. | ### Create a strategy ```json { "data": { "type": "contentStrategy", "attributes": { "name": "Featured Gas Offers", "sort": "HIGHEST_CASHBACK", "categories": ["Gas"] } } } ``` The response returns `201` with the new strategy, including the `id` you'll reference when creating a placement: ```json { "type": "contentStrategy", "id": "01961e5a-b74c-7d42-8456-d3a1f2c90e71", "attributes": { "name": "Featured Gas Offers", "organizationId": "org-123", "sort": "HIGHEST_CASHBACK", "categories": ["Gas"], "categoryExclusions": [], "merchantExclusions": [] } } ``` Link that `id` to any placement through `contentStrategyId`. Updating the strategy changes every placement that uses it — no per-channel rework. ## Placements Placements are the building blocks for delivering offers, and a placement's type decides how and where offers reach the user: * **`placement`**: renders offers inside your app, holding multiple offers via `availableSlots`. An optional `displayName` carries the title cardholders see above the section; `name` stays internal. * **`placementPushNotification`**: sends a single offer as a push notification on a recurring schedule. * **`placementEmail`**: sends offers as an email on a recurring schedule, filling up to `availableSlots` offers per send. * **`placementOnDemand`**: sends a one-time push or email, outside of any schedule. The first three are created once and link to a content strategy through a `contentStrategyId` — Kard selects and surfaces the offers, and any update to the strategy flows through automatically. On-demand sends are fired as you need them, reusing a placement's strategy or a set of offers you specify. The sections below walk through each. ### Create a placement Create a placement through the [Placements API](/2024-10-01/api/organizations/placements/create), setting `type` to the kind you want to create. This example creates an in-app main-page placement with five slots, linked to the content strategy created above: ```json { "data": { "type": "placement", "attributes": { "name": "Featured Gas Offers", "displayName": "Save on gas", "availableSlots": 5, "contentStrategyId": "01961e5a-b74c-7d42-8456-d3a1f2c90e71" } } } ``` ### Mediums Mediums are configured as part of your placements, and the same content strategy can power offers across all three: * **In-app:** a `placement` renders offers when the user opens the screen. It's evaluated at request time, so no schedule is needed — to display it, call [Get Placement Content](/2024-10-01/api/rewards/placement-content) for the user, and Kard returns the offers the placement's content strategy selects as `standardOffer` resources (up to `availableSlots`), ready to render. * **Push:** offers are sent to the user's device as a notification (see [Placement notifications](#placement-notifications)). * **Email:** offers are sent on a recurring `cadence` (a `placementEmail`) or as a one-time on-demand send. #### Curating the in-app placement [Get Placement Content](/2024-10-01/api/rewards/placement-content) is how you curate the in-app placement experience. When a user opens the screen, call the endpoint with their `userId` and the `placementId`, and Kard resolves the linked content strategy in real time — handing back the offers already selected and ranked for that user. Render them exactly as they arrive; there's no client-side filtering or sorting to do. Pass `supportedComponents` to shape which UI components come back. ```bash GET /v2/issuers/{organizationId}/users/{userId}/placements/{placementId}/content?include=categories ``` A standard (in-app) placement returns offers — render these in the order returned, up to the placement's `availableSlots`: ```json { "data": [ { "type": "standardOffer", "id": "5e27318c9b346f00087fbb5c", "attributes": { "name": "World's Greatest Chicken", "terms": "Offer valid at US locations only.", "purchaseChannel": ["INSTORE"], "userReward": { "type": "PERCENT", "value": 5.7 }, "startDate": "2024-11-17T05:00:00Z", "expirationDate": "2025-03-17T05:00:00Z", "isTargeted": true, "assets": [ { "type": "IMG_VIEW", "url": "https://attribution.getkard.com/logos/wgc_logo.png?token=example", "alt": "" } ], "websiteUrl": "https://worldsgreatestchicken.test.com", "description": "Crispy, double-fried spicy chicken." }, "relationships": { "category": { "data": [ { "type": "category", "id": "65920081b524d126068de24a" } ] } } } ], "included": [ { "type": "category", "id": "65920081b524d126068de24a", "attributes": { "name": "Food & Beverage" } } ], "links": { "self": "/v2/issuers/organization-123/users/user-123/placements/placement-homepage-banner/content", "prev": null, "next": null }, "meta": { "placementName": "Homepage Banner", "availableCategories": [ { "type": "category", "id": "65920081b524d126068de24a", "attributes": { "name": "Food & Beverage" } } ] } } ``` The `data` array holds the offers to display, `included` carries the referenced categories (when requested), and `meta.placementName` echoes the placement's display name. Because selection and ranking happen server-side from the content strategy, updating that strategy re-curates the in-app placement automatically — no client changes needed. > **Note** > > This endpoint serves in-app placements only. Push and email placements return `400` and deliver their offers through the content-file webhook instead. ## Placement Notifications Placements can also reach users as notifications, over push or email. These come in two forms: **recurring** sends on a cadence, and **one-time** on-demand sends. ### Cadence notifications A `placementPushNotification` delivers a single offer to the user on the recurring schedule you define. Its `availableSlots` is fixed at `1`, so the linked content strategy picks the most relevant offer for each run. You set the schedule with a `cadence` object: | Field | Purpose | | ------------ | ------------------------------------------------------------------- | | `frequency` | `DAILY`, `WEEKLY`, or `MONTHLY`. | | `timeOfDay` | Time to deliver, `HH:mm` (24-hour, UTC). Optional. | | `dayOfWeek` | `MON`–`SUN`, used when `frequency` is `WEEKLY` (defaults to `MON`). | | `dayOfMonth` | 1–31, used when `frequency` is `MONTHLY` (defaults to 1). | Create type `placementPushNotification` through the [Placements API](/2024-10-01/api/organizations/placements/create). This example sends a daily offer at 09:00 UTC: ```json { "data": { "type": "placementPushNotification", "attributes": { "name": "Daily Gas Offers", "cadence": { "frequency": "DAILY", "timeOfDay": "09:00" }, "contentStrategyId": "01961e5a-b74c-7d42-8456-d3a1f2c90e71" } } } ``` For a weekly send, set `frequency` to `WEEKLY` and add a `dayOfWeek` — for example, `{ "frequency": "WEEKLY", "timeOfDay": "14:00", "dayOfWeek": "FRI" }`. #### Email on a cadence Email placements follow the same schedule model. Create a `placementEmail` with a `cadence` and an `availableSlots` count — unlike push (fixed at one offer), an email send can feature several offers, so the linked content strategy fills up to `availableSlots`: ```json { "data": { "type": "placementEmail", "attributes": { "name": "Weekly Deals Email", "availableSlots": 10, "cadence": { "frequency": "WEEKLY", "timeOfDay": "10:00", "dayOfWeek": "MON" }, "contentStrategyId": "01961e5a-b74c-7d42-8456-d3a1f2c90e71" } } } ``` ### Pausing a scheduled placement Scheduled placements carry a `status` — `ACTIVE` (the default) or `INACTIVE`. Setting it to `INACTIVE` pauses the placement: the delivery schedule is removed, so no further content files are produced or delivered until you reactivate it. The placement and its configuration are untouched, and flipping the status back to `ACTIVE` resumes deliveries on the same cadence. Pause a placement through the [Update Placement](/2024-10-01/api/organizations/placements/update) endpoint by setting `status` alongside the other attributes: ```json { "data": { "type": "placementPushNotification", "attributes": { "name": "Daily Gas Offers", "cadence": { "frequency": "DAILY", "timeOfDay": "09:00" }, "contentStrategyId": "01961e5a-b74c-7d42-8456-d3a1f2c90e71", "status": "INACTIVE" } } } ``` A placement created with `status: "INACTIVE"` starts paused and sends nothing until activated. Omitting `status` on an update leaves the current value unchanged, so routine edits never accidentally resume (or pause) deliveries. > **Note** > > `status` only exists on scheduled placements (`placementPushNotification` and `placementEmail`). In-app placements are always served on request and have no status field. ### Receiving the content file Kard selects the offers for a scheduled send, but you deliver the message to your users. On each run, Kard resolves the linked content strategy, compiles the selected offers into a **gzipped JSONL** file, and notifies you through your **notification webhook**. > **Note** > > Kard does not push to devices or send email on your behalf. Every scheduled send hands you a content file to deliver through your own channels. The handoff has three steps: 1. **Subscribe once.** Register a webhook for the event with the [Create Subscriptions](/2024-10-01/api/notifications/subscriptions/create) endpoint. Use `pushNotificationPlacementFile` for push placements and `emailNotificationPlacementFile` for email placements. 2. **The cadence fires.** At the scheduled time, Kard builds the file and `POST`s a notification to your subscribed `webhookUrl`. 3. **Download and send.** The payload carries a presigned `downloadUrl`; `GET` it before it expires to retrieve the gzipped JSONL, then deliver the offers to your users. Subscribe to the push placement-file event: ```json { "data": [ { "type": "subscription", "attributes": { "eventName": "pushNotificationPlacementFile", "webhookUrl": "https://your-domain.com/webhooks/kard", "enabled": true } } ] } ``` When the cadence runs, Kard delivers a webhook payload like the one below (push shown; email uses `emailNotificationPlacementFile`): ```json { "data": { "id": "669e3823-1688-4d6e-b46e-cf1999d4a25d", "type": "pushNotificationPlacementFile", "attributes": { "placementName": "Top gas cashback", "availableSlots": 3, "cadence": "WEEKLY", "downloadUrl": "https://example.com/placements/669e3823-1688-4d6e-b46e-cf1999d4a25d.jsonl.gz" }, "relationships": { "placement": { "data": { "type": "placement", "id": "669e3823-1688-4d6e-b46e-cf1999d4a25d" } }, "contentStrategy": { "data": { "type": "contentStrategy", "id": "8df56d4f-0dbf-47ab-b081-0c6534dddd34" } } } } } ``` #### Email placement files An email placement (`placementEmail`) hands off its file the same way, on the event `emailNotificationPlacementFile` — subscribe to it just as you would the push event. Because an email send isn't capped at a single offer, `availableSlots` tells you how many offers the file holds. ```json { "data": { "id": "0192a1b2-c3d4-7e8f-9012-3456789abc01", "type": "emailNotificationPlacementFile", "attributes": { "name": "Monthly Top Cashback Email", "availableSlots": 3, "cadence": "MONTHLY", "downloadUrl": "https://example.com/placements/0192a1b2-c3d4-7e8f-9012-3456789abc01.jsonl.gz" }, "relationships": { "placement": { "data": { "type": "placement", "id": "0192a1b2-c3d4-7e8f-9012-aaaa5678cccc" } }, "contentStrategy": { "data": { "type": "contentStrategy", "id": "8df56d4f-0dbf-47ab-b081-0c6534dddd34" } } } } } ``` The `downloadUrl` resolves to the same gzipped JSONL format as the push file (one record per eligible user). Download it before it expires, decompress, and render the offers into your email template. The field reference below applies to both the push and email payloads. | Field | Purpose | | ----------------------------------------------------------- | ---------------------------------------------------------------------------------- | | `attributes.downloadUrl` | Presigned URL to the gzipped JSONL placement file. Download promptly — it expires. | | `attributes.placementName` / `attributes.name` | Display name of the placement (`placementName` on push, `name` on email). | | `attributes.availableSlots` | Number of offers contained in the file. | | `attributes.cadence` | The cadence that produced the file (e.g. `WEEKLY`). | | `attributes.organizationId` | Issuer organization the placement belongs to (email payloads only). | | `relationships.placement` / `relationships.contentStrategy` | References to the source placement and the strategy that selected the offers. | #### Inside the content file The `downloadUrl` points to a **gzipped JSONL** file: decompress it, then parse **one line per eligible user** — not one line per offer. Each line is a self-contained JSON:API `data` envelope whose `id` is the placement the content was generated for. Push and email use different record types. > **Note** > > The `title` and `message` are identical for every user in the file — Kard derives them from the linked content strategy's `sort` and `categories`, so they aren't set per send. **Push** — each line is a `pushNotificationPlacementContent` record. The content strategy's top offer is already resolved into the `title`, `message`, and a per-user `attributionUrl`, so you deliver the line as-is. Two records follow, one per user — `id`, `title`, and `message` are placement-level (identical across users), while `userId` and `attributionUrl` are per-user. The bodies are expanded below for readability; in the file each record is written as a single line. ```json { "data": { "type": "pushNotificationPlacementContent", "id": "669e3823-1688-4d6e-b46e-cf1999d4a25d", "attributes": { "userId": "b3f1c2a4-9d80-4e21-8a77-2c1f5e6d0a9b", "title": "Top Gas cashback", "message": "Explore Gas offers with our highest cashback rates.", "attributionUrl": "https://attribution.getkard.com/p/eyJhbGciOiJIUzI1NiJ9.aaa..." } } } ... { "data": { "type": "pushNotificationPlacementContent", "id": "669e3823-1688-4d6e-b46e-cf1999d4a25d", "attributes": { "userId": "7c2e9a10-4b3d-4f88-91a2-6de0f1c3b5a4", "title": "Top Gas cashback", "message": "Explore Gas offers with our highest cashback rates.", "attributionUrl": "https://attribution.getkard.com/p/eyJhbGciOiJIUzI1NiJ9.bbb..." } } } ``` **Email** — each line is an `emailNotificationPlacementContent` record. Because an email send can feature several offers, the attributes carry an `offers` array (up to `availableSlots`) plus a single per-user `attributionToken`; append that token to each offer's `img` URL when you render it. Two records follow, one per user — `id`, `title`, and `message` are placement-level, while `userId`, `attributionToken`, and the `offers` array are per-user (each user gets the offers they're eligible for, up to `availableSlots`). The bodies are expanded below for readability; in the file each record is written as a single line. ```json { "data": { "type": "emailNotificationPlacementContent", "id": "0192a1b2-c3d4-7e8f-9012-aaaa5678cccc", "attributes": { "userId": "b3f1c2a4-9d80-4e21-8a77-2c1f5e6d0a9b", "title": "Top Gas cashback", "message": "Explore Gas offers with our highest cashback rates.", "attributionToken": "eyJhbGciOiJIUzI1NiJ9.aaa...", "offers": [ { "id": "629fc220b7a4290009a188ec", "name": "Shell", "reward": "5% cash back", "img": "https://cdn.getkard.com/offers/629fc220b7a4290009a188ec.png" }, { "id": "629fc220b7a4290009a188ff", "name": "Chevron", "reward": "3% cash back", "img": "https://cdn.getkard.com/offers/629fc220b7a4290009a188ff.png" } ] } } } ... { "data": { "type": "emailNotificationPlacementContent", "id": "0192a1b2-c3d4-7e8f-9012-aaaa5678cccc", "attributes": { "userId": "7c2e9a10-4b3d-4f88-91a2-6de0f1c3b5a4", "title": "Top Gas cashback", "message": "Explore Gas offers with our highest cashback rates.", "attributionToken": "eyJhbGciOiJIUzI1NiJ9.bbb...", "offers": [ { "id": "629fc220b7a4290009a188ec", "name": "Shell", "reward": "5% cash back", "img": "https://cdn.getkard.com/offers/629fc220b7a4290009a188ec.png" } ] } } } ``` | Field | Purpose | | ----------------------------------------- | --------------------------------------------------------------------------------------------------- | | `data.type` | Record type of the line: `pushNotificationPlacementContent` or `emailNotificationPlacementContent`. | | `data.id` | ID of the placement the content was generated for. | | `attributes.userId` | The user this line should be delivered to. | | `attributes.title` / `attributes.message` | Title and body copy for the push or email (both records). | | `attributes.attributionUrl` | Per-user attribution link for the offer (push records only). | | `attributes.attributionToken` | Per-user attribution token; append it to each offer's `img` URL (email records only). | | `attributes.offers[]` | Featured offers, each with `id`, `name`, `reward`, and `img` (email records only). | See the [Attributions guide](/2024-10-01/api/integration-guides/attributions) for how to track engagement on the links and tokens these records carry. ### One-time sends (on-demand) When you need to reach users outside of a schedule, fire a one-time, on-demand send. Set the `medium` to `push` or `email`, and pin the audience with **exactly one** of `placementId` (to reuse an existing placement's strategy) or `offerIds` (to feature a specific set of offers). ```json { "data": { "type": "placementOnDemand", "attributes": { "medium": "email", "offerIds": ["629fc220b7a4290009a188ec"] } } } ``` The request returns `202` with a `queued` status, and the send is processed asynchronously. > **Warning** > > On-demand sends are limited to **once per 24 hours per issuer**. A request inside that window returns `429` with a `Retry-After` header telling you how many seconds to wait. ### Receiving the on-demand file On-demand fires hand off the content file exactly like cadence sends. Once the queued job finishes building the file, Kard posts a notification to your subscribed webhook — `pushNotificationPlacementFile` when `medium` is `push`, `emailNotificationPlacementFile` when it's `email` — carrying the presigned `downloadUrl` for the gzipped JSONL. Subscribe and consume the payload exactly as described in [Receiving the content file](#receiving-the-content-file) above. The file itself uses the same per-user JSONL records documented in [Inside the content file](#inside-the-content-file) — `pushNotificationPlacementContent` or `emailNotificationPlacementContent`, one line per eligible user. What fills those records depends on how you fired the send: * **By `placementId`:** the send reuses that placement's content strategy, `availableSlots`, and derived copy, so the file matches the placement's cadence send — records carry the placement's `id`, and `title`/`message` come from the strategy's `sort` and `categories`. * **By `offerIds`:** `availableSlots` equals the number of offers you pinned and the copy falls back to the default template. A pinned-offer (`offerIds`) email send — like the on-demand request above — produces records like this: ```json { "data": { "type": "emailNotificationPlacementContent", "id": "b3f1c2a4-9d80-4e21-8a77-2c1f5e6d0a9b", "attributes": { "userId": "b3f1c2a4-9d80-4e21-8a77-2c1f5e6d0a9b", "title": "Earn 5% at Shell", "message": "Explore this offer and discover more ways to earn cash back.", "attributionToken": "eyJhbGciOiJIUzI1NiJ9.aaa...", "offers": [ { "id": "629fc220b7a4290009a188ec", "name": "Shell", "reward": "5% cash back", "img": "https://cdn.getkard.com/offers/629fc220b7a4290009a188ec.png" } ] } } } ``` Firing the same pinned offer with `medium: "push"` produces a `pushNotificationPlacementContent` record instead — same default copy, with the offer resolved into a per-user `attributionUrl`: ```json { "data": { "type": "pushNotificationPlacementContent", "id": "b3f1c2a4-9d80-4e21-8a77-2c1f5e6d0a9b", "attributes": { "userId": "b3f1c2a4-9d80-4e21-8a77-2c1f5e6d0a9b", "title": "Earn 5% at Shell", "message": "Explore this offer and discover more ways to earn cash back.", "attributionUrl": "https://attribution.getkard.com/p/eyJhbGciOiJIUzI1NiJ9.aaa..." } } } ``` > Control where and when offers appear across in-app, push, and email