> ## 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.

# Custom message

> The open model: free text with variables, header and buttons

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>;
};

export const PreviewCycle = ({items = [], lang = 'pt', seconds = 6, height = 540}) => {
  const L = ({
    pt: {
      pause: 'passa sozinho, pare o cursor em cima para ler com calma'
    },
    en: {
      pause: 'it cycles on its own; hover to stop and read'
    },
    es: {
      pause: 'pasa solo, deja el cursor encima para leer con calma'
    }
  })[lang];
  const [order, setOrder] = useState(items.map((_, index) => index));
  const [at, setAt] = useState(0);
  const [paused, setPaused] = useState(false);
  const shuffle = () => {
    const next = items.map((_, index) => index);
    for (let i = next.length - 1; i > 0; i -= 1) {
      const j = Math.floor(Math.random() * (i + 1));
      const keep = next[i];
      next[i] = next[j];
      next[j] = keep;
    }
    return next;
  };
  useEffect(() => {
    setOrder(shuffle());
    setAt(0);
  }, []);
  useEffect(() => {
    if (paused || items.length < 2) return undefined;
    const timer = setTimeout(() => {
      setAt(current => {
        const next = current + 1;
        if (next >= items.length) {
          setOrder(shuffle());
          return 0;
        }
        return next;
      });
    }, seconds * 1000);
    return () => clearTimeout(timer);
  }, [at, paused, items.length, seconds]);
  const index = order[at] === undefined ? 0 : order[at];
  const item = items[index] || ({});
  return <div style={{
    display: 'flex',
    flexDirection: 'column',
    alignItems: 'flex-start',
    gap: 10,
    margin: '16px 0'
  }} onMouseEnter={() => setPaused(true)} onMouseLeave={() => setPaused(false)}>
      <style>{'@keyframes zpFade{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}'}</style>

      <div key={at} style={{
    animation: 'zpFade .45s ease both'
  }}>
        <TemplatePreview {...item} lang={lang} height={height} />
      </div>

      <div style={{
    display: 'flex',
    alignItems: 'center',
    gap: 6,
    width: 320,
    maxWidth: '100%'
  }}>
        {items.map((_, dot) => <button key={dot} type="button" aria-label={String(dot + 1)} onClick={() => setAt(dot)} style={{
    flex: 1,
    height: 3,
    padding: 0,
    border: 'none',
    borderRadius: 999,
    cursor: 'pointer',
    background: dot === at ? '#477722' : 'var(--zp-border, #d8dcd4)'
  }} />)}
      </div>

      <span style={{
    fontSize: 11,
    color: 'var(--zp-ink-tertiary, #8a9183)'
  }}>{L.pause}</span>
    </div>;
};

`custom`

Free text with variables, header and buttons. It is the most used model and **the only fully open one**: nothing here is written by Meta, and no field depends on a catalog, a payment or a flow.

## Nine ideas to start from

export const IDEIAS = [{
  headerKind: "campaign image",
  headerIcon: "image",
  body: "🔥 *The pre-sale has been extended*\nHurry and secure your spot before the price goes up.\n\nIt ends at 11:59pm: after that, the same ticket costs more.",
  footer: "Don't want these here? Reply STOP",
  buttons: ['I WANT MY TICKET'],
  caption: "Promotion · marketing · image, bold, paragraphs and one URL button"
}, {
  body: "{{customer_name}}, you left {{item_count}} items in your cart. They are still reserved for 24 hours.",
  buttons: ['Complete purchase', 'Not interested'],
  caption: "Cart recovery · marketing · a URL plus a way out as a quick reply"
}, {
  headerKind: "welcome video",
  headerIcon: "video",
  body: "Welcome, {{customer_name}}! In 40 seconds you'll see how to get the most out of your account. Where do you want to start?",
  buttons: ['Set it up now', 'Talk to someone'],
  caption: "Welcome · utility · video header and two quick replies"
}, {
  body: "{{customer_name}}, your appointment is tomorrow at {{time}} with {{professional}}. Can we confirm?",
  buttons: ['Confirm', 'Reschedule'],
  caption: "Appointment reminder · utility · quick replies only, no header or footer"
}, {
  headerKind: "PDF document",
  headerIcon: "document",
  body: "Your {{billing_month}} invoice is available. Amount {{amount}}, due on {{due_date}}.",
  footer: "Questions? Talk to us",
  buttons: ['Pay now', 'Call billing'],
  caption: "Billing · utility · attached document, URL with a variable and a phone button"
}, {
  headerKind: "address and map, set at send time",
  headerIcon: "location",
  body: "Your pickup is ready, {{customer_name}}. We'll be waiting for you until {{cutoff_time}} today.",
  buttons: ['On my way'],
  caption: "Store pickup · utility · location header, which becomes a map"
}, {
  header: "Scheduled maintenance",
  body: "On Saturday, from 2am to 5am, the system will be unavailable for maintenance. Nothing you have already sent will be lost.",
  footer: "Operations team",
  caption: "Operational notice · utility · no buttons at all: not every template needs an action"
}, {
  body: "Hi! Your experience means a lot to us 🔥 Tell us how it went in under a minute.",
  buttons: ['I want to answer!'],
  caption: "Survey · marketing · as simple as it gets: a message and one button"
}, {
  header: "Order {{order_number}} confirmed",
  body: "Hi {{customer_name}}, your order {{order_number}} is confirmed and goes out for delivery today by 6pm.",
  footer: "Reply STOP to unsubscribe",
  buttons: ['Track order', 'See details', 'Call the store'],
  caption: "Order confirmation · utility · this page's full payload, with all three button types"
}];

<PreviewCycle lang="en" items={IDEIAS} />

<Info>
  Drag the strip sideways. These nine are **starting points, not a catalog**: they are all the same `custom`, and what changes is which components go in. You combine whichever you want — a header in any format, or none; a footer, or none; up to ten buttons, or none.
</Info>

Open the example closest to what you want and **copy the JSON** with the button in the corner of the block. It creates exactly the template in the preview; from there, change text, variables and buttons.

<AccordionGroup>
  <Accordion title="1. Promotion with an image">
    <TemplatePreview {...IDEIAS[0]} lang="en" />

    ```json theme={null}
    {
      "name": "presale_extended",
      "category": "MARKETING",
      "language": "en_US",
      "allow_category_change": true,
      "components": [
        {
          "type": "HEADER",
          "format": "IMAGE",
          "example": { "header_handle": ["4::aW1hZ2UvcG5n..."] }
        },
        {
          "type": "BODY",
          "text": "🔥 *The pre-sale has been extended*\nHurry and secure your spot before the price goes up.\n\nIt ends at 11:59pm: after that, the same ticket costs more."
        },
        { "type": "FOOTER", "text": "Don't want these here? Reply STOP" },
        {
          "type": "BUTTONS",
          "buttons": [
            { "type": "URL", "text": "I WANT MY TICKET", "url": "https://yourstore.com/ticket" }
          ]
        }
      ]
    }
    ```

    With no variables, the body carries no `example`. Bold is WhatsApp's `*asterisk*`, and the paragraphs are `\n` — only the message accepts line breaks. At send time, a media header **requires** the image parameter: the one at creation is just the sample Meta reviews.
  </Accordion>

  <Accordion title="2. Cart recovery">
    <TemplatePreview {...IDEIAS[1]} lang="en" />

    ```json theme={null}
    {
      "name": "abandoned_cart",
      "category": "MARKETING",
      "language": "en_US",
      "allow_category_change": true,
      "parameter_format": "NAMED",
      "components": [
        {
          "type": "BODY",
          "text": "{{customer_name}}, you left {{item_count}} items in your cart. They are still reserved for 24 hours.",
          "example": {
            "body_text_named_params": [
              { "param_name": "customer_name", "example": "Marina" },
              { "param_name": "item_count", "example": "3" }
            ]
          }
        },
        {
          "type": "BUTTONS",
          "buttons": [
            { "type": "URL", "text": "Complete purchase", "url": "https://yourstore.com/cart" },
            { "type": "QUICK_REPLY", "text": "Not interested" }
          ]
        }
      ]
    }
    ```

    The quick reply comes **after** the URL, not between other types: buttons sit in two groups. Giving an explicit way out reduces blocks, which is what hurts number quality the most.
  </Accordion>

  <Accordion title="3. Welcome with a video">
    <TemplatePreview {...IDEIAS[2]} lang="en" />

    ```json theme={null}
    {
      "name": "welcome_onboarding",
      "category": "UTILITY",
      "language": "en_US",
      "parameter_format": "NAMED",
      "components": [
        {
          "type": "HEADER",
          "format": "VIDEO",
          "example": { "header_handle": ["4::dmlkZW8vbXA0..."] }
        },
        {
          "type": "BODY",
          "text": "Welcome, {{customer_name}}! In 40 seconds you'll see how to get the most out of your account. Where do you want to start?",
          "example": {
            "body_text_named_params": [{ "param_name": "customer_name", "example": "Marina" }]
          }
        },
        {
          "type": "BUTTONS",
          "buttons": [
            { "type": "QUICK_REPLY", "text": "Set it up now" },
            { "type": "QUICK_REPLY", "text": "Talk to someone" }
          ]
        }
      ]
    }
    ```

    A video header accepts MP4 and 3GPP, up to 16 MB. The quick reply opens the 24-hour window when the customer taps it, and from there you talk without a template.
  </Accordion>

  <Accordion title="4. Appointment reminder">
    <TemplatePreview {...IDEIAS[3]} lang="en" />

    ```json theme={null}
    {
      "name": "appointment_reminder",
      "category": "UTILITY",
      "language": "en_US",
      "parameter_format": "NAMED",
      "components": [
        {
          "type": "BODY",
          "text": "{{customer_name}}, your appointment is tomorrow at {{time}} with {{professional}}. Can we confirm?",
          "example": {
            "body_text_named_params": [
              { "param_name": "customer_name", "example": "Marina" },
              { "param_name": "time", "example": "2:30pm" },
              { "param_name": "professional", "example": "Dr. Helena" }
            ]
          }
        },
        {
          "type": "BUTTONS",
          "buttons": [
            { "type": "QUICK_REPLY", "text": "Confirm" },
            { "type": "QUICK_REPLY", "text": "Reschedule" }
          ]
        }
      ]
    }
    ```

    The customer's answer comes back on your webhook with the button text. Note the variable neither opens nor closes the text — if it did, Meta would reject it with `2388299`.
  </Accordion>

  <Accordion title="5. Billing with a PDF">
    <TemplatePreview {...IDEIAS[4]} lang="en" />

    ```json theme={null}
    {
      "name": "invoice_available",
      "category": "UTILITY",
      "language": "en_US",
      "parameter_format": "NAMED",
      "components": [
        {
          "type": "HEADER",
          "format": "DOCUMENT",
          "example": { "header_handle": ["4::YXBwbGljYXRpb24vcGRm..."] }
        },
        {
          "type": "BODY",
          "text": "Your {{billing_month}} invoice is available. Amount {{amount}}, due on {{due_date}}.",
          "example": {
            "body_text_named_params": [
              { "param_name": "billing_month", "example": "September" },
              { "param_name": "amount", "example": "$ 249.90" },
              { "param_name": "due_date", "example": "10/10" }
            ]
          }
        },
        { "type": "FOOTER", "text": "Questions? Talk to us" },
        {
          "type": "BUTTONS",
          "buttons": [
            {
              "type": "URL",
              "text": "Pay now",
              "url": "https://yourstore.com/invoice/{{1}}",
              "example": ["https://yourstore.com/invoice/1042"]
            },
            { "type": "PHONE_NUMBER", "text": "Call billing", "phone_number": "+5511988881234" }
          ]
        }
      ]
    }
    ```

    The URL button accepts **one** variable, only at the end of the address. It is positional even on a named template, because that is the button's own numbering.
  </Accordion>

  <Accordion title="6. Store pickup, with a map">
    <TemplatePreview {...IDEIAS[5]} lang="en" />

    ```json theme={null}
    {
      "name": "pickup_ready",
      "category": "UTILITY",
      "language": "en_US",
      "parameter_format": "NAMED",
      "components": [
        { "type": "HEADER", "format": "LOCATION" },
        {
          "type": "BODY",
          "text": "Your pickup is ready, {{customer_name}}. We'll be waiting for you until {{cutoff_time}} today.",
          "example": {
            "body_text_named_params": [
              { "param_name": "customer_name", "example": "Marina" },
              { "param_name": "cutoff_time", "example": "6pm" }
            ]
          }
        },
        {
          "type": "BUTTONS",
          "buttons": [{ "type": "QUICK_REPLY", "text": "On my way" }]
        }
      ]
    }
    ```

    The location header has **nothing to fill in at creation**: the address, the place name and the coordinates go at send time. It lets you send the same message pointing at different stores.
  </Accordion>

  <Accordion title="7. Operational notice, no buttons">
    <TemplatePreview {...IDEIAS[6]} lang="en" />

    ```json theme={null}
    {
      "name": "scheduled_maintenance",
      "category": "UTILITY",
      "language": "en_US",
      "components": [
        { "type": "HEADER", "format": "TEXT", "text": "Scheduled maintenance" },
        {
          "type": "BODY",
          "text": "On Saturday, from 2am to 5am, the system will be unavailable for maintenance. Nothing you have already sent will be lost."
        },
        { "type": "FOOTER", "text": "Operations team" }
      ]
    }
    ```

    Not every template needs an action. With no variables and no buttons, this is the cheapest shape to get approved and the hardest to get rejected.
  </Accordion>

  <Accordion title="8. Satisfaction survey">
    <TemplatePreview {...IDEIAS[7]} lang="en" />

    ```json theme={null}
    {
      "name": "experience_survey",
      "category": "MARKETING",
      "language": "en_US",
      "allow_category_change": true,
      "components": [
        {
          "type": "BODY",
          "text": "Hi! Your experience means a lot to us 🔥 Tell us how it went in under a minute."
        },
        {
          "type": "BUTTONS",
          "buttons": [
            { "type": "URL", "text": "I want to answer!", "url": "https://yourstore.com/survey" }
          ]
        }
      ]
    }
    ```

    The least a template can be: a message and nothing else. With no variables and a fixed link, the send carries only the name and the language.
  </Accordion>

  <Accordion title="9. Order confirmation">
    <TemplatePreview {...IDEIAS[8]} lang="en" />

    It is this page's full payload: text header with a variable, message, footer and the three free button types. It sits complete in the [creation JSON](#1-create-the-template), with the send JSON next to it.
  </Accordion>
</AccordionGroup>

## How it works

You write the text once, mark what changes per customer as a variable, and Meta approves the mold. At send time, each variable gets the value for that dispatch.

It is the model for everything that isn't commerce, authentication or a promotion with its own structure: order confirmation, status update, billing, appointment reminder, re-engagement.

## At creation and at send time

| At creation                         | At send time                                                |
| ----------------------------------- | ----------------------------------------------------------- |
| The header, message and footer text | The value of each variable                                  |
| The buttons and where they lead     | The tail of the address, when the URL button has a variable |
| The header media sample             | The real file for that dispatch                             |

## Fields in this model

| Field      | What it is for                                                   | Rule                                                                         |
| ---------- | ---------------------------------------------------------------- | ---------------------------------------------------------------------------- |
| `category` | `UTILITY` or `MARKETING`, depending on the content               | You choose. With `allow_category_change`, Meta corrects instead of rejecting |
| `HEADER`   | Optional. Text, image, video, document or map                    | Text up to 60 characters and at most one variable                            |
| `BODY`     | The main text. **Required**                                      | Up to 1024 characters, with line breaks                                      |
| `FOOTER`   | Optional. Discreet notice below the message                      | Up to 60 characters, no variables                                            |
| `BUTTONS`  | Optional. Quick reply, URL, phone, copy code, voice call or flow | Up to 10, with quick replies grouped                                         |

## 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_custom",
  "category": "UTILITY",
  "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": "QUICK_REPLY",
          "text": "Track order"
        },
        {
          "type": "URL",
          "text": "See details",
          "url": "https://z-api.io/pedido/{{numero_do_pedido}}",
          "example": [
            "https://z-api.io/pedido/1042"
          ]
        },
        {
          "type": "PHONE_NUMBER",
          "text": "Call the store",
          "phone_number": "+5511988881234"
        },
        {
          "type": "COPY_CODE",
          "example": "PROMO10"
        },
        {
          "type": "VOICE_CALL",
          "text": "Talk by voice"
        }
      ]
    }
  ]
}
```

### 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_custom",
          "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": "quick_reply",
              "index": 0,
              "parameters": [
                {
                  "type": "payload",
                  "payload": "{{quick_reply_payload_1}}"
                }
              ]
            },
            {
              "type": "button",
              "sub_type": "url",
              "index": 1,
              "parameters": [
                {
                  "type": "text",
                  "text": "{{url_suffix_2}}"
                }
              ]
            },
            {
              "type": "button",
              "sub_type": "phone_number",
              "index": 2,
              "parameters": []
            },
            {
              "type": "button",
              "sub_type": "copy_code",
              "index": 3
            },
            {
              "type": "button",
              "sub_type": "voice_call",
              "index": 4
            },
            {
              "type": "button",
              "sub_type": "flow",
              "index": 5,
              "parameters": [
                {
                  "type": "action",
                  "action": {
                    "flow_token": "{{flow_token}}",
                    "flow_action_data": {}
                  }
                }
              ]
            }
          ]
        }
      }
    ]
  }
}
```

## What Meta rejects here

* A variable **opening or closing** the header or message text.
* A variable **without an example**.
* Short text with too many variables.
* A quick reply in the middle of the other buttons.
* A button address without `http` or `https`, and a phone without the country code.

The error code for each case is in [Validation](/en/templates/validation).
