Skip to main content

Overview

Follow these best practices to ensure your Sticker integration is secure, performant, and provides the best user experience.

Authentication & Security

DO:
  • Store API keys in environment variables
  • Use secrets management systems (AWS Secrets Manager, HashiCorp Vault)
  • Use different keys for sandbox and production
  • Keep keys server-side only
DON’T:
  • Hardcode API keys in source code
  • Commit keys to version control
  • Share keys in chat/email
  • Expose keys in client-side code
DO:
  • Generate tokens on-demand when user opens module
  • Handle token expiration gracefully (tokens last 5 min)
  • Regenerate tokens on authentication errors
  • Clear any references when user logs out
DON’T:
  • Pre-generate tokens in advance
  • Cache or store session tokens
  • Reuse tokens across sessions
  • Share tokens between users
DO:
  • Use sandbox attribute with proper permissions
  • Implement Content Security Policy
  • Use HTTPS for all environments
DON’T:
  • Remove sandbox permissions unnecessarily
  • Mix HTTP and HTTPS content
  • Disable security features for convenience

Error Handling

Implement Retry Logic

Use exponential backoff for transient failures:
async function callStickerAPI(endpoint, options, maxRetries = 3) {
  let lastError;
  
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      const response = await fetch(endpoint, options);
      
      // Don't retry client errors (4xx except 429)
      if (response.status >= 400 && response.status < 500 && response.status !== 429) {
        const error = await response.json();
        throw new Error(`Client error: ${error.message}`);
      }
      
      if (!response.ok) {
        throw new Error(`HTTP ${response.status}`);
      }
      
      return await response.json();
    } catch (error) {
      lastError = error;
      
      // Don't retry on last attempt
      if (attempt === maxRetries - 1) break;
      
      // Exponential backoff: 1s, 2s, 4s
      const delay = Math.pow(2, attempt) * 1000;
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }
  
  throw lastError;
}

Handle Rate Limiting

Respect rate limits and implement backoff:
async function handleRateLimit(response, retryFn) {
  if (response.status === 429) {
    const data = await response.json();
    const retryAfter = data.retry_after || 60;
    
    console.warn(`Rate limited. Retrying after ${retryAfter} seconds`);
    
    await new Promise(resolve => 
      setTimeout(resolve, retryAfter * 1000)
    );
    
    // Retry the request
    return retryFn();
  }
  
  return response;
}

User-Friendly Error Messages

Transform technical errors into helpful messages:
function getUserFriendlyError(error) {
  const errorMap = {
    'VALIDATION_ERROR': 'Please check your information and try again.',
    'UNAUTHORIZED': 'Authentication failed. Please refresh and try again.',
    'USER_ALREADY_EXISTS': 'This user is already set up.',
    'Profile not found': 'Your account needs to be set up. Please contact your administrator.',
    'SESSION_EXPIRED': 'Your session has expired. Refreshing...',
    'RATE_LIMIT_EXCEEDED': 'Too many requests. Please wait a moment and try again.',
  };
  
  return errorMap[error.code] || errorMap[error.message] || 
    'An error occurred. Please try again or contact support.';
}

Performance Optimization

Don’t Cache Session Tokens

Session tokens are single-use and short-lived—never cache them:
// ❌ BAD - Never cache session tokens
const tokenCache = new Map();
function getCachedToken(userId) {
  return tokenCache.get(userId);
}

// ✅ GOOD - Generate fresh each time
async function getSessionToken(userId) {
  const response = await fetch('/api/supplies/auth', {
    method: 'POST',
    body: JSON.stringify({ userId })
  });
  return response.json();
}

Preconnect to Sticker Domain

Speed up iframe loading:
<!-- Add to your HTML head -->
<link rel="preconnect" href="https://shop.usesticker.com" />
<link rel="dns-prefetch" href="https://shop.usesticker.com" />

Monitor Performance

Track integration performance metrics:
class StickerMetrics {
  static trackHandshake(startTime, success) {
    const duration = Date.now() - startTime;
    
    // Send to your analytics
    analytics.track('sticker_handshake', {
      duration_ms: duration,
      success: success
    });
  }
  
  static trackIframeLoad(startTime) {
    const duration = Date.now() - startTime;
    
    analytics.track('sticker_iframe_load', {
      duration_ms: duration
    });
  }
  
  static trackError(endpoint, error) {
    analytics.track('sticker_error', {
      endpoint: endpoint,
      error_code: error.code,
      error_message: error.message
    });
  }
}

// Usage
const start = Date.now();
try {
  const result = await getSessionToken(userId);
  StickerMetrics.trackHandshake(start, true);
} catch (error) {
  StickerMetrics.trackHandshake(start, false);
  StickerMetrics.trackError('handshake', error);
}

User Experience

Loading States

Always show loading indicators:
function SuppliesModule({ userId }) {
  const [state, setState] = useState('initializing');
  const [iframeUrl, setIframeUrl] = useState(null);

  useEffect(() => {
    async function load() {
      try {
        setState('loading');
        const { iframe_url } = await getSessionToken(userId);
        setIframeUrl(iframe_url);
        setState('ready');
      } catch (error) {
        setState('error');
      }
    }
    load();
  }, [userId]);

  return (
    <div className="supplies-wrapper">
      {state === 'loading' && (
        <div className="loading-state">
          <Spinner />
          <p>Loading supplies...</p>
        </div>
      )}
      
      {state === 'error' && (
        <div className="error-state">
          <ErrorIcon />
          <p>Failed to load supplies</p>
          <button onClick={() => window.location.reload()}>Try Again</button>
        </div>
      )}
      
      {iframeUrl && (
        <iframe
          src={iframeUrl}
          className="w-full h-full border-0"
          style={{ display: state === 'ready' ? 'block' : 'none' }}
          onLoad={() => setState('ready')}
          title="Sticker Embedded Procurement"
          sandbox="allow-same-origin allow-scripts allow-forms allow-popups allow-popups-to-escape-sandbox allow-top-navigation-by-user-activation"
          allow="payment; publickey-credentials-get; fullscreen"
        />
      )}
    </div>
  );
}

Graceful Degradation

Provide fallback options when integration fails:
function SuppliesModuleWithFallback() {
  const [integrationFailed, setIntegrationFailed] = useState(false);
  
  if (integrationFailed) {
    return (
      <div className="fallback-message">
        <h3>Supplies Module Unavailable</h3>
        <p>We're experiencing technical difficulties. Please:</p>
        <ul>
          <li>Try again in a few minutes</li>
          <li>Contact support: suyash@usesticker.com</li>
        </ul>
        <button onClick={() => setIntegrationFailed(false)}>
          Try Again
        </button>
      </div>
    );
  }
  
  return <StickerEmbed onError={() => setIntegrationFailed(true)} />;
}

Data Management

Use Your Internal IDs

Always use your internal identifiers for easy correlation:
// Good - uses your internal IDs
await setupOrganization({
  internalOrgId: organization.id,           // Your org ID
  internalUserId: user.id,                  // Your user ID
  shippingLocations: locations.map(loc => ({
    internalShippingLocationId: loc.id,     // Your location ID
    ...
  }))
});

// Later, for handshake
await handshake({
  internal_user_id: user.id                 // Same ID you used in setup
});

Handle Existing Users Gracefully

The setup endpoint returns 409 if user already exists—don’t treat this as an error:
async function ensureUserSetup(org, user) {
  try {
    const result = await setupOrganization(org, user);
    
    if (result.data.isNewOrganization) {
      console.log('New organization created');
    } else {
      console.log('User added to existing organization');
    }
    
    return result.data.profile.id;
  } catch (error) {
    // 409 means user already exists - that's fine!
    if (error.code === 'USER_ALREADY_EXISTS') {
      console.log('User already set up:', error.data.profileId);
      return error.data.profileId;
    }
    throw error;
  }
}

Testing

Integration Tests

Test your integration thoroughly:
describe('Sticker Integration', () => {
  it('should setup organization successfully', async () => {
    const result = await setupOrganization(mockOrgData, mockUser, mockLocations);
    
    expect(result.success).toBe(true);
    expect(result.data.profile.id).toBeDefined();
    expect(result.data.organization.id).toBeDefined();
  });
  
  it('should handle handshake and return session token', async () => {
    const result = await getHandshakeToken('test-user-123');
    
    expect(result.session_key).toBeDefined();
    expect(result.session_key).toHaveLength(64);
    expect(result.iframe_embed_url).toContain('session_key');
  });
  
  it('should retry on transient failures', async () => {
    mockAPI.failNextRequests(2);  // Fail twice, succeed third
    
    const result = await callStickerAPI(endpoint, options);
    
    expect(result).toBeDefined();
    expect(mockAPI.callCount).toBe(3);
  });
  
  it('should return user-friendly error for missing profile', async () => {
    mockAPI.returnError(404, { error: 'Profile not found' });
    
    const error = await callStickerAPI(endpoint, options).catch(e => e);
    const friendlyMessage = getUserFriendlyError(error);
    
    expect(friendlyMessage).not.toContain('404');
    expect(friendlyMessage).toContain('account');
  });
});

Use Sandbox Environment

Always test in sandbox first:
const config = {
  staging: {
    apiUrl: 'https://api.staging.usesticker.com/v1',
    apiKey: process.env.STICKER_STAGING_API_KEY
  },
  production: {
    apiUrl: 'https://api.usesticker.com/v1',
    apiKey: process.env.STICKER_API_KEY
  }
};

const env = process.env.NODE_ENV === 'production' ? 'production' : 'staging';
const stickerConfig = config[env];

Deployment Checklist

Before going live, verify:
1

Security

  • API keys stored securely in environment variables
  • API key never exposed in client-side code
  • iframe sandbox attributes configured
  • HTTPS enabled on all pages
2

Error Handling

  • Retry logic with exponential backoff
  • Rate limiting handled gracefully
  • User-friendly error messages
  • Fallback UI for failures
3

User Experience

  • Loading states for all async operations
  • Responsive design on mobile devices
  • Clear error messaging
  • Easy retry/refresh options
4

Performance

  • iframe loads in < 3 seconds
  • API calls complete in < 2 seconds
  • Metrics tracking implemented
5

Testing

  • All integration tests passing
  • Tested in sandbox environment
  • Manual QA completed
  • Cross-browser testing done
  • Mobile testing completed

Getting Help