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.

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.
| Part | Source | Changes per customer |
|---|---|---|
| Background and shop name | Generated, 3:2 | No |
| Ten empty circles | Generated, check positions | No |
| Stamps | Code | Yes |
| Customer name | Code, optional | Yes |
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
- Coffee shop window screen: 15-second 9:16 clip at 1080p
A 15-second 1080p vertical clip for a shop screen costs $2.10 to $21.32 on Sume: Kling 3, H3 Max, Wan 3.0, Seedance 2.0 and 2.5. Omni Flash stops at 10 seconds.
- Convert a YouTube Shorts playlist into a show with Add show features
In YouTube Studio, Options on a playlist offers Add show features, and every video lands in Season 1. What to fix first and how to prepare Shorts for it.
- Criteo Retail Media onsite video: 720p, 15 s, a Sume avatar clip
Criteo onsite video asks for MP4 at 720p, 15 s recommended and 30 s max, in 16:9, 1:1 or 9:16. Sume avatar output is 720p, so script to 15 seconds.
- Crop to 1:1 and dim a bright studio clip: read ops_applied
White studio clips wash out captions. Sume video-filter crops 16:9 to 1:1 and dims 15 percent in one $0.02 job. Check it free first, then read ops_applied.
Written by Sume