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
ExtensionManifestwithconfig_schemaforaccount_sid,auth_token,from_number, and optionalmessaging_service_sid. InstantiateTwilioSmsStrategyfrom 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, optionalsender_id, andsms_type(TransactionalorPromotional, defaultTransactional). On activate it registersSnsStrategywithstrategy_resolverasSmsDeliveryStrategy/ namesns. Installpip install vectis[sns](orvectis[ses]— sameaiobotocoredependency).
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.pyvectis/backend/vectis/extensions/ext_aftership/client.pyvectis/backend/vectis/extensions/ext_aftership/activities.pyvectis/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¶
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 checkAgeVerificationStrategy.validate_token(token)— validates popup SDK tokensAgeVerificationStrategy.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.pyvectis/backend/vectis/extensions/ext_agechecker/strategy.pyvectis/backend/vectis/extensions/ext_agechecker/resolvers.pyvectis/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.pyvectis/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:
- Admin clicks "Approve" on a submitted claim
- System issues store credit for the claimed item value
- System creates a replacement order from the claimed line items
- Store credit is applied to the replacement order
- Claim status transitions to
complete