Skip to content

NetSuite Integration

Syncs Volāre business records (clients, suppliers, bookings, payments) to NetSuite ERP via the REST API, using queue-based jobs with automatic retry and a polymorphic sync tracking system.

  • Sync client, supplier, booking, and payment data to NetSuite for accounting
  • Monitor sync status and retry failures from the Filament admin panel
  • Reconcile local records against NetSuite to detect drift
  • Create service items in NetSuite for booking upsell line items
Model (with HasNetsuiteSync trait)
-> Sync Job dispatched to `netsuite-sync` queue
-> AbstractNetsuiteSyncJob.handle()
-> NetsuiteClient (REST requests with retry + 401 re-auth)
-> NetsuiteAuthenticationService (OAuth 2.0 JWT assertion, cached token)
-> NetsuiteSync record updated (Pending -> Syncing -> Synced/Failed)

Key components:

  • NetsuiteAuthenticationService – OAuth 2.0 client_credentials flow with EC256 JWT assertion. Tokens are cached with a 5-minute buffer before expiry.
  • NetsuiteClient – REST client with configurable timeout, retry (with backoff), automatic 401 re-authentication, typed error handling (429 rate limit, 400/422 validation, 5xx server errors), and structured logging with credential sanitization.
  • NetsuiteSyncRegistry – Maps model types to their sync job class and NetSuite REST endpoint. Used by Artisan commands to dispatch jobs generically.
  • NetsuiteSync model – Polymorphic sync tracking. Stores netsuite_id, external_id, status, payload, attempts, and error.
  • HasNetsuiteSync trait – Applied to Booking, Client, Payment, Supplier. Provides netsuiteSync() relation, getOrCreateNetsuiteSync(), and needsNetsuiteSync().

Source: backend/app/Services/Netsuite/

Variable Description
NETSUITE_ENVIRONMENT sandbox or production (default: sandbox)
NETSUITE_ACCOUNT_ID NetSuite account ID (underscores replaced with hyphens for URLs)
NETSUITE_CLIENT_ID OAuth 2.0 client ID from NetSuite Integration record
NETSUITE_CLIENT_SECRET OAuth 2.0 client secret
NETSUITE_CERTIFICATE_ID Certificate ID for JWT signing
NETSUITE_PRIVATE_KEY_PATH Path to EC256 private key (default: storage/netsuite/private.pem)
Variable Default Description
NETSUITE_REST_URL Auto-derived from account ID Override REST API base URL
NETSUITE_TOKEN_URL Auto-derived from account ID Override token endpoint
NETSUITE_SANDBOX_TIMEOUT 30 Request timeout (seconds)
NETSUITE_SANDBOX_RETRY_ATTEMPTS 3 Max retry attempts
NETSUITE_SANDBOX_RETRY_DELAY 500 Retry delay (ms)
NETSUITE_LOGGING_ENABLED true Enable API request logging
NETSUITE_LOG_CHANNEL single Log channel
NETSUITE_QUEUE netsuite-sync Queue name for sync jobs
NETSUITE_SUBSIDIARY_ID 1 NetSuite subsidiary internal ID
NETSUITE_ITEM_FLIGHT Service item ID for flight line items
NETSUITE_ITEM_HOTEL Service item ID for hotel line items
NETSUITE_ITEM_ACTIVITY Service item ID for activity line items
NETSUITE_ITEM_TRANSFER Service item ID for transfer line items
NETSUITE_ITEM_INSURANCE Service item ID for insurance line items
NETSUITE_ITEM_FLIGHT_UPGRADE Service item ID for flight upgrade line items

Config file: backend/config/netsuite.php

Volāre Model NetSuite Record REST Endpoint Sync Job
Client Customer /record/v1/customer SyncClientToNetsuiteJob
Supplier Vendor /record/v1/vendor SyncSupplierToNetsuiteJob
Booking Invoice /record/v1/invoice SyncBookingToNetsuiteJob
Payment CustomerPayment /record/v1/customerPayment SyncPaymentToNetsuiteJob
Booking upsells VendorBill /record/v1/vendorBill SyncVendorBillToNetsuiteJob

The registry (NetsuiteSyncRegistry) covers the first four. SyncVendorBillToNetsuiteJob is standalone – it groups a booking’s upsells by supplier and creates one Vendor Bill per supplier, using contract cost prices when available.

Source: backend/app/Services/Netsuite/NetsuiteSyncRegistry.php

Tracked by NetsuiteSyncStatus enum:

  • Pending – Awaiting sync (initial state, or reset for retry)
  • Syncing – Job is currently executing
  • Synced – Successfully synced, netsuite_id is populated
  • Failed – Sync failed, error field has details
  • Skipped – Intentionally excluded from sync

Source: backend/app/Enums/NetsuiteSyncStatus.php

  • Dependency ordering: Client must be synced before its Booking or Payment. Supplier must be synced before its VendorBill. Jobs throw RuntimeException if the dependency is missing.
  • Idempotency: Each model gets a deterministic externalId (volare_{table}_{id}). POST creates new records, PUT updates existing ones (when netsuite_id is already set).
  • Retry: Jobs retry up to 3 times with backoff (30s, 60s, 180s). After all retries, the sync record is marked Failed. Non-retryable errors (e.g., validation) fail immediately without retrying.
  • Rate limiting: 429 responses release the job back to the queue with the Retry-After delay (default 60s). The sync record stays Pending during the wait.
  • Currency mapping: ISO codes mapped to NetSuite internal IDs in config/netsuite.php (currencies key). Currently: EUR, USD, CAD, GBP.
  • Invoice line items: Built from BookingUpsell records, mapped to NetSuite service items by upsell type. Falls back to a single “Flight” line if no upsells exist.
  • Vendor Bill cost: Looks up SupplierContractService price from the active contract valid at booking date.

Dispatch sync jobs for all unsynced records.

Terminal window
vendor/bin/sail artisan netsuite:sync-all
vendor/bin/sail artisan netsuite:sync-all --model=client --limit=100
vendor/bin/sail artisan netsuite:sync-all --force # re-sync already synced records

Re-dispatch jobs for failed or pending sync records.

Terminal window
vendor/bin/sail artisan netsuite:retry-failed
vendor/bin/sail artisan netsuite:retry-failed --status=pending --model=booking --limit=50
vendor/bin/sail artisan netsuite:retry-failed --status=all

Compare local sync records against NetSuite. Reports verified, missing, and errored records.

Terminal window
vendor/bin/sail artisan netsuite:reconcile
vendor/bin/sail artisan netsuite:reconcile --fix # reset stuck/failed records

The --fix flag resets “syncing” records older than 1 hour to Failed, then resets all Failed records to Pending.

Create service items in NetSuite for each upsell type (flight, hotel, activity, transfer, insurance, flight_upgrade). Outputs the NETSUITE_ITEM_* env vars to add to .env.

Terminal window
vendor/bin/sail artisan netsuite:sync-service-items

Source: backend/app/Console/Commands/

Admin-only page under System > NetSuite Sync. Shows a table of all NetsuiteSync records with:

  • Filters by status and model type
  • Retry action on failed records (resets to Pending)
  • Auto-polls every 30 seconds

Stats overview (total, synced, failed, pending) displayed as header widget on the monitor page.

Source: backend/app/Filament/Pages/NetsuiteSyncMonitor.php, backend/app/Filament/Widgets/NetsuiteSyncStatsWidget.php

  1. Add the trait to your model: use HasNetsuiteSync;
  2. Create a sync job extending AbstractNetsuiteSyncJob. Implement buildPayload(), getEndpoint(), and getRecordType().
  3. Register in NetsuiteSyncRegistry::MODELS with the model class, job class, and NetSuite endpoint.
  4. Run netsuite:sync-all --model=yourmodel to dispatch.

Source: backend/app/Jobs/Netsuite/AbstractNetsuiteSyncJob.php for the base job contract.

All exceptions extend NetsuiteException and implement isRetryable():

Exception HTTP Status Retryable Description
NetsuiteException any No Base exception for all NetSuite errors
NetsuiteAuthenticationException 401 Yes OAuth token expired or credentials invalid
NetsuiteTimeoutException Yes Connection timeout
NetsuiteRateLimitException 429 Yes Rate limit exceeded, carries retryAfter delay
NetsuiteValidationException 400/422 No Payload validation failed, carries validationErrors

Source: backend/app/Exceptions/Services/Netsuite/

Symptom Cause Fix
InvalidArgumentException: NetSuite account ID is not configured Missing env vars Set NETSUITE_ACCOUNT_ID, NETSUITE_CLIENT_ID, NETSUITE_CERTIFICATE_ID
NetsuiteAuthenticationException: Private key file not found Key not deployed Place EC256 private key at NETSUITE_PRIVATE_KEY_PATH (default: storage/netsuite/private.pem)
NetsuiteValidationException on sync Invalid payload data Check the error field on the sync record for NetSuite validation details
NetsuiteRateLimitException Too many API calls Job auto-retries after delay; reduce batch size in netsuite:sync-all --limit
RuntimeException: Client must be synced before booking Dependency not synced Sync clients first: netsuite:sync-all --model=client, then bookings
Sync stuck in “Syncing” state Job crashed mid-execution Run netsuite:reconcile --fix to reset stuck records
401 after token refresh Certificate/credentials rotated in NetSuite Update NETSUITE_CLIENT_ID, NETSUITE_CLIENT_SECRET, NETSUITE_CERTIFICATE_ID in .env
  • Config: backend/config/netsuite.php
  • Services: backend/app/Services/Netsuite/
  • Jobs: backend/app/Jobs/Netsuite/
  • Model: backend/app/Models/NetsuiteSync.php
  • Trait: backend/app/Traits/HasNetsuiteSync.php
  • Enum: backend/app/Enums/NetsuiteSyncStatus.php
  • Commands: backend/app/Console/Commands/Netsuite*.php
  • Exceptions: backend/app/Exceptions/Services/Netsuite/