Skip to content

NetSuite Integration

Syncs Volare 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/

VariableDescription
NETSUITE_ENVIRONMENTsandbox or production (default: sandbox)
NETSUITE_ACCOUNT_IDNetSuite account ID (underscores replaced with hyphens for URLs)
NETSUITE_CLIENT_IDOAuth 2.0 client ID from NetSuite Integration record
NETSUITE_CLIENT_SECRETOAuth 2.0 client secret
NETSUITE_CERTIFICATE_IDCertificate ID for JWT signing
NETSUITE_PRIVATE_KEY_PATHPath to EC256 private key (default: storage/netsuite/private.pem)
VariableDefaultDescription
NETSUITE_REST_URLAuto-derived from account IDOverride REST API base URL
NETSUITE_TOKEN_URLAuto-derived from account IDOverride token endpoint
NETSUITE_SANDBOX_TIMEOUT30Request timeout (seconds)
NETSUITE_SANDBOX_RETRY_ATTEMPTS3Max retry attempts
NETSUITE_SANDBOX_RETRY_DELAY500Retry delay (ms)
NETSUITE_LOGGING_ENABLEDtrueEnable API request logging
NETSUITE_LOG_CHANNELsingleLog channel
NETSUITE_QUEUEnetsuite-syncQueue name for sync jobs
NETSUITE_SUBSIDIARY_ID1NetSuite subsidiary internal ID
NETSUITE_ITEM_FLIGHTService item ID for flight line items
NETSUITE_ITEM_HOTELService item ID for hotel line items
NETSUITE_ITEM_ACTIVITYService item ID for activity line items
NETSUITE_ITEM_TRANSFERService item ID for transfer line items
NETSUITE_ITEM_INSURANCEService item ID for insurance line items
NETSUITE_ITEM_FLIGHT_UPGRADEService item ID for flight upgrade line items

Config file: backend/config/netsuite.php

Volare ModelNetSuite RecordREST EndpointSync Job
ClientCustomer/record/v1/customerSyncClientToNetsuiteJob
SupplierVendor/record/v1/vendorSyncSupplierToNetsuiteJob
BookingInvoice/record/v1/invoiceSyncBookingToNetsuiteJob
PaymentCustomerPayment/record/v1/customerPaymentSyncPaymentToNetsuiteJob
Booking upsellsVendorBill/record/v1/vendorBillSyncVendorBillToNetsuiteJob

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():

ExceptionHTTP StatusRetryableDescription
NetsuiteExceptionanyNoBase exception for all NetSuite errors
NetsuiteAuthenticationException401YesOAuth token expired or credentials invalid
NetsuiteTimeoutExceptionYesConnection timeout
NetsuiteRateLimitException429YesRate limit exceeded, carries retryAfter delay
NetsuiteValidationException400/422NoPayload validation failed, carries validationErrors

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

SymptomCauseFix
InvalidArgumentException: NetSuite account ID is not configuredMissing env varsSet NETSUITE_ACCOUNT_ID, NETSUITE_CLIENT_ID, NETSUITE_CERTIFICATE_ID
NetsuiteAuthenticationException: Private key file not foundKey not deployedPlace EC256 private key at NETSUITE_PRIVATE_KEY_PATH (default: storage/netsuite/private.pem)
NetsuiteValidationException on syncInvalid payload dataCheck the error field on the sync record for NetSuite validation details
NetsuiteRateLimitExceptionToo many API callsJob auto-retries after delay; reduce batch size in netsuite:sync-all --limit
RuntimeException: Client must be synced before bookingDependency not syncedSync clients first: netsuite:sync-all --model=client, then bookings
Sync stuck in “Syncing” stateJob crashed mid-executionRun netsuite:reconcile --fix to reset stuck records
401 after token refreshCertificate/credentials rotated in NetSuiteUpdate 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/