Skip to content

ShipYo API Integration Guide ​

This guide explains how any client applicationβ€”web, mobile, desktop, or server-sideβ€”can interact with the ShipYo REST API. No assumptions are made about the JavaScript framework (React, Vue, Svelte, Astro, Node) or even the language; only the concepts and HTTP/JSON interactions are covered.

For detailed endpoint specifications, see the API Reference documentation.

πŸš€ Quick Start with Postman ​

The fastest way to get started is using our official Postman collection:

Postman Collection

Download the complete Postman collection to test the ShipYo API

Complete Collection

Postman collection with all ShipYo API endpoints organized by category.

All endpointsAutomatic testsJWT token managementPre-request scripts
Download Collection

Environment File

ShipYo API

Environment file with pre-configured variables for quick testing.

URL: https://shipyo.itExample credentials included
Download Environment

Quick Setup

  1. Import the collection: Download and import the collection file into Postman
  2. Import the environment: Download and import the environment file
  3. Configure variables:api_key, user_email, user_password
  4. Start testing: Run login and test the endpoints!

Includes collection + environment file



1. Environment Variables ​

Define three variables in the environment where your application runs (build-time for static apps, run-time for servers or CLIs):

VariablePurposeExample
SHIPYO_ENDPOINTBase URL of the ShipYo APIhttps://shipyo.it
SHIPYO_API_KEYAPI key issued by ShipYo54e6d32ce...
SHIPYO_TENANT_IDNumeric tenant identifier7

Why environment variables? They let you switch tenants, keys, or staging endpoints without touching code.

Tip : In browsers built with Vite or Astro, prefix variables that must be exposed to the client with PUBLIC_ (e.g., PUBLIC_SHIPYO_TENANT_ID). In Node back-ends, this is not needed.


2. Build an HTTP Client Wrapper ​

Create a small utility that every request in your app goes through. Pseudocode:

pseudo
config.endpoint   = ENV["SHIPYO_ENDPOINT"]   or "https://shipyo.it"
config.apiKey     = ENV["SHIPYO_API_KEY"]    // may be undefined in dev
config.tenantId   = ENV["SHIPYO_TENANT_ID"]   // numeric string

client = new HttpClient(baseUrl = config.endpoint)

client.onRequest(request):
  if token = LocalStorage.get("token") or MemoryCache.get("token"):
      request.headers["Authorization"] = "Bearer " + token
  if config.apiKey:
      request.headers["x-api-key"]     = config.apiKey
  if runtimeTenantId = LocalStorage.get("selectedTenantId") or config.tenantId:
      request.headers["X-Tenant-Id"]   = runtimeTenantId

Use whatever library you prefer:

JavaScript example with fetch ​

js
export async function shipyoFetch(path, options = {}) {
  const endpoint   = process.env.SHIPYO_ENDPOINT  || 'https://shipyo.it';
  const apiKey     = process.env.SHIPYO_API_KEY;
  const tenantId   = localStorage.getItem('selectedTenantId') || process.env.SHIPYO_TENANT_ID;
  const token      = localStorage.getItem('token');

  const headers = {
    ...options.headers,
    ...(apiKey   && { 'x-api-key':   apiKey }),
    ...(tenantId && { 'X-Tenant-Id': tenantId }),
    ...(token    && { 'Authorization': `Bearer ${token}` })
  };

  const resp = await fetch(`${endpoint}${path}`, { ...options, headers });
  if (!resp.ok) throw new Error(`Shipyo request failed: ${resp.status}`);
  return resp.json();
}

JavaScript example with axios ​

js
import axios from 'axios';

const client = axios.create({ baseURL: process.env.SHIPYO_ENDPOINT || 'https://shipyo.it' });

client.interceptors.request.use(cfg => {
  const apiKey   = process.env.SHIPYO_API_KEY;
  const tenantId = localStorage.getItem('selectedTenantId') || process.env.SHIPYO_TENANT_ID;
  const token    = localStorage.getItem('token');

  if (apiKey)   cfg.headers['x-api-key']   = apiKey;
  if (tenantId) cfg.headers['X-Tenant-Id'] = tenantId;
  if (token)    cfg.headers['Authorization'] = `Bearer ${token}`;
  return cfg;
});

export default client;

Implement the same idea in Swift (Alamofire), Kotlin (OkHttp), C# (HttpClient), etc.


3. Basic Usage Patterns ​

3.1 Read data ​

js
const users = await shipyoFetch('/api/User/getAll');
console.log(users);

3.2 Write data ​

js
await shipyoFetch('/api/User/login', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ email, password, tenantId })
});

3.3 Connection / Health Check ​

js
const ok = await shipyoFetch('/health').then(() => true).catch(() => false);
console.log('Shipyo reachable?', ok);

4. Error Handling Recommendations ​

ScenarioWhat to do
Network failureRetry or prompt user; if running a browser SPA you may want to log out the user to force a clean state.
HTTP 401 (Unauthorized)Token expired or invalid. Clear session and redirect to login.
HTTP 403 (Forbidden)User lacks permissions. Show a "not authorized" message.
HTTP 4xx/5xxDisplay the message property returned by Shipyo API or log it for support.

5. Multi-Tenant at Runtime (Optional) ​

If users can switch tenants while the app is running:

  1. Save the new id: localStorage.setItem('selectedTenantId', newTenantId)
  2. All subsequent requests will automatically carry the tenant through the X-Tenant-Id header (see wrapper).

6. Testing Checklist ​

  • [ ] Environment variables set for both build & runtime.
  • [ ] HTTP client attaches x-api-key, X-Tenant-Id, and Authorization (when present).
  • [ ] /health returns HTTP 200 in your logs.
  • [ ] Error handler logs/displaying non-200 responses.
  • [ ] Static build pipeline (e.g., Docker, CI) injects the same SHIPYO_* variables.

7. Extending the Wrapper (Optional) ​

Nothing stops you from turning the thin wrapper into a mini-SDK:

js
export const shipyo = {
  /** GET /api/User/getAll */
  async listUsers() { return shipyoFetch('/api/User/getAll'); },

  /** POST /api/User/login */
  async login(credentials) { return shipyoFetch('/api/User/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(credentials)
  }); },

  /** POST /api/Token/generate */
  async generateToken(credentials) { return shipyoFetch('/api/Token/generate', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(credentials)
  }); },

  // ... add more endpoints here ...
};

Consumers of your application then call shipyo.login() instead of remembering the raw paths.


8. Common API Endpoints ​

Here are some frequently used endpoints to get you started:

  • Authentication: POST /api/User/login
  • Generate Token: POST /api/Token/generate
  • Get Users: GET /api/User/getAll
  • Create User: POST /api/User/create
  • Get Tenants: GET /api/Tenant/getAll
  • API Key Management: GET /api/ApiKey/getAll

For complete endpoint documentation, see the API Reference page.


9. Complete Integration Scenarios ​

This section provides step-by-step workflows for common integration use cases, showing complete request/response cycles with practical examples.

9.1 πŸš€ User Registration & Onboarding Flow ​

Complete workflow for registering a new user and getting them ready to use the system.

Step 1: Check Email Availability ​

js
// Check if email is available before registration
const isEmailAvailable = async (email) => {
  try {
    const response = await shipyoFetch(`/api/User/isEmailUnique/${encodeURIComponent(email)}`);
    return response.isUnique;
  } catch (error) {
    console.error('Email check failed:', error);
    return false;
  }
};

// Usage
const available = await isEmailAvailable('newuser@example.com');
if (!available) {
  throw new Error('Email is already registered');
}

Step 2: Create New User ​

js
const createUser = async (userData) => {
  const newUser = await shipyoFetch('/api/User/create', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      name: userData.name,
      email: userData.email,
      street: userData.address.street,
      zip: userData.address.zip,
      pec: userData.pec || null,
      isCompany: userData.isCompany || false,
      isActive: true,
      sdi: userData.sdi || null,
      vatCode: userData.vatCode || null,
      phone: userData.phone || null,
      roleId: userData.roleId || 3, // Default to User role
      municipality: { value: userData.municipalityId }
    })
  });
  
  console.log('βœ… User created with ID:', newUser.data.id);
  console.log('πŸ“§ Password sent to:', newUser.data.email);
  
  return newUser.data;
};

Step 3: User First Login ​

js
const userFirstLogin = async (email, temporaryPassword) => {
  const loginResponse = await shipyoFetch('/api/User/login', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      email,
      password: temporaryPassword,
      tenantId: process.env.SHIPYO_TENANT_ID
    })
  });

  // Store token for subsequent requests
  localStorage.setItem('token', loginResponse.data.token);
  
  // Check if password reset is required
  if (!loginResponse.success && loginResponse.data.resetUrl) {
    console.log('πŸ”„ Password reset required:', loginResponse.data.resetUrl);
    // Redirect user to password reset flow
    window.location.href = loginResponse.data.resetUrl;
    return;
  }

  console.log('βœ… Login successful for:', loginResponse.data.user.email);
  console.log('πŸ‘€ User role:', loginResponse.data.user.roleName);
  
  return loginResponse.data;
};

Complete Registration Example ​

js
const completeUserRegistration = async () => {
  try {
    // 1. Validate email availability
    const email = 'newuser@example.com';
    const isAvailable = await isEmailAvailable(email);
    if (!isAvailable) throw new Error('Email already exists');

    // 2. Create user account
    const userData = {
      name: 'John Doe',
      email: email,
      address: { street: '123 Main St', zip: '12345' },
      isCompany: false,
      phone: '+1234567890',
      roleId: 3, // User role
      municipalityId: 1
    };
    
    const newUser = await createUser(userData);
    
    // 3. Notify user about account creation
    console.log('πŸŽ‰ Registration complete!');
    console.log('πŸ“§ Check email for login credentials');
    
    return {
      success: true,
      userId: newUser.id,
      message: 'Account created successfully. Check email for login credentials.'
    };
    
  } catch (error) {
    console.error('❌ Registration failed:', error.message);
    throw error;
  }
};

9.2 πŸ’³ Subscription Management Workflow ​

Complete workflow for managing user subscriptions from plan selection to subscription lifecycle management.

Step 1: Fetch Available Plans ​

js
const getAvailablePlans = async () => {
  try {
    const response = await shipyoFetch('/api/Plan/getAll');
    
    return response.data.items.map(plan => ({
      id: plan.id,
      name: plan.name,
      costBeforeVAT: plan.recurringCostBeforeVAT,
      vatPercentage: plan.vatPercentage,
      totalCost: plan.recurringCostBeforeVAT * (1 + plan.vatPercentage),
      recurrenceType: plan.recurrenceType, // 0=Monthly, 1=Yearly
      features: plan.planFeatures || [],
      isActive: plan.isActive,
      description: plan.description || '',
      currency: plan.currency || 'EUR'
    }));
  } catch (error) {
    console.error('Failed to fetch plans:', error);
    throw error;
  }
};

Step 2: Plan Selection and Validation ​

js
const validatePlanSelection = async (planId, userId) => {
  try {
    // 1. Get user information
    const user = await shipyoFetch(`/api/User/getById/${userId}`);
    if (!user.data.isActive) {
      throw new Error('User account is not active');
    }

    // 2. Get plan details
    const plans = await getAvailablePlans();
    const selectedPlan = plans.find(p => p.id === planId);
    
    if (!selectedPlan) {
      throw new Error(`Plan with ID ${planId} not found`);
    }

    if (!selectedPlan.isActive) {
      throw new Error('Selected plan is no longer available');
    }

    // 3. Check if user already has an active subscription (if business rules require)
    // Note: This would require a get user subscriptions endpoint
    
    console.log('βœ… Plan validation successful');
    console.log('πŸ“‹ Selected plan:', selectedPlan.name);
    console.log('πŸ’° Total cost:', selectedPlan.totalCost, selectedPlan.currency);
    
    return {
      isValid: true,
      plan: selectedPlan,
      user: user.data
    };

  } catch (error) {
    console.error('❌ Plan validation failed:', error.message);
    return {
      isValid: false,
      error: error.message
    };
  }
};

Step 3: Create Subscription (Checkout Process) ​

js
const createSubscription = async (userId, planId, paymentInfo = null) => {
  try {
    // 1. Validate plan selection
    const validation = await validatePlanSelection(planId, userId);
    if (!validation.isValid) {
      throw new Error(validation.error);
    }

    // 2. Prepare subscription data
    const subscriptionData = {
      userID: userId,
      planID: planId
    };

    // 3. Create subscription
    console.log('πŸ›’ Creating subscription...');
    const subscription = await shipyoFetch('/api/Subscription/subscribe', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(subscriptionData)
    });

    // 4. Process subscription response
    const subscriptionDetails = {
      id: subscription.data.id,
      userId: subscription.data.userID,
      planId: subscription.data.planID,
      status: getSubscriptionStatusName(subscription.data.subscriptionStatus),
      startDate: subscription.data.startDate,
      plan: validation.plan
    };

    console.log('βœ… Subscription created successfully');
    console.log('🎫 Subscription ID:', subscriptionDetails.id);
    console.log('πŸ“… Start date:', subscriptionDetails.startDate);
    console.log('πŸ”„ Status:', subscriptionDetails.status);

    // 5. Handle different subscription statuses
    switch (subscription.data.subscriptionStatus) {
      case 0: // Pending
        console.log('⏳ Subscription is pending payment confirmation');
        break;
      case 1: // Active
        console.log('πŸŽ‰ Subscription is now active!');
        break;
      case 4: // PaymentFailed
        console.log('❌ Payment failed - please retry');
        break;
    }

    return subscriptionDetails;

  } catch (error) {
    console.error('❌ Subscription creation failed:', error.message);
    throw error;
  }
};

Step 4: Subscription Status Management ​

js
// Helper function to get human-readable status names
const getSubscriptionStatusName = (status) => {
  const statusMap = {
    0: 'Pending',
    1: 'Active', 
    2: 'Cancelled',
    3: 'Expired',
    4: 'PaymentFailed',
    5: 'ActiveNoRenewal'
  };
  return statusMap[status] || 'Unknown';
};

// Monitor subscription status (polling example)
const monitorSubscription = async (subscriptionId, maxAttempts = 10) => {
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    try {
      // Note: This assumes a getSubscriptionById endpoint exists
      // If not available, you might need to get user subscriptions
      const subscription = await getSubscriptionDetails(subscriptionId);
      
      const status = getSubscriptionStatusName(subscription.subscriptionStatus);
      console.log(`πŸ“Š Subscription ${subscriptionId} status: ${status} (attempt ${attempt})`);

      // Break if subscription is no longer pending
      if (subscription.subscriptionStatus !== 0) {
        return {
          id: subscription.id,
          status: status,
          finalStatus: subscription.subscriptionStatus
        };
      }

      // Wait before next check
      await new Promise(resolve => setTimeout(resolve, 2000)); // 2 seconds

    } catch (error) {
      console.error(`Monitoring attempt ${attempt} failed:`, error.message);
      
      if (attempt === maxAttempts) {
        throw new Error('Subscription monitoring timeout');
      }
    }
  }
};

Complete Subscription Flow Example ​

js
const completeSubscriptionFlow = async (userId, planId) => {
  try {
    console.log('πŸš€ Starting subscription flow...');
    
    // 1. Get and display available plans
    const plans = await getAvailablePlans();
    console.log('πŸ“‹ Available plans:', plans.length);
    
    // 2. Validate selected plan
    const validation = await validatePlanSelection(planId, userId);
    if (!validation.isValid) {
      throw new Error(validation.error);
    }
    
    // 3. Show plan summary to user
    console.log('πŸ’° Plan Summary:');
    console.log(`  Name: ${validation.plan.name}`);
    console.log(`  Cost: ${validation.plan.totalCost} ${validation.plan.currency}`);
    console.log(`  Billing: ${validation.plan.recurrenceType === 0 ? 'Monthly' : 'Yearly'}`);
    
    // 4. Create subscription
    const subscription = await createSubscription(userId, planId);
    
    // 5. Monitor subscription activation (if needed)
    if (subscription.status === 'Pending') {
      console.log('⏳ Monitoring subscription activation...');
      const finalStatus = await monitorSubscription(subscription.id);
      
      if (finalStatus.finalStatus === 1) {
        console.log('πŸŽ‰ Subscription activated successfully!');
      } else {
        console.log('❌ Subscription activation failed:', finalStatus.status);
      }
    }
    
    return {
      success: true,
      subscription: subscription,
      message: 'Subscription flow completed successfully'
    };
    
  } catch (error) {
    console.error('❌ Subscription flow failed:', error.message);
    
    return {
      success: false,
      error: error.message,
      message: 'Subscription flow failed. Please try again.'
    };
  }
};

Subscription Management Utilities ​

js
// Helper function to get subscription details (when endpoint becomes available)
const getSubscriptionDetails = async (subscriptionId) => {
  // Note: This endpoint may not exist yet in the current API
  // This is a placeholder for future subscription detail retrieval
  try {
    const response = await shipyoFetch(`/api/Subscription/getById/${subscriptionId}`);
    return response.data;
  } catch (error) {
    console.error('Failed to get subscription details:', error);
    throw error;
  }
};

// Get user's active subscriptions (when endpoint becomes available)
const getUserSubscriptions = async (userId) => {
  try {
    // Note: This would require an endpoint like /api/User/{userId}/subscriptions
    const response = await shipyoFetch(`/api/User/${userId}/subscriptions`);
    return response.data.items || [];
  } catch (error) {
    console.error('Failed to get user subscriptions:', error);
    return [];
  }
};

// Cancel subscription (when endpoint becomes available)
const cancelSubscription = async (subscriptionId, reason = '') => {
  try {
    const response = await shipyoFetch(`/api/Subscription/${subscriptionId}/cancel`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ reason })
    });
    
    console.log('βœ… Subscription cancelled successfully');
    return response.data;
  } catch (error) {
    console.error('❌ Subscription cancellation failed:', error);
    throw error;
  }
};

// Upgrade/downgrade subscription (when endpoint becomes available)
const changeSubscriptionPlan = async (subscriptionId, newPlanId) => {
  try {
    const response = await shipyoFetch(`/api/Subscription/${subscriptionId}/changePlan`, {
      method: 'PUT',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ planID: newPlanId })
    });
    
    console.log('βœ… Subscription plan changed successfully');
    return response.data;
  } catch (error) {
    console.error('❌ Plan change failed:', error);
    throw error;
  }
};

9.3 πŸ”‘ API Key Lifecycle Management ​

Complete workflow for managing API keys throughout their lifecycle.

Create API Key with Validation ​

js
const createApiKey = async (keyConfig) => {
  try {
    const apiKey = await shipyoFetch('/api/ApiKey/create', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        name: keyConfig.name,
        allowedIp: keyConfig.allowedIp || null,
        isActive: true,
        description: keyConfig.description || `API key for ${keyConfig.name}`
      })
    });

    // ⚠️ CRITICAL: Store the key immediately - it's only shown once!
    const keyValue = apiKey.data.key;
    
    // Store securely (example with different storage options)
    if (typeof window !== 'undefined') {
      // Client-side: store in secure location
      sessionStorage.setItem(`apikey_${apiKey.data.id}`, keyValue);
    } else {
      // Server-side: save to secure config/database
      await saveApiKeySecurely(apiKey.data.id, keyValue);
    }

    console.log('βœ… API Key created:', apiKey.data.name);
    console.log('πŸ”‘ Key ID:', apiKey.data.id);
    console.log('πŸ›‘οΈ IP restriction:', apiKey.data.allowedIp || 'None');
    
    return {
      id: apiKey.data.id,
      name: apiKey.data.name,
      keyValue: keyValue, // Return for immediate use
      createdDate: apiKey.data.createdDate
    };
    
  } catch (error) {
    console.error('❌ API Key creation failed:', error.message);
    throw error;
  }
};

Test Newly Created API Key ​

js
const testApiKey = async (keyValue) => {
  try {
    // Test key by generating a token
    const response = await fetch(`${process.env.SHIPYO_ENDPOINT}/api/Token/generate-from-apikey`, {
      method: 'GET',
      headers: {
        'x-api-key': keyValue,
        'Accept': 'application/json'
      }
    });

    if (!response.ok) {
      throw new Error(`API Key test failed: ${response.status}`);
    }

    const tokenData = await response.json();
    console.log('βœ… API Key test successful');
    console.log('🎟️ Generated token length:', tokenData.data.length);
    
    return true;
  } catch (error) {
    console.error('❌ API Key test failed:', error.message);
    return false;
  }
};

API Key Rotation Workflow ​

js
const rotateApiKey = async (oldKeyId, keyConfig) => {
  try {
    // 1. Create new API key
    console.log('πŸ”„ Creating new API key...');
    const newKey = await createApiKey({
      ...keyConfig,
      name: `${keyConfig.name} (Rotated ${new Date().toISOString().split('T')[0]})`
    });

    // 2. Test new key
    console.log('πŸ§ͺ Testing new API key...');
    const isWorking = await testApiKey(newKey.keyValue);
    if (!isWorking) {
      throw new Error('New API key failed validation');
    }

    // 3. Update application configuration
    console.log('βš™οΈ Updating application configuration...');
    await updateApplicationConfig('SHIPYO_API_KEY', newKey.keyValue);

    // 4. Wait for deployment/propagation
    console.log('⏳ Waiting for configuration propagation...');
    await new Promise(resolve => setTimeout(resolve, 30000)); // 30 seconds

    // 5. Deactivate old key
    console.log('πŸ”’ Deactivating old API key...');
    await shipyoFetch('/api/ApiKey/update', {
      method: 'PUT',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        id: oldKeyId,
        isActive: false
      })
    });

    console.log('βœ… API key rotation complete');
    return {
      oldKeyId,
      newKeyId: newKey.id,
      rotationDate: new Date().toISOString()
    };

  } catch (error) {
    console.error('❌ API key rotation failed:', error.message);
    // Consider rollback procedures here
    throw error;
  }
};

9.4 🏒 Multi-Tenant Runtime Switching ​

Complete workflow for switching between tenants during application runtime.

Tenant Discovery ​

js
const getUserTenants = async () => {
  try {
    const tenants = await shipyoFetch('/api/Tenant/getAll');
    return tenants.data.items.map(tenant => ({
      id: tenant.id,
      name: tenant.name,
      configuration: tenant.configuration
    }));
  } catch (error) {
    console.error('Failed to fetch tenants:', error);
    return [];
  }
};

Switch Tenant Context ​

js
const switchTenant = async (newTenantId) => {
  try {
    // 1. Validate tenant access
    const userTenants = await getUserTenants();
    const targetTenant = userTenants.find(t => t.id === newTenantId);
    
    if (!targetTenant) {
      throw new Error(`Access denied to tenant ${newTenantId}`);
    }

    // 2. Update local tenant context
    localStorage.setItem('selectedTenantId', newTenantId.toString());
    
    // 3. Test access to new tenant
    const testAccess = await shipyoFetch('/api/User/getAll', {
      method: 'GET'
    });

    if (!testAccess.success) {
      throw new Error('Failed to access new tenant context');
    }

    // 4. Update UI context
    await refreshApplicationState();
    
    console.log('βœ… Switched to tenant:', targetTenant.name);
    console.log('πŸ‘₯ Available users:', testAccess.data.totalCount);
    
    return {
      tenantId: newTenantId,
      tenantName: targetTenant.name,
      switchedAt: new Date().toISOString()
    };

  } catch (error) {
    console.error('❌ Tenant switch failed:', error.message);
    // Revert to previous tenant
    const previousTenant = localStorage.getItem('selectedTenantId');
    if (previousTenant) {
      localStorage.setItem('selectedTenantId', previousTenant);
    }
    throw error;
  }
};

9.5 πŸ” Authentication Flow Variants ​

Standard Login Flow ​

js
const standardLogin = async (credentials) => {
  const { email, password, tenantId } = credentials;
  
  try {
    const response = await shipyoFetch('/api/User/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email, password, tenantId })
    });

    if (response.success) {
      // Store authentication data
      localStorage.setItem('token', response.data.token);
      localStorage.setItem('userEmail', response.data.user.email);
      localStorage.setItem('userRole', response.data.user.roleName);
      
      if (response.data.defaultTenantId) {
        localStorage.setItem('selectedTenantId', response.data.defaultTenantId.toString());
      }

      return {
        success: true,
        user: response.data.user,
        token: response.data.token,
        defaultTenantId: response.data.defaultTenantId
      };
    }
  } catch (error) {
    if (error.message.includes('password reset required')) {
      return {
        success: false,
        requiresPasswordReset: true,
        resetUrl: error.resetUrl
      };
    }
    throw error;
  }
};

Password Reset Flow ​

js
const passwordResetFlow = async (email) => {
  try {
    // 1. Request password reset
    await shipyoFetch('/api/User/request-password-reset', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email })
    });

    console.log('πŸ“§ Password reset email sent to:', email);
    return { success: true, message: 'Password reset email sent' };

  } catch (error) {
    console.error('Password reset request failed:', error);
    throw error;
  }
};

const validateResetToken = async (token) => {
  try {
    const response = await shipyoFetch(`/api/User/validate-reset-token?token=${encodeURIComponent(token)}`);
    return response.data; // true if valid
  } catch (error) {
    console.error('Token validation failed:', error);
    return false;
  }
};

const resetPassword = async (token, newPassword) => {
  try {
    await shipyoFetch('/api/User/reset-password', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ token, newPassword })
    });

    console.log('βœ… Password reset successful');
    return { success: true, message: 'Password reset successful' };

  } catch (error) {
    console.error('Password reset failed:', error);
    throw error;
  }
};

API Key to JWT Token Flow ​

js
const authenticateWithApiKey = async (apiKey) => {
  try {
    const response = await fetch(`${process.env.SHIPYO_ENDPOINT}/api/Token/generate-from-apikey`, {
      method: 'GET',
      headers: {
        'x-api-key': apiKey,
        'Accept': 'application/json'
      }
    });

    if (!response.ok) {
      throw new Error(`API Key authentication failed: ${response.status}`);
    }

    const tokenData = await response.json();
    const jwtToken = tokenData.data;

    // Store for subsequent API calls
    localStorage.setItem('token', jwtToken);
    
    console.log('βœ… API Key authentication successful');
    return { success: true, token: jwtToken };

  } catch (error) {
    console.error('❌ API Key authentication failed:', error);
    throw error;
  }
};

9.6 πŸ› οΈ Error Handling Patterns ​

Comprehensive Error Handler ​

js
const handleApiError = (error, context = '') => {
  console.error(`API Error in ${context}:`, error);

  // Network errors
  if (!navigator.onLine) {
    return {
      type: 'network',
      message: 'No internet connection. Please check your network.',
      action: 'retry',
      retryable: true
    };
  }

  // HTTP status code errors
  if (error.status) {
    switch (error.status) {
      case 401:
        // Clear session and redirect to login
        localStorage.clear();
        window.location.href = '/login';
        return {
          type: 'auth',
          message: 'Session expired. Please login again.',
          action: 'redirect',
          retryable: false
        };

      case 403:
        return {
          type: 'permission',
          message: 'You do not have permission to perform this action.',
          action: 'notify',
          retryable: false
        };

      case 429:
        return {
          type: 'rate_limit',
          message: 'Too many requests. Please wait and try again.',
          action: 'wait',
          retryable: true,
          retryAfter: 60000 // 1 minute
        };

      case 500:
      case 502:
      case 503:
        return {
          type: 'server',
          message: 'Server error. Please try again in a few minutes.',
          action: 'retry',
          retryable: true,
          retryAfter: 30000 // 30 seconds
        };

      default:
        return {
          type: 'api',
          message: error.message || 'An unexpected error occurred.',
          action: 'notify',
          retryable: false
        };
    }
  }

  // Generic error fallback
  return {
    type: 'unknown',
    message: 'An unexpected error occurred. Please try again.',
    action: 'retry',
    retryable: true
  };
};

Retry Logic with Exponential Backoff ​

js
const apiCallWithRetry = async (apiCall, maxRetries = 3, baseDelay = 1000) => {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      return await apiCall();
    } catch (error) {
      const errorInfo = handleApiError(error, `Attempt ${attempt}/${maxRetries}`);
      
      if (!errorInfo.retryable || attempt === maxRetries) {
        throw error;
      }

      const delay = errorInfo.retryAfter || (baseDelay * Math.pow(2, attempt - 1));
      console.log(`⏳ Retrying in ${delay}ms (attempt ${attempt + 1}/${maxRetries})`);
      
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }
};

9.7 πŸ§ͺ Integration Testing Patterns ​

Health Check and Connectivity ​

js
const performHealthCheck = async () => {
  const checks = {
    connectivity: false,
    authentication: false,
    authorization: false,
    dataAccess: false
  };

  try {
    // 1. Basic connectivity
    const healthResponse = await fetch(`${process.env.SHIPYO_ENDPOINT}/health`);
    checks.connectivity = healthResponse.ok;

    if (!checks.connectivity) {
      console.error('❌ Health check failed');
      return checks;
    }

    // 2. API key authentication
    try {
      await shipyoFetch('/api/Token/generate-from-apikey');
      checks.authentication = true;
    } catch (error) {
      console.error('❌ API key authentication failed:', error.message);
    }

    // 3. JWT authorization
    try {
      await shipyoFetch('/api/User/getAll');
      checks.authorization = true;
      checks.dataAccess = true;
    } catch (error) {
      console.error('❌ JWT authorization failed:', error.message);
    }

  } catch (error) {
    console.error('❌ Health check error:', error);
  }

  console.log('πŸ₯ Health Check Results:', checks);
  return checks;
};

Integration Test Suite ​

js
const runIntegrationTests = async () => {
  console.log('πŸ§ͺ Running ShipYo Integration Tests...');
  
  const results = {
    healthCheck: await performHealthCheck(),
    apiKeyGeneration: false,
    userManagement: false,
    tenantAccess: false,
    errorHandling: false
  };

  try {
    // Test API key operations
    const testKey = await createApiKey({
      name: 'Integration Test Key',
      description: 'Temporary key for integration testing'
    });
    results.apiKeyGeneration = await testApiKey(testKey.keyValue);
    
    // Clean up test key
    await shipyoFetch(`/api/ApiKey/delete/${testKey.id}`, { method: 'DELETE' });

    // Test user operations
    const users = await shipyoFetch('/api/User/getAll');
    results.userManagement = users.data.items.length >= 0;

    // Test tenant access
    const tenants = await shipyoFetch('/api/Tenant/getAll');
    results.tenantAccess = tenants.data.items.length >= 0;

    // Test error handling
    try {
      await shipyoFetch('/api/User/getById/999999'); // Non-existent user
    } catch (error) {
      results.errorHandling = error.status === 404;
    }

  } catch (error) {
    console.error('❌ Integration test failed:', error);
  }

  console.log('πŸ“Š Integration Test Results:', results);
  
  const allPassed = Object.values(results).every(result => 
    typeof result === 'boolean' ? result : Object.values(result).every(Boolean)
  );

  if (allPassed) {
    console.log('βœ… All integration tests passed!');
  } else {
    console.log('❌ Some integration tests failed. Check configuration.');
  }

  return results;
};

10. Production Best Practices & Real-World Examples ​

πŸ”§ Environment Configuration ​

js
// config/shipyo.js - Production configuration pattern
const config = {
  development: {
    endpoint: 'https://dev-api.shipyo.it',
    logLevel: 'debug'
  },
  production: {
    endpoint: 'https://shipyo.it',
    logLevel: 'error'
  }
};

export const shipyoConfig = config[process.env.NODE_ENV || 'development'];

πŸ“± React Native Integration Example ​

js
// utils/shipyo-mobile.js
import AsyncStorage from '@react-native-async-storage/async-storage';

export class MobileShipYoClient {
  async getStoredToken() {
    return await AsyncStorage.getItem('shipyo_token');
  }

  async login(credentials) {
    const response = await this.apiCall('/api/User/login', {
      method: 'POST',
      body: JSON.stringify(credentials)
    });

    if (response.success) {
      await AsyncStorage.setItem('shipyo_token', response.data.token);
    }
    return response;
  }

  async apiCall(endpoint, options = {}) {
    const token = await this.getStoredToken();
    
    return fetch(`${shipyoConfig.endpoint}${endpoint}`, {
      ...options,
      headers: {
        'Content-Type': 'application/json',
        ...options.headers,
        ...(token && { 'Authorization': `Bearer ${token}` }),
        ...(process.env.SHIPYO_API_KEY && { 'x-api-key': process.env.SHIPYO_API_KEY })
      }
    }).then(r => r.json());
  }
}

πŸš€ Deployment Checklist ​

  • [ ] Environment variables configured for all environments
  • [ ] API keys generated and stored securely
  • [ ] SSL certificates in place for HTTPS
  • [ ] Error monitoring configured
  • [ ] Rate limiting implemented
  • [ ] Unit tests covering ShipYo integration
  • [ ] Integration tests for complete workflows
  • [ ] Documentation updated for team

πŸ” Monitoring & Debugging ​

js
// utils/shipyo-monitoring.js
export const logShipYoError = (error, context) => {
  if (process.env.NODE_ENV === 'production') {
    // Send to monitoring service (Sentry, LogRocket, etc.)
    console.error('ShipYo Integration Error:', { error, context });
  }
};

export const withErrorHandling = (apiCall) => async (...args) => {
  try {
    return await apiCall(...args);
  } catch (error) {
    logShipYoError(error, { apiCall: apiCall.name, args });
    throw error;
  }
};

You have now integrated ShipYo in a way that is portable across any tech stack with comprehensive scenarios, error handling, and production-ready patterns. Happy shipping! πŸŽ‰