asciisd / pelican
Pelican Laravel SDK - a Laravel package for integrating with the CopyTrade (Pelican) API
Requires
- php: ^8.2
- guzzlehttp/guzzle: ^7.8
- illuminate/http: ^12.0|^13.0
- illuminate/support: ^12.0|^13.0
Requires (Dev)
- orchestra/testbench: ^10.0|^11.0
- pestphp/pest: ^3.0|^4.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Latest Version on Packagist Tests Total Downloads License PHP Version
A modern, clean Laravel package for integrating with the CopyTrade (Pelican) API. Built with SOLID principles, type-safety, and developer experience in mind.
Features
- Full API coverage — every endpoint in the CopyTrade API collection is available as a typed service method
- Built-in OAuth2 login — Authorization Code with PKCE, driven entirely server-side (no browser needed)
- Single Sign-On (SSO) — OpenID Connect redirect login, callback, session, and federated logout
- Type-safe DTOs — fully typed data transfer objects for all API responses
- Interface-based services — easy mocking and testing
- Laravel-native — service container bindings, facade, config publishing, and package auto-discovery
- Comprehensive error handling — specific exceptions for different error types
Requirements
- PHP 8.2 or higher
- Laravel 12.0 or 13.0
Installation
Install the package via Composer from Packagist:
composer require asciisd/pelican
The package is auto-discovered by Laravel — no manual provider registration needed.
Configuration
Publish the configuration file (optional — sensible defaults are provided):
php artisan vendor:publish --tag=copytrade-config
Environment Variables
COPYTRADE_BASE_URI=https://papi.copy-trade.io COPYTRADE_IDENTITY_URI=https://identity.copy-trade.io COPYTRADE_ASSET_URI=https://assets.copy-trade.io COPYTRADE_CLIENT_ID=pelican # Optional — defaults to "{client_id}://authenticated" (pelican://authenticated), # which is what the credential-based login() requires. Only set this if you use # the browser-based redirect flow with your own registered redirect URI. COPYTRADE_CALLBACK_URL= # Optional — a pre-issued access token applied to every service automatically COPYTRADE_ACCESS_TOKEN= # OAuth2 Authorization Code with PKCE COPYTRADE_SCOPES="openid profile email copytrade" COPYTRADE_CLIENT_SECRET= # leave empty for the public Pelican client # Single Sign-On (browser redirect). Requires a Pelican OAuth client whose # redirect URI matches COPYTRADE_CALLBACK_URL or the package callback route. COPYTRADE_SSO_ENABLED=false COPYTRADE_SSO_PREFIX=copytrade/sso COPYTRADE_SSO_REDIRECT_AFTER_LOGIN=/ COPYTRADE_SSO_REDIRECT_AFTER_LOGOUT=/ COPYTRADE_SSO_REDIRECT_ON_ERROR=/ # COPYTRADE_SSO_PROMPT=login # optional: login | none | consent # HTTP client COPYTRADE_TIMEOUT=30
Quick Start
use Asciisd\Copytrade\Facades\Copytrade; // 1. Log in and get a token $token = Copytrade::auth()->login('user@example.com', 'secret-password'); // 2. Scope the SDK to that token $copytrade = Copytrade::withToken($token->accessToken); // 3. Call the API through the domain services $user = $copytrade->profiles()->getUserInfo(); $profile = $copytrade->profiles()->getProfile($user->profileId); $copiers = $copytrade->copiers()->getCopiers($user->profileId); $strategies = $copytrade->strategies()->getStrategies($user->profileId);
Every API area is exposed as a service off the facade:
| Accessor | Service | Responsibility |
|---|---|---|
Copytrade::auth() |
AuthService |
OAuth2 login, token exchange, refresh, revoke |
Copytrade::sso() |
SsoService |
OpenID Connect SSO redirect, callback, session, logout |
Copytrade::profiles() |
ProfileService |
User info and profile management |
Copytrade::servers() |
ServerService |
Available trading servers |
Copytrade::strategies() |
StrategyService |
Strategies, stats, search, signals, images |
Copytrade::copiers() |
CopierService |
Copiers, copy settings, copier signals, images |
Copytrade::sections() |
SectionService |
Discover sections |
Authentication
The CopyTrade API uses the OAuth2 Authorization Code flow with PKCE. The package drives the whole flow for you — log in with an email and password and get a token back in one call.
Login with credentials (recommended)
use Asciisd\Copytrade\Facades\Copytrade; $token = Copytrade::auth()->login('user@example.com', 'secret-password'); $token->accessToken; // "eyJhbGciOi..." — use this for API calls $token->refreshToken; // used to renew the access token $token->expiresIn; // lifetime in seconds (e.g. 3600) $token->expiresAt; // CarbonImmutable expiry timestamp $token->tokenType; // "Bearer"
Behind the scenes the package walks the Pelican identity server's hosted login page server-side (authorize → login form → authorization code → token exchange), so no browser, iframe, or callback route is needed.
A failed login (wrong credentials, misconfigured client, etc.) throws an
Asciisd\Copytrade\Exceptions\AuthenticationException:
use Asciisd\Copytrade\Exceptions\AuthenticationException; try { $token = Copytrade::auth()->login($email, $password); } catch (AuthenticationException $e) { // e.g. "Login failed: invalid email or password." report($e); }
Storing and reusing the token
TokenDTO serializes cleanly, so you can cache it and refresh it when it
expires:
use Asciisd\Copytrade\DTOs\Auth\TokenDTO; use Illuminate\Support\Facades\Cache; $token = Cache::get('copytrade_token') ? TokenDTO::fromArray(Cache::get('copytrade_token')) : null; if ($token === null) { $token = Copytrade::auth()->login($email, $password); } elseif ($token->isExpired(buffer: 60)) { $token = $token->canRefresh() ? Copytrade::auth()->refresh($token->refreshToken) : Copytrade::auth()->login($email, $password); } Cache::put('copytrade_token', $token->toArray(), $token->expiresAt);
Revoke a token when you are done with it:
Copytrade::auth()->revoke($token->accessToken); Copytrade::auth()->revoke($token->refreshToken, 'refresh_token');
Browser-based login (advanced)
If you prefer to send the user to Pelican's hosted login page in their own browser, generate an authorization request, store its state and PKCE verifier in the session, and redirect:
use Asciisd\Copytrade\Facades\Copytrade; $authorization = Copytrade::auth()->authorizationRequest( redirectUri: 'https://your-app.example.com/copytrade/callback', ); session([ 'copytrade_oauth_state' => $authorization->state, 'copytrade_code_verifier' => $authorization->codeVerifier, ]); return redirect()->away($authorization->authorizationUrl);
Then validate the returned state on your callback route before exchanging the one-time authorization code:
use Illuminate\Http\Request; public function callback(Request $request) { abort_unless( hash_equals( (string) $request->session()->pull('copytrade_oauth_state'), (string) $request->query('state'), ), 403, 'Invalid OAuth state.', ); $token = Copytrade::auth()->exchangeCode( code: (string) $request->query('code'), codeVerifier: (string) $request->session()->pull('copytrade_code_verifier'), redirectUri: 'https://your-app.example.com/copytrade/callback', ); $request->session()->put('copytrade_access_token', $token->accessToken); return redirect('/dashboard'); }
The redirect URI must exactly match a redirect URI registered for the Pelican
OAuth client. Note: the default public pelican client only allows its custom
scheme callback (pelican://authenticated), which is why the credential-based
login() above is the recommended path for headless / API use. For a browser
login that signs the user into your app, use Single Sign-On.
Single Sign-On (SSO)
SSO sends the user to Pelican's identity server in their browser, then brings them back to your Laravel app with an access token and OpenID Connect claims. Use this when you want "Login with CopyTrade" instead of collecting a Pelican email and password yourself.
Copytrade::sso() wraps the OAuth building blocks (authorizationRequest() /
exchangeCode()) into a drop-in flow: start URL, callback, session, token
refresh, and federated logout.
1. Register an OAuth client
The public pelican client cannot complete a browser SSO flow — its only
allowed callback is pelican://authenticated. Ask Pelican / CopyTrade to
register your application with:
| Setting | Example |
|---|---|
| Client ID | your-app |
| Client type | public (PKCE) or confidential |
| Redirect URI | https://your-app.example.com/copytrade/sso/callback |
| Post-logout redirect URI | https://your-app.example.com/ (optional, for federated logout) |
Then point the package at that client:
COPYTRADE_CLIENT_ID=your-app COPYTRADE_ACR_VALUES=tenant:your-app COPYTRADE_CALLBACK_URL=https://your-app.example.com/copytrade/sso/callback COPYTRADE_SCOPES="openid profile email copytrade offline_access" COPYTRADE_CLIENT_SECRET= # only if the client is confidential COPYTRADE_SSO_ENABLED=true COPYTRADE_SSO_REDIRECT_AFTER_LOGIN=/dashboard COPYTRADE_SSO_REDIRECT_AFTER_LOGOUT=/ COPYTRADE_SSO_REDIRECT_ON_ERROR=/login
offline_access is what makes Pelican issue a refresh token so the SSO
session can outlive the access token.
If you enable the package routes and leave COPYTRADE_CALLBACK_URL empty (or
set to the custom pelican:// scheme), the package uses
route('copytrade.sso.callback') as the redirect URI. That URL must still be
registered on the OAuth client.
2. Enable the package routes (recommended)
With COPYTRADE_SSO_ENABLED=true the package registers:
| Route | Name | Purpose |
|---|---|---|
GET /copytrade/sso/redirect |
copytrade.sso.redirect |
Send the user to Pelican |
GET /copytrade/sso/callback |
copytrade.sso.callback |
Exchange the code and start a session |
GET|POST /copytrade/sso/logout |
copytrade.sso.logout |
Revoke tokens and end the Pelican session |
Change the path with COPYTRADE_SSO_PREFIX if you need a different prefix.
Wire them in your UI:
<a href="{{ route('copytrade.sso.redirect', ['intended' => '/dashboard']) }}"> Continue with CopyTrade </a> <form method="POST" action="{{ route('copytrade.sso.logout') }}"> @csrf <button type="submit">Sign out of CopyTrade</button> </form>
Optional query parameters on the start URL:
intended— path to return to after a successful login. Pass a same-app relative path such as/dashboard. Do not pass an absolute URL (https://…or//…); Laravel will redirect there after login.login_hint— pre-fill the email on Pelican's hosted login page
?local=1 on the logout URL skips federated logout and only clears the local
SSO session. Prefer POST for logout so the request is CSRF-protected.
After a failed callback the user is sent to COPYTRADE_SSO_REDIRECT_ON_ERROR
with a flashed copytrade_sso_error message.
3. Map the Pelican identity to a local user
The package does not call Auth::login() for you — host apps own their
user model. Listen for SsoAuthenticated and create or look up the local
account:
use Asciisd\Copytrade\Events\SsoAuthenticated; use Asciisd\Copytrade\Events\SsoLoggedOut; use App\Models\User; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Event; Event::listen(SsoAuthenticated::class, function (SsoAuthenticated $event) { $user = User::query()->firstOrCreate( ['email' => $event->user->email], ['name' => $event->user->name ?? $event->user->email], ); Auth::login($user); }); Event::listen(SsoLoggedOut::class, function () { Auth::logout(); });
$event->user is a UserInfoDTO with profileId, sub, email, name,
givenName, familyName, and emailVerified. $event->token is the
TokenDTO you can cache per local user if you need the API from a queue or
a later request.
4. Use the SSO token for API calls
use Asciisd\Copytrade\Facades\Copytrade; if (! Copytrade::sso()->isAuthenticated()) { return redirect()->route('copytrade.sso.redirect'); } $token = Copytrade::sso()->token(); // refreshes automatically when expired $user = Copytrade::sso()->user(); // OpenID Connect claims + profile id $copytrade = Copytrade::withToken($token->accessToken); $profile = $copytrade->profiles()->getProfile($user->profileId);
You can drive the same flow without the package routes:
use Asciisd\Copytrade\Facades\Copytrade; use Illuminate\Http\Request; public function redirect() { return redirect()->away( Copytrade::sso()->authorizationUrl(intendedUrl: '/dashboard') ); } public function callback(Request $request) { $session = Copytrade::sso()->handleCallback( (string) $request->query('code'), (string) $request->query('state'), ); // $session->token and $session->user return redirect()->intended('/dashboard'); } public function logout() { return redirect()->away( Copytrade::sso()->logout(postLogoutRedirectUri: url('/')) ); }
If you disable the package routes, COPYTRADE_CALLBACK_URL must be an
http:// or https:// URL that matches the redirect URI registered on the
OAuth client. The custom pelican://authenticated scheme is rejected for SSO.
Token scoping
withToken() returns token-scoped copies of the services and never mutates
the shared singletons, so it is safe to work with several accounts side by
side:
$accountA = Copytrade::withToken($tokenA->accessToken); $accountB = Copytrade::withToken($tokenB->accessToken); $profileA = $accountA->profiles()->getProfile($profileIdA); $profileB = $accountB->profiles()->getProfile($profileIdB);
If COPYTRADE_ACCESS_TOKEN is set, that token is applied to every service
automatically and withToken() is only needed for overrides.
API Reference
ProfileService — Copytrade::profiles()
Manage user profiles and account information.
| Method | Returns | API Endpoint |
|---|---|---|
getUserInfo() |
UserInfoDTO |
GET {identity}/connect/userinfo |
getProfile($profileId) |
ProfileDTO |
GET /api/profiles/{id} |
updateProfile($profileId, $data) |
ProfileDTO |
PUT /api/profiles/{id} |
// The user info response contains your profile ID $user = Copytrade::withToken($accessToken)->profiles()->getUserInfo(); $profile = Copytrade::withToken($accessToken)->profiles()->getProfile($user->profileId); $updated = Copytrade::withToken($accessToken)->profiles()->updateProfile($user->profileId, [ 'name' => 'New Name', // optional, string 'riskProfile' => 'Moderate', // optional, string 'countryCode' => 'EG', // optional, ISO country code ]);
ServerService — Copytrade::servers()
| Method | Returns | API Endpoint |
|---|---|---|
getServers() |
ServerDTO[] |
GET /api/servers |
$servers = Copytrade::withToken($accessToken)->servers()->getServers();
Use the returned server codes when creating copier or strategy connections.
StrategyService — Copytrade::strategies()
Manage trading strategies, stats, search, signals, and strategy images.
| Method | Returns | API Endpoint |
|---|---|---|
getStrategies($profileId) |
StrategyDTO[] |
GET /api/profiles/{id}/strategies |
addStrategy($profileId, $data) |
StrategyDTO |
POST /api/profiles/{id}/strategies |
updateStrategy($profileId, $strategyId, $data) |
StrategyDTO |
PUT /api/profiles/{id}/strategies/{strategyId} |
removeStrategy($profileId, $strategyId) |
bool |
DELETE /api/profiles/{id}/strategies/{strategyId} |
getStrategy($strategyId) |
StrategyDTO |
GET /api/strategies/{id} |
getStrategyStats($strategyId) |
StrategyStatsDTO |
GET /api/strategies/{id}/stats |
searchStrategies($filter) |
SearchStrategyDTO[] |
GET /api/strategies?filter= |
getStrategyCopiers($strategyId) |
CopierDTO[] |
GET /api/strategies/{id}/copiers |
getStrategyOpenSignals($strategyId) |
SignalDTO[] |
GET /api/strategies/{id}/signals/open |
getStrategyClosedSignals($strategyId, $startDate, $endDate) |
SignalDTO[] |
GET /api/strategies/{id}/signals/closed?startDate=&endDate= |
uploadStrategyImage($profileId, $strategyId, $fileContent, $filename) |
array |
PUT /api/profiles/{id}/strategies/{strategyId}/image |
getStrategyImageUrl($strategyId) |
string |
{assets}/images/strategies/thumbnails/{id} (URL builder) |
$strategies = Copytrade::withToken($accessToken)->strategies(); // Create a strategy (a master account whose trades will be copied) $strategy = $strategies->addStrategy($profileId, [ 'name' => 'My Trading Strategy', // required, string 'riskProfile' => 'Moderate', // required, string 'fee' => 15.0, // required, float (percentage) 'connection' => [ 'brokerCode' => 'XM', // required, string 'serverCode' => 'XM-Real', // required, string 'username' => '12345678', // required, string (MT login) 'password' => 'MySecurePass', // required, string (MT password) ], ]); // Fetch a single strategy $fetched = $strategies->getStrategy($strategy->id); // Discover strategies $results = $strategies->searchStrategies('forex'); $stats = $strategies->getStrategyStats($strategy->id); // Signal history $open = $strategies->getStrategyOpenSignals($strategy->id); $closed = $strategies->getStrategyClosedSignals($strategy->id, '2024-01-01', '2024-01-31'); // Images $strategies->uploadStrategyImage($profileId, $strategy->id, file_get_contents($path), 'logo.png'); $thumbnailUrl = $strategies->getStrategyImageUrl($strategy->id); // public URL, no auth needed // Remove the strategy from the profile $strategies->removeStrategy($profileId, $strategy->id);
CopierService — Copytrade::copiers()
Manage copiers (follower accounts), copy settings, copier signals, and copier images.
| Method | Returns | API Endpoint |
|---|---|---|
getCopiers($profileId) |
CopierDTO[] |
GET /api/profiles/{id}/copiers |
addCopier($profileId, $data) |
CopierDTO |
POST /api/profiles/{id}/copiers |
updateCopier($profileId, $copierId, $data) |
CopierDTO |
PUT /api/profiles/{id}/copiers/{copierId} |
removeCopier($profileId, $copierId) |
bool |
DELETE /api/profiles/{id}/copiers/{copierId} |
getCopierStats($copierId) |
CopierStatsDTO |
GET /api/copiers/{id}/stats |
getCopierStrategies($copierId) |
StrategyDTO[] |
GET /api/copiers/{id}/strategies |
copyStrategy($copierId, $strategyId, $data) |
CopySettingsDTO |
POST /api/copiers/{id}/strategies/{strategyId}/copy-settings |
getCopySettings($copierId, $strategyId) |
CopySettingsDTO |
GET /api/copiers/{id}/strategies/{strategyId}/copy-settings |
updateCopySettings($copierId, $strategyId, $data) |
CopySettingsDTO |
PUT /api/copiers/{id}/strategies/{strategyId}/copy-settings |
stopCopying($copierId, $strategyId, $mode = null) |
bool |
DELETE /api/copiers/{id}/strategies/{strategyId}/copy-settings?mode= |
getCopierOpenSignals($copierId) |
SignalDTO[] |
GET /api/copiers/{id}/signals/open |
getCopierClosedSignals($copierId, $startDate, $endDate) |
SignalDTO[] |
GET /api/copiers/{id}/signals/closed?startDate=&endDate= |
getMissedSignals($profileId, $copierId) |
SignalDTO[] |
GET /api/profiles/{id}/copiers/{copierId}/signals/missed |
uploadCopierImage($profileId, $copierId, $fileContent, $filename) |
array |
PUT /api/profiles/{id}/copiers/{copierId}/image |
getCopierImageUrl($copierId) |
string |
{assets}/images/copiers/thumbnails/{id} (URL builder) |
$copiers = Copytrade::withToken($accessToken)->copiers(); // Create a copier (a follower account that copies strategies) $copier = $copiers->addCopier($profileId, [ 'name' => 'My Copier Account', // required, string 'connection' => [ 'brokerCode' => 'XM', // required, string 'serverCode' => 'XM-Real', // required, string 'username' => '87654321', // required, string (MT login) 'password' => 'MyPassword', // required, string (MT password) ], 'drawdown' => [ 'currentLevel' => 0.0, // required, float 'softStopLevel' => 15.0, // required, float (percentage) 'hardStopLevel' => 25.0, // required, float (percentage) ], ]); // Start copying a strategy $settings = $copiers->copyStrategy($copier->id, $strategyId, [ 'TradeSizeType' => 'Fixed', // required, string (Fixed, Multiplier, Balance) 'TradeSizeValue' => 0.01, // required, float 'IsOpenExistingTrades' => true, // optional, bool (default: false) 'IsRoundUpToMinimumSize' => false, // optional, bool (default: false) ]); // Adjust or stop copying $copiers->updateCopySettings($copier->id, $strategyId, [ 'TradeSizeType' => 'Multiplier', 'TradeSizeValue' => 2.0, 'IsRoundUpToMinimumSize' => true, ]); $copiers->stopCopying($copier->id, $strategyId); // Monitor the copier $stats = $copiers->getCopierStats($copier->id); $copied = $copiers->getCopierStrategies($copier->id); $open = $copiers->getCopierOpenSignals($copier->id); $closed = $copiers->getCopierClosedSignals($copier->id, '2024-05-01', '2024-05-28'); $missed = $copiers->getMissedSignals($profileId, $copier->id);
SectionService — Copytrade::sections()
Retrieve the "Discover" sections used to browse featured strategies.
| Method | Returns | API Endpoint |
|---|---|---|
getSections() |
SectionDTO[] |
GET /api/discover/ |
getSection($code) |
SectionDTO |
GET /api/discover/{code} |
$sections = Copytrade::withToken($accessToken)->sections()->getSections(); $spotlight = Copytrade::withToken($accessToken)->sections()->getSection('Spotlight');
Endpoint Coverage
Every request in the CopyTrade API Postman collection maps to a package method:
| Postman Request | Package Method |
|---|---|
| Get userInfo | profiles()->getUserInfo() |
| Get profile | profiles()->getProfile($profileId) |
| Update profile | profiles()->updateProfile($profileId, $data) |
| Get list of available servers | servers()->getServers() |
| Get copiers connected to profile | copiers()->getCopiers($profileId) |
| Add copier to a profile | copiers()->addCopier($profileId, $data) |
| Update copier | copiers()->updateCopier($profileId, $copierId, $data) |
| Remove copier | copiers()->removeCopier($profileId, $copierId) |
| Get copiers stats | copiers()->getCopierStats($copierId) |
| Get strategies being copied by a copier | copiers()->getCopierStrategies($copierId) |
| Copy strategy | copiers()->copyStrategy($copierId, $strategyId, $data) |
| Get copied strategy settings | copiers()->getCopySettings($copierId, $strategyId) |
| Update copied strategy settings | copiers()->updateCopySettings($copierId, $strategyId, $data) |
| Stop copying a strategy | copiers()->stopCopying($copierId, $strategyId) |
| Get open copied signals | copiers()->getCopierOpenSignals($copierId) |
| Get closed copied signals | copiers()->getCopierClosedSignals($copierId, $start, $end) |
| Get missed signals | copiers()->getMissedSignals($profileId, $copierId) |
| Upload copier image | copiers()->uploadCopierImage($profileId, $copierId, $file, $name) |
| Get copier image | copiers()->getCopierImageUrl($copierId) |
| Get strategies connected to profile | strategies()->getStrategies($profileId) |
| Add strategy to profile | strategies()->addStrategy($profileId, $data) |
| Update strategy | strategies()->updateStrategy($profileId, $strategyId, $data) |
| Remove strategy | strategies()->removeStrategy($profileId, $strategyId) |
| Get strategy | strategies()->getStrategy($strategyId) |
| Get strategy stats | strategies()->getStrategyStats($strategyId) |
| Search for a strategy | strategies()->searchStrategies($filter) |
| Get copiers that are copying a strategy | strategies()->getStrategyCopiers($strategyId) |
| Get strategy open signals | strategies()->getStrategyOpenSignals($strategyId) |
| Get strategy closed signals | strategies()->getStrategyClosedSignals($strategyId, $start, $end) |
| Upload strategy image | strategies()->uploadStrategyImage($profileId, $strategyId, $file, $name) |
| Get strategy image | strategies()->getStrategyImageUrl($strategyId) |
| Get discover sections | sections()->getSections() |
| Get discover section | sections()->getSection($code) |
Data Transfer Objects
All API responses are wrapped in DTOs. Every DTO exposes typed properties for
the common fields plus a rawData array with the complete, untouched API
response, and serializes with toArray() / json_encode():
$profile = Copytrade::withToken($accessToken)->profiles()->getProfile($profileId); $profile->name; // typed accessor $profile->rawData; // full raw API response $profile->toArray(); // array representation
| DTO | Key Properties |
|---|---|
TokenDTO |
accessToken, refreshToken, idToken, expiresIn, expiresAt, tokenType |
UserInfoDTO |
profileId, sub, email, name, givenName, familyName, emailVerified |
ProfileDTO |
id, name, riskProfile, countryCode |
ServerDTO |
id, name |
StrategyDTO |
id, name, riskProfile, fee, connection |
SearchStrategyDTO |
search result fields |
StrategyStatsDTO |
strategy performance statistics |
CopierDTO |
id, name, connection, drawdown |
CopierStatsDTO |
copier performance statistics |
CopySettingsDTO |
trade size type/value and copy flags |
SignalDTO |
rawData (signal payload) |
SectionDTO |
code, title, strategies |
Error Handling
The package throws specific exception types, all extending CopytradeException:
| Exception | When |
|---|---|
AuthenticationException |
Login/token failures, invalid tokens |
NotFoundException |
Resource not found |
ValidationException |
Invalid request data |
RateLimitException |
API rate limit exceeded |
CopytradeException |
Any other API error |
use Asciisd\Copytrade\Exceptions\AuthenticationException; use Asciisd\Copytrade\Exceptions\CopytradeException; use Asciisd\Copytrade\Exceptions\NotFoundException; try { $profile = Copytrade::withToken($accessToken)->profiles()->getProfile($profileId); } catch (AuthenticationException $e) { // Invalid or expired access token — re-login or refresh } catch (NotFoundException $e) { abort(404, 'Profile not found'); } catch (CopytradeException $e) { Log::error('CopyTrade API error', ['message' => $e->getMessage()]); }
Testing Your Application
All services are bound to interfaces in the container, so they are trivial to mock in your application tests:
use Asciisd\Copytrade\Contracts\ProfileServiceInterface; use Asciisd\Copytrade\DTOs\Profile\ProfileDTO; $this->mock(ProfileServiceInterface::class, function ($mock) { $mock->shouldReceive('getProfile') ->with('profile-123') ->andReturn(ProfileDTO::fromResponse([ 'id' => 'profile-123', 'name' => 'Test User', ])); });
Because the services use Laravel's HTTP client internally, you can also fake at the HTTP layer:
use Illuminate\Support\Facades\Http; Http::fake([ 'papi.copy-trade.io/api/servers' => Http::response([ ['id' => 'srv-1', 'name' => 'Demo Server'], ]), ]);
Running the package test suite
composer test
Architecture
src/
├── Config/
│ └── copytrade.php # Configuration
├── Contracts/ # Service interfaces
│ ├── AuthServiceInterface.php
│ ├── SsoServiceInterface.php
│ ├── CopierServiceInterface.php
│ ├── ProfileServiceInterface.php
│ ├── SectionServiceInterface.php
│ ├── ServerServiceInterface.php
│ └── StrategyServiceInterface.php
├── DTOs/ # Typed request/response objects
│ ├── Auth/
│ ├── Copier/
│ ├── Profile/
│ ├── Section/
│ ├── Server/
│ └── Strategy/
├── Events/ # SsoAuthenticated, SsoLoggedOut
├── Exceptions/ # Exception hierarchy
├── Http/
│ ├── Controllers/SsoController.php
│ └── routes.php # Optional SSO routes
├── Facades/
│ └── Copytrade.php
├── Services/ # Service implementations
│ ├── AbstractService.php # Shared HTTP + token handling
│ ├── AuthService.php
│ ├── SsoService.php
│ ├── CopierService.php
│ ├── ProfileService.php
│ ├── SectionService.php
│ ├── ServerService.php
│ └── StrategyService.php
├── Copytrade.php # Main entry point behind the facade
└── CopytradeServiceProvider.php
Each service handles exactly one API domain, every service is consumed through
its interface, and token scoping is immutable (withToken() returns a copy),
so a token from one request can never leak into another.
License
This package is open-sourced software licensed under the MIT license.