target365/sdk
PHP SDK for Target365’s messaging platform, making it easy to send SMS and RCS messages, manage contacts and subscriptions, and handle delivery reports from Laravel or any PHP app via a clean, PSR-compatible API.
Target365 is a Norwegian SMS gateway (with premium-SMS/Strex payment support). This SDK is a thin PHP client over their REST API — not a Laravel package. There is no service provider, no facade, no config to publish. Plan to wire it up yourself.
1. Install
composer require target365/sdk
The last release was 2025-03-11, so pin a version ("target365/sdk": "^2.0" or whatever the README shows) and commit composer.lock.
2. Add credentials to config/services.php
'target365' => [
'base_url' => env('TARGET365_BASE_URL', 'https://test.target365.io'),
'key' => env('TARGET365_KEY'),
'secret' => env('TARGET365_SECRET'),
],
Target365 uses an API key/secret pair that the SDK signs per-request; both test and api (production) hosts exist with different credentials.
3. Look at first in the README
Read the constructor signature for Target365Client and the Models\OutboundMessage class — those two define how you actually send anything. Most of the rest of the SDK is DTOs (StrexTransaction, Keyword, Content, etc.) that mirror the REST payloads.
4. First use case: send an SMS
use Target365\Sdk\Target365Client;
use Target365\Sdk\Models\OutboundMessage;
$client = new Target365Client(
config('services.target365.base_url'),
config('services.target365.key'),
config('services.target365.secret'),
);
$message = new OutboundMessage();
$message->recipient = '+4799999999'; // E.164
$message->content = 'Thanks for your order #1234';
$message->transactionId = (string) Str::uuid(); // your idempotency key
$message->sender = 'MyShop';
$client->send($message); // adjust to README — often ->outbound->send(...) or ->sendSms(...)
Once a single SMS lands on a real handset in test mode, you have enough to build everything else.
Bind as a singleton in a service provider
// AppServiceProvider::register()
$this->app->singleton(Target365Client::class, fn () => new Target365Client(
config('services.target365.base_url'),
config('services.target365.key'),
config('services.target365.secret'),
));
Wrap it behind your own interface (SmsGateway) so tests and non-Target365 paths can fake it without depending on SDK internals.
Send from a queued job, never the request cycle
Sending takes hundreds of milliseconds and can fail. Put it in a job:
final class SendSms implements ShouldQueue
{
use Queueable;
public function __construct(public string $to, public string $body) {}
public function handle(Target365Client $client): void
{
$msg = new OutboundMessage();
$msg->recipient = $this->to;
$msg->content = $this->body;
$msg->transactionId = $this->job->uuid(); // stable per job, safe on retry
$client->send($msg);
}
public function backoff(): array { return [10, 60, 300]; }
public function retryUntil() { return now()->addHours(6); }
}
The transactionId is your friend: use a deterministic value (Str::uuid() stored before dispatch, or the job UUID) so a queued retry after a timeout doesn't double-send.
Use it for the whole messaging workflow
Typical flows: OTPs, order/shipping notifications, appointment reminders, and premium-SMS/Strex payments. Strex has its own transaction model and a callback flow — treat that as a separate, more careful integration than plain outbound SMS.
Receive callbacks on a public endpoint
Delivery reports and inbound replies are pushed to a URL you configure in the Target365 portal. Build a Route::post('/webhooks/target365', ...) that is api.php-style, no CSRF, no auth middleware — and verify the signature yourself. Look up the message by your own transactionId, not by Target365's internal ID, so your system can find the original record.
Test by mocking the transport
If the SDK accepts an injected GuzzleHttp\Client or PSR-18 client, use MockHandler:
$mock = new MockHandler([new Response(200, [], '{"id":"abc"}')]);
$client = new Target365Client(new GuzzleClient(['handler' => HandlerStack::create($mock)]), ...);
If it doesn't, don't fight it — resolve your SmsGateway interface to a fake in tests and only exercise the SDK in one integration test.
ServiceProvider, no publish, no facade. If you find a tutorial referencing Target365::send(...), it doesn't exist here.https://test.target365.io is right for local and CI; make sure TARGET365_BASE_URL is set per environment, and make sure prod credentials never leak into test config.Target365Client, OutboundMessage, send() vs. outbound->send() differ across tags. Pin the version and confirm method names against the README for that tag, not the latest docs on the web.+47… for Norway. Validate on the way in; Target365 will reject 99999999 and the error surfaces late.æøå or emoji flips to UCS-2 and cuts the limit to ~70. Segments cost money — validate lengths and warn users in the UI, otherwise a "short" SMS quietly becomes 3 parts.4xx. Validate, parse, and log the error body — 401 is auth, 400 is malformed payload, 429 is rate limit (retry with backoff), 5xx is transient. Blind ->retry(3) in the job will triple-bill you on a 400.200 from send means "accepted", not "delivered". Track delivery reports via webhook and persist state (sent → delivered / failed) against your transactionId.sendTime is UTC ISO-8601. If you schedule messages in local time you'll be off by one or two hours (CET/CEST). Use now()->utc()->toIso8601String().200 quickly — enqueue the actual processing. Target365 will retry failed callbacks.SmsGateway abstraction so you can swap or fork later.transactionId so support tickets can be traced end-to-end.How can I help you explore Laravel packages today?