Appearance
Process large batches of certificates efficiently with Pocketbook's bulk operations system. Create hundreds or thousands of certificates in a single API call, monitor progress in real-time, and manage results at scale.
Overview
The Bulk Operations API enables you to:
- Create multiple certificates from a single CSV file
- Upload images in bulk via ZIP files
- Monitor job progress in real-time
- Retry failed operations automatically
- Download results in multiple formats
- Track job history and analytics
Use Cases
- Educational Institutions: Issue certificates to graduating classes
- Corporate Training: Distribute credentials to course completers
- Events: Generate badges for conference attendees
- Compliance: Create audit certificates for multiple entities
- Rewards Programs: Mint achievement NFTs for program participants
Key Features
✓ Asynchronous Processing - Jobs process in the background ✓ Progress Tracking - Real-time status updates ✓ Error Handling - Automatic retries and detailed error logs ✓ Result Downloads - CSV reports and ZIP files ✓ Job Management - Cancel, retry, and monitor jobs ✓ CSV Templates - Standardized format for easy batch creation
Authentication
Bulk operations require authentication via API key or JWT token:
http
# Using API Key (Recommended for bulk operations)
POST /api/bulk-mint
x-api-key: pk_0000000000000000000000000000000000000000000000000000000000000001
# Using JWT Token
POST /api/bulk-mint
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...Required Scopes (for API keys):
bulk:mint- Create and manage bulk mint jobsbulk:retry- Retry failed jobs
CSV Format Specification
Required Columns
| Column | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Certificate name/title |
description | string | Yes | Certificate description |
imageUrl | string | Conditional* | URL to certificate image |
category | string | Yes | Certificate category |
recipientEmail | string | Yes | Recipient's email address |
*Required if not providing images ZIP file
Optional Columns
| Column | Type | Description |
|---|---|---|
recipientName | string | Recipient's full name |
attribute_* | string | Custom attributes (e.g., attribute_Course, attribute_Level) |
expiryDays | number | Days until voucher expires (default: 30) |
Example CSV
csv
name,description,imageUrl,category,recipientEmail,recipientName,attribute_Course,attribute_Grade
Web Development Certificate,Completed advanced web development course,https://cdn.example.com/certs/web-dev-001.png,education,john.doe@example.com,John Doe,Web Development 101,A+
Data Science Certificate,Completed data science fundamentals,https://cdn.example.com/certs/data-sci-001.png,education,jane.smith@example.com,Jane Smith,Data Science Fundamentals,A
Cloud Architecture Certificate,Certified cloud architect,https://cdn.example.com/certs/cloud-001.png,professional,bob.wilson@example.com,Bob Wilson,Cloud Architecture,PassCSV Best Practices
- UTF-8 Encoding: Always save CSV files with UTF-8 encoding
- No Empty Rows: Remove any blank rows from your CSV
- Consistent Headers: Use exact column names (case-sensitive)
- Escape Commas: Use quotes for values containing commas:
"Smith, John" - Validate URLs: Ensure all imageUrl values are accessible
- Email Format: Use valid email addresses for recipientEmail
- File Size: Keep individual CSV files under 10MB for optimal processing
Create Bulk Mint Job
Create a new bulk minting job to process multiple certificates.
Endpoint
http
POST /api/bulk-mintRequest Format
Content-Type: multipart/form-data
| Field | Type | Required | Description |
|---|---|---|---|
metadata | File (CSV) | Yes | CSV file with certificate data |
images | File (ZIP) | No | ZIP file containing images (optional if imageUrl provided) |
createBatch | Boolean | No | Create batch for collective management |
Code Example
javascript
const FormData = require('form-data');
const fs = require('fs');
async function createBulkMintJob() {
const form = new FormData();
// Add CSV file
form.append('metadata', fs.createReadStream('./certificates.csv'));
// Optionally add images ZIP
form.append('images', fs.createReadStream('./images.zip'));
// Optional: Create batch
form.append('createBatch', 'true');
const response = await fetch('https://api.pocketbook.studio/api/bulk-mint', {
method: 'POST',
headers: {
'x-api-key': process.env.POCKETBOOK_API_KEY,
...form.getHeaders()
},
body: form
});
const result = await response.json();
console.log('Job created:', result.data);
return result.data.jobId;
}python
import requests
def create_bulk_mint_job():
url = 'https://api.pocketbook.studio/api/bulk-mint'
files = {
'metadata': open('certificates.csv', 'rb'),
# Optional: include images
# 'images': open('images.zip', 'rb'),
}
data = {
'createBatch': 'true' # Optional
}
headers = {
'x-api-key': 'pk_your_api_key_here'
}
response = requests.post(url, files=files, data=data, headers=headers)
result = response.json()
print(f"Job created: {result['data']['jobId']}")
return result['data']['jobId']bash
curl -X POST https://api.pocketbook.studio/api/bulk-mint \
-H "x-api-key: pk_your_api_key_here" \
-F "metadata=@certificates.csv" \
-F "images=@images.zip" \
-F "createBatch=true"Response
json
{
"success": true,
"data": {
"jobId": "507f1f77bcf86cd799439013",
"status": "queued",
"totalCount": 100,
"processedCount": 0,
"successCount": 0,
"failedCount": 0,
"createdAt": "2025-01-15T10:00:00Z"
}
}Job Status Values
| Status | Description |
|---|---|
queued | Job is waiting to be processed |
processing | Job is currently being processed |
completed | Job completed successfully |
failed | Job failed (check error details) |
cancelled | Job was cancelled by user |
Monitor Job Status
Track the progress of your bulk minting job in real-time.
Endpoint
http
GET /api/bulk-mint/:jobIdCode Example
javascript
async function pollJobStatus(jobId) {
let status = 'queued';
while (status === 'queued' || status === 'processing') {
const response = await fetch(
`https://api.pocketbook.studio/api/bulk-mint/${jobId}`,
{
headers: {
'x-api-key': process.env.POCKETBOOK_API_KEY
}
}
);
const result = await response.json();
const job = result.data;
status = job.status;
console.log(`Progress: ${job.processedCount}/${job.totalCount}`);
console.log(`Success: ${job.successCount}, Failed: ${job.failedCount}`);
if (status === 'completed') {
console.log('Job completed successfully!');
return job;
} else if (status === 'failed') {
console.error('Job failed:', job.error);
throw new Error(job.error);
}
// Wait 5 seconds before next poll
await new Promise(resolve => setTimeout(resolve, 5000));
}
}python
import requests
import time
def poll_job_status(job_id):
url = f'https://api.pocketbook.studio/api/bulk-mint/{job_id}'
headers = {'x-api-key': 'pk_your_api_key_here'}
status = 'queued'
while status in ['queued', 'processing']:
response = requests.get(url, headers=headers)
result = response.json()
job = result['data']
status = job['status']
print(f"Progress: {job['processedCount']}/{job['totalCount']}")
print(f"Success: {job['successCount']}, Failed: {job['failedCount']}")
if status == 'completed':
print('Job completed successfully!')
return job
elif status == 'failed':
print(f"Job failed: {job.get('error', 'Unknown error')}")
raise Exception(job.get('error', 'Job failed'))
# Wait 5 seconds before next poll
time.sleep(5)Response
json
{
"success": true,
"data": {
"jobId": "507f1f77bcf86cd799439013",
"status": "processing",
"totalCount": 100,
"processedCount": 45,
"successCount": 43,
"failedCount": 2,
"createdAt": "2025-01-15T10:00:00Z",
"updatedAt": "2025-01-15T10:15:00Z",
"estimatedCompletionTime": "2025-01-15T10:30:00Z",
"errors": [
{
"row": 12,
"error": "Invalid image URL"
},
{
"row": 34,
"error": "Invalid email format"
}
]
}
}List Bulk Jobs
Retrieve a paginated list of all bulk minting jobs for your account.
Endpoint
http
GET /api/bulk-mintQuery Parameters
| Parameter | Type | Description | Default |
|---|---|---|---|
status | string | Filter by status (queued, processing, completed, failed) | All |
page | number | Page number | 1 |
limit | number | Items per page (max: 100) | 20 |
Code Example
javascript
async function listBulkJobs(status = null, page = 1, limit = 20) {
const params = new URLSearchParams({
page: page.toString(),
limit: limit.toString()
});
if (status) {
params.append('status', status);
}
const response = await fetch(
`https://api.pocketbook.studio/api/bulk-mint?${params}`,
{
headers: {
'x-api-key': process.env.POCKETBOOK_API_KEY
}
}
);
return await response.json();
}
// Example: Get all completed jobs
const completedJobs = await listBulkJobs('completed');Response
json
{
"success": true,
"data": {
"jobs": [
{
"jobId": "507f1f77bcf86cd799439013",
"status": "completed",
"totalCount": 100,
"successCount": 98,
"failedCount": 2,
"createdAt": "2025-01-15T10:00:00Z",
"completedAt": "2025-01-15T10:30:00Z"
},
{
"jobId": "507f1f77bcf86cd799439014",
"status": "processing",
"totalCount": 250,
"processedCount": 120,
"createdAt": "2025-01-15T11:00:00Z"
}
],
"pagination": {
"page": 1,
"limit": 20,
"totalPages": 5,
"totalItems": 98
}
}
}Retry Failed Job
Retry a failed bulk minting job to process any certificates that didn't complete successfully.
Endpoint
http
POST /api/bulk-mint/:jobId/retryCode Example
javascript
async function retryFailedJob(jobId) {
const response = await fetch(
`https://api.pocketbook.studio/api/bulk-mint/${jobId}/retry`,
{
method: 'POST',
headers: {
'x-api-key': process.env.POCKETBOOK_API_KEY
}
}
);
const result = await response.json();
console.log('Job retry initiated:', result.data);
return result.data;
}Response
json
{
"success": true,
"data": {
"jobId": "507f1f77bcf86cd799439013",
"status": "queued",
"retriedCount": 2,
"message": "Job retry initiated for 2 failed certificates"
}
}Retry Behavior
- Automatic Retry: Failed items are automatically retried once
- Manual Retry: Use this endpoint to retry again manually
- Idempotency: Safe to call multiple times
- Only Failed Items: Only processes certificates that failed previously
Cancel Job
Cancel a queued or processing bulk minting job.
Endpoint
http
POST /api/bulk-mint/:jobId/cancelCode Example
javascript
async function cancelJob(jobId) {
const response = await fetch(
`https://api.pocketbook.studio/api/bulk-mint/${jobId}/cancel`,
{
method: 'POST',
headers: {
'x-api-key': process.env.POCKETBOOK_API_KEY
}
}
);
return await response.json();
}Response
json
{
"success": true,
"data": {
"jobId": "507f1f77bcf86cd799439013",
"status": "cancelled",
"message": "Job cancelled successfully",
"processedCount": 45,
"totalCount": 100
}
}Important Notes
- Cancellation may not be immediate for jobs already processing
- Completed certificates will not be rolled back
- You can still download results for processed certificates
Download Results
Download job results in CSV or ZIP format containing vouchers and certificates.
Endpoint
http
GET /api/bulk-mint/:jobId/download/:typeDownload Types
| Type | Description | Content |
|---|---|---|
csv | CSV file | Certificate details, voucher references, status |
zip | ZIP archive | All certificate images and metadata files |
Code Example
javascript
const fs = require('fs');
async function downloadResults(jobId, type = 'csv') {
const response = await fetch(
`https://api.pocketbook.studio/api/bulk-mint/${jobId}/download/${type}`,
{
headers: {
'x-api-key': process.env.POCKETBOOK_API_KEY
}
}
);
if (!response.ok) {
throw new Error(`Download failed: ${response.statusText}`);
}
// Save to file
const buffer = await response.arrayBuffer();
const filename = type === 'csv' ? `job-${jobId}.csv` : `job-${jobId}.zip`;
fs.writeFileSync(filename, Buffer.from(buffer));
console.log(`Results saved to ${filename}`);
}
// Download CSV
await downloadResults('507f1f77bcf86cd799439013', 'csv');
// Download ZIP
await downloadResults('507f1f77bcf86cd799439013', 'zip');python
import requests
def download_results(job_id, download_type='csv'):
url = f'https://api.pocketbook.studio/api/bulk-mint/{job_id}/download/{download_type}'
headers = {'x-api-key': 'pk_your_api_key_here'}
response = requests.get(url, headers=headers, stream=True)
if response.status_code == 200:
filename = f"job-{job_id}.{download_type}"
with open(filename, 'wb') as f:
for chunk in response.iter_content(chunk_size=8192):
f.write(chunk)
print(f"Results saved to {filename}")
else:
raise Exception(f"Download failed: {response.status_code}")
# Download CSV
download_results('507f1f77bcf86cd799439013', 'csv')
# Download ZIP
download_results('507f1f77bcf86cd799439013', 'zip')CSV Output Format
The downloaded CSV contains the following columns:
csv
row,name,status,certificateId,voucherId,referenceNumber,recipientEmail,error
1,Web Development Certificate,success,507f1f77bcf86cd799439011,507f1f77bcf86cd799439012,PB-ABC123,john@example.com,
2,Data Science Certificate,success,507f1f77bcf86cd799439015,507f1f77bcf86cd799439016,PB-ABC124,jane@example.com,
3,Cloud Certificate,failed,,,,,bob@example.com,Invalid image URLGet CSV Template
Download a template CSV file with the correct format and example data.
Endpoint
http
GET /api/bulk-mint/template/csvNo authentication required
Code Example
javascript
async function downloadTemplate() {
const response = await fetch(
'https://api.pocketbook.studio/api/bulk-mint/template/csv'
);
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'pocketbook-bulk-mint-template.csv';
a.click();
}Template Content
csv
name,description,imageUrl,category,recipientEmail,recipientName,attribute_Course,attribute_Level
Example Certificate,This is an example certificate description,https://example.com/certificate.png,education,recipient@example.com,John Doe,Introduction to Blockchain,BeginnerComplete Workflow Example
Here's a complete example showing the full bulk minting workflow:
javascript
const FormData = require('form-data');
const fs = require('fs');
class BulkMintClient {
constructor(apiKey) {
this.apiKey = apiKey;
this.baseUrl = 'https://api.pocketbook.studio/api';
}
async createJob(csvPath, imagesZipPath = null) {
const form = new FormData();
form.append('metadata', fs.createReadStream(csvPath));
if (imagesZipPath) {
form.append('images', fs.createReadStream(imagesZipPath));
}
const response = await fetch(`${this.baseUrl}/bulk-mint`, {
method: 'POST',
headers: {
'x-api-key': this.apiKey,
...form.getHeaders()
},
body: form
});
const result = await response.json();
return result.data.jobId;
}
async waitForCompletion(jobId, pollInterval = 5000) {
console.log(`Waiting for job ${jobId} to complete...`);
let status = 'queued';
while (status === 'queued' || status === 'processing') {
await new Promise(resolve => setTimeout(resolve, pollInterval));
const response = await fetch(`${this.baseUrl}/bulk-mint/${jobId}`, {
headers: { 'x-api-key': this.apiKey }
});
const result = await response.json();
const job = result.data;
status = job.status;
const progress = ((job.processedCount / job.totalCount) * 100).toFixed(1);
console.log(`Progress: ${progress}% (${job.processedCount}/${job.totalCount})`);
console.log(`Success: ${job.successCount}, Failed: ${job.failedCount}`);
if (status === 'completed') {
console.log('✓ Job completed successfully!');
return job;
} else if (status === 'failed') {
console.error('✗ Job failed:', job.error);
throw new Error(job.error);
}
}
}
async downloadResults(jobId, type = 'csv') {
const response = await fetch(
`${this.baseUrl}/bulk-mint/${jobId}/download/${type}`,
{
headers: { 'x-api-key': this.apiKey }
}
);
const buffer = await response.arrayBuffer();
const filename = `results-${jobId}.${type}`;
fs.writeFileSync(filename, Buffer.from(buffer));
console.log(`✓ Results saved to ${filename}`);
}
async retryFailed(jobId) {
const response = await fetch(
`${this.baseUrl}/bulk-mint/${jobId}/retry`,
{
method: 'POST',
headers: { 'x-api-key': this.apiKey }
}
);
const result = await response.json();
console.log(`✓ Retry initiated for ${result.data.retriedCount} certificates`);
return result.data.jobId;
}
}
// Usage
async function main() {
const client = new BulkMintClient(process.env.POCKETBOOK_API_KEY);
try {
// Step 1: Create bulk mint job
console.log('Creating bulk mint job...');
const jobId = await client.createJob('./certificates.csv', './images.zip');
console.log(`✓ Job created: ${jobId}`);
// Step 2: Wait for completion
const job = await client.waitForCompletion(jobId);
// Step 3: Handle failures
if (job.failedCount > 0) {
console.log(`\nRetrying ${job.failedCount} failed certificates...`);
const retryJobId = await client.retryFailed(jobId);
await client.waitForCompletion(retryJobId);
}
// Step 4: Download results
console.log('\nDownloading results...');
await client.downloadResults(jobId, 'csv');
await client.downloadResults(jobId, 'zip');
console.log('\n✓ Bulk minting completed successfully!');
} catch (error) {
console.error('✗ Error:', error.message);
process.exit(1);
}
}
main();Best Practices
Performance Optimization
Batch Size
- Optimal: 100-500 certificates per batch
- Maximum: 1,000 certificates per batch
- For larger volumes, split into multiple jobs
Image Optimization
- Use CDN-hosted images when possible
- Compress images before uploading (recommended: under 1MB each)
- Use consistent image dimensions across batch
Polling Strategy
javascript// Adaptive polling based on job size function getPollingInterval(totalCount) { if (totalCount < 100) return 2000; // 2 seconds if (totalCount < 500) return 5000; // 5 seconds return 10000; // 10 seconds }
Error Handling
Validate CSV Before Upload
javascriptfunction validateCSV(csvContent) { const lines = csvContent.split('\n'); const headers = lines[0].split(','); // Check required columns const required = ['name', 'description', 'imageUrl', 'category', 'recipientEmail']; const missing = required.filter(col => !headers.includes(col)); if (missing.length > 0) { throw new Error(`Missing required columns: ${missing.join(', ')}`); } // Validate each row for (let i = 1; i < lines.length; i++) { const values = lines[i].split(','); if (values.length !== headers.length) { throw new Error(`Row ${i + 1}: Column count mismatch`); } } return true; }Handle Network Errors
javascriptasync function robustFetch(url, options, maxRetries = 3) { for (let i = 0; i < maxRetries; i++) { try { const response = await fetch(url, options); if (response.ok) return response; // Retry on 5xx errors if (response.status >= 500) { await new Promise(r => setTimeout(r, 1000 * Math.pow(2, i))); continue; } throw new Error(`HTTP ${response.status}`); } catch (error) { if (i === maxRetries - 1) throw error; await new Promise(r => setTimeout(r, 1000 * Math.pow(2, i))); } } }
Security Best Practices
API Key Management
- Store API keys in environment variables
- Never commit API keys to version control
- Rotate keys periodically
- Use separate keys for production and development
Rate Limiting
javascriptclass RateLimiter { constructor(maxRequests, windowMs) { this.maxRequests = maxRequests; this.windowMs = windowMs; this.requests = []; } async throttle() { const now = Date.now(); this.requests = this.requests.filter(t => now - t < this.windowMs); if (this.requests.length >= this.maxRequests) { const oldestRequest = Math.min(...this.requests); const waitTime = this.windowMs - (now - oldestRequest); await new Promise(r => setTimeout(r, waitTime)); } this.requests.push(Date.now()); } } // Usage const limiter = new RateLimiter(10, 60000); // 10 requests per minute await limiter.throttle();
Data Management
Backup CSV Files
javascriptfunction backupCSV(csvPath) { const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const backupPath = `${csvPath}.backup.${timestamp}`; fs.copyFileSync(csvPath, backupPath); console.log(`Backup created: ${backupPath}`); }Track Job History
javascriptclass JobTracker { constructor(dbPath) { this.jobs = new Map(); } addJob(jobId, metadata) { this.jobs.set(jobId, { ...metadata, startTime: Date.now(), status: 'queued' }); } updateJob(jobId, updates) { const job = this.jobs.get(jobId); if (job) { this.jobs.set(jobId, { ...job, ...updates }); } } getJobStats() { const jobs = Array.from(this.jobs.values()); return { total: jobs.length, completed: jobs.filter(j => j.status === 'completed').length, failed: jobs.filter(j => j.status === 'failed').length, processing: jobs.filter(j => j.status === 'processing').length }; } }
Monitoring and Logging
Structured Logging
javascriptclass Logger { log(level, message, metadata = {}) { console.log(JSON.stringify({ timestamp: new Date().toISOString(), level, message, ...metadata })); } info(message, metadata) { this.log('INFO', message, metadata); } error(message, metadata) { this.log('ERROR', message, metadata); } } const logger = new Logger(); logger.info('Job started', { jobId, totalCount: 100 });Progress Notifications
javascriptasync function monitorWithNotifications(jobId, onProgress) { let lastProgress = 0; while (true) { const job = await getJobStatus(jobId); const progress = (job.processedCount / job.totalCount) * 100; // Notify on every 10% progress if (Math.floor(progress / 10) > Math.floor(lastProgress / 10)) { onProgress(progress, job); } lastProgress = progress; if (job.status === 'completed' || job.status === 'failed') { break; } await new Promise(r => setTimeout(r, 5000)); } }
Troubleshooting
Common Issues
Issue: Job Stuck in "Queued" Status
Possible Causes:
- High system load
- Large job queue
- CSV validation errors
Solutions:
javascript
// Check job details for errors
const job = await getJobStatus(jobId);
if (job.errors && job.errors.length > 0) {
console.log('Validation errors:', job.errors);
}
// If stuck for > 10 minutes, contact supportIssue: High Failure Rate
Possible Causes:
- Invalid image URLs
- Incorrect email formats
- Missing required fields
Solutions:
javascript
// Download results CSV to see specific errors
await downloadResults(jobId, 'csv');
// Fix errors and retry
await retryFailedJob(jobId);Issue: Slow Processing
Possible Causes:
- Large image files
- Network latency
- External image hosting issues
Solutions:
- Compress images before upload
- Use CDN for image hosting
- Include images in ZIP file instead of URLs
Getting Help
If you encounter issues:
- Check job errors:
GET /api/bulk-mint/:jobId - Download error report:
GET /api/bulk-mint/:jobId/download/csv - Review API Status
- Contact support: seth@pocketbook.studio
Rate Limits
| Operation | Limit | Window |
|---|---|---|
| Create Job | 10 jobs | 1 hour |
| Poll Status | 100 requests | 15 minutes |
| Download Results | 50 downloads | 1 hour |
| Retry Job | 5 retries | 1 hour |
Enterprise customers can request higher limits.
Pricing
Bulk operations follow standard certificate pricing:
- Cost per certificate: Based on your pricing plan
- No bulk operation fees: Pay only for certificates created
- Failed certificates: Not charged
- Volume discounts: Available for enterprise customers
Use the cost preview endpoint to estimate costs:
javascript
const preview = await fetch(
'https://api.pocketbook.studio/api/billing/cost-preview/1000',
{
headers: { 'x-api-key': process.env.POCKETBOOK_API_KEY }
}
);
console.log('Cost for 1000 certificates:', await preview.json());Related Resources
- API Overview - General API information
- Authentication Guide - Authentication methods
- Webhooks - Real-time notifications
- Error Handling - Error codes and solutions
Next Steps
Ready to start bulk minting? Here's what to do next:
- Get API Key: Visit your enterprise dashboard
- Download Template: Use the CSV template endpoint
- Prepare Data: Fill in your certificate data
- Test Small Batch: Start with 5-10 certificates
- Scale Up: Gradually increase batch sizes
Need help? Join our Discord community or email seth@pocketbook.studio
