> For the complete documentation index, see [llms.txt](https://apidocs.akinon.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://apidocs.akinon.com/commerce-openapis/dynamic-bin-scheduler/promotions/get-promotions-for-multiple-saved-cards-bulk.md).

# Get promotions for multiple saved cards (bulk)

Returns available promotions for up to 50 saved cards in a single request. Designed for wallet/card-listing pages.

### Optimisation Pipeline

1. **BIN pre-filter** — All active BINs for the currency are loaded in one query. Cards whose `masked_card_number[:6]` is not in any active promotion skip the TapPay call entirely.
2. **Parallel TapPay resolution** — Matching cards are resolved in parallel (≤10 concurrent workers; 5 s per-card timeout; 30 s overall timeout).
3. **Batch DB queries** — All promotions for matched BINs are fetched in one query and grouped in memory. Usage counts use a single `annotate(Count)` query.
4. **Per-card limit filtering** — Promotions where current `usage_count ≥ per_card_limit` (within the active reset window) are excluded.

### Partial Results

The endpoint always returns HTTP 200. Per-card failures appear in `results[].error` and are aggregated in the top-level `errors` array. Successful cards are unaffected.

### Deduplication

Duplicate `card_token` values in the input are silently removed (order preserved).

### Per-Card Error Values

| Error                                | Cause                                  |
| ------------------------------------ | -------------------------------------- |
| `"Invalid token format"`             | Token does not contain `\|\|`          |
| `"Card not found"`                   | TapPay returned no card for this token |
| `"Card lookup timeout"`              | Per-card 5 s timeout exceeded          |
| `"Request timeout exceeded"`         | Overall 30 s timeout exceeded          |
| `"Card lookup failed"`               | Unexpected TapPay error                |
| `"Rate limit exceeded, retry later"` | TapPay returned HTTP 429               |

```json
{"openapi":"3.0.3","info":{"title":"Bankaci API","version":"1.0.0"},"tags":[{"name":"Promotions","description":"Query promotions for a basket or multiple saved cards"}],"servers":[{"url":"{BASE_URL}/api/v1","description":"Bankaci API","variables":{"BASE_URL":{"default":"https://your-bankaci-instance.example.com","description":"Base URL of the Bankaci service"}}}],"paths":{"/promotions/saved-cards/":{"post":{"tags":["Promotions"],"summary":"Get promotions for multiple saved cards (bulk)","description":"Returns available promotions for up to 50 saved cards in a single request.\nDesigned for wallet/card-listing pages.\n\n## Optimisation Pipeline\n\n1. **BIN pre-filter** — All active BINs for the currency are loaded in one query.\n   Cards whose `masked_card_number[:6]` is not in any active promotion skip the\n   TapPay call entirely.\n2. **Parallel TapPay resolution** — Matching cards are resolved in parallel\n   (≤10 concurrent workers; 5 s per-card timeout; 30 s overall timeout).\n3. **Batch DB queries** — All promotions for matched BINs are fetched in one query\n   and grouped in memory. Usage counts use a single `annotate(Count)` query.\n4. **Per-card limit filtering** — Promotions where current `usage_count ≥ per_card_limit`\n   (within the active reset window) are excluded.\n\n## Partial Results\n\nThe endpoint always returns HTTP 200. Per-card failures appear in `results[].error`\nand are aggregated in the top-level `errors` array. Successful cards are unaffected.\n\n## Deduplication\n\nDuplicate `card_token` values in the input are silently removed (order preserved).\n\n## Per-Card Error Values\n\n| Error | Cause |\n|-------|-------|\n| `\"Invalid token format\"` | Token does not contain `\\|\\|` |\n| `\"Card not found\"` | TapPay returned no card for this token |\n| `\"Card lookup timeout\"` | Per-card 5 s timeout exceeded |\n| `\"Request timeout exceeded\"` | Overall 30 s timeout exceeded |\n| `\"Card lookup failed\"` | Unexpected TapPay error |\n| `\"Rate limit exceeded, retry later\"` | TapPay returned HTTP 429 |\n","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkPromotionsRequest"}}}},"responses":{"200":{"description":"Promotion results for each card","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BulkPromotionsResponse"}}}},"422":{"description":"Request-level validation error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidationError"}}}}}}}},"components":{"schemas":{"BulkPromotionsRequest":{"type":"object","required":["cards"],"properties":{"cards":{"type":"array","items":{"$ref":"#/components/schemas/CardInput"},"minItems":1,"maxItems":50,"description":"List of saved cards to query. Duplicates (by `card_token`) are silently removed\nwhile preserving original order.\n"},"currency":{"$ref":"#/components/schemas/CurrencyType"}}},"CardInput":{"type":"object","required":["card_token","masked_card_number"],"properties":{"card_token":{"type":"string","description":"TapPay card token in the format `{card_id}||{customer_id}`"},"masked_card_number":{"type":"string","description":"Masked card number (min 6 chars; first 6 used as BIN-6)","minLength":6}}},"CurrencyType":{"type":"string","enum":["try","eur","usd","egp","gbp","mad","pln","sar","ron","uah","czk","huf","rub","bgn","iqd","kwd","bhd","omr","qar","aed","ngn","inr","lei","kzt","jod","rsd"],"description":"ISO 4217 currency codes supported by Bankaci"},"BulkPromotionsResponse":{"type":"object","properties":{"results":{"type":"array","items":{"$ref":"#/components/schemas/CardPromotions"},"description":"One entry per input card (after deduplication), in original input order.\nCards whose BIN has no active promotion are included with `promotions: []` and\n`error: null` — they were skipped without a TapPay call.\n"},"errors":{"type":"array","items":{"type":"object","properties":{"card_token":{"type":"string"},"error":{"type":"string"}}},"description":"Aggregated list of per-card errors (subset of `results` where `error != null`)"},"meta":{"$ref":"#/components/schemas/BulkPromotionsMeta"}}},"CardPromotions":{"type":"object","properties":{"card_token":{"type":"string","description":"The input card token (echoed back)"},"bin_6":{"type":"string","nullable":true,"description":"First 6 digits of the resolved card number. `null` when the TapPay lookup failed.\nNote: for cards whose BIN has no matching promotions, `bin_6` is still returned\n(extracted from `masked_card_number`) with an empty `promotions` array.\n"},"promotions":{"type":"array","items":{"$ref":"#/components/schemas/PromotionDetail"},"description":"Available promotions for this card. Filtered by `per_card_limit` within the\ncurrent reset period. Empty when no promotions match or the card lookup failed.\n"},"error":{"type":"string","nullable":true,"description":"Per-card error message. `null` on success. Do not display to end users.\nPossible values: `\"Invalid token format\"`, `\"Card not found\"`,\n`\"Card lookup timeout\"`, `\"Request timeout exceeded\"`, `\"Card lookup failed\"`,\n`\"Rate limit exceeded, retry later\"`.\n"}}},"PromotionDetail":{"type":"object","properties":{"title":{"type":"string","description":"Human-readable promotion title"},"basket_offer_ids":{"type":"array","items":{"type":"string"},"description":"Omnitron BasketOffer IDs associated with this promotion"},"priority":{"type":"integer","description":"Promotion priority. Higher value = higher priority in offer ordering."},"per_card_limit":{"type":"integer","description":"Maximum uses per tokenized card within the current reset period"},"end_date":{"type":"string","format":"date-time","description":"Promotion expiry date (ISO 8601)"},"usable_with_voucher_code":{"type":"boolean","description":"Whether this promotion can be stacked with a voucher code.\nWhen `false`, the promotion is excluded when the basket has a voucher code applied.\n"}}},"BulkPromotionsMeta":{"type":"object","properties":{"total":{"type":"integer","description":"Total number of unique card tokens processed (after deduplication)"},"success_count":{"type":"integer","description":"Number of cards successfully resolved via TapPay"},"error_count":{"type":"integer","description":"Number of cards that failed during lookup"}}},"ValidationError":{"type":"object","properties":{"detail":{"type":"array","items":{"type":"object","properties":{"loc":{"type":"array","items":{"type":"string"}},"msg":{"type":"string"},"type":{"type":"string"}}}}}}}}}
```


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://apidocs.akinon.com/commerce-openapis/dynamic-bin-scheduler/promotions/get-promotions-for-multiple-saved-cards-bulk.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
