Skip to content

Bulk Operations API

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 jobs
  • bulk:retry - Retry failed jobs

CSV Format Specification

Required Columns

ColumnTypeRequiredDescription
namestringYesCertificate name/title
descriptionstringYesCertificate description
imageUrlstringConditional*URL to certificate image
categorystringYesCertificate category
recipientEmailstringYesRecipient's email address

*Required if not providing images ZIP file

Optional Columns

ColumnTypeDescription
recipientNamestringRecipient's full name
attribute_*stringCustom attributes (e.g., attribute_Course, attribute_Level)
expiryDaysnumberDays 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,Pass

CSV Best Practices

  1. UTF-8 Encoding: Always save CSV files with UTF-8 encoding
  2. No Empty Rows: Remove any blank rows from your CSV
  3. Consistent Headers: Use exact column names (case-sensitive)
  4. Escape Commas: Use quotes for values containing commas: "Smith, John"
  5. Validate URLs: Ensure all imageUrl values are accessible
  6. Email Format: Use valid email addresses for recipientEmail
  7. 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-mint

Request Format

Content-Type: multipart/form-data

FieldTypeRequiredDescription
metadataFile (CSV)YesCSV file with certificate data
imagesFile (ZIP)NoZIP file containing images (optional if imageUrl provided)
createBatchBooleanNoCreate 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

StatusDescription
queuedJob is waiting to be processed
processingJob is currently being processed
completedJob completed successfully
failedJob failed (check error details)
cancelledJob was cancelled by user

Monitor Job Status

Track the progress of your bulk minting job in real-time.

Endpoint

http
GET /api/bulk-mint/:jobId

Code 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-mint

Query Parameters

ParameterTypeDescriptionDefault
statusstringFilter by status (queued, processing, completed, failed)All
pagenumberPage number1
limitnumberItems 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/retry

Code 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/cancel

Code 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/:type

Download Types

TypeDescriptionContent
csvCSV fileCertificate details, voucher references, status
zipZIP archiveAll 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 URL

Get CSV Template

Download a template CSV file with the correct format and example data.

Endpoint

http
GET /api/bulk-mint/template/csv

No 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,Beginner

Complete 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

  1. Batch Size

    • Optimal: 100-500 certificates per batch
    • Maximum: 1,000 certificates per batch
    • For larger volumes, split into multiple jobs
  2. Image Optimization

    • Use CDN-hosted images when possible
    • Compress images before uploading (recommended: under 1MB each)
    • Use consistent image dimensions across batch
  3. 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

  1. Validate CSV Before Upload

    javascript
    function 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;
    }
  2. Handle Network Errors

    javascript
    async 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

  1. 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
  2. Rate Limiting

    javascript
    class 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

  1. Backup CSV Files

    javascript
    function backupCSV(csvPath) {
      const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
      const backupPath = `${csvPath}.backup.${timestamp}`;
      fs.copyFileSync(csvPath, backupPath);
      console.log(`Backup created: ${backupPath}`);
    }
  2. Track Job History

    javascript
    class 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

  1. Structured Logging

    javascript
    class 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 });
  2. Progress Notifications

    javascript
    async 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 support

Issue: 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:

  1. Check job errors: GET /api/bulk-mint/:jobId
  2. Download error report: GET /api/bulk-mint/:jobId/download/csv
  3. Review API Status
  4. Contact support: seth@pocketbook.studio

Rate Limits

OperationLimitWindow
Create Job10 jobs1 hour
Poll Status100 requests15 minutes
Download Results50 downloads1 hour
Retry Job5 retries1 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());


Next Steps

Ready to start bulk minting? Here's what to do next:

  1. Get API Key: Visit your enterprise dashboard
  2. Download Template: Use the CSV template endpoint
  3. Prepare Data: Fill in your certificate data
  4. Test Small Batch: Start with 5-10 certificates
  5. Scale Up: Gradually increase batch sizes

Need help? Join our Discord community or email seth@pocketbook.studio

Released under the MIT License.