Skip to content

decisa-ai/decisa-php

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Decisa PHP SDK

The official PHP SDK for the Decisa server-side ingest API. Record conversions (sales, signups, leads, refunds…) and events (PageView, Purchase…) from your backend with a typed, framework-agnostic client.

  • Framework-agnostic — built on PSR-18 / PSR-17, auto-discovers your HTTP client. Works in plain PHP, Laravel, Symfony, Slim, anywhere.
  • Money guardvalue_cents is integer-only; passing a decimal is rejected before it ever hits the wire (the #1 install footgun).
  • Typed errors — every API failure maps to a specific exception you can catch.
  • Safe retries — automatic exponential backoff with jitter on network errors, 5xx, and 429 (honoring Retry-After). Duplicate sends are idempotent, so retries can never double-count.
  • Secret-safe — your API key is kept private and never appears in an exception message, log line, or __toString().

Requirements

  • PHP 8.2+
  • A Decisa API key (starts with dcs_ak_) with the conversions:write and/or events:write scope.

Install

composer require decisa/decisa-php

The SDK depends only on PSR HTTP abstractions and auto-discovers a PSR-18 client. If your project doesn't already ship one, add Guzzle (or any PSR-18 client):

composer require guzzlehttp/guzzle

Quickstart — record a sale

use Decisa\Client;

$decisa = new Client(getenv('DECISA_API_KEY')); // dcs_ak_...

$result = $decisa->conversions()->record([
    'type'        => 'sale',
    'externalId'  => 'order-1001',   // your idempotency key — required
    'valueCents'  => 1999,           // R$19.99 as INTEGER cents
    'currency'    => 'BRL',
    'visitorId'   => $_COOKIE['dcs_vid'] ?? null,
    'customerEmail' => 'buyer@example.com', // hashed server-side, raw never stored
]);

$result->id();          // "conv_abc123"
$result->externalId();  // "order-1001"
$result->valueCents();  // 1999
$result->attributed();  // true if Decisa matched it to a click/campaign
$result->raw();         // full decoded `data` payload

Use the cents() helper for major-unit amounts

value_cents must always be an integer. To convert a decimal price safely, use Money::cents() (rounds half-up):

use Decisa\Money;

Money::cents(19.99);  // 1999
Money::cents('49.50'); // 4950

$decisa->conversions()->record([
    'type'       => 'sale',
    'externalId' => 'order-1002',
    'valueCents' => Money::cents($order->total), // never pass the float directly
    'currency'   => 'USD',
]);

Passing a float straight into valueCents is rejected before any network call:

$decisa->conversions()->record([
    'type'       => 'sale',
    'externalId' => 'order-1003',
    'valueCents' => 19.99, // throws Decisa\Exception\InvalidArgumentException
]);

Typed input objects (optional)

Arrays are ergonomic, but you can build fully typed input with enums:

use Decisa\Input\ConversionInput;
use Decisa\Enum\ConversionType;
use Decisa\Money;

$input = new ConversionInput(
    type:       ConversionType::Sale,
    externalId: 'order-1004',
    valueCents: Money::cents(19.99),
    currency:   'BRL',
);

$decisa->conversions()->record($input);

Recording events

use Decisa\Client;

$decisa = new Client(getenv('DECISA_API_KEY'));

$decisa->events()->record([
    'eventId'   => 'evt-9f3a1c20',   // your idempotency key — required, 8..64 chars
    'eventName' => 'Purchase',       // PageView, ViewContent, AddToCart, Lead, Purchase, ...
    'valueCents'=> 4999,
    'currency'  => 'USD',
    'visitorId' => $_COOKIE['dcs_vid'] ?? null,
    'url'       => 'https://shop.example.com/thank-you',
]);

Events accept multiple pixel keys. Pass an array and the SDK joins it into the comma-separated string this endpoint expects:

$decisa->events()->record([
    'eventId'   => 'evt-aa11bb22',
    'eventName' => 'PageView',
    'pixelKeys' => ['pk_main', 'pk_remarketing'], // sent as "pk_main,pk_remarketing"
]);

Note: /v1/conversions takes pixel_keys as a JSON array, while /v1/events takes it as a comma-separated string. The SDK handles each correctly for you — always pass an array.

Error handling

Every failure maps to a typed exception. All extend Decisa\Exception\DecisaException and carry ->httpStatus(), ->code(), ->message, and ->requestId() (when the server provides one).

use Decisa\Exception\DecisaValidationException;
use Decisa\Exception\DecisaAuthException;
use Decisa\Exception\DecisaForbiddenException;
use Decisa\Exception\DecisaRateLimitException;
use Decisa\Exception\DecisaServerException;
use Decisa\Exception\DecisaException;

try {
    $decisa->conversions()->record([
        'type'       => 'sale',
        'externalId' => 'order-1005',
        'valueCents' => 1999,
        'currency'   => 'BRL',
    ]);
} catch (DecisaValidationException $e) {
    // 422 — show the per-field messages back to the operator.
    foreach ($e->fields() as $field => $messages) {
        echo "$field: " . implode(', ', $messages) . PHP_EOL;
    }
} catch (DecisaAuthException $e) {
    // 401 — the API key is missing, malformed, unknown, revoked, or expired.
} catch (DecisaForbiddenException $e) {
    // 403 — valid key, but not allowed. Use ->code() to tell which:
    if ($e->code() === 'INSUFFICIENT_SCOPE') {
        // the key lacks conversions:write / events:write
    } elseif ($e->code() === 'ENTITLEMENT_MISSING') {
        // the workspace hasn't activated attribution
        // $e->details()['capability'] === 'attribution'
    }
} catch (DecisaRateLimitException $e) {
    // 429 — retries were exhausted. Back off and retry later.
    $waitSeconds = $e->retryAfter(); // ?int, from the Retry-After header
} catch (DecisaServerException $e) {
    // 5xx — already retried; treat as transient and queue for later.
} catch (DecisaException $e) {
    // catch-all (e.g. a network error after retries).
}
Exception HTTP Notes
DecisaValidationException 422 ->fields() returns ['<field>' => ['msg', …]]
DecisaAuthException 401 key missing / invalid / revoked / expired
DecisaForbiddenException 403 ->code() is INSUFFICIENT_SCOPE or ENTITLEMENT_MISSING
DecisaRateLimitException 429 ->retryAfter() returns ?int seconds
DecisaServerException 5xx transient
InvalidArgumentException client-side guard (e.g. a float value_cents); thrown before the wire

Retries & idempotency

The SDK automatically retries on network errors, 5xx, and 429 with exponential backoff plus jitter (honoring the Retry-After header on 429). It never retries other 4xx responses — those are deterministic client errors.

This is safe because the API is idempotent:

  • Conversions dedupe on external_id. Re-sending the same external_id returns the stored record and still responds 201 — a duplicate is not an error.
  • Events dedupe on event_id. Re-sending the same event_id returns the stored record and still responds 202.

So you can safely retry a request whose response you never received without double-counting. Always reuse a stable external_id / event_id per logical event.

Configuration

use Decisa\Client;

$decisa = new Client('dcs_ak_...', [
    'base_url'    => 'https://api.decisa.ai', // default
    'timeout'     => 10.0,                     // seconds, default 10
    'max_retries' => 2,                        // default 2

    // Optional: inject your own PSR-18 client / PSR-17 factories.
    // 'http_client'     => $psr18Client,
    // 'request_factory' => $psr17RequestFactory,
    // 'stream_factory'  => $psr17StreamFactory,
]);

Testing against the SDK

Inject a PSR-18 client (for example Guzzle's MockHandler) so no real network calls are made:

use Decisa\Client;
use GuzzleHttp\Client as GuzzleClient;
use GuzzleHttp\Handler\MockHandler;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Psr7\Response;

$mock = new MockHandler([
    new Response(201, [], json_encode([
        'data' => ['conversion' => ['id' => 'conv_test', 'external_id' => 'order-1', 'attributed' => false]],
        'meta' => null,
        'error' => null,
    ])),
]);

$guzzle = new GuzzleClient(['handler' => HandlerStack::create($mock), 'http_errors' => false]);
$decisa = new Client('dcs_ak_test', ['http_client' => $guzzle]);

$result = $decisa->conversions()->record(['type' => 'sale', 'externalId' => 'order-1']);

Laravel bridge (planned)

A Laravel bridge (service provider + facade for auto-wired config and a Decisa::conversions() facade) is a planned fast-follow. The core SDK here stays framework-agnostic; the bridge will be a thin, optional package on top of it.

License

MIT. See LICENSE.

About

Official PHP SDK for the Decisa ingest API (conversions + events), PSR-18 framework-agnostic

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages