> ## Documentation Index
> Fetch the complete documentation index at: https://developer.omni.z-api.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Multi-product

> Showcase with sections and up to 30 products, assembled at send time

export const TemplatePreview = ({header, headerKind = 'text', headerIcon, productLine, body, footer, buttons = [], cards = [], offer, coupon, order, callPermission, caption, height, lang = 'pt'}) => {
  const L = ({
    pt: {
      contact: 'Cliente teste',
      label: 'pré-visualização',
      orderNote: 'exemplo: os valores e os itens vêm no envio',
      item: 'Produto',
      qty: 'Qtd. 1',
      subtotal: 'Subtotal',
      tax: 'Taxas',
      total: 'Total',
      callAsk: 'A empresa pode te ligar?',
      callAllow: 'Permitir chamadas',
      callAllowTemp: 'Permitir por 7 dias',
      money: 'R$ 00,00',
      price: 'R$ preço',
      name: 'nome do produto'
    },
    en: {
      contact: 'Test customer',
      label: 'preview',
      orderNote: 'sample: amounts and items come at send time',
      item: 'Product',
      qty: 'Qty. 1',
      subtotal: 'Subtotal',
      tax: 'Taxes',
      total: 'Total',
      callAsk: 'Can the business call you?',
      callAllow: 'Allow calls',
      callAllowTemp: 'Temporarily allow calls',
      money: '$ 00.00',
      price: '$ price',
      name: 'product name'
    },
    es: {
      contact: 'Cliente de prueba',
      label: 'previsualización',
      orderNote: 'ejemplo: los valores y los ítems vienen en el envío',
      item: 'Producto',
      qty: 'Cant. 1',
      subtotal: 'Subtotal',
      tax: 'Impuestos',
      total: 'Total',
      callAsk: '¿La empresa puede llamarte?',
      callAllow: 'Permitir llamadas',
      callAllowTemp: 'Permitir por 7 días',
      money: '$ 00,00',
      price: '$ precio',
      name: 'nombre del producto'
    }
  })[lang];
  const S = {
    wrap: {
      display: 'flex',
      flexDirection: 'column',
      alignItems: 'flex-start',
      gap: 8,
      margin: '16px 0',
      flexShrink: 0,
      scrollSnapAlign: 'start'
    },
    device: {
      width: 320,
      maxWidth: '100%',
      padding: 10,
      borderRadius: 30,
      background: '#1f201e',
      boxSizing: 'border-box',
      display: 'flex',
      flexDirection: 'column',
      height,
      boxShadow: '0 18px 40px -28px rgb(31 32 30 / .55)'
    },
    speaker: {
      display: 'block',
      width: '22%',
      height: 3,
      margin: '1px auto 6px',
      borderRadius: 999,
      background: 'rgb(255 255 255 / .35)'
    },
    screen: {
      borderRadius: 22,
      overflow: 'hidden',
      background: '#e9e4dc',
      display: 'flex',
      flexDirection: 'column',
      flex: 1,
      minHeight: 0
    },
    bar: {
      display: 'flex',
      alignItems: 'center',
      gap: 9,
      padding: '11px 13px',
      background: '#477722'
    },
    avatar: {
      width: 28,
      height: 28,
      borderRadius: 999,
      flexShrink: 0,
      background: 'rgb(255 255 255 / .22)'
    },
    name: {
      display: 'block',
      fontSize: 12.5,
      fontWeight: 600,
      lineHeight: 1,
      color: '#fff'
    },
    sub: {
      display: 'block',
      marginTop: 4,
      fontSize: 10.5,
      lineHeight: 1,
      color: 'rgb(255 255 255 / .72)'
    },
    chat: {
      padding: '14px 13px',
      minHeight: 230,
      flex: 1,
      overflowY: 'auto'
    },
    bubble: {
      background: '#fff',
      borderRadius: 10,
      borderTopLeftRadius: 2,
      maxWidth: 252,
      boxShadow: '0 1px 1px rgb(0 0 0 / .13)',
      overflow: 'hidden',
      fontSize: 13,
      lineHeight: 1.45,
      color: '#1f201e'
    },
    pad: {
      padding: '8px 10px 6px'
    },
    media: {
      height: 92,
      background: '#f1efe9',
      display: 'flex',
      alignItems: 'center',
      justifyContent: 'center',
      flexDirection: 'column',
      gap: 3,
      fontSize: 10,
      color: '#8a9183',
      letterSpacing: '.04em',
      textTransform: 'uppercase',
      textAlign: 'center',
      padding: '0 10px',
      lineHeight: 1.35
    },
    mediaSub: {
      textTransform: 'none',
      letterSpacing: 0,
      fontSize: 9.5
    },
    head: {
      fontWeight: 700,
      marginBottom: 4
    },
    foot: {
      fontSize: 11,
      color: '#8a9183',
      marginTop: 6
    },
    time: {
      display: 'block',
      textAlign: 'right',
      fontSize: 10,
      color: '#9aa093',
      marginTop: 2
    },
    btn: {
      borderTop: '1px solid #e7e9e3',
      padding: '8px 10px',
      textAlign: 'center',
      color: '#027eb5',
      fontSize: 12.5,
      fontWeight: 500
    },
    chip: {
      display: 'inline-block',
      background: '#e3eed9',
      color: '#477722',
      borderRadius: 5,
      padding: '2px 7px',
      fontSize: 11,
      fontWeight: 700,
      marginBottom: 6
    },
    invoice: {
      margin: '8px 0 2px',
      padding: 8,
      borderRadius: 8,
      background: '#f6f5f1',
      display: 'flex',
      flexDirection: 'column',
      gap: 6
    },
    invoiceNote: {
      fontSize: 9,
      color: '#9aa093',
      textTransform: 'uppercase',
      letterSpacing: '.04em'
    },
    line: {
      display: 'flex',
      alignItems: 'center',
      gap: 7
    },
    thumb: {
      width: 26,
      height: 26,
      borderRadius: 5,
      flexShrink: 0,
      background: '#fff',
      border: '1px solid #e7e9e3'
    },
    lineName: {
      flex: 1,
      minWidth: 0,
      fontSize: 10.5,
      lineHeight: 1.3
    },
    lineQty: {
      display: 'block',
      fontSize: 9,
      color: '#9aa093'
    },
    lineValue: {
      fontSize: 10.5,
      flexShrink: 0
    },
    totals: {
      display: 'flex',
      flexDirection: 'column',
      gap: 3,
      paddingTop: 5,
      borderTop: '1px solid #e7e9e3'
    },
    totalRow: {
      display: 'flex',
      justifyContent: 'space-between',
      fontSize: 10.5
    },
    callCard: {
      display: 'flex',
      alignItems: 'center',
      gap: 7,
      padding: '8px 10px',
      fontSize: 11.5,
      color: '#1f201e'
    },
    callIcon: {
      width: 20,
      height: 20,
      borderRadius: 999,
      flexShrink: 0,
      background: '#eef0ea'
    },
    row: {
      display: 'flex',
      gap: 7,
      marginTop: 7,
      maxWidth: 252
    },
    card: {
      background: '#fff',
      borderRadius: 10,
      flex: 1,
      minWidth: 0,
      overflow: 'hidden',
      fontSize: 12,
      boxShadow: '0 1px 1px rgb(0 0 0 / .13)'
    },
    cap: {
      fontSize: 11.5,
      color: 'var(--zp-ink-tertiary, #8a9183)',
      width: 320,
      maxWidth: '100%',
      minHeight: 34
    }
  };
  const ICON = {
    catalog: '🛍',
    product: '🛍',
    location: '📍',
    image: '🖼',
    video: '▶'
  };
  const withVars = text => String(text || '').split(/(\{\{[^}]+\}\})/g).map((part, i) => part.startsWith('{{') ? <span key={i} style={{
    background: '#e3eed9',
    color: '#477722',
    borderRadius: 3,
    padding: '0 3px'
  }}>{part}</span> : <span key={i}>{part}</span>);
  const mediaBlock = (label, sub, height) => <div style={height ? {
    ...S.media,
    height
  } : S.media}>
      {headerIcon && <span style={{
    fontSize: 15
  }}>{ICON[headerIcon] || ''}</span>}
      <span>{label}</span>
      {sub && <span style={S.mediaSub}>{sub}</span>}
    </div>;
  return <div style={S.wrap}>
      <div style={S.device}>
        <span aria-hidden="true" style={S.speaker} />

        <div style={S.screen}>
          <div style={S.bar}>
            <span style={S.avatar} />
            <span style={{
    minWidth: 0
  }}>
              <span style={S.name}>{L.contact}</span>
              <span style={S.sub}>{L.label}</span>
            </span>
          </div>

          <div style={S.chat}>
            <div style={S.bubble}>
              {headerKind !== 'text' && headerKind !== 'none' && mediaBlock(headerKind, productLine === true ? `${L.name} · ${L.price}` : productLine)}

              <div style={S.pad}>
                {offer && <div style={S.chip}>{offer}</div>}
                {header && <div style={S.head}>{withVars(header)}</div>}
                {body && <div style={{
    whiteSpace: 'pre-wrap'
  }}>{withVars(body)}</div>}
                {coupon && <div style={{
    ...S.chip,
    marginTop: 8,
    marginBottom: 0
  }}>{coupon}</div>}

                {order && <div style={S.invoice}>
                    <span style={S.invoiceNote}>{L.orderNote}</span>
                    {[1, 2].map(i => <span key={i} style={S.line}>
                        <span style={S.thumb} />
                        <span style={S.lineName}>
                          {L.item} {i}
                          <span style={S.lineQty}>{L.qty}</span>
                        </span>
                        <span style={S.lineValue}>{L.money}</span>
                      </span>)}
                    <span style={S.totals}>
                      {[[L.subtotal, false], [L.tax, false], [L.total, true]].map(([label, strong]) => <span key={label} style={S.totalRow}>
                          <span style={{
    fontWeight: strong ? 600 : 400,
    color: strong ? '#1f201e' : '#6b7264'
  }}>{label}</span>
                          <span style={{
    fontWeight: strong ? 600 : 400
  }}>{L.money}</span>
                        </span>)}
                    </span>
                  </div>}

                {footer && <div style={S.foot}>{footer}</div>}
                <span style={S.time}>10:24</span>
              </div>

              {callPermission && <div style={{
    borderTop: '1px solid #e7e9e3'
  }}>
                  <div style={S.callCard}>
                    <span style={S.callIcon} />
                    <span>{L.callAsk}</span>
                  </div>
                  <div style={S.btn}>{L.callAllow}</div>
                  <div style={S.btn}>{L.callAllowTemp}</div>
                </div>}

              {buttons.map((b, i) => <div key={i} style={S.btn}>{b}</div>)}
            </div>

            {cards.length > 0 && <div style={S.row}>
                {cards.map((c, i) => <div key={i} style={S.card}>
                    <div style={{
    ...S.media,
    height: 64,
    gap: 2
  }}>
                      <span>{c.media || 'imagem'}</span>
                      {c.product && <span style={S.mediaSub}>{L.name} · {L.price}</span>}
                    </div>
                    {c.text && <div style={{
    padding: '6px 8px'
  }}>{c.text}</div>}
                    {(c.buttons || (c.button ? [c.button] : [])).map((b, j) => <div key={j} style={{
    ...S.btn,
    padding: '6px 8px',
    fontSize: 11.5
  }}>{b}</div>)}
                  </div>)}
              </div>}
          </div>
        </div>
      </div>

      {caption && <div style={S.cap}>{caption}</div>}
    </div>;
};

`mpm`

Showcase with sections and several products.

<TemplatePreview lang="en" headerKind="product image, comes from the catalog" headerIcon="catalog" header="This week's picks" body="We picked a few products that go with what you bought. See the full list." footer="Prices valid while stock lasts" buttons={['View items']} caption="The header is text; the image above it comes from the thumbnail of the product picked at send time." />

## How it works

The template is approved once and the showcase is assembled at send time, with up to **30 products** from the catalog in up to **10 sections**. The button is fixed by Meta (`View items`), translated to the customer's language.

The customer opens the list, adds and removes items from the cart and sends the order back, which arrives on your webhook as an order message. **Payment is the next step**: you reply with [order details](/en/templates/models/order-details) or with a link.

Because the products go at send time, the same showcase serves different campaigns.

## At creation and at send time

| At creation                                 | At send time                                     |
| ------------------------------------------- | ------------------------------------------------ |
| The text header, the message and the footer | Up to 30 products, in up to 10 sections          |
| The button, which is fixed                  | The top photo, from the chosen product thumbnail |

## Fields in this model

| Field        | What it is for                       | Rule                                                                                                                                                                             |
| ------------ | ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `category`   | Fixed                                | Always `MARKETING`                                                                                                                                                               |
| `HEADER`     | The showcase title                   | **Required, and text only**, up to 60 characters and at most one variable. It is the only model that requires a header. The top image comes from the product picked at send time |
| `BODY`       | The text presenting the selection    | Up to 1024 characters                                                                                                                                                            |
| `FOOTER`     | Optional                             | Up to 60 characters                                                                                                                                                              |
| `MPM` button | Opens the product list with sections | `text` always `View items`, in English, even on a `pt_BR` template                                                                                                               |

## Full JSON

Two JSON payloads: one **creates** the template, and Meta approves it once. The other **sends** the message, and goes on every dispatch, with that send's values in the variables marked with braces.

### 1. Create the template

`POST /whatsapp/businesses/{wabaId}/templates` — see [API reference](/en/templates/create-template).

```json theme={null}
{
  "name": "exemplo_mpm",
  "category": "MARKETING",
  "language": "en_US",
  "allow_category_change": true,
  "parameter_format": "NAMED",
  "components": [
    {
      "type": "HEADER",
      "format": "TEXT",
      "text": "Order {{order_number}}",
      "example": {
        "header_text_named_params": [
          {
            "param_name": "order_number",
            "example": "1042"
          }
        ]
      }
    },
    {
      "type": "BODY",
      "text": "Hi {{customer_name}}, your order {{order_number}} is confirmed and ships today.",
      "example": {
        "body_text_named_params": [
          {
            "param_name": "customer_name",
            "example": "Marina"
          },
          {
            "param_name": "order_number",
            "example": "1042"
          }
        ]
      }
    },
    {
      "type": "FOOTER",
      "text": "Reply STOP to opt out"
    },
    {
      "type": "BUTTONS",
      "buttons": [
        {
          "type": "MPM",
          "text": "View items"
        }
      ]
    }
  ]
}
```

### 2. Send it once approved

`POST /v1/channels/{channelId}/messages` — see [Send template](/en/messages/send-template).

```json theme={null}
{
  "recipient": {
    "identifier": "5511999999999"
  },
  "content": {
    "type": "TEMPLATE",
    "attachments": [
      {
        "template": {
          "name": "exemplo_mpm",
          "language": {
            "policy": "deterministic",
            "code": "en_US"
          },
          "components": [
            {
              "type": "header",
              "parameters": [
                {
                  "type": "text",
                  "text": "{{order_number}}"
                }
              ]
            },
            {
              "type": "body",
              "parameters": [
                {
                  "type": "text",
                  "text": "{{customer_name}}"
                },
                {
                  "type": "text",
                  "text": "{{order_number}}"
                }
              ]
            },
            {
              "type": "button",
              "sub_type": "mpm",
              "index": 0,
              "parameters": [
                {
                  "type": "action",
                  "action": {
                    "thumbnail_product_retailer_id": "{{thumbnail_product_retailer_id}}",
                    "sections": [
                      {
                        "title": "{{section_title}}",
                        "product_items": [
                          {
                            "product_retailer_id": "{{product_retailer_id}}"
                          }
                        ]
                      }
                    ]
                  }
                }
              ]
            }
          ]
        }
      }
    ]
  }
}
```

## What Meta rejects here

* A media header: here it is text only.
* A label other than `View items`, with code `2388153`.
* More than one variable in the header, with code `2388029`.
