Search by

Official Callisto Signal API SDK for PHP

1.3.0 2026-10-14 23:16 UTC

This package is auto-updated.

Last update: 2026-10-19 00:04:57 UTC


README

Official Callisto Signal SDK for PHP 8.1+. Two capabilities in one package:

  • Messaging API — SMS, OTP, WhatsApp, Notify, and balance, over a typed, Guzzle-backed client. See Quick start and Resources.
  • Error tracking — opt-in, Sentry-style exception reporting with a source window on the failing line, plus drop-in Laravel, Symfony and BowPHP integrations. See Error reporting → Framework integration.
  • Logs — ship your application's own log lines to callisto-logs (OTLP/HTTP), with trace correlation and a PSR-3 adapter. See Logs.

Requirements

  • PHP 8.1+ (uses enums, readonly properties, named arguments)
  • ext-json (bundled with PHP)
  • Guzzle ^7.5 — the HTTP transport (installed automatically)

Install

composer require callisto/sdk

Configuration

The client is constructed directly with named arguments. The constructor signature is:

public function __construct(
    ?string $clientId = null,
    ?string $apiKey = null,
    ?string $baseUrl = null,
    float $timeout = 30.0,
    ?GuzzleHttp\Client $httpClient = null,
)
use Callisto\Sdk\Client;

$callisto = new Client(
    clientId: 'your-client-id',
    apiKey: 'your-api-key',
    baseUrl: 'https://api.callistosignal.com/v1', // optional
    timeout: 30.0,                                 // optional, seconds
);

Defaults

Argument Default
baseUrl https://api.callistosignal.com/v1
timeout 30.0 (seconds)
httpClient a fresh GuzzleHttp\Client (inject your own for testing)

Environment-variable fallback — when clientId, apiKey, or baseUrl are omitted (or null), they are resolved from the environment:

Argument Env var
clientId CALLISTO_CLIENT_ID
apiKey CALLISTO_API_KEY
baseUrl CALLISTO_BASE_URL
use Callisto\Sdk\Client;

// Reads CALLISTO_CLIENT_ID / CALLISTO_API_KEY / CALLISTO_BASE_URL.
$callisto = new Client();

If neither the clientId/apiKey arguments nor their env vars are set, the constructor throws InvalidArgumentException. A trailing slash on baseUrl is stripped automatically.

Authentication is HTTP Basic and applied automatically on every request — clientId is the username and apiKey is the password. You never set headers yourself.

Quick start

use Callisto\Sdk\Client;
use Callisto\Sdk\Exception\CallistoException;

$callisto = new Client(clientId: 'your-client-id', apiKey: 'your-api-key');

try {
    // Check your balance
    $balance = $callisto->balance()->get();

    // Send an SMS
    $result = $callisto->sms()->send(
        sender: 'Acme',
        to: '+2250700000000',
        message: 'Welcome to Acme!',
    );

    echo $result['status'];
} catch (CallistoException $e) {
    echo "API error ({$e->getStatusCode()}): {$e->getMessage()}";
}

Resources

Resources are reached through accessor methods on the client: balance(), sms(), otp(), whatsApp(), and notify(). Each accessor returns a cached resource instance.

Reads vs. writes. Write/action endpoints (send, verify, balance) return a raw array<string, mixed> decoded straight from the JSON response. Typed read endpoints (getStatus, getInstance, getMessage, and the list* methods) return typed model objects instead. The per-method tables below state the exact return type.

balance()

get(string $format = 'full', ?string $currency = null): array

Parameter Type Required Description
format string no Response detail level. Defaults to 'full'.
currency ?string no Currency code to express the balance in (optional).

Returns array<string, mixed>.

$balance = $callisto->balance()->get();
$balance = $callisto->balance()->get(format: 'full', currency: 'XOF');

sms()

send(string $sender, string|array $to, string $message, ?string $notifyUrl = null, ?string $scheduledAt = null): array

Parameter Type Required Description
sender string yes Approved sender name, e.g. "Acme".
to string | array<string> yes One recipient (string) or many (array of E.164 numbers).
message string yes Message body.
notifyUrl ?string no Webhook URL for delivery-status callbacks.
scheduledAt ?string no Schedule send time, e.g. "2026-06-02 10:00:00".

Returns array<string, mixed>.

$callisto->sms()->send(
    sender: 'Acme',
    to: '+2250700000000',
    message: 'Your code is 1234',
);

// Multiple recipients + scheduling
$callisto->sms()->send(
    sender: 'Acme',
    to: ['+2250700000000', '+2250700000001'],
    message: 'Sale starts tomorrow!',
    notifyUrl: 'https://acme.example/webhooks/sms',
    scheduledAt: '2026-06-02 10:00:00',
);

list(?string $startedAt = null, ?string $endedAt = null, ?int $page = null, ?int $perPage = null): Paginated

Parameter Type Required Description
startedAt ?string no Filter: start of date range.
endedAt ?string no Filter: end of date range.
page ?int no Page number.
perPage ?int no Items per page.

Returns Paginated of SmsMessage.

$messages = $callisto->sms()->list(page: 1, perPage: 50);
foreach ($messages->items as $msg) {
    echo "{$msg->recipient}: {$msg->status}\n";
}

getStatus(string $messageId): SmsMessage

Parameter Type Required Description
messageId string yes The SMS message ID.

Returns SmsMessage.

$msg = $callisto->sms()->getStatus('msg_abc123');
echo $msg->status;

otp()

send(string $to, string $message, ?string $sender = null, ?int $expiredIn = null, OtpType|string|null $type = null, ?int $digitSize = null, OtpProvider|string|null $provider = null, ?string $instanceCode = null): array

Parameter Type Required Description
to string yes Recipient phone number.
message string yes Message template; typically contains a placeholder for the code.
sender ?string no Sender name.
expiredIn ?int no Code lifetime in seconds.
type OtpType|string no Code character set (digit, alpha, alphanumeric).
digitSize ?int no Number of characters in the code.
provider OtpProvider|string no Delivery channel (sms, whatsapp).
instanceCode ?string conditional WhatsApp instance code. Required when provider is whatsapp — the SDK throws ValidationException otherwise.

Returns array<string, mixed>.

use Callisto\Sdk\Enum\OtpType;
use Callisto\Sdk\Enum\OtpProvider;

// SMS OTP
$otp = $callisto->otp()->send(
    to: '+2250700000000',
    message: 'Your Acme code is {{code}}',
    sender: 'Acme',
    type: OtpType::Digit,
    digitSize: 6,
    expiredIn: 300,
);

// WhatsApp OTP — instanceCode is required
$callisto->otp()->send(
    to: '+2250700000000',
    message: 'Your code is {{code}}',
    provider: OtpProvider::Whatsapp,
    instanceCode: 'inst_abc123',
);

verify(string $otpId, string $code): array

Parameter Type Required Description
otpId string yes The OTP ID from send().
code string yes The code entered by the user.

Returns array<string, mixed>.

$result = $callisto->otp()->verify(otpId: 'otp_abc123', code: '123456');

getStatus(string $otpId): Otp

Parameter Type Required Description
otpId string yes The OTP ID.

Returns Otp.

$otp = $callisto->otp()->getStatus('otp_abc123');
echo $otp->status;

list(?string $startedAt = null, ?string $endedAt = null, ?int $page = null, ?int $limit = null): Paginated

Parameter Type Required Description
startedAt ?string no Filter: start of date range.
endedAt ?string no Filter: end of date range.
page ?int no Page number.
limit ?int no Items per page.

Returns Paginated of Otp.

$otps = $callisto->otp()->list(page: 1, limit: 20);
foreach ($otps->items as $otp) {
    echo "{$otp->recipient}: {$otp->status}\n";
}

whatsApp()

createInstance(string $name, ?string $phoneNumber = null, ?string $webhookUrl = null, ?string $idempotencyKey = null): WhatsAppInstance

Parameter Type Required Description
name string yes Display name for the instance.
phoneNumber ?string no Phone number to bind to the instance.
webhookUrl ?string no Webhook URL for inbound/status events.
idempotencyKey ?string no Key to make instance creation idempotent.

Returns WhatsAppInstance.

$instance = $callisto->whatsApp()->createInstance(
    name: 'Acme Support',
    phoneNumber: '+2250700000000',
    webhookUrl: 'https://acme.example/webhooks/wa',
);
echo $instance->code;

listInstances(int $page = 1): Paginated

Parameter Type Required Description
page int no Page number (default 1).

Returns Paginated of WhatsAppInstance.

$instances = $callisto->whatsApp()->listInstances(page: 1);

getInstance(string $code): WhatsAppInstance

Parameter Type Required Description
code string yes The instance code.

Returns WhatsAppInstance.

$instance = $callisto->whatsApp()->getInstance('inst_abc123');

getQr(string $code): array

Parameter Type Required Description
code string yes The instance code.

Returns array<string, mixed> (the QR-code payload to scan for pairing).

$qr = $callisto->whatsApp()->getQr('inst_abc123');

getStatus(string $code): array

Parameter Type Required Description
code string yes The instance code.

Returns array<string, mixed> (connection/session status of the instance).

$status = $callisto->whatsApp()->getStatus('inst_abc123');

listMessages(string $code, ?string $startedAt = null, ?string $endedAt = null, ?int $page = null, ?int $perPage = null): Paginated

Parameter Type Required Description
code string yes The instance code.
startedAt ?string no Filter: start of date range.
endedAt ?string no Filter: end of date range.
page ?int no Page number.
perPage ?int no Items per page.

Returns Paginated of WhatsAppMessage.

$messages = $callisto->whatsApp()->listMessages('inst_abc123', page: 1, perPage: 50);

getMessage(string $messageId): WhatsAppMessage

Parameter Type Required Description
messageId string yes The WhatsApp message ID.

Returns WhatsAppMessage.

$msg = $callisto->whatsApp()->getMessage('wamsg_abc123');
echo $msg->status;

sendText(string $code, string $to, string $message, ?string $scheduledAt = null): array

Parameter Type Required Description
code string yes The instance code.
to string yes Recipient phone number.
message string yes Text message body.
scheduledAt ?string no Schedule send time.

Returns array<string, mixed>.

$callisto->whatsApp()->sendText(
    code: 'inst_abc123',
    to: '+2250700000000',
    message: 'Hello from Acme!',
);

sendMedia(string $code, string $to, WhatsAppMediaType|string $type, string $mediaUrl, ?string $caption = null, ?string $filename = null, ?string $scheduledAt = null): array

Parameter Type Required Description
code string yes The instance code.
to string yes Recipient phone number.
type WhatsAppMediaType|string yes image, video, document, audio.
mediaUrl string yes URL of the media to send.
caption ?string no Caption text.
filename ?string no Filename (useful for documents).
scheduledAt ?string no Schedule send time.

Returns array<string, mixed>.

use Callisto\Sdk\Enum\WhatsAppMediaType;

$callisto->whatsApp()->sendMedia(
    code: 'inst_abc123',
    to: '+2250700000000',
    type: WhatsAppMediaType::Image,
    mediaUrl: 'https://acme.example/promo.jpg',
    caption: 'Check this out!',
);

sendButtons(string $code, string $to, string $body, array $buttons, ?string $header = null, ?string $footer = null, ?string $scheduledAt = null): array

Parameter Type Required Description
code string yes The instance code.
to string yes Recipient phone number.
body string yes Main message text.
buttons array<int, array<string, mixed>> yes List of button definitions.
header ?string no Header text.
footer ?string no Footer text.
scheduledAt ?string no Schedule send time.

Returns array<string, mixed>.

$callisto->whatsApp()->sendButtons(
    code: 'inst_abc123',
    to: '+2250700000000',
    body: 'Confirm your order?',
    buttons: [
        ['id' => 'yes', 'title' => 'Yes'],
        ['id' => 'no',  'title' => 'No'],
    ],
);

sendLocation(string $code, string $to, float $latitude, float $longitude, ?string $name = null, ?string $address = null, ?string $scheduledAt = null): array

Parameter Type Required Description
code string yes The instance code.
to string yes Recipient phone number.
latitude float yes Latitude.
longitude float yes Longitude.
name ?string no Location name.
address ?string no Location address.
scheduledAt ?string no Schedule send time.

Returns array<string, mixed>.

$callisto->whatsApp()->sendLocation(
    code: 'inst_abc123',
    to: '+2250700000000',
    latitude: 5.3599,
    longitude: -4.0083,
    name: 'Acme HQ',
    address: 'Abidjan, Côte d\'Ivoire',
);

sendList(string $code, string $to, string $body, string $buttonText, array $sections, ?string $header = null, ?string $footer = null, ?string $scheduledAt = null): array

Parameter Type Required Description
code string yes The instance code.
to string yes Recipient phone number.
body string yes Main message text.
buttonText string yes Label for the list-open button.
sections array<int, array<string, mixed>> yes List sections with their rows.
header ?string no Header text.
footer ?string no Footer text.
scheduledAt ?string no Schedule send time.

Returns array<string, mixed>.

$callisto->whatsApp()->sendList(
    code: 'inst_abc123',
    to: '+2250700000000',
    body: 'Pick a plan',
    buttonText: 'View plans',
    sections: [
        [
            'title' => 'Plans',
            'rows'  => [
                ['id' => 'basic', 'title' => 'Basic'],
                ['id' => 'pro',   'title' => 'Pro'],
            ],
        ],
    ],
);

notify()

send(string $topic, ?array $email = null, ?array $sms = null, ?array $mobilePush = null, ?array $webPush = null, ?array $webhook = null, ?array $messaging = null, ?array $realTime = null): array

Multi-channel notification dispatch. At least one event block must be provided — calling send() with only a topic (or with every block empty) throws ValidationException.

Parameter Type Required Description
topic string yes Notification topic / template key.
email array<int, array<string, mixed>> no* Email event block.
sms array<int, array<string, mixed>> no* SMS event block.
mobilePush array<int, array<string, mixed>> no* Mobile-push event block.
webPush array<int, array<string, mixed>> no* Web-push event block.
webhook array<int, array<string, mixed>> no* Webhook event block.
messaging array<int, array<string, mixed>> no* Messaging event block.
realTime array<int, array<string, mixed>> no* Real-time event block.

* Each block is individually optional, but at least one of them must be present and non-empty.

Returns array<string, mixed>.

$callisto->notify()->send(
    topic: 'order.shipped',
    email: [
        ['to' => 'customer@example.com', 'subject' => 'Your order shipped'],
    ],
    sms: [
        ['to' => '+2250700000000', 'message' => 'Your order is on the way!'],
    ],
);

Pagination

List endpoints return a Paginated object — a final readonly value object with these properties:

Property Type Description
items list<Model> The page of typed model objects.
total int Total number of items across all pages.
perPage int Items per page.
currentPage int The current page number.
next ?int Next page number, or null if none.
previous ?int Previous page number, or null if none.
totalPages int Total number of pages.
$page = $callisto->sms()->list(page: 1, perPage: 50);

echo "Page {$page->currentPage} of {$page->totalPages} ({$page->total} total)\n";

foreach ($page->items as $message) {
    // $message is an SmsMessage instance
    echo "{$message->id} → {$message->status}\n";
}

if ($page->next !== null) {
    $next = $callisto->sms()->list(page: $page->next, perPage: 50);
}

Typed models

All models live under Callisto\Sdk\Model, are declared final readonly, and are built via a static fromArray() factory. fromArray() tolerates missing or extra keys: absent values fall back to null (or ''/0 for non-nullable scalars), and unknown keys are ignored — so new API fields will not break deserialization.

Paginated

See Pagination above.

SmsMessage

Property Type
id string
senderName ?string
recipient ?string
content ?string
status ?string
createdAt ?string
updatedAt ?string

Otp (model)

Callisto\Sdk\Model\Otp. This model carries both otpId and id: getStatus() responses populate otpId, while list() rows populate id. Read whichever is relevant to the call you made.

Property Type
otpId ?string
id ?string
status ?string
recipient ?string
expiresAt ?string
verifiedAt ?string
attempts ?int
createdAt ?string

WhatsAppInstance

Property Type
id string
code ?string
clientId ?string
name ?string
phoneNumber ?string
phoneName ?string
status ?string
billingStatus ?string
trialDaysRemaining ?int
monthlyFee ?float
messagesSentToday ?int
messagesSentMonth ?int
dailyLimit ?int
lastMessageAt ?string
webhookUrl ?string
isActive ?bool
createdAt ?string
updatedAt ?string

WhatsAppMessage

Property Type
id string
instanceId ?string
clientId ?string
clientApiId ?string
recipient ?string
recipientName ?string
messageType ?string
content ?string
mediaUrl ?string
mediaMimetype ?string
mediaFilename ?string
extraData ?array<string, mixed>
direction ?string
status ?string
whatsappMessageId ?string
errorCode ?int
errorMessage ?string
retryCount ?int
isBillable ?bool
cost ?float
sentAt ?string
deliveredAt ?string
readAt ?string
scheduledAt ?string
createdAt ?string
updatedAt ?string
processorIdentifier ?string

Enums

All enums live under Callisto\Sdk\Enum and are backed by strings. Anywhere an enum is accepted you may also pass its raw string value.

OtpType

Case Value
Digit 'digit'
Alpha 'alpha'
Alphanumeric 'alphanumeric'

OtpProvider

Case Value
Sms 'sms'
Whatsapp 'whatsapp'

WhatsAppMediaType

Case Value
Image 'image'
Video 'video'
Document 'document'
Audio 'audio'

Error handling

All SDK errors extend Callisto\Sdk\Exception\CallistoException, which exposes:

  • getMessage(): string — the error message (from the API message field when present, else HTTP <status>).
  • getStatusCode(): int — the HTTP status code (0 for transport-level failures).
  • getBody(): mixed — the decoded JSON response body, when available.

HTTP responses are mapped to specific subclasses:

Exception When
AuthenticationException HTTP 401
ValidationException HTTP 400 or 422 (also thrown client-side for invalid input)
NotFoundException HTTP 404
RateLimitException HTTP 429 — adds getRetryAfter(): ?int (seconds, from the Retry-After header)
ApiException Any other >= 400 status
NetworkException Transport failure (DNS, connection, timeout)
use Callisto\Sdk\Exception\AuthenticationException;
use Callisto\Sdk\Exception\ValidationException;
use Callisto\Sdk\Exception\NotFoundException;
use Callisto\Sdk\Exception\RateLimitException;
use Callisto\Sdk\Exception\NetworkException;
use Callisto\Sdk\Exception\CallistoException;

try {
    $callisto->sms()->send(
        sender: 'Acme',
        to: '+2250700000000',
        message: 'Hello!',
    );
} catch (RateLimitException $e) {
    $wait = $e->getRetryAfter() ?? 1;
    sleep($wait);
    // ...retry
} catch (ValidationException $e) {
    echo "Invalid request: {$e->getMessage()}";
    var_dump($e->getBody());
} catch (AuthenticationException $e) {
    echo 'Check your client ID and API key.';
} catch (NotFoundException $e) {
    echo 'Resource not found.';
} catch (NetworkException $e) {
    echo "Network problem: {$e->getMessage()}";
} catch (CallistoException $e) {
    // Catch-all for any remaining API error (ApiException, etc.)
    echo "API error ({$e->getStatusCode()}): {$e->getMessage()}";
}

Error reporting

The SDK ships an opt-in, Sentry-style error reporter that POSTs captured errors to a Callisto error-tracking ingest endpoint. It auto-captures the SDK's own CallistoExceptions (API + network + client-side validation) and exposes a public API so your application can report its own exceptions. Reporting is fully disabled unless a DSN is configured.

Each captured event carries the exception message, type, level, and a normalized stack trace that includes a source window — the failing line plus up to ten lines of surrounding context on each side — so the dashboard highlights exactly where the error occurred. (The window is omitted for the SDK's own transport errors, whose call sites could embed request data — see PII guarantee.)

The first frame is the line that raised the error, inside the function that raised it; a PHP warning or notice turned into an ErrorException by an error handler points at the faulting line, not at the handler. Each frame is flagged in_app unless it lives under vendor/ or in this SDK. The event's culprit (the issue's location, part of how events are grouped) is the first in_app frame, Class->method in path/from/project/root.php; the request route is used only when no frame is in your code.

Delivery is synchronous best-effort (PHP-specific). PHP has no portable background threads in a request context, so the reporter delivers each event inline with a short timeout (2s). Every failure — any exception, any non-202 — is swallowed. Reporting never alters or blocks the original error path, and captureException/captureMessage never throw.

Enabling

Pass a DSN (constructor argument or env var). The DSN is the full ingest URL ({APP_URL}/apps/{id}?key={public_key}):

use Callisto\Sdk\Client;

$callisto = new Client(
    clientId: 'your-client-id',
    apiKey: 'your-api-key',
    errorDsn: 'https://ingest.callistosignal.com/apps/<uuid>?key=<hex>', // enables reporting
    captureUnhandled: true,        // optional, default false — install global handler
    environment: 'production',     // optional, tagged in context.environment
);

Standalone (without the API client)

Don't need the API client (SMS / OTP / WhatsApp)? Use the Callisto static facade to report errors with just a DSN — no clientId / apiKey. Initialise once at boot, then capture from anywhere in the process:

use Callisto\Sdk\Callisto;

Callisto::init(
    dsn: 'https://ingest.callistosignal.com/apps/<uuid>?key=<hex>',
    environment: 'production', // optional
    captureUnhandled: true,    // optional, default false — install global handler
);

Callisto::captureException($throwable, level: 'error', extra: ['feature' => 'checkout']);
Callisto::captureMessage('payment retried', level: 'info');
Callisto::setUser(['id' => 'u-123', 'email' => 'user@example.com']);
Callisto::flush();

Every argument falls back to the same environment variables when omitted, so Callisto::init() with no arguments works once CALLISTO_APP_ERROR_DSN is set. The static methods mirror the instance API one-for-one, and are safe no-ops before init() (or when the DSN is absent) — they never throw. Callisto::reporter() exposes the underlying reporter for advanced use.

Environment variables

When the corresponding argument is omitted (or null), it is resolved from the environment:

Argument Env var Default Meaning
errorDsn CALLISTO_APP_ERROR_DSN none Ingest DSN. Absent (or not a valid URL) → reporting disabled (no-op).
captureUnhandled CALLISTO_CAPTURE_UNHANDLED false Install the global unhandled-exception / fatal handler.
environment CALLISTO_ENVIRONMENT none Optional tag included in context.environment.

Public API

// Report your own exceptions and messages.
$callisto->captureException($throwable, level: 'error', extra: ['feature' => 'checkout']);
$callisto->captureMessage('payment retried', level: 'info');

// Attach user context to subsequent events (pass null to clear).
$callisto->setUser(['id' => 'u-123', 'email' => 'user@example.com']);

// Best-effort flush (no-op for the synchronous PHP reporter).
$callisto->flush();

// Advanced: the reporter itself.
$reporter = $callisto->errorReporter();

level is constrained to fatal | error | warning | info (anything else falls back to error).

Opt-in global handler

When captureUnhandled is true and a DSN is set, the client installs set_exception_handler and a register_shutdown_function (for fatal errors) that report at level fatal. Both chain any previously-registered handler / preserve PHP's default behavior (the exception is still re-raised), so they never clobber existing error handling.

PII guarantee

The reporter never transmits your clientId, apiKey, the Authorization header, or the outgoing request body (which carries phone numbers and message content). Only the server's error response body, status_code, the HTTP method, and the request path may leave the process. This is enforced and covered by tests.

Framework integration

For web apps, prefer wiring the reporter into your framework's exception pipeline instead of the global handler — you get the request method/path, the authenticated user, and a source window on the failing line automatically. Each bridge filters out client-error (4xx) HTTP exceptions (404s, validation, auth redirects) so the tracker isn't flooded with routing noise, and re-raises / re-renders the error exactly as before.

All three read CALLISTO_APP_ERROR_DSN (required to activate) and CALLISTO_ENVIRONMENT (optional) — no clientId/apiKey needed: error tracking is independent of the API client. The frameworks themselves are optional (declared under composer suggest); a bridge is inert unless its framework is installed.

Laravel — zero config. The SDK ships Callisto\Sdk\Framework\Laravel\CallistoServiceProvider, auto-discovered via composer. Set CALLISTO_APP_ERROR_DSN in .env and you're done. It registers a reportable callback, so Laravel's own logging and error pages are untouched. (Override the DSN/environment via a published config/callisto.php with dsn / environment keys.)

Symfony — register the listener for the kernel.exception event:

# config/services.yaml
services:
    Callisto\Sdk\Framework\Symfony\CallistoExceptionListener:
        tags:
            - { name: kernel.event_listener, event: kernel.exception, method: onKernelException }

BowPHP — report from your configured error handler (no middleware). Bow renders uncaught exceptions through the class set as error_handle in config/app.php; add one line to its handle():

use Callisto\Sdk\Framework\Bowphp\CallistoErrorHandler;

class ErrorHandle extends \Bow\Application\Exception\BaseErrorHandler
{
    public function handle($exception): mixed
    {
        CallistoErrorHandler::report($exception); // report to Callisto, then render as usual
        // ... your existing rendering
    }
}

It reads the current request automatically. Optionally configure the integration once at boot (otherwise it builds from env): CallistoErrorHandler::using(CallistoIntegration::fromEnv());

Any framework — the framework-neutral core is reusable directly:

use Callisto\Sdk\Integration\CallistoIntegration;

$callisto = CallistoIntegration::fromEnv();
try {
    $kernel->handle($request);
} catch (\Throwable $e) {
    $callisto->captureUnhandled($e, CallistoIntegration::request($method, $path), $user);
    throw $e;
}

Logs

The SDK can ship your application's own log lines to callisto-logs, without an OpenTelemetry SDK. (Already running an OpenTelemetry SDK or Collector? Keep using it; this is for apps that do not.) Records are queued, batched and sent as OTLP JSON to https://logs.callistosignal.com/v1/logs. A log call never throws.

Do not log passwords, tokens, card numbers or other secrets. The SDK does not scrub what you log, and callisto-logs stores it as received: nothing is masked. Remove secrets and personal data you do not need before logging (platform terms, CGU 7.6).

Create a Logs key

Logs use their own API key, of scope Logs: in the dashboard, Organization → API keys (or Monitoring → Logs → Ingest keys). It has an access key pk_… (32 lowercase hex characters after the prefix) and a secret sk_… (48). A messaging key (clientId / apiKey) does not work for logs, and a Logs key does not work for the messaging API.

Configuration

Each setting is taken from the argument, then from the environment:

Logger argument Client argument Env var Default
accessKey logsAccessKey CALLISTO_LOGS_ACCESS_KEY none
secret logsSecret CALLISTO_LOGS_SECRET none
endpoint logsEndpoint CALLISTO_LOGS_ENDPOINT https://logs.callistosignal.com/v1/logs
serviceName serviceName CALLISTO_SERVICE_NAME, then OTEL_SERVICE_NAME unknown_service
environment environment CALLISTO_ENVIRONMENT none
  • No key and no secret: logs are disabled. Every call is a cheap no-op and nothing is sent.
  • Only one of the two, a value that is not a well-formed pk_ / sk_ key, or an endpoint that is not an absolute http(s) URL: the constructor throws InvalidArgumentException (the same configuration error as missing messaging credentials), so a typo does not silently lose every log.
  • serviceName is sent as the OTel resource service.name, environment as deployment.environment.name.

Quick start

Standalone, no messaging credentials needed:

use Callisto\Sdk\Logs\Logger;

$logs = new Logger(
    accessKey: 'pk_…',          // or CALLISTO_LOGS_ACCESS_KEY
    secret: 'sk_…',             // or CALLISTO_LOGS_SECRET
    serviceName: 'checkout',    // or CALLISTO_SERVICE_NAME
    environment: 'production',  // or CALLISTO_ENVIRONMENT
);

$logs->info('order paid', ['order_id' => 42, 'paid' => true]);
$logs->warn('stock low', ['sku' => 'A-12', 'left' => 3]);
$logs->error('charge failed', ['provider' => 'hub2', 'attempt' => 2]);
$logs->log('debug', 'cart', ['items' => [['sku' => 'A-12', 'qty' => 1]]]);

On the messaging client, the same logger is $client->logs (or $client->logs()). It is a no-op logger when no Logs key resolves, and it never uses clientId / apiKey:

use Callisto\Sdk\Client;

$callisto = new Client(
    clientId: 'your-client-id',
    apiKey: 'your-api-key',
    logsAccessKey: 'pk_…',
    logsSecret: 'sk_…',
    serviceName: 'checkout',
);

$callisto->logs->info('sms sent', ['to_country' => 'CI']);
$callisto->flush(); // also flushes the logs; $callisto->close() flushes then closes the logger

Methods: debug, info, warn, error, fatal (each (message, attributes = [])), log(level, message, attributes = []), flush(), close(), plus isEnabled() and droppedCount() for diagnostics. The message is converted to a string. Attribute values can be strings, booleans, integers, floats, null (omitted), lists and maps (nested up to 5 levels); other values are converted to strings.

Limits: the message is cut to 32 768 characters and each string attribute to 8 192 (the cut ends with …), at most 128 attributes per record.

Trace correlation

Pass trace_id (32 hex characters, not all zeros) and, optionally, span_id (16 hex characters) as attributes. They are moved to the record's traceId / spanId, which ties the log to the trace, and through context.trace.trace_id to the matching error issue. An invalid value stays an ordinary attribute.

$logs->error('payment declined', [
    'trace_id' => '5b8efff798038103d269b633813fc60c',
    'span_id' => 'eee19b7ec3c1b174',
    'order_id' => 42,
]);

PSR-3 adapter

With psr/log installed (it is a suggest, not a requirement; Laravel, Symfony and Monolog already bring it), Callisto\Sdk\Logs\PsrLogger implements Psr\Log\LoggerInterface and forwards to a Logger:

use Callisto\Sdk\Logs\PsrLogger;

$psr = new PsrLogger($logs, 'app'); // the optional name is sent as `logger.name`

$psr->info('User {user} paid {amount}', ['user' => 'awa', 'amount' => 1500]);
$psr->error('charge failed', ['exception' => $e, 'order_id' => 42]);
  • {placeholder}s are interpolated from the context (PSR-3); the context keys are also sent as attributes.
  • context['exception'] (a Throwable) becomes exception.type, exception.message and exception.stacktrace.
  • Levels: debug → debug; info, notice → info; warning → warn; error, critical, alert → error; emergency → fatal. An unknown level is logged as info; the adapter never throws.
  • A record logged into the adapter while it is sending (for example by an HTTP client that logs through the same PSR-3 logger) is dropped, never re-shipped.

Delivery (PHP: synchronous)

PHP has no portable background thread in a request, so the PHP logger differs from the other SDKs:

  • records are queued in memory (at most 2 000; when full, new records are dropped and counted in droppedCount());
  • a batch is sent synchronously when 100 records are queued, on flush() / close(), and from a shutdown function registered once per logger, so a normal request ships its logs when it ends;
  • a request carries at most 500 records or about 1 MB, with a 2 s connect / 3 s total timeout, and is not retried: a failed batch is dropped, so a slow ingest never holds a request for long. One flush() stops sending new batches after 5 s;
  • after a 401 or 403 (bad key, or the plan has no logs) sending pauses 60 s; records logged during the pause are dropped.

In a long-running process (queue worker, daemon), call $logs->flush() at the end of each job, otherwise records wait until 100 are queued or the process exits. Under PHP-FPM, calling fastcgi_finish_request() before the end of the script sends the response first, so the shutdown flush does not delay it.

The logger uses its own HTTP path: it never sends clientId / apiKey to callisto-logs, and never sends the Logs secret anywhere else. Its own failures are swallowed; they never reach the error reporter and are never logged through the logger itself.