Skip to content

Building Extensions

Extensions add capabilities — payment gateways, tax providers, shipping carriers — without modifying the core engine. They are installed as Python packages and discovered automatically at startup via entry points.

Extension Structure

ext_authorize_net/
├── __init__.py
├── extension.py    # ExtensionProtocol implementation
├── strategy.py     # Strategy implementations
├── resolvers.py    # GraphQL resolvers (saved cards, admin transactions)
└── pyproject.toml  # Package metadata + entry point

ext_elastic_email follows the same layout (__init__.py, extension.py, strategy.py): it registers an ExtensionManifest with config_schema for the Elastic Email API and provides ElasticEmailStrategy, an EmailDeliveryStrategy implementation (vectis.modules.notification.strategies). Wire it where notification delivery is resolved (e.g. admin email settings), using channel extension config for api_key, default_from_email, default_from_name, and api_url.

Implementing ExtensionProtocol

The on_activate hook registers strategies and metadata at startup:

from vectis.core.extension import ExtensionRegistry, ExtensionManifest
from vectis.core.strategy import strategy_resolver

class AuthorizeNetExtension:
    name = "authorize_net"
    version = "1.0.0"
    description = "Authorize.Net payment gateway"

    async def on_activate(self, registry: ExtensionRegistry) -> None:
        from ext_authorize_net.strategy import AuthorizeNetPayment
        strategy_resolver.register(
            PaymentProcessStrategy, AuthorizeNetPayment(),
            name="authorize_net", extension_name=self.name,
        )
        registry.register_manifest(ExtensionManifest(
            name=self.name, version=self.version,
            description=self.description,
            category="payment",
            strategies=["authorize_net"],
            permissions=["payment.configure_authorize_net"],
            config_schema={
                "api_login_id": {"type": "string", "required": True},
                "transaction_key": {"type": "string", "required": True, "secret": True},
                "sandbox": {"type": "boolean", "default": True},
                "capture_mode": {"type": "string", "enum": ["authorize", "capture"], "default": "authorize"},
                "allowed_card_brands": {"type": "array", "default": ["visa", "mastercard", "amex", "discover"]},
                "supported_currencies": {"type": "array", "default": ["USD"]},
                "require_cvv": {"type": "boolean", "default": True},
                "require_billing_address": {"type": "boolean", "default": True},
            },
        ))

Entry Point Registration

Register in pyproject.toml so the engine discovers it:

[project]
name = "ext-authorize-net"
version = "1.0.0"

[project.entry-points."vectis.extensions"]
authorize_net = "ext_authorize_net.extension:AuthorizeNetExtension"

After pip install -e ./ext_authorize_net, the engine picks it up on restart.

Strategy Implementation

Payment gateway strategies implement the PaymentProcessStrategy ABC:

from decimal import Decimal
from vectis.modules.payment.strategies import PaymentProcessStrategy, PaymentResult

class AuthorizeNetCIMStrategy(PaymentProcessStrategy):
    @property
    def gateway_name(self) -> str:
        return "authorize_net"

    async def authorize(self, amount: Decimal, currency: str, payment_data: dict) -> PaymentResult:
        # Auth-only: holds funds without settling
        ...

    async def capture(self, transaction_id: str, amount: Decimal) -> PaymentResult:
        # Settle a previously authorized transaction
        ...

    async def charge(self, amount: Decimal, currency: str, payment_data: dict) -> PaymentResult:
        # Auth + capture in a single call
        ...

    async def refund(self, transaction_id: str, amount: Decimal) -> PaymentResult:
        # Return funds for a captured transaction
        ...

    async def void(self, transaction_id: str) -> PaymentResult:
        # Cancel an authorization before settlement
        ...

    async def approve_held_transaction(self, transaction_id: str) -> PaymentResult:
        # Release a transaction held by fraud filters (FDS)
        ...

Fraud Filter Handling

The Authorize.net strategy detects responseCode=="4" (held for review by FDS filters) and returns a PaymentResult with held_for_review=True. The PaymentService stores the transaction as status="held_for_review" and the order enters the HeldForReview state.

The approve_held_transaction method calls Authorize.net's updateHeldTransactionRequest API to release the hold. This is exposed as the adminReleaseFraudHold GraphQL mutation.

Email delivery (ext_ses)

The ext_ses extension declares an ExtensionManifest with config_schema for Amazon SES (aws_region, aws_access_key_id, aws_secret_access_key, default_from_email, default_from_name, optional configuration_set). The SesEmailStrategy class in vectis.extensions.ext_ses.strategy implements EmailDeliveryStrategy using aiobotocore and SendEmail. Install the optional extra pip install vectis[ses] (or add aiobotocore to your environment).

Notification SMS (Twilio, SNS)

The core module defines SmsDeliveryStrategy and SmsMessage in vectis.modules.notification.sms_strategies.

  • ext_twilio declares an ExtensionManifest with config_schema for account_sid, auth_token, from_number, and optional messaging_service_sid. Instantiate TwilioSmsStrategy from extension config when wiring SMS delivery (manifest only; no global resolver registration).

  • ext_sns declares a manifest for aws_region, aws_access_key_id, aws_secret_access_key, optional sender_id, and sms_type (Transactional or Promotional, default Transactional). On activate it registers SnsStrategy with strategy_resolver as SmsDeliveryStrategy / name sns. Install pip install vectis[sns] (or vectis[ses] — same aiobotocore dependency).

Admin SSO (ext_keycloak)

The ext_keycloak extension registers an ExtensionManifest with config_schema for server_url, realm, client_id, client_secret, admin_only (default true), and role_mapping (object: Keycloak realm role name → Vectis role slug). Use vectis.extensions.ext_keycloak.service.KeycloakService with ChannelExtension.config or explicit constructor arguments: get_authorize_url(redirect_uri) loads {server_url}/realms/{realm}/.well-known/openid-configuration and returns url + state; handle_callback(code, redirect_uri, session) exchanges the code, reads realm_access.roles from the id_token, maps roles, and creates or updates a staff User, OAuthAccount (provider="keycloak"), and global UserRole rows for mapped slugs. Wire routes/BFF to this service where admin OIDC login is enabled.

OnTrac Shipping (ext_ontrac)

The ext_ontrac extension registers a ShippingCalculatorStrategy for OnTrac Ground service. It calls the OnTrac ServicesAndCharges v3 JSON API for live rate quotes and falls back to weight-based static rates when credentials are absent or the API is unavailable.

Config

Key Type Default Description
wsid string (required) OnTrac Web Services ID
wskey string (required, secret) OnTrac Web Services Key

Services

Only Ground (GRND) is supported. The service code map is structured for future expansion but currently returns a single rate.

AfterShip Tracking (ext_aftership)

Multi-carrier shipment tracking and delivery notifications powered by AfterShip. Supports 1,200+ carriers with automatic tracking registration, webhook-based status updates, and scheduled polling fallback.

AfterShip does not register a ShippingCalculatorStrategy — it is a tracking-only extension that complements rate-quote providers like ShipStation, GoShippo, UPS, or OnTrac.

Config

Key Type Default Description
api_key string (required, secret) AfterShip API key
webhook_secret string (optional, secret) HMAC secret for webhook signature verification
auto_register boolean true Auto-register tracking numbers on fulfillment creation
tracking_poll_enabled boolean false Poll AfterShip for updates (fallback for webhooks)
poll_interval_minutes integer 30 Poll interval in minutes (minimum 15)
custom_domain string (optional) Custom domain for branded tracking pages
notify_customer_on_update boolean false Send Vectis notifications on status changes

Temporal Workflows

  • AfterShipRegisterTrackingWorkflow — on-demand, registers a tracking number with AfterShip when a fulfillment is created.
  • AfterShipTrackingPollWorkflow — scheduled (every 30 min), polls AfterShip for tracking status updates across all active trackings.

Files

  • vectis/backend/vectis/extensions/ext_aftership/extension.py
  • vectis/backend/vectis/extensions/ext_aftership/client.py
  • vectis/backend/vectis/extensions/ext_aftership/activities.py
  • vectis/backend/vectis/extensions/ext_aftership/workflows.py

ShipStation (ext_shipstation)

Multi-carrier rate quotes via ShipStation v2 API, plus order sync and tracking poll via v1 API.

Strategy: _ShipStationProxyStrategy — resolves credentials per-call from ShippingProvider.config where carrier_code = 'shipstation'.

Configuration (ShippingProvider.config JSONB):

Key Type Default Description
api_key string (required, secret) ShipStation v2 API key
origin_postal_code string Ship-from postal code
origin_country string "US" Ship-from country
auto_sync_orders boolean false Push orders on placement
tracking_poll_enabled boolean false Poll for tracking updates
sync_trigger string "order_placed" order_placed, awaiting_fulfillment, or manual
v1_api_key string (secret) Legacy v1 API key (order sync)
v1_api_secret string (secret) Legacy v1 API secret

GoShippo (ext_goshippo)

Multi-carrier rate quotes, labels, and tracking via the Shippo API.

Strategy: _ShippoProxyStrategy — resolves credentials per-call from ShippingProvider.config where carrier_code = 'goshippo'.

Configuration (ShippingProvider.config JSONB):

Key Type Default Description
api_token string (required, secret) Shippo API token
origin_postal_code string Ship-from postal code
origin_country string "US" Ship-from country
origin_state string Ship-from state
origin_city string Ship-from city

Shipping Extension Config Pattern

All shipping carrier extensions store their settings in ShippingProvider.config (JSONB), not in ChannelExtension.config or the extension manifest's config_schema. The strategy reads config from the provider where carrier_code matches, with env var fallback for development. See conventions.md for the full rule.


Channel-Scoped Activation

Extensions are toggled per-channel via ChannelExtension. The config JSONB column stores per-channel settings:

from vectis.core.extension import ChannelExtension

channel_ext = ChannelExtension(
    channel_id=1, extension_name="authorize_net", enabled=True,
    config={"api_login_id": "xxx", "transaction_key": "yyy", "sandbox": True},
)
session.add(channel_ext)

ExtensionManifest Fields

Field Purpose
category Functional category for admin filtering: payment, shipping, tax, compliance, communication, or authentication
strategies Strategy names this extension provides
depends_on / conflicts_with Dependency and exclusivity declarations
config_schema JSON schema for admin config UI
permissions Permission codenames introduced
admin_pages / admin_widgets Custom admin UI contributions

Lifecycle Hooks

on_activate (every startup — keep fast), on_install (first enable), on_deactivate, on_uninstall, on_upgrade (version change at startup).

Warning

on_activate runs on every startup. Register strategies and manifests only — use on_install for one-time database setup.

Testing

pip install -e ./ext_authorize_net
pytest ext_authorize_net/tests/ -v

The strategy_resolver fixture from conftest.py provides an isolated resolver for unit tests.

AgeChecker.Net Extension (ext_agechecker)

Age verification powered by AgeChecker.Net. Registers an AgeVerificationStrategy under the name "agechecker".

Strategy ABC

Defined in vectis/modules/auth/strategies.py:

  • AgeVerificationStrategy.verify_age(customer_data) — seamless server-side check
  • AgeVerificationStrategy.validate_token(token) — validates popup SDK tokens
  • AgeVerificationStrategy.get_popup_config(channel_config) — returns storefront config

GraphQL

Operation Name Description
Query ageCheckerConfig Returns popup config (null when disabled)
Mutation verifyAge Seamless verification with customer data
Mutation validateAgeToken Validates AgeChecker.Net popup token

Config Schema

Key Type Default Description
merchant_id string (required) AgeChecker.Net merchant ID
api_key string (required, secret) API key for server calls
sandbox boolean true Use sandbox environment
mode enum "both" seamless, popup, or both
trigger enum "checkout" store_wide, checkout, or disabled
minimum_age integer 21 Minimum age for verification
require_dob_on_checkout boolean false Show DOB field on checkout

Storefront Integration

  • Checkout gate (trigger: "checkout"): verification step after payment, before order placement. Seamless API called first; popup shown if photo ID needed.
  • Store-wide gate (trigger: "store_wide"): full-screen overlay in root layout gates all content until the visitor verifies their age.

Files

  • vectis/backend/vectis/extensions/ext_agechecker/extension.py
  • vectis/backend/vectis/extensions/ext_agechecker/strategy.py
  • vectis/backend/vectis/extensions/ext_agechecker/resolvers.py
  • vectis/storefront/src/lib/components/AgeCheckerPopup.svelte

Fraud Scoring Extensions

Four fraud scoring extensions implement FraudScoringStrategy. Only one can be active at a time (resolved via strategy_resolver.resolve("fraud_scoring")).

ext_ipqs (IPQualityScore)

Pre-payment IP reputation and email validation. Calls two REST endpoints per checkout: Proxy/VPN Detection and Email Verification.

Key Type Default Description
api_key string (required, secret) IPQS API key
strictness integer 1 0=lenient, 1=moderate, 2=strict
review_threshold integer 75 Score (0-100) to hold for review
block_threshold integer 90 Score (0-100) to block checkout

ext_maxmind (MaxMind minFraud)

Full-featured minFraud v2.0 integration with three API tiers, comprehensive request enrichment, and the Report Transaction feedback loop.

Scoring: Pre-payment (score_checkout), post-payment (score_post_payment), and login (score_login) scoring. Each uses the correct minFraud event.type (purchase vs account_login) so MaxMind's ML models apply the right behavioral baseline.

Request enrichment: Sends all available checkout context to minFraud — device (IP, user agent, session, Accept-Language), email (MD5-hashed), billing/shipping addresses (full name, address, region, phone), credit card (BIN, last 4, AVS, CVV, 3-D Secure), payment (method, processor, authorization outcome), order (amount, currency, discount code), shopping cart (item IDs, categories, prices, quantities), and account ID.

Response parsing (Insights / Factors tiers): Extracts IP risk, anonymous IP flags, email intelligence (disposable, free, first seen), device intelligence (device ID, confidence, last seen), billing/shipping address intelligence (distance to IP, in-IP-country), credit card issuer match, risk score reasons (Factors), disposition (custom rules), and API warnings.

Report Transaction: Implements report_decision() to send chargeback, false-positive, and suspected-fraud feedback to MaxMind via the /minfraud/v2.0/transactions/report endpoint. MaxMind reports that this feedback loop improves scoring accuracy by 10–50%.

Key Type Default Description
account_id string (required) MaxMind account ID
license_key string (required, secret) License key
tier enum "score" score, insights, or factors
review_threshold integer 50 risk_score (0-99) for review
block_threshold integer 80 risk_score (0-99) for block

Files:

  • vectis/backend/vectis/extensions/ext_maxmind/extension.py
  • vectis/backend/vectis/extensions/ext_maxmind/strategy.py

ext_signifyd (Signifyd)

Post-authorization chargeback guarantee. score_post_payment is the primary method — login and checkout return allow by default.

Key Type Default Description
api_key string (required, secret) Signifyd API key
team_id string optional Signifyd team ID
review_threshold integer 500 Score (0-1000) below which to review
block_threshold integer 250 Score (0-1000) below which to block

ext_riskified (Riskified)

Post-authorization chargeback guarantee with HMAC-SHA256 authentication.

Key Type Default Description
shop_domain string (required) Shop domain registered with Riskified
auth_token string (required, secret) HMAC authentication token
sandbox boolean true Use sandbox environment

Storage Extensions

Storage extensions implement FileStorageStrategy (defined in vectis/core/storage.py) and are registered via strategy_resolver.register(FileStorageStrategy, ...).

ext_s3 (Amazon S3)

Amazon S3 cloud object storage using the shared S3Service (SigV4 + httpx, no boto3).

Key Type Default Description
s3_region string (required) AWS region
s3_access_key string (required, secret) Access Key ID
s3_secret_key string (required, secret) Secret Access Key
s3_bucket string (required) Bucket name
s3_public_url string Optional CloudFront CDN URL

ext_minio (MinIO)

Self-hosted S3-compatible storage, also uses S3Service.

ext_digitalocean_spaces (DigitalOcean Spaces)

S3-compatible storage on DigitalOcean with optional CDN.

ext_dropbox (Dropbox)

File storage backed by a Dropbox account via the HTTP API v2 with OAuth2 refresh-token auth. No SDK dependency — uses httpx directly.

Key Type Default Description
app_key string (required) Dropbox App Key
app_secret string (required, secret) Dropbox App Secret
refresh_token string (required, secret) OAuth2 Refresh Token
access_token string Short-lived access token (auto-refreshed)
base_folder string /vectis-uploads Root folder path in Dropbox

Setup: Create a Dropbox App at https://www.dropbox.com/developers/apps, generate a refresh token via the OAuth2 flow, then configure the extension in Settings > Extensions.

ext_priority1 (Priority1 LTL Freight)

Live LTL freight rate quotes from Priority1, a freight broker returning competing carrier quotes identified by SCAC code. Includes built-in pallet calculation with two modes (simple weight-based and per-product dimensions), NMFC freight class support via product traits, and carrier filtering.

Strategy: Priority1ShippingStrategy — registered as ShippingCalculatorStrategy with name="priority1".

API: Priority1 v2 — POST /v2/ltl/quotes/rates with X-API-KEY header. Responses are cached in Redis for 10 minutes.

Configuration (ShippingProvider.config JSONB):

Note

All shipping extension settings live on ShippingProvider.config, not on ChannelExtension.config or the extension manifest. Edited from Settings > Shipping > Providers & Methods > [Provider] > Edit.

Key Type Default Description
api_key string (required, secret) Priority1 API key
environment string "dev" dev or live
api_timeout integer 30 API timeout in seconds
default_freight_class string "70" NMFC freight class fallback
pallet_mode string "simple" simple or per_product_dims
pallet_length number 48 Pallet length in inches
pallet_width number 40 Pallet width in inches
max_pallet_weight number 2500 Max weight per pallet (lbs)
pallet_tare_weight number 45 Empty pallet weight (lbs)
pallet_min_weight number 0 Min weight sent to API (0=none)
pallet_min_height number 0 Min height sent to API (0=default)
max_pallet_height number 94 Max stack height in inches
min_weight number 0 Min cart weight to offer LTL (0=always)
max_rates integer 0 Max carrier rates to show (0=all)
transit_days_padding integer 0 Extra days added to display
allowed_carriers string "" Comma-separated SCAC codes (blank=all)
fallback_enabled boolean false Show fallback rate on API failure
fallback_rate number 0 Fallback rate amount ($)
fallback_label string "LTL Freight Shipping" Fallback rate label

Carrier Discovery: The admin provides discoverPriority1Carriers and testPriority1Connection GraphQL mutations. The discover mutation runs test quotes across 7 diverse US routes to collect all carrier SCAC codes, merged with the KNOWN_LTL_CARRIERS dict. The admin UI renders a checkbox picker for selecting allowed carriers.

Per-product freight class: Assign NMFC freight classes to individual products using the "LTL Freight Class" trait (seeded on extension activation). Products without the trait use the default_freight_class from config.

Pallet calculation modes:

  • Simple: Aggregates all items onto pallets by weight. Uses cubic volume to calculate realistic stack height on the configured pallet footprint. Splits to a new pallet when weight or height limits are exceeded.
  • Per-product dimensions: Same as simple, but also forces a new pallet when an individual item exceeds the pallet footprint dimensions.

Fee Extensions

ext_package_protection (Package Protection)

Checkout fee for shipment protection with a claims workflow for damaged or missing items. Supports all five built-in calculation types: flat rate, percentage, flat + percentage, per weight, and per box.

Strategy: PackageProtectionCalculator — registered as FeeCalculationStrategy with name="package_protection".

Claim Reasons (seeded by default):

Reason Slug Customer Message
Damaged damaged Apology + prompt to submit
Missing - All Quantity missing-all Video-verified packing notice
Missing - Partial Quantity missing-partial Video-verified packing notice

Configuration (ChannelExtension.config):

Key Type Default Description
auto_attach boolean true Auto-add fee to eligible carts
default_calculation_type string "flat" One of: flat, percent, flat_plus_percent, per_weight, per_box
default_flat_amount string "4.99" Default flat fee amount
claim_header_message string see code Message shown at top of claim form
claim_footer_message string see code Message shown at bottom of claim form
reason_messages object see code Per-reason override messages keyed by slug

Claim Resolution Workflow:

  1. Admin clicks "Approve" on a submitted claim
  2. System issues store credit for the claimed item value
  3. System creates a replacement order from the claimed line items
  4. Store credit is applied to the replacement order
  5. Claim status transitions to complete