# PHP and Laravel server SDK

URL: https://traqio.app/docs/sdks/php
Last updated: 2026-10-16

Report sign-ups and purchases from a PHP or Laravel backend with traqio/traqio-php, and tie each one to the ad click it came from. Auto-discovered in Laravel.

`traqio/traqio-php` is the Traqio SDK for PHP and Laravel backends — the server half of Traqio's web SDK. The conversion that counts on a website — a sign-up, a subscription, a purchase — happens on your backend, where the user id is known. This package reports it with the **trackID**, the ad click id that [@traqio/web](https://traqio.app/docs/sdks/web) kept in first-party storage since the landing page, and Traqio credits the campaign that brought the user — [deterministically](https://traqio.app/docs/concepts/web-attribution), by that click's id. No dependency beyond `ext-curl`; auto-discovered in Laravel.

## Requirements

- PHP 8.1 or later, with `ext-curl` and `ext-json`. Laravel 12 or 13 for the auto-discovered integration; plain PHP, Symfony or any PSR-7 framework otherwise.
- A **server key** for the product (`tq_sk_…`). Neither the app's SDK key nor a web key works here.
- [@traqio/web](https://traqio.app/docs/sdks/web) on the site, and ads pointing at [tracking links](https://traqio.app/docs/concepts/tracking-links) with a website destination — that is how the trackID reaches your sign-up form.

## Set up

### Create a server key

In the console, open **Integrations → SDK** and create a **server key**. It is shown once: put it in `.env`, never in code or in a page. Traqio keeps only a fingerprint of it; a lost key is replaced by a new one, and the old one revoked.

.env:
```bash
TRAQIO_SECRET_KEY=tq_sk_...
```

### Install the package

```bash
composer require traqio/traqio-php
```

Until the package is on Packagist, add the folder we send you as a path repository:

```bash
# the folder you received, committed inside the backend's repository
composer config repositories.traqio path ./vendor-src/traqio-php
composer require traqio/traqio-php:@dev
```

### Configure the client

**Laravel**: nothing to write — the service provider and the `Traqio` facade are discovered, and read `TRAQIO_SECRET_KEY`. To change the endpoint, the timeout or the retries, publish the config file:

```bash
php artisan vendor:publish --tag=traqio-config
```

**Plain PHP**: one client, built once.

```php
use Traqio\Traqio;

$traqio = new Traqio(getenv('TRAQIO_SECRET_KEY'));
```

### Report the sign-up with its trackID

Call `signup()` once, where the account is created. @traqio/web writes the trackID in the `traq_click` cookie on your own domain, so the browser sends it with the sign-up request; `TrackId::fromRequest()` reads it.

Laravel:
```php
use Illuminate\Http\Request;
use Traqio\Laravel\Facades\Traqio;
use Traqio\TraqioException;
use Traqio\TrackId;

public function store(Request $request)
{
    $user = User::create($request->validated());

    try {
        // Reads the raw Cookie header: Laravel's cookie decryption would drop traq_click.
        Traqio::signup((string) $user->id, TrackId::fromRequest($request));
    } catch (TraqioException $e) {
        report($e); // never fail the sign-up itself
    }

    return redirect('/welcome');
}
```

PHP:
```php
use Traqio\TrackId;

$user = $users->create($_POST);
$result = $traqio->signup((string) $user->id, TrackId::fromRequest()); // the current request
```

In Laravel, to keep the call out of the request, run it after the response is sent:

```php
dispatch(fn () => Traqio::signup((string) $user->id, $trackId))->afterResponse();
```

If the backend is on another domain than the site, the cookie does not reach it: send `Traqio.getTrackId()` from the browser with the form instead. The sign-up is the moment the user is **acquired**; the last click before it, inside your [attribution window](https://traqio.app/docs/concepts/attribution-windows), gets the credit.

### Track the conversions

```php
Traqio::track((string) $user->id, 'purchase', [
    'value' => 49,
    'currency' => 'EUR',
    'transactionId' => $paymentIntent->id, // one payment, one row — whoever reports it
    'store' => 'stripe',
]);
```

Conversions follow the user: they are credited to the campaign that acquired them.

## What the response tells you

```php
$result = Traqio::signup((string) $user->id, $trackId);

$result->trackId;                 // "matched" | "unknown" | "expired" | "claimed" | "refused" | null
$result->acquired;                // true: the sign-up is recorded
$result->attribution->network;    // "meta", "tiktok"… or "organic"
$result->attribution->campaignId; // the campaign that brought the user
```

| trackId | Meaning |
| --- | --- |
| matched | The click is now this user's. A retry answers matched again. |
| unknown | No click of this product has that id — a typo, another product's link, or a value @traqio/web did not write. |
| expired | The click is older than the product's attribution window. |
| claimed | Another user, or an app install, already owns that click. A click credits one identity. |
| refused | The click failed an anti-fraud rule (it came through an anonymising network). It is counted, never credited. |

## What it does today, and what it does not

|  | Today |
| --- | --- |
| Tie a sign-up to its click, and say who was credited | Yes — in the response |
| Record purchases with value, currency and payment id, deduplicated | Yes |
| Leave your apps' installs, revenue and ROAS untouched | Yes — never attached to an app install |
| Show web conversions in the console's reports | Yes — per campaign, beside installs |
| Send web conversions to Meta or TikTok | Not yet |

Conversions sent here appear in **Live events**, marked as not matched to an install — expected for a website. A sign-up credited to a campaign is counted in the ROAS report as a web conversion of that campaign, beside its installs, and the response of each call gives you the attribution at once.

## Errors and retries

A network error, a timeout, a `429` or a `5xx` is retried — twice by default, with the same body, so nothing is counted twice. A `4xx` (wrong or revoked key, invalid payload) is not retried. Both end as a `Traqio\TraqioException`:

```php
use Traqio\TraqioException;

try {
    Traqio::signup((string) $user->id, $trackId);
} catch (TraqioException $e) {
    // $e->status: 401 = wrong or revoked key, 400 = invalid payload, null = network
    Log::warning($e->getMessage(), ['status' => $e->status, 'body' => $e->body]);
}
```

> **Report after your own write** Call Traqio once the account or the payment is saved, and never let a Traqio error fail it.

## API reference

config/traqio.php:
```php
// config/traqio.php
return [
    'secret_key'  => env('TRAQIO_SECRET_KEY'),                       // required
    'endpoint'    => env('TRAQIO_ENDPOINT', 'https://api.traqio.app'),
    'timeout'     => (float) env('TRAQIO_TIMEOUT', 10),               // seconds per attempt
    'max_retries' => (int) env('TRAQIO_MAX_RETRIES', 2),              // network error, 429, 5xx
];
```

| Method | What it does |
| --- | --- |
| signup($userId, $trackId = null, $events = []) | Reports the sign-up — the acquisition — with its trackID. The first one is kept. |
| track($userId, $name, $properties = []) | Records one conversion: value, currency, transactionId, store, eventId, trackId. |
| send($input) | The raw call behind both, for several events at once. |
| TrackId::fromRequest($request = null) | The trackID from a Laravel, Symfony or PSR-7 request — or the current PHP request — or null. |

In plain PHP the same options are the constructor's second argument: `endpoint`, `timeout`, `max_retries`, and `transport` to route requests through your own HTTP client.

> **Proof so far** Tested with PHPUnit — the Laravel integration on Laravel 12 and 13, the client on PHP 8.1 and 8.5 — and run end to end against a running Traqio API: a sign-up carrying a trackID came back matched with its campaign, a second user claiming the same click was told claimed, a wrong key was refused. Not yet run in a customer's production backend.

## Frequently asked questions

**Why does $request->cookie('traq_click') return null in Laravel?**
Laravel encrypts its cookies and decrypts them on every request; a cookie written by JavaScript — the one @traqio/web sets — fails that decryption, so Laravel hands back null. TrackId::fromRequest reads the raw Cookie header instead, which the decryption never touches. You do not need to add traq_click to the EncryptCookies exceptions.

**Which Laravel versions are supported?**
Laravel 12 and 13, through package auto-discovery: the service provider, the config file and the Traqio facade register themselves. The client itself is plain PHP 8.1+ and works in Symfony, Slim or any PSR-7 application.

**What if the visitor did not come from an ad?**
TrackId::fromRequest returns null and the sign-up is still recorded, as organic: no campaign is credited.

**Will a retry count a purchase twice?**
No. Each call mints its event ids once and retries the identical body, and a purchase is also keyed on its transactionId, so the same payment reported twice is stored once.
