Logo
Apdf tutorials February 2026 8 min read

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

Here is a complete working example. We will break it down step by step afterward.
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

First, initialize your Go module and install the QR code library:
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

Now that you have automated ticket generation, consider these enhancements:
After the API call

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.

Your PDF, after sending Live
document:loaded CFO
page:read p4 · 38s
link:clicked pricing

API · Webhook · MCP