Skip to main contentCertyo Developer Portal

Oracle NetSuite Integration

Connect Oracle NetSuite to Certyo using RESTlets, User Event Scripts, and the SuiteTalk REST API. Automatically anchor item fulfillment data, inventory movements, and sales order records to the blockchain.

Prerequisites

  • Oracle NetSuite account with SuiteCloud enabled (Administrator or Developer role)
  • Token-Based Authentication (TBA) configured in Setup > Company > Enable Features > SuiteCloud > Manage Authentication
  • A Certyo API key — see the Authentication Guide
  • SuiteScript 2.x runtime (NetSuite 2020.1+)

Architecture

Integration flowbash
┌─────────────────────────┐
│   Oracle NetSuite       │
│                         │
│  Item Fulfillment       │
│  record saved           │
│         │               │
│         v               │
│  User Event Script      │
│  (afterSubmit)          │
│         │               │
│         v               │
│  RESTlet / N/https      │──────────┐
│  module                 │          │
└─────────────────────────┘          │
                                     │  HTTPS POST
                                     │  X-API-Key header
                                     v
                          ┌─────────────────────┐
                          │  Certyo API          │
                          │  POST /api/v1/records│
                          │                      │
                          │  202 Accepted        │
                          └──────────┬───────────┘
                                     │
                                     v
                          ┌─────────────────────┐
                          │  Pipeline            │
                          │  Kafka -> Accumulate │
                          │  -> Merkle Tree      │
                          │  -> IPFS -> Polygon  │
                          └─────────────────────┘

Authentication setup

NetSuite uses Token-Based Authentication (TBA) for server-to-server access. You need four credentials from Setup > Integration > Manage Integrations:

  • Consumer Key / Consumer SecretConsumer Key and Consumer Secret (from the integration record)
  • Token ID / Token SecretToken ID and Token Secret (from the access token assigned to a role)

For the Certyo side, store your API key as a script parameter or in a custom configuration record — never hard-code it in SuiteScript source files.

Field mapping

NetSuite FieldCertyo FieldNotes
tranId (Item Fulfillment)recordIdUnique fulfillment number, e.g. IF-10042
subsidiary.nametenantIdMaps to your Certyo tenant
"netsuite"databaseStatic value identifying the source system
"item_fulfillments"collectionTransaction type as collection name
tranDatesourceTimestampConvert to ISO 8601 (e.g. 2026-04-14T00:00:00Z)
item[].itemId, quantity, serialNumbersrecordPayloadFulfillment line details
createdDate + tranIdidempotencyKeyPrevents duplicate ingestion on retries
"1"recordVersionIncrement on subsequent updates
"insert"operationTypeUse "update" for modified fulfillments

Implementation

RESTlet concurrency limit
NetSuite allows a maximum of 10 concurrent RESTlet executions per account. For bulk operations (initial backfill, mass imports), use a SuiteScript Map/Reduce script instead of individual RESTlet calls.

Deploy this RESTlet to receive fulfillment data from external systems or internal SuiteScript calls, and forward it to Certyo.

certyo_restlet.js — SuiteScript 2.x RESTletjavascript
/**
 * @NApiVersion 2.1
 * @NScriptType Restlet
 * @NModuleScope SameAccount
 *
 * Certyo Integration RESTlet
 * Accepts item fulfillment data and ingests it into Certyo for
 * blockchain anchoring.
 *
 * Deploy: Setup > Scripting > Scripts > New > RESTlet
 * URL:    /app/site/hosting/restlet.nl?script=<id>&deploy=1
 */
define(['N/https', 'N/log', 'N/runtime'], function (https, log, runtime) {

  var CERTYO_API_URL = 'https://www.certyos.com/api/v1/records';

  /**
   * POST handler — accepts a fulfillment payload and sends it to Certyo.
   * @param {Object} requestBody - The fulfillment data from the caller
   * @returns {Object} Certyo API response or error details
   */
  function post(requestBody) {
    var scriptObj = runtime.getCurrentScript();
    var apiKey = scriptObj.getParameter({ name: 'custscript_certyo_api_key' });

    if (!apiKey) {
      log.error('Configuration Error', 'custscript_certyo_api_key parameter is not set');
      return { success: false, error: 'API key not configured' };
    }

    try {
      var certRecord = {
        tenantId:        requestBody.subsidiary || 'default',
        database:        'netsuite',
        collection:      'item_fulfillments',
        recordId:        requestBody.tranId,
        recordVersion:   String(requestBody.version || '1'),
        operationType:   requestBody.operationType || 'insert',
        recordPayload: {
          tranId:          requestBody.tranId,
          entity:          requestBody.entity,
          entityName:      requestBody.entityName,
          tranDate:        requestBody.tranDate,
          shipMethod:      requestBody.shipMethod,
          trackingNumbers: requestBody.trackingNumbers || [],
          items:           requestBody.items || [],
          memo:            requestBody.memo || '',
          location:        requestBody.location,
          createdFrom:     requestBody.createdFrom
        },
        sourceTimestamp:  requestBody.tranDate
          ? new Date(requestBody.tranDate).toISOString()
          : new Date().toISOString(),
        idempotencyKey:   requestBody.tranId + '-v' +
                          (requestBody.version || '1') + '-' +
                          new Date().toISOString().substring(0, 10)
      };

      log.audit('Certyo Ingestion', 'Sending record: ' + certRecord.recordId);

      var response = https.post({
        url: CERTYO_API_URL,
        headers: {
          'X-API-Key':    apiKey,
          'Content-Type': 'application/json',
          'Accept':       'application/json'
        },
        body: JSON.stringify(certRecord)
      });

      var statusCode = response.code;
      var responseBody = JSON.parse(response.body);

      if (statusCode === 202) {
        log.audit('Certyo Ingestion Success', JSON.stringify({
          recordId:   responseBody.recordId,
          recordHash: responseBody.recordHash,
          acceptedAt: responseBody.acceptedAt
        }));
        return {
          success:    true,
          recordHash: responseBody.recordHash,
          acceptedAt: responseBody.acceptedAt
        };
      } else {
        log.error('Certyo Ingestion Failed', 'HTTP ' + statusCode +
                  ': ' + response.body);
        return {
          success:    false,
          statusCode: statusCode,
          error:      responseBody.message || response.body
        };
      }

    } catch (e) {
      log.error('Certyo Ingestion Error', e.message + '\n' + e.stack);
      return { success: false, error: e.message };
    }
  }

  return { post: post };
});

Verification: Scheduled Script

After records are ingested, they take approximately 60-90 seconds to be anchored on Polygon. Deploy a Scheduled Script that runs every 15 minutes to poll Certyo for anchoring status and update a custom field on the Item Fulfillment record.

certyo_ss_verify.js — SuiteScript 2.x Scheduled Scriptjavascript
/**
 * @NApiVersion 2.1
 * @NScriptType ScheduledScript
 * @NModuleScope SameAccount
 *
 * Certyo Verification Poller
 * Runs on a schedule to check anchoring status of pending records
 * and update the Item Fulfillment with verification results.
 *
 * Deploy: Setup > Scripting > Script Deployments
 *   Schedule: Every 15 minutes
 */
define(['N/https', 'N/search', 'N/record', 'N/log', 'N/runtime'],
  function (https, search, record, log, runtime) {

  var CERTYO_VERIFY_URL = 'https://www.certyos.com/api/v1/verify/record';
  var CERTYO_QUERY_URL  = 'https://www.certyos.com/api/v1/records';

  function execute(context) {
    var scriptObj = runtime.getCurrentScript();
    var apiKey = scriptObj.getParameter({ name: 'custscript_certyo_api_key' });
    var tenantId = scriptObj.getParameter({ name: 'custscript_certyo_tenant_id' });

    // Find fulfillments with Pending Certyo status
    var pendingSearch = search.create({
      type: search.Type.ITEM_FULFILLMENT,
      filters: [
        ['custbody_certyo_status', 'is', 'Pending'],
        'AND',
        ['custbody_certyo_record_hash', 'isnotempty', ''],
        'AND',
        ['mainline', 'is', 'T']
      ],
      columns: [
        search.createColumn({ name: 'tranid' }),
        search.createColumn({ name: 'custbody_certyo_record_hash' })
      ]
    });

    var results = [];
    pendingSearch.run().each(function (result) {
      results.push({
        internalId: result.id,
        tranId:     result.getValue('tranid'),
        recordHash: result.getValue('custbody_certyo_record_hash')
      });
      return results.length < 200; // Process up to 200 per run
    });

    log.audit('Certyo Verify', 'Found ' + results.length + ' pending records');

    for (var i = 0; i < results.length; i++) {
      if (runtime.getCurrentScript().getRemainingUsage() < 100) {
        log.audit('Certyo Verify', 'Low governance — stopping at record ' + i);
        break;
      }

      var item = results[i];
      try {
        // Query record status
        var queryResp = https.get({
          url: CERTYO_QUERY_URL +
               '?tenantId=' + encodeURIComponent(tenantId) +
               '&recordId=' + encodeURIComponent(item.tranId),
          headers: { 'X-API-Key': apiKey }
        });

        if (queryResp.code !== 200) continue;

        var queryData = JSON.parse(queryResp.body);
        var snapshot = (queryData.items || [])[0];
        if (!snapshot || snapshot.status !== 'Anchored') continue;

        // Record is anchored — update the fulfillment
        record.submitFields({
          type: record.Type.ITEM_FULFILLMENT,
          id:   item.internalId,
          values: {
            'custbody_certyo_status':       'Anchored',
            'custbody_certyo_polygon_tx':   snapshot.polygonTxHash || '',
            'custbody_certyo_verified_at':  new Date().toISOString()
          },
          options: { enableSourcing: false, ignoreMandatoryFields: true }
        });

        log.audit('Certyo Verified', item.tranId + ' -> Anchored');

      } catch (e) {
        log.error('Certyo Verify Error', item.tranId + ': ' + e.message);
      }
    }
  }

  return { execute: execute };
});

Custom fields setup

Create these custom fields on the Item Fulfillment record type via Customization > Lists, Records, & Fields > Transaction Body Fields:

  • custbody_certyo_record_hashFree-form Text, store only. Holds the SHA-256 hash returned by Certyo on ingestion.
  • custbody_certyo_statusList/Record (custom list with values: Pending, Anchored, Failed). Displayed on the fulfillment form.
  • custbody_certyo_polygon_txFree-form Text, store only. The Polygon transaction hash once anchored.
  • custbody_certyo_verified_atDate/Time, store only. Timestamp of last successful verification.

Script parameters

Add a script parameter to each SuiteScript deployment to securely store the Certyo API key:

  • ID: custscript_certyo_api_key
  • Type: Password (or Free-Form Text if Password is unavailable)
  • Description: Certyo API key for blockchain record anchoring

For the Scheduled Script, also add:

  • ID: custscript_certyo_tenant_id
  • Type: Free-Form Text
  • Description: Certyo tenant identifier for API queries
SuiteQL alternative
If your team prefers querying NetSuite data from an external service rather than deploying SuiteScript, use the SuiteTalk REST API with SuiteQL (shown in the Python tab). This approach avoids NetSuite governance limits and is better suited for large-scale initial backfills.

AI Integration · v1.0.0

AI Integration Skill

Download a skill file that enables AI agents to generate working Oracle NetSuite + Certyo integration code for any language or framework.

v1.0.0
What is this?
A markdown file containing Oracle NetSuite-specific field mappings, authentication setup, code examples, and integration patterns for Certyo. Drop it into your AI agent's context and ask it to generate integration code.

What's inside

  • AuthenticationToken-Based Auth (TBA) and OAuth 2.0 for NetSuite API access
  • ArchitectureItem Fulfillment afterSubmit → N/https module → Certyo API
  • Field mappingFulfillment tranid, trandate, item fields to Certyo record schema
  • Code examplesSuiteScript 2.x RESTlet, User Event Script, Python batch sync with SuiteQL
  • VerificationScheduled Script polling with governance-aware processing
  • ConcurrencyRESTlet limit of 10 and Map/Reduce patterns for bulk operations

How to use

Claude Code

Place the file in your project's .claude/commands/ directory, then use it as a slash command:

# Download the skill file
mkdir -p .claude/commands
curl -o .claude/commands/certyo-netsuite.md \
  https://www.certyos.com/developers/skills/certyo-netsuite-skill.md

# Use it in Claude Code
/certyo-netsuite "Generate a SuiteScript User Event that sends fulfilled items to Certyo"

Cursor / Copilot / Any AI Agent

Add the file to your project root or attach it to a conversation. The AI agent will use the Oracle NetSuite-specific patterns, field mappings, and code examples to generate correct integration code.

# Add to your project
curl -o CERTYO_NETSUITE.md \
  https://www.certyos.com/developers/skills/certyo-netsuite-skill.md

# Then in your AI agent:
"Using the Certyo Oracle NetSuite spec in CERTYO_NETSUITE.md,
 generate a suitescript user event that sends fulfilled items to certyo"

CLAUDE.md Context File

Append the skill file to your project's CLAUDE.md so every Claude conversation has Oracle NetSuite + Certyo context automatically.

# Append to your project's CLAUDE.md
echo "" >> CLAUDE.md
echo "## Certyo Oracle NetSuite Integration" >> CLAUDE.md
cat CERTYO_NETSUITE.md >> CLAUDE.md