Appearance
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
- Import the collection: Download and import the collection file into Postman
- Import the environment: Download and import the environment file
- Configure variables:
api_key,user_email,user_password - 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):
| Variable | Purpose | Example |
|---|---|---|
SHIPYO_ENDPOINT | Base URL of the ShipYo API | https://shipyo.it |
SHIPYO_API_KEY | API key issued by ShipYo | 54e6d32ce... |
SHIPYO_TENANT_ID | Numeric tenant identifier | 7 |
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"] = runtimeTenantIdUse 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 β
| Scenario | What to do |
|---|---|
| Network failure | Retry 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/5xx | Display 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:
- Save the new id:
localStorage.setItem('selectedTenantId', newTenantId) - All subsequent requests will automatically carry the tenant through the
X-Tenant-Idheader (see wrapper).
6. Testing Checklist β
- [ ] Environment variables set for both build & runtime.
- [ ] HTTP client attaches
x-api-key,X-Tenant-Id, andAuthorization(when present). - [ ]
/healthreturns 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! π