GET /api/designsDescription:
Retrieve all certificate designs of the authenticated user, each with its custom field names and its recipient lists. This is the best starting point for an integration: one call gives you every list_id you can add recipients to and the custom fields each list accepts. The response is not paginated.
Method: GET
Authentication:
Requires user authentication via token (e.g., Bearer Token).
Request Headers:
<token>Response:
200 OKResponse Body:
{
"data": [
{
"id": 12,
"name": "Data Science Certificate of Completion",
"custom_fields": ["course_name", "completion_date"],
"screenshot_url": "https://certfusion.com/storage/screenshots/12.png",
"lists": [
{ "id": 57, "name": "September 2026 cohort", "recipients_count": 48 },
{ "id": 58, "name": "October 2026 cohort", "recipients_count": 0 }
],
"created_at": "2026-08-01T09:00:00+00:00",
"updated_at": "2026-09-01T12:00:00+00:00"
}
]
}
Response Fields:
data: A collection of designs.
id: The unique identifier of the design.name: The name of the design.custom_fields: The custom field names defined on the design. Recipients added to any list of this design may supply values for these fields.screenshot_url: A preview image of the design, or null if none has been generated yet.lists: The recipient lists that belong to this design.
id: The list ID (use it as list_id when creating recipients).name: The list name.recipients_count: Number of recipients currently in the list.created_at / updated_at: Timestamps.Errors:
GET /api/listsDescription:
Retrieve a list of all certificate lists associated with the authenticated user.
Method: GET
Authentication:
Requires user authentication via token (e.g., Bearer Token).
Request Parameters:
design_id (optional):
Only return lists that belong to this design.
page (optional):
The page number for pagination. Default is 1.
per_page (optional):
Number of results per page. Default is 15, maximum 100.
Request Headers:
<token>Example Request:
GET /api/lists?design_id=12&per_page=50
Response:
200 OKResponse Body:
{
"data": [
{
"id": 1,
"name": "Certificate List 1",
"custom_fields": ["course_name", "completion_date", "instructor_name"],
"created_at": "2023-10-01T12:00:00.000000Z",
"updated_at": "2023-10-02T12:00:00.000000Z"
},
{
"id": 2,
"name": "Certificate List 2",
"custom_fields": [],
"created_at": "2023-10-02T12:30:00.000000Z",
"updated_at": "2023-10-03T14:00:00.000000Z"
}
],
"links": {
"first": "http://example.com/api/lists?page=1",
"last": "http://example.com/api/lists?page=5",
"prev": null,
"next": "http://example.com/api/lists?page=2"
},
"meta": {
"current_page": 1,
"from": 1,
"last_page": 5,
"per_page": 15,
"to": 15,
"total": 75
}
}
Response Fields:
data: A collection of certificate lists.
id: The unique identifier of the list.name: The name of the certificate list.custom_fields: An array of custom field names available for recipients in this list. These fields are defined in the certificate design associated with the list.created_at: The date and time when the certificate list was created.updated_at: The date and time when the certificate list was last updated.links: Pagination links.
first: URL to the first page of results.last: URL to the last page of results.prev: URL to the previous page of results, if applicable.next: URL to the next page of results, if applicable.meta: Pagination metadata.
current_page: The current page number.from: The starting index of the current page of results.last_page: The last page number.per_page: The number of items per page.to: The ending index of the current page of results.total: The total number of available certificate lists.Errors:
GET /api/lists/{id}Description:
Retrieve a single certificate list, including the custom field names of its design.
Method: GET
Authentication:
Requires user authentication via token (e.g., Bearer Token).
URL Parameters:
id (required, integer):Request Headers:
<token>Response:
200 OKResponse Body:
{
"data": {
"id": 57,
"name": "September 2026 cohort",
"custom_fields": ["course_name", "completion_date"],
"created_at": "2026-08-01T09:00:00+00:00",
"updated_at": "2026-09-01T12:00:00+00:00"
}
}
Response (Errors):
404 Not Found:
If the list does not exist or does not belong to the authenticated user.
{
"status": false,
"message": "List not found or not associated with your account."
}
GET /api/recipientsDescription:
Retrieve a list of all recipients associated with certificate lists belonging to the authenticated user, newest first.
Method: GET
Authentication:
Requires user authentication via token (e.g., Bearer Token).
Request Parameters:
list_id (optional):
Only return recipients of this list.
page (optional):
The page number for pagination. Default is 1.
per_page (optional):
Number of results per page. Default is 15, maximum 100.
Request Headers:
<token>Response:
200 OKResponse Body:
{
"data": [
{
"id": 1,
"list_id": 1,
"email": "recipient@example.com",
"name": "John Doe",
"custom_fields": {
"course_name": "Introduction to AI",
"completion_date": "2023-10-01"
},
"status": "issued",
"uuid": "3kTbZ9qL2mN8xR",
"admin_download_link": "https://certs.certfusion.com/download/3kTbZ9qL2mN8xR?sa=...&st=1696161600",
"issued_date": "2023-10-01T12:00:00+00:00",
"created_at": "2023-10-01T12:00:00+00:00",
"updated_at": "2023-10-02T12:00:00+00:00"
},
{
"id": 2,
"list_id": 2,
"email": "another@example.com",
"name": "Jane Smith",
"custom_fields": {},
"status": "draft",
"uuid": "7pQeR4vW1zK6yH",
"admin_download_link": "https://certs.certfusion.com/download/7pQeR4vW1zK6yH?sa=...&st=1696161600",
"issued_date": null,
"created_at": "2023-09-28T10:00:00+00:00",
"updated_at": "2023-09-29T10:00:00+00:00"
}
],
"links": {
"first": "http://example.com/api/recipients?page=1",
"last": "http://example.com/api/recipients?page=5",
"prev": null,
"next": "http://example.com/api/recipients?page=2"
},
"meta": {
"current_page": 1,
"from": 1,
"last_page": 5,
"per_page": 15,
"to": 15,
"total": 75
}
}
Response Fields:
data: A collection of recipient records.
id: The unique identifier of the recipient.list_id: The ID of the certificate list the recipient is associated with.email: The email address of the recipient.name: The name of the recipient.custom_fields: Key-value pairs of the recipient's custom field values (empty object when none).status: The current status of the recipient: draft (not issued), processing (certificate is being generated) or issued.uuid: The public certificate identifier (Base62). It is part of the download and verification URLs.admin_download_link: A signed download link for the certificate PDF, valid for the account owner. Only useful once status is issued.issued_date: The date when the certificate was issued to the recipient. If not set, null will be returned.created_at: The timestamp when the recipient was created.updated_at: The timestamp when the recipient was last updated.links: Pagination links.
first: URL to the first page of results.last: URL to the last page of results.prev: URL to the previous page of results, if applicable.next: URL to the next page of results, if applicable.meta: Pagination metadata.
current_page: The current page number.from: The starting index of the current page of results.last_page: The last page number.per_page: The number of items per page.to: The ending index of the current page of results.total: The total number of available recipients.Errors:
GET /api/recipients/{id}Description:
Retrieve a single recipient. Poll this endpoint after issuing to see the status move from processing to issued, or subscribe to the certificate.issued webhook instead (see Webhook Subscriptions below).
Method: GET
Authentication:
Requires user authentication via token (e.g., Bearer Token).
URL Parameters:
id (required, integer):Request Headers:
<token>Response:
200 OKResponse Body:
{
"data": {
"id": 1042,
"list_id": 57,
"email": "jane.doe@example.com",
"name": "Jane Doe",
"custom_fields": {
"course_name": "Introduction to Data Science",
"completion_date": "2026-09-01"
},
"status": "issued",
"uuid": "3kTbZ9qL2mN8xR",
"admin_download_link": "https://certs.certfusion.com/download/3kTbZ9qL2mN8xR?sa=...&st=1756900000",
"issued_date": "2026-09-03T09:15:42+00:00",
"created_at": "2026-09-01T14:02:11+00:00",
"updated_at": "2026-09-03T09:15:42+00:00"
}
}
Response (Errors):
404 Not Found:
If the recipient does not exist or is not associated with the authenticated user.
{
"status": false,
"message": "Recipient not found or not associated with your account."
}
POST /api/recipientsDescription: Add a new recipient to a certificate list associated with the authenticated user. If a recipient with the same email already exists in the list, it will be updated with new custom field data or skipped if the data is identical.
Method: POST
Authentication: Requires user authentication via token (e.g., Bearer Token).
Request Parameters (Body):
{
"list_id": 1,
"email": "recipient@example.com",
"name": "John Doe",
"custom_fields": {
"course_name": "Introduction to AI",
"completion_date": "2023-10-01"
}
}
list_id (required, integer):
The ID of the certificate list the recipient will be added to. This list must exist and belong to the authenticated user.
email (required, string):
The email address of the recipient. It must be a valid email format.
name (required, string):
The full name of the recipient. Maximum length is 255 characters.
custom_fields (optional, object):
Key-value pairs of custom fields for the certificate. Each key must match a custom field defined in the certificate design. Keys must contain only alphanumeric characters, hyphens, and underscores. Values must be strings with a maximum length of 500 characters.
Request Headers:
<token>
A valid API token must be included in the request header to authenticate the user.Response:
201 Created (when a new recipient is created)200 OK (when an existing recipient is updated or skipped)Response Body (Success):
{
"status": true,
"message": "Recipient created successfully.",
"data": {
"id": 1,
"list_id": 1,
"email": "recipient@example.com",
"name": "John Doe",
"custom_fields": {
"course_name": "Introduction to AI",
"completion_date": "2023-10-01"
},
"status": "draft",
"uuid": "3kTbZ9qL2mN8xR",
"admin_download_link": "https://certs.certfusion.com/download/3kTbZ9qL2mN8xR?sa=...&st=1696161600",
"issued_date": null,
"created_at": "2023-10-01T12:00:00+00:00",
"updated_at": "2023-10-01T12:00:00+00:00",
"action": "created"
}
}
status: Indicates whether the operation was successful (true).message: A message describing the result of the operation. Possible values:
"Recipient created successfully." - New recipient was created"Recipient updated with new custom field data." - Existing recipient was updated"Recipient already exists with identical data." - No changes were madedata: The recipient object with the following fields:
id: The unique identifier of the recipient.list_id: The ID of the certificate list the recipient is associated with.email: The email address of the recipient.name: The name of the recipient.custom_fields: The stored custom field values.status: The status of the recipient (draft, processing or issued).uuid: The public certificate identifier.admin_download_link: Signed download link for the certificate PDF (useful once issued).issued_date: The date when the certificate was issued to the recipient. If not issued yet, null.created_at: The timestamp when the recipient was created.updated_at: The timestamp when the recipient was last updated.action: The action taken. Possible values: created, updated, skipped.Response (Errors):
404 Not Found:
If the specified list_id does not exist.
Response Example:
{
"status": false,
"message": "The list you are trying to access does not exist."
}
403 Forbidden: If the authenticated user does not have permission to access the specified list.
Response Example:
{
"status": false,
"message": "You are not authorized to perform this action."
}
422 Unprocessable Entity: If the input validation fails (e.g., missing required fields, invalid email format, or invalid custom fields).
Response Example (Validation Error):
{
"status": false,
"message": "Validation failed.",
"errors": {
"email": ["The email must be a valid email address."]
}
}
Response Example (Invalid Custom Field):
{
"status": false,
"message": "Custom field 'invalid_field' is not available for this certificate design. Available fields: course_name, completion_date"
}
500 Internal Server Error: If there is an unexpected server error during recipient creation.
Response Example:
{
"status": false,
"message": "An error occurred while creating the recipient.",
"error": "Error details here"
}
POST /api/recipients/bulkDescription:
Add up to 500 recipients to one certificate list in a single call. Each row follows the same create/update/skip rules as POST /api/recipients. The call is transactional: if any row fails validation (bad email, unknown custom field, ...) no recipient is created and the error is returned.
Method: POST
Authentication:
Requires user authentication via token (e.g., Bearer Token).
Request Parameters (Body):
{
"list_id": 57,
"recipients": [
{
"email": "jane.doe@example.com",
"name": "Jane Doe",
"custom_fields": { "course_name": "Introduction to Data Science", "completion_date": "2026-09-01" }
},
{
"email": "sam.lee@example.com",
"name": "Sam Lee"
}
]
}
list_id (required, integer):
The ID of the certificate list. It must belong to the authenticated user.
recipients (required, array, 1–500 items):
email (required, string): A valid email address.name (required, string, max 255).custom_fields (optional, object): Same rules as the single-recipient endpoint.Response:
201 CreatedResponse Body (Success):
{
"status": true,
"created": 1,
"updated": 1,
"skipped": 0,
"results": [
{ "index": 0, "recipient_id": 1042, "email": "jane.doe@example.com", "action": "created" },
{ "index": 1, "recipient_id": 998, "email": "sam.lee@example.com", "action": "updated" }
]
}
created / updated / skipped: Counts per action.results: One entry per input row, in input order.
index: Position of the row in the request.recipient_id: The ID of the created or existing recipient.email: The recipient's email.action: created, updated or skipped.Response (Errors):
422 Unprocessable Entity:
Validation failed for one or more rows, or a custom field is not defined on the design. Nothing is created.
{
"status": false,
"message": "Validation failed.",
"errors": {
"recipients.1.email": ["The recipients.1.email field must be a valid email address."]
}
}
{
"status": false,
"message": "No recipients were created. Custom field 'grade' is not available for this certificate design. Available fields: course_name, completion_date"
}
403 Forbidden / 404 Not Found:
The list does not belong to the authenticated user / does not exist.
GET /api/recipients/searchDescription: Search for recipients by email address within certificate lists belonging to the authenticated user. Optionally filter by recipient status.
Method: GET
Authentication: Requires user authentication via token (e.g., Bearer Token).
Request Parameters (Query):
email (required, string):
The email address to search for. Partial matches are supported.
status (optional, string):
Filter results by recipient status. Allowed values: draft, processing, issued.
page (optional, integer):
The page number for pagination. Default is 1.
per_page (optional, integer):
Number of results per page. Default is 15, maximum 100.
Request Headers:
<token>
A valid API token must be included in the request header to authenticate the user.Example Request:
GET /api/recipients/search?email=john@example.com&status=issued
Response:
200 OKResponse Body:
{
"data": [
{
"id": 1,
"list_id": 1,
"email": "john@example.com",
"name": "John Doe",
"status": "issued",
"uuid": "abcd-1234-efgh-5678",
"issued_date": "2023-10-01T12:00:00+00:00",
"created_at": "2023-10-01T12:00:00+00:00",
"updated_at": "2023-10-02T12:00:00+00:00"
}
],
"links": {
"first": "http://example.com/api/recipients/search?email=john@example.com&page=1",
"last": "http://example.com/api/recipients/search?email=john@example.com&page=1",
"prev": null,
"next": null
},
"meta": {
"current_page": 1,
"from": 1,
"last_page": 1,
"per_page": 15,
"to": 1,
"total": 1
}
}
Response Fields:
data: A collection of matching recipient records.links: Pagination links.meta: Pagination metadata.Response (Errors):
422 Unprocessable Entity: If the email parameter is missing or invalid.
Response Example:
{
"message": "The email field is required.",
"errors": {
"email": ["The email field is required."]
}
}
401 Unauthorized: If the user is not authenticated.
DELETE /api/recipients/{id}Description:
Delete a recipient from a certificate list associated with the authenticated user.
Method: DELETE
Authentication:
Requires user authentication via token (e.g., Bearer Token).
URL Parameters:
id (required, integer):Request Headers:
<token>Response:
200 OK (on success)Response Body (Success):
{
"status": true,
"message": "Recipient deleted successfully."
}
Response Fields:
status: Indicates whether the operation was successful (true or false).message: A message describing the result of the operation.Response (Errors):
404 Not Found:
If the recipient does not exist or is not associated with the authenticated user.
Response Example:
{
"status": false,
"message": "Recipient not found or not associated with your account."
}
500 Internal Server Error:
If an unexpected error occurs during the deletion process.
Response Example:
{
"status": false,
"message": "An error occurred while deleting the recipient.",
"error": "Error details here"
}
POST /api/recipients/{id}/issueDescription:
Issue a certificate to a specific recipient in the authenticated user's certificate list. The recipient must be in draft status.
Issuance is asynchronous: the request queues certificate generation and returns immediately with status: "processing". The status becomes issued (and issued_date is set) a few seconds later, once the PDF has been generated and emailed to the recipient. Poll GET /api/recipients/{id} or subscribe to the certificate.issued webhook to be notified. Each issued certificate counts against the monthly quota shown by GET /api/me.
Method: POST
Authentication:
Requires user authentication via token (e.g., Bearer Token).
URL Parameters:
id (required, integer):Request Headers:
<token>Response:
200 OK (on success)Response Body (Success):
{
"status": true,
"recipient": {
"id": 1,
"list_id": 1,
"email": "recipient@example.com",
"name": "John Doe",
"custom_fields": {},
"status": "processing",
"uuid": "3kTbZ9qL2mN8xR",
"admin_download_link": "https://certs.certfusion.com/download/3kTbZ9qL2mN8xR?sa=...&st=1696161600",
"issued_date": null,
"created_at": "2023-10-01T12:00:00+00:00",
"updated_at": "2023-10-01T12:00:00+00:00"
}
}
status: Indicates whether the operation was successful (true).recipient: The recipient with status: "processing". issued_date is filled in once generation completes.Response (Errors):
404 Not Found:
If the recipient does not exist or is not associated with the authenticated user.
Response Example:
{
"status": false,
"message": "Recipient not found or you do not have access to this recipient."
}
409 Conflict:
If the recipient is not in draft status (the certificate is already processing or issued). The current recipient is returned so you can read its download link.
Response Example:
{
"status": false,
"message": "Certificate has already been issued to this recipient.",
"recipient": {
"id": 1,
"status": "issued",
"uuid": "3kTbZ9qL2mN8xR",
"admin_download_link": "https://certs.certfusion.com/download/3kTbZ9qL2mN8xR?sa=...&st=1696161600",
"issued_date": "2023-10-01T12:00:00+00:00"
}
}
422 Unprocessable Entity:
If the account may not issue certificates right now. reason is machine-readable:
reason |
Meaning |
|---|---|
monthly_limit |
The monthly certificate quota is used up (see GET /api/me). |
trial_cap |
The trial account has reached its cap of 5 issued certificates. |
subscription_past_due |
The last payment failed; update the payment method. |
subscription_ended |
The subscription has been cancelled or has expired. |
Response Example:
{
"status": false,
"message": "You have reached your monthly certificate limit.",
"reason": "monthly_limit"
}
500 Internal Server Error:
If an unexpected error occurs while issuing the certificate.
Response Example:
{
"status": false,
"message": "An error occurred while issuing the certificate.",
"error": "Error details here"
}
POST /api/recipients/issueDescription:
Issue certificates to a group of up to 500 recipients belonging to the authenticated user's certificate lists. Only recipients in draft status are issued; the rest are reported, not failed. Issuance is asynchronous exactly as for the single-recipient endpoint (returned recipients have status: "processing").
Method: POST
Authentication:
Requires user authentication via token (e.g., Bearer Token).
Request Body:
{
"recipient_ids": [1, 2, 3]
}
recipient_ids (required, array of integers, 1–500 items):Request Headers:
<token>Response:
200 OKResponse Body (Success):
{
"status": true,
"message": "2 certificate(s) issued.",
"recipients": [
{
"id": 1,
"list_id": 1,
"email": "recipient1@example.com",
"name": "John Doe",
"custom_fields": {},
"status": "processing",
"uuid": "3kTbZ9qL2mN8xR",
"admin_download_link": "https://certs.certfusion.com/download/3kTbZ9qL2mN8xR?sa=...&st=1696161600",
"issued_date": null,
"created_at": "2023-10-01T12:00:00+00:00",
"updated_at": "2023-10-01T12:00:00+00:00"
},
{
"id": 2,
"list_id": 1,
"email": "recipient2@example.com",
"name": "Jane Doe",
"custom_fields": {},
"status": "processing",
"uuid": "7pQeR4vW1zK6yH",
"admin_download_link": "https://certs.certfusion.com/download/7pQeR4vW1zK6yH?sa=...&st=1696161600",
"issued_date": null,
"created_at": "2023-10-01T12:00:00+00:00",
"updated_at": "2023-10-01T12:00:00+00:00"
}
],
"skipped": [
{ "id": 3, "email": "recipient3@example.com", "reason": "already issued" }
],
"not_found_ids": [999],
"blocked": null
}
status: true when at least one certificate was queued.message: Human-readable summary.recipients: The recipients that were queued for issuance (status: "processing").skipped: Recipients that were found but not issued. reason is already issued (status was not draft) or error.not_found_ids: IDs that do not exist or do not belong to the authenticated user.blocked: null, or { "reason", "message" } when issuance stopped part-way because the quota or subscription blocks further certificates (same reason codes as the single-recipient endpoint). Recipients after the block point are neither in recipients nor in skipped; retry them once the block is resolved.Response (Errors):
422 Unprocessable Entity (Validation Error):
If recipient_ids is missing, empty, contains non-integers or has more than 500 items.
Response Example:
{
"status": false,
"message": "Validation failed.",
"errors": {
"recipient_ids": [
"You can issue to at most 500 recipients per call."
]
}
}
422 Unprocessable Entity (Blocked):
If issuance is blocked before any certificate could be queued. The body has the same shape as the success response with an empty recipients array, plus a top-level reason.
Response Example:
{
"status": false,
"message": "You have reached your monthly certificate limit.",
"recipients": [],
"skipped": [],
"not_found_ids": [],
"blocked": { "reason": "monthly_limit", "message": "You have reached your monthly certificate limit." },
"reason": "monthly_limit"
}
404 Not Found:
If none of the IDs match a recipient the authenticated user has access to.
Response Example:
{
"status": false,
"message": "No valid recipients found or you do not have access to them."
}
500 Internal Server Error:
If an unexpected error occurs while issuing the certificates.
Response Example:
{
"status": false,
"message": "An error occurred while issuing certificates.",
"error": "Error details here"
}
Webhook subscriptions let CertFusion call your HTTPS endpoint when something happens, instead of you polling the API. They are what the Make, Zapier and n8n instant triggers use under the hood, and you can use them directly.
Events:
| Event | Fires when |
|---|---|
recipient.created |
A recipient is added to a list, by any means (web app, CSV upload, form/meeting integrations, API). |
certificate.issued |
A certificate has finished generating and has been issued to the recipient. download_link and verify_link are populated. |
A subscription can be scoped to one list (list_id) or receive the event for every list in the account (list_id: null).
POST /api/webhooks/subscriptionsDescription:
Register a webhook URL for an event. Creating a subscription with the same event and url as an existing one does not create a duplicate: the existing subscription is updated (scope, label), re-activated, its failure counter reset, and returned with 200 OK. The signing secret is kept in that case.
Method: POST
Request Parameters (Body):
{
"url": "https://hook.eu1.make.com/abc123",
"event": "certificate.issued",
"list_id": 57,
"source": "make",
"label": "Send certificate to Slack"
}
url (required, string, max 500): The endpoint to POST to. Must use https.event (required, string): recipient.created or certificate.issued.list_id (optional, integer or null): Restrict to one list. Omit or send null for all lists. The list must belong to the authenticated user.source (optional, string, max 32): Free-form origin tag, e.g. make, zapier, n8n.label (optional, string, max 100): A human-readable name shown in the CertFusion UI.Response:
201 Created (new subscription) or 200 OK (existing subscription re-attached){
"data": {
"id": 31,
"url": "https://hook.eu1.make.com/abc123",
"event": "certificate.issued",
"list_id": 57,
"source": "make",
"label": "Send certificate to Slack",
"secret": "qP4x9Lm2vTn8wRz1cKd6bHf3sYe7gJa0uNi5oXl2",
"is_active": true,
"failure_count": 0,
"last_delivered_at": null,
"created_at": "2026-09-03T08:00:00+00:00"
}
}
secret: The HMAC key used to sign every delivery (see Verifying deliveries). Store it; it is returned on every read of the subscription but cannot be rotated except by deleting and re-creating the subscription.is_active: false once CertFusion has switched the subscription off (see Retries and deactivation).failure_count: Consecutive failed deliveries.last_delivered_at: Timestamp of the last successful delivery.Response (Errors):
422 Unprocessable Entity: Invalid or non-https url, unknown event.
{
"message": "The webhook URL must use https.",
"errors": { "url": ["The webhook URL must use https."] }
}
403 Forbidden / 404 Not Found: list_id belongs to another account / does not exist.
GET /api/webhooks/subscriptionsDescription:
List every webhook subscription of the authenticated user, newest first. Not paginated.
Response:
200 OK{
"data": [
{
"id": 31,
"url": "https://hook.eu1.make.com/abc123",
"event": "certificate.issued",
"list_id": 57,
"source": "make",
"label": "Send certificate to Slack",
"secret": "qP4x9Lm2vTn8wRz1cKd6bHf3sYe7gJa0uNi5oXl2",
"is_active": true,
"failure_count": 0,
"last_delivered_at": "2026-09-03T09:15:43+00:00",
"created_at": "2026-09-03T08:00:00+00:00"
}
]
}
DELETE /api/webhooks/subscriptions/{id}Description:
Remove a subscription. Pending deliveries that were already queued may still be attempted once; nothing new is queued.
Response:
200 OK{
"status": true,
"message": "Webhook subscription deleted."
}
Response (Errors):
Every delivery is an HTTP POST with a JSON body:
{
"event": "certificate.issued",
"timestamp": "2026-09-03T09:15:42+00:00",
"data": {
"id": 1042,
"list_id": 57,
"email": "jane.doe@example.com",
"name": "Jane Doe",
"custom_fields": {
"course_name": "Introduction to Data Science",
"completion_date": "2026-09-01"
},
"status": "issued",
"uuid": "3kTbZ9qL2mN8xR",
"admin_download_link": "https://certs.certfusion.com/download/3kTbZ9qL2mN8xR?sa=...&st=1756900000",
"issued_date": "2026-09-03T09:15:42+00:00",
"created_at": "2026-09-01T14:02:11+00:00",
"updated_at": "2026-09-03T09:15:42+00:00",
"list_name": "September 2026 cohort",
"design_id": 12,
"design_name": "Data Science Certificate of Completion",
"download_link": "https://certs.certfusion.com/download/3kTbZ9qL2mN8xR",
"verify_link": "https://certs.certfusion.com/verify/3kTbZ9qL2mN8xR"
}
}
event: The event name.timestamp: When the event was raised (ISO 8601).data: The recipient, in the same shape as the REST API, plus context so you do not need extra lookups:
list_name, design_id, design_name.download_link: Public link to the certificate PDF (the one recipients receive). null until the certificate is issued, so it is null in recipient.created payloads.verify_link: Public verification page for the certificate. null until issued.Request headers sent with every delivery:
| Header | Value |
|---|---|
Content-Type |
application/json |
User-Agent |
CertFusion-Webhooks/1.0 |
X-CertFusion-Event |
The event name, e.g. certificate.issued. |
X-CertFusion-Delivery |
A UUID unique to this delivery attempt. Use it for de-duplication. |
X-CertFusion-Signature |
sha256=<hex> — HMAC-SHA256 of the raw request body using the subscription secret. |
Your endpoint should respond with any 2xx status within 10 seconds. The response body is ignored.
Compute the HMAC over the raw bytes of the body (before any JSON parsing or re-encoding) and compare it to the header using a constant-time comparison.
PHP:
$secret = getenv('CERTFUSION_WEBHOOK_SECRET');
$rawBody = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_CERTFUSION_SIGNATURE'] ?? '';
$expected = 'sha256=' . hash_hmac('sha256', $rawBody, $secret);
if (! hash_equals($expected, $signature)) {
http_response_code(401);
exit('Invalid signature');
}
$payload = json_decode($rawBody, true);
// $payload['event'], $payload['data']['download_link'], ...
http_response_code(200);
Node.js (Express):
const crypto = require('crypto');
const express = require('express');
const app = express();
const secret = process.env.CERTFUSION_WEBHOOK_SECRET;
// Keep the raw body: express.json() would re-encode it.
app.post('/certfusion', express.raw({ type: 'application/json' }), (req, res) => {
const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(req.body).digest('hex');
const received = req.get('X-CertFusion-Signature') || '';
const a = Buffer.from(expected);
const b = Buffer.from(received);
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
return res.status(401).send('Invalid signature');
}
const payload = JSON.parse(req.body.toString('utf8'));
// payload.event, payload.data.download_link, ...
res.sendStatus(200);
});
| Your response | What CertFusion does |
|---|---|
2xx |
Delivery succeeded; failure_count reset to 0. |
5xx, timeout, connection error |
Retried up to 5 attempts in total with backoff of 30 s, 2 min, 10 min and 30 min between attempts. |
404 or 410 |
Treated as "endpoint gone": the subscription is deactivated immediately and not retried. |
Any other 4xx |
Recorded as a failure; not retried. |
After 10 consecutive failures (across deliveries) the subscription is set to is_active: false and stops receiving events. Re-create it (POST with the same url and event) to re-activate it with the counter reset; the secret is preserved.
Deliveries are not guaranteed to arrive in order and may, in rare cases, be delivered more than once (a retry after a timed-out request that did in fact reach you). Use X-CertFusion-Delivery or data.id + event to de-duplicate.