Generate PDF Event Tickets with QR Codes in A6 Format using Go
If you are building an event management system, you have probably faced the "ticket generation problem." Most HTML-to-PDF tutorials show you how to create A4 invoices or reports, but what about smaller formats like tickets, badges, or shipping labels?
Event tickets are typically A6 size (105mm × 148mm), and they need QR codes for check-in scanning. Building this from scratch using low-level PDF libraries is painful. Positioning elements, managing fonts, and embedding images requires hundreds of lines of code.
The smarter approach: design your ticket using HTML/CSS, generate a QR code image, and send it to an API. In this tutorial, we will build a Go program that generates professional A6 event tickets with embedded QR codes using the free Apdf API.
The Quick Code
package main
import (
"bytes"
"encoding/json"
"fmt"
"image/png"
"io"
"net/http"
"net/url"
"strings"
"time"
"github.com/skip2/go-qrcode"
)
const apiToken = "YOUR_API_TOKEN_HERE"
const apiURL = "https://apdf.io/api/pdf/file/create"
const statusURL = "https://apdf.io/api/job/status/check"
type TicketData struct {
EventName string
Attendee string
TicketID string
Date string
Venue string
QRCodeImage string
}
type InitialResponse struct {
JobID string `json:"job_id"`
}
type PDFResult struct {
File string `json:"file"`
Expiration string `json:"expiration"`
Pages int `json:"pages"`
Size int `json:"size"`
}
type JobStatus struct {
Status string `json:"status"`
Result PDFResult `json:"result"`
Error string `json:"error"`
}
func main() {
ticket := TicketData{
EventName: "Tech Summit 2025",
Attendee: "Alex Johnson",
TicketID: "TICKET-2025-789123",
Date: "December 20, 2025, 18:00",
Venue: "Grand Convention Center",
}
// Generate QR code
qrCode, err := qrcode.Encode(ticket.TicketID, qrcode.Medium, 200)
if err != nil {
fmt.Println("Error generating QR code:", err)
return
}
// Convert to base64 data URL
ticket.QRCodeImage = "data:image/png;base64," + base64EncodeBytes(qrCode)
// Generate HTML
html := generateTicketHTML(ticket)
// Create PDF
pdfURL, err := createPDF(html)
if err != nil {
fmt.Println("Error creating PDF:", err)
return
}
fmt.Println("Ticket generated!")
fmt.Println("Download:", pdfURL)
}
func generateTicketHTML(ticket TicketData) string {
return fmt.Sprintf(`
<!DOCTYPE html>
<html>
<head>
<style>
@page {
size: 105mm 148mm;
margin: 0;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Arial', sans-serif;
width: 105mm;
height: 148mm;
background: linear-gradient(135deg, #667eea 0%%, #764ba2 100%%);
color: #fff;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 15mm;
}
.header {
text-align: center;
border-bottom: 2px dashed rgba(255,255,255,0.3);
padding-bottom: 10mm;
}
.event-name {
font-size: 20px;
font-weight: bold;
margin-bottom: 3mm;
}
.date {
font-size: 11px;
opacity: 0.9;
}
.attendee-section {
margin-top: 8mm;
}
.label {
font-size: 9px;
text-transform: uppercase;
letter-spacing: 1px;
opacity: 0.8;
margin-bottom: 2mm;
}
.attendee-name {
font-size: 18px;
font-weight: bold;
}
.venue {
font-size: 12px;
margin-top: 3mm;
opacity: 0.9;
}
.qr-section {
text-align: center;
margin-top: auto;
}
.qr-code {
width: 50mm;
height: 50mm;
background: white;
padding: 3mm;
border-radius: 8px;
margin: 0 auto;
}
.ticket-id {
font-size: 9px;
margin-top: 3mm;
font-family: monospace;
opacity: 0.8;
}
</style>
</head>
<body>
<div class="header">
<div class="event-name">%%s</div>
<div class="date">%%s</div>
</div>
<div class="attendee-section">
<div class="label">Attendee</div>
<div class="attendee-name">%%s</div>
<div class="venue">%%s</div>
</div>
<div class="qr-section">
<img src="%%s" class="qr-code" alt="QR Code"/>
<div class="ticket-id">%%s</div>
</div>
</body>
</html>`, ticket.EventName, ticket.Date, ticket.Attendee, ticket.Venue,
ticket.QRCodeImage, ticket.TicketID)
}
func createPDF(html string) (string, error) {
data := url.Values{}
data.Set("html", html)
data.Set("format", "a6")
data.Set("margin_top", "0")
data.Set("margin_bottom", "0")
data.Set("margin_left", "0")
data.Set("margin_right", "0")
req, err := http.NewRequest("POST", apiURL, strings.NewReader(data.Encode()))
if err != nil {
return "", err
}
req.Header.Set("Authorization", "Bearer "+apiToken)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Accept", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("API error: %%s", string(body))
}
var initial InitialResponse
if err := json.Unmarshal(body, &initial); err != nil {
return "", err
}
result, err := waitForJob(client, initial.JobID)
if err != nil {
return "", err
}
return result.File, nil
}
// Poll until the async job finishes, then return its result.
func waitForJob(client *http.Client, jobID string) (*PDFResult, error) {
data := url.Values{}
data.Set("id", jobID)
for i := 0; i < 1200; i++ {
req, err := http.NewRequest("POST", statusURL, strings.NewReader(data.Encode()))
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+apiToken)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Accept", "application/json")
resp, err := client.Do(req)
if err != nil {
return nil, err
}
body, _ := io.ReadAll(resp.Body)
resp.Body.Close()
var status JobStatus
if err := json.Unmarshal(body, &status); err != nil {
return nil, err
}
if status.Status == "successful" {
return &status.Result, nil
}
if status.Status == "failed" {
return nil, fmt.Errorf("job failed: %%s", status.Error)
}
time.Sleep(2 * time.Second)
}
return nil, fmt.Errorf("job did not finish in time")
}
func base64EncodeBytes(data []byte) string {
// Use standard encoding library
return base64.StdEncoding.EncodeToString(data)
}
Step-by-Step Breakdown
1. Set Up Your Project
go mod init ticket-generator
go get github.com/skip2/go-qrcode
Then get your API token from Apdf (it's free).
2. The Key Challenge: A6 Format
Most HTML-to-PDF examples show A4 pages. The secret to small formats is the @page
CSS rule combined with the API's format parameter:
@page {
size: 105mm 148mm; // A6 dimensions
margin: 0; // Full bleed design
}
body {
width: 105mm;
height: 148mm;
}
The API call then specifies format: "a6" to match. This ensures your ticket
prints at the correct physical size without scaling issues.
3. Generating the QR Code
The go-qrcode library makes this simple. Generate a QR code and convert
it to a base64 data URL so it can be embedded directly in the HTML:
import "github.com/skip2/go-qrcode"
import "encoding/base64"
// Generate QR code as PNG bytes
qrCode, err := qrcode.Encode("TICKET-2025-789123", qrcode.Medium, 200)
if err != nil {
panic(err)
}
// Convert to data URL for HTML embedding
dataURL := "data:image/png;base64," + base64.StdEncoding.EncodeToString(qrCode)
Now you can use this data URL as an <img src="..."> directly in your HTML template.
No need to upload the image anywhere.
4. The API Request
Send the complete HTML (with embedded QR code) to the API. The key parameters:
data := url.Values{}
data.Set("html", html)
data.Set("format", "a6") // A6 ticket size
data.Set("margin_top", "0") // Full bleed
data.Set("margin_bottom", "0")
data.Set("margin_left", "0")
data.Set("margin_right", "0")
req, _ := http.NewRequest("POST", apiURL, strings.NewReader(data.Encode()))
req.Header.Set("Authorization", "Bearer "+apiToken)
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
req.Header.Set("Accept", "application/json")
Run the Program
go run main.go
Output
Ticket generated!
Download: https://apdf-files.s3.eu-central-1.amazonaws.com/abc123def456.pdf
That URL can be emailed directly to your attendee or saved to your database. Note: Download URLs are valid for 1 hour. If you need permanent storage, download the file and save it to your own storage.
Real-World Integration
In a production event system, you would generate tickets when a user completes payment. Here is how you might structure it:
func HandleTicketPurchase(w http.ResponseWriter, r *http.Request) {
// 1. Process payment
payment := processStripePayment(r)
// 2. Create database record
ticket := createTicketRecord(payment.UserID, payment.EventID)
// 3. Generate PDF ticket
pdfURL, err := generateTicket(ticket)
if err != nil {
http.Error(w, "Failed to generate ticket", 500)
return
}
// 4. Email to attendee
sendTicketEmail(ticket.Email, pdfURL)
// 5. Respond to user
json.NewEncoder(w).Encode(map[string]string{
"status": "success",
"ticket_url": pdfURL,
})
}
Other Small Format Use Cases
This technique works for any small-format document:
- Shipping Labels: Use A6 or custom dimensions (e.g., 100mm × 150mm)
- Conference Badges: Set custom width/height for badge printers
- Product Tags: Generate small labels with barcodes for inventory
- Parking Passes: Print compact permits with QR codes
The key is always the same: set the @page size in CSS and match it with the
API's format parameter or custom dimensions.
Next Steps
- Password Protect VIP Tickets: Use the Password Protection endpoint to secure premium tickets
- Merge Multiple Tickets: If someone buys multiple tickets, use the Merge endpoint to combine them into a single PDF
Related tutorials
How to Generate PDFs from Claude
Describe the document, get the PDF: Claude drafts the HTML layout and renders it through the Apdf MCP tools — invoices, reports and one-pagers straight from the conversation, with page format, margins and headers steered in plain English. Revisions are just more chat.
Dynamic Certificate Generation for Online Courses (LMS)
Learn how to dynamically generate professional PDF certificates for online course platforms using PHP. Design certificates with HTML/CSS, use landscape orientation, and automatically generate them when students complete courses.
Build an Automated Invoice Generator using HTML & Python
The tutorial explains how to build a Python invoice generator by using Jinja2 to merge data into standard HTML/CSS templates. Instead of manually coding complex PDF layouts, the script sends the rendered HTML to the Apdf API, which instantly converts it into a professional, downloadable PDF.
Your code made the PDF.
Then it went dark.
Opened, read, re-read, dropped on page 4 — you never see any of it. Share the PDFs you generate through Apdf recipient links, and every signal becomes something you can act on: ping Slack, update the CRM, let an agent follow up. Same account, same API token, one more call.
API · Webhook · MCP