- What an Open Badge actually is
- Step 1: Generate the badge image
- Step 2: Describe the issuer and the badge
- Step 3: Issue an Assertion per learner
- Step 4: Bake the badge so the image proves itself
- What you end up with
A certificate is a picture. A badge is a picture that can prove what it claims. Open Badges is the standard that makes that difference real: a PNG with verifiable metadata baked into it, so anyone can drop the image into a backpack or a profile and confirm who earned it, for what, and from whom. If you run courses, issuing Open Badges turns each completion into a portable credential your learners actually own. This article walks through generating the badge image and wrapping it in the Open Badges data so it verifies.
What an Open Badge actually is
An Open Badge is three pieces of JSON and one image. The image is what people see. The data is what makes it a credential rather than a graphic.
The pieces are the Issuer (you), the BadgeClass (the achievement itself, for example "Completed Advanced PHP"), and the Assertion (the award of that badge to one person on one date). The Assertion is the personal bit, and it is the thing that gets verified. The final move, called baking, embeds the Assertion into the PNG so the image and its proof travel together.
If you are already issuing certificate images and want the same idea with built-in verification, the approach here sits neatly alongside the one in our guide on generating signed digital certificates at scale. A certificate proves completion to a human reading it. A badge proves completion to a machine checking it.
Step 1: Generate the badge image
A badge image is small, square and bold so it reads at any size. Design it as HTML and render it to a PNG. This example uses the HTML to Image API, which renders your markup in real Chromium and returns a hosted PNG.
use Illuminate\Support\Facades\Http;
function badgeImage(string $title, string $level): string
{
$html = <<<HTML
<div style="width:600px;height:600px;display:flex;flex-direction:column;
align-items:center;justify-content:center;font-family:Arial;
background:radial-gradient(circle at 50% 30%,#4f46e5,#312e81);color:#fff;">
<div style="width:240px;height:240px;border-radius:50%;border:8px solid #fbbf24;
display:flex;align-items:center;justify-content:center;font-size:90px;">★</div>
<h1 style="margin:32px 0 4px;font-size:40px;text-align:center;">{$title}</h1>
<p style="font-size:22px;opacity:.8;">{$level}</p>
</div>
HTML;
return Http::withHeaders(['X-API-Key' => config('services.html2img.key')])
->post('https://app.html2img.com/api/html', [
'html' => $html, 'width' => 600, 'height' => 600,
])->throw()->json('url');
}
Render this once per BadgeClass, not once per learner. Every person who earns "Completed Advanced PHP" shares the same badge image; only their Assertion differs.
Step 2: Describe the issuer and the badge
These two objects are public and stable. Host them at fixed URLs on your site and serve them as application/json. Open Badges v2 looks like this.
The Issuer:
{
"@context": "https://w3id.org/openbadges/v2",
"type": "Issuer",
"id": "https://example.com/badges/issuer.json",
"name": "Example Academy",
"url": "https://example.com",
"email": "[email protected]"
}
The BadgeClass, which points at the image you just generated:
{
"@context": "https://w3id.org/openbadges/v2",
"type": "BadgeClass",
"id": "https://example.com/badges/advanced-php.json",
"name": "Completed Advanced PHP",
"description": "Awarded for completing the Advanced PHP course.",
"image": "https://cdn.example.com/badges/advanced-php.png",
"criteria": { "narrative": "Complete all modules and the final assessment." },
"issuer": "https://example.com/badges/issuer.json"
}
Step 3: Issue an Assertion per learner
The Assertion is the award. It names the recipient by a hashed email so the address is not exposed, references the BadgeClass, and carries a verification block pointing back to its own hosted URL.
function assertion(User $user, string $badgeUrl): array
{
$salt = bin2hex(random_bytes(8));
$id = (string) Str::uuid();
return [
'@context' => 'https://w3id.org/openbadges/v2',
'type' => 'Assertion',
'id' => "https://example.com/badges/assertions/{$id}.json",
'recipient' => [
'type' => 'email',
'hashed' => true,
'salt' => $salt,
'identity' => 'sha256$'.hash('sha256', strtolower($user->email).$salt),
],
'badge' => $badgeUrl,
'verification' => ['type' => 'HostedBadge'],
'issuedOn' => now()->toIso8601String(),
];
}
Store that JSON and serve it at the id URL. The HostedBadge verification type tells any consumer to fetch the Assertion from that address and trust what it finds there, which is why the URL must be stable and public.
Step 4: Bake the badge so the image proves itself
Baking writes the Assertion URL into the PNG itself, in an iTXt chunk with the keyword openbadges. After baking, the image is the credential: a learner can upload just the PNG to any Open Badges backpack and it resolves to the full record.
// Insert an iTXt chunk straight after the PNG signature.
function bakeBadge(string $png, string $assertionUrl): string
{
$type = 'iTXt';
$data = "openbadges\x00\x00\x00\x00\x00".$assertionUrl;
$crc = pack('N', crc32($type.$data));
$chunk = pack('N', strlen($data)).$type.$data.$crc;
return substr($png, 0, 8).$chunk.substr($png, 8);
}
Download the rendered PNG, bake the Assertion URL into it, and that baked file is what you hand to the learner.
$png = Http::get($badgeImageUrl)->body();
$baked = bakeBadge($png, $assertion['id']);
Storage::disk('s3')->put("badges/{$user->id}-advanced-php.png", $baked);
What you end up with
Every learner who finishes the course gets a single PNG that displays their badge and carries the proof inside it. Paste it into a LinkedIn profile, a CV or a Badgr backpack and it verifies against the Assertion you host. You generate the artwork once, mint a small JSON record per award, and bake the two together. The credential is portable, it belongs to the learner, and it cannot be faked, because the verification always resolves back to a URL on your domain that only you control.