Coffee shop loyalty card art with an API: stamps drawn in code

Generate 3:2 loyalty card art on Sume with empty stamp spaces, then draw each customer's stamps in code so the count is always right and the art is reused.

5 min readSume
All posts

For coffee shop loyalty card art, generate a 3:2 card on Sume with a shop name and empty circles for stamps, then draw each customer's stamps in code. The count is data, and drawing it yourself means the art is paid for once while every card is a free local render.

Ideogram 4.5 suits the shop name; fal's page describes it as strong at posters and logos with accurate text rendering, and lists $0.03, $0.06 and $0.22 per image for low, medium and high (read 2026-10-06, fal). Sume bills the list price times 1.25. As always, proofread the name in the result, and see the Image API docs for the ratios each model accepts.

What the model draws and what you draw

The model draws the card: background, shop name and ten empty circles. Your script draws stamps in the circles. Because the circle positions come from the generated art, you place the stamps by measuring or by fixing the layout in the prompt.

Loyalty card parts, read 2026-10-06
PartSourceChanges per customer
Background and shop nameGenerated, 3:2No
Ten empty circlesGenerated, check positionsNo
StampsCodeYes
Customer nameCode, optionalYes

Generate, then stamp

The script creates the card, then draws a dot in the first N circles. The coordinates here assume a layout of five circles per row; measure your own result and adjust them.

import io
import os
import requests
from PIL import Image, ImageDraw

r = requests.post(
    "https://api.sume.com/v1/images",
    headers={"Authorization": f"Bearer {os.environ['SUME_API_KEY']}"},
    json={
        "model": "ideogram/ideogram-v4.5",
        "prompt": 'A coffee shop loyalty card reading "Corner Beans" at the '
        "top, with two rows of five empty circles below. Cream background, "
        "brown ink.",
        "aspect_ratio": "3:2",
        "quality": "medium",
    },
    timeout=60,
)
r.raise_for_status()
raw = requests.get(r.json()["data"][0]["url"], timeout=60).content
img = Image.open(io.BytesIO(raw)).convert("RGB")
w, h = img.size
d = ImageDraw.Draw(img)
stamps = 4
for i in range(stamps):
    x = int(w * (0.15 + 0.175 * (i % 5)))
    y = int(h * (0.50 + 0.25 * (i // 5)))
    d.ellipse((x - 25, y - 25, x + 25, y + 25), fill=(90, 50, 20))
img.save("card-4-stamps.png")

Design choices for a punch card

A loyalty card is small and handled often, so the design should be plain and sturdy. Keep the art calm and leave obvious, large spaces for the stamps.

  • Ten stamp circles in two rows are easy to count and to fill.
  • Use a light background so a stamp's ink shows.
  • Keep the shop name large and the rest small.
  • Leave the back blank or add your address in code.
  • Print on card stock that holds up in a wallet.

Digital option

The same approach works on screen. Instead of printing, render the card with the stamps already drawn for a customer's current count, and send that image to their phone. The art is generated once; each customer's card is a free local render from their count. Keep the count in your own system, since the image is only a view of it.

Seasonal cards without a new design

A shop may want a pumpkin card in autumn and a snowflake card in winter. Keep the layout sentence the same, the shop name quoted, and the ten circles, and change only the decoration. Customers then recognise the card and still feel a seasonal change. Each season costs one image at the quality you choose, and the stamping script does not change.

Retire old designs by date in your code, so a card printed for last winter does not keep appearing in a digital wallet.

Fraud and fairness

A drawn card is only a picture. If a stamp has real value, such as a free coffee, keep the true count in your own database and treat the image as a view of it. A customer who edits an image on their phone should not be able to claim a reward that your records do not show. The code that renders the card should read the count from the same place that redeems it.

Print one card and stamp it with your actual stamp to see if the circles are the right size. A circle that looks fine on screen may be too small for a stamp pad, and it is cheaper to learn that from one test card than from a printed run. Adjust the prompt to ask for larger circles if needed.

Open the generated card and confirm that the circles are where you expect. If they drift, ask for a simpler layout or place the circles yourself in code and generate only the background. Test a full ten-stamp card too, since the last circle is the one customers look at.

Sources

Related posts

More in Use cases

All Use cases posts

Written by Sume