- Why render your own
- Step 1: A function to render the certificate
- Step 2: Hook LearnDash completion
- Step 3: Hook TutorLMS completion
- Step 4: Give the learner a download
- Where to take it next
LearnDash and TutorLMS will both issue a certificate, but you are stuck with their certificate builder: a clunky editor, limited layout control, and a PDF that never quite matches your brand. If you want a certificate that looks the way you designed it, the cleaner path is to render your own from HTML when a learner completes a course, then give them a download link. This article hooks into both LearnDash and TutorLMS completion events, generates a branded certificate image, and serves it from the learner's account.
Why render your own
The LMS certificate builders work in their own world. Your design system does not apply, web fonts are awkward, and anything beyond a centred name on a background fights the editor. Rendering from HTML puts you back in control: the certificate is a normal HTML document, styled however you like, turned into an image by an API.
If you want to nail the layout before writing any code, the certificate generator tool lets you design and preview one in the browser, and there is a certificate of completion template you can adapt. Once the design is settled, the rest is plumbing.
Step 1: A function to render the certificate
Write one function that takes a learner and a course title and returns a hosted image URL. It posts your HTML to the rendering API and gets a PNG back.
function ai_render_certificate( string $name, string $course ): ?string {
$name = esc_html( $name );
$course = esc_html( $course );
$date = esc_html( date_i18n( 'j F Y' ) );
$html = <<<HTML
<div style="width:1600px;height:1130px;display:flex;align-items:center;
justify-content:center;font-family:Georgia,serif;background:#f7f5ef;">
<div style="text-align:center;border:3px solid #c9a13b;background:#fff;padding:90px 130px;">
<p style="letter-spacing:.3em;color:#c9a13b;">CERTIFICATE OF COMPLETION</p>
<h1 style="font-size:56px;margin:20px 0;">{$course}</h1>
<p style="color:#6b7280;">This is awarded to</p>
<p style="font-size:44px;margin:6px 0;">{$name}</p>
<p style="color:#6b7280;">on {$date}</p>
</div>
</div>
HTML;
$response = wp_remote_post( 'https://app.html2img.com/api/html', [
'headers' => [ 'X-API-Key' => HTML2IMG_API_KEY, 'Content-Type' => 'application/json' ],
'timeout' => 25,
'body' => wp_json_encode( [ 'html' => $html, 'width' => 1600, 'height' => 1130 ] ),
] );
if ( is_wp_error( $response ) ) {
return null;
}
return json_decode( wp_remote_retrieve_body( $response ), true )['url'] ?? null;
}
Keep HTML2IMG_API_KEY in wp-config.php. Store the result against the learner so you generate it once, not on every visit to their dashboard.
function ai_store_certificate( int $user_id, int $course_id, string $course_title ): void {
$user = get_userdata( $user_id );
$url = ai_render_certificate( $user->display_name, $course_title );
if ( $url ) {
update_user_meta( $user_id, "_certificate_{$course_id}", esc_url_raw( $url ) );
}
}
Step 2: Hook LearnDash completion
LearnDash fires learndash_course_completed with a data array when a learner finishes. Pull the user and course out of it and store the certificate.
add_action( 'learndash_course_completed', function ( array $data ): void {
$user_id = (int) $data['user']->ID;
$course_id = (int) $data['course']->ID;
ai_store_certificate( $user_id, $course_id, get_the_title( $course_id ) );
} );
Step 3: Hook TutorLMS completion
TutorLMS fires tutor_course_complete_after with the course id, for the current user. The shape is different, the idea is the same.
add_action( 'tutor_course_complete_after', function ( int $course_id ): void {
$user_id = get_current_user_id();
if ( $user_id ) {
ai_store_certificate( $user_id, $course_id, get_the_title( $course_id ) );
}
} );
With both hooks in place, a completion in either LMS produces the same branded certificate through the same code path.
Step 4: Give the learner a download
Now expose the stored image as a download. A small query-var endpoint keeps it tidy and lets you check the learner actually owns the certificate before serving it.
add_action( 'init', function () {
add_rewrite_endpoint( 'certificate', EP_ROOT );
} );
add_action( 'template_redirect', function () {
$course_id = absint( get_query_var( 'certificate' ) );
if ( ! $course_id || ! is_user_logged_in() ) {
return;
}
$url = get_user_meta( get_current_user_id(), "_certificate_{$course_id}", true );
if ( ! $url ) {
wp_die( 'No certificate found for this course.' );
}
header( 'Content-Type: image/png' );
header( 'Content-Disposition: attachment; filename="certificate.png"' );
echo wp_remote_retrieve_body( wp_remote_get( $url ) );
exit;
} );
Link to it from the learner's dashboard, for example https://example.com/certificate/123 where 123 is the course id. Because the endpoint checks get_current_user_id() against the stored meta, one learner cannot pull another's certificate by guessing the URL.
Where to take it next
You now have certificates that look the way you built them, issued automatically on completion in either LearnDash or TutorLMS, and downloadable from the learner's account. The design lives in HTML, so changing the border, the font or the wording is a one-line edit rather than a fight with a page builder. If you run cohorts, move the render call into a queued action so issuing a hundred certificates at once never blocks the request that triggered it.
Interested in proving your knowledge of this topic? Take the WordPress Development certification.
WordPress Development
Covering all aspects of WordPress web development, from theme development, plugin development, server set up and configuration and optimisation.
$99