Customers
Every billable model can be linked to a Vatly customer. Customers are created automatically by Vatly during checkout and synced to your application via webhooks. You can also create customers explicitly if needed.
Creating a customer
// Create a Vatly customer for this user
$user->createAsVatlyCustomer();
// Create with extra data
$user->createAsVatlyCustomer([
'locale' => 'nl_NL',
'metadata' => ['internal_id' => $user->id],
]);
Checking customer status
// Check if the user has a Vatly customer ID
$user->hasVatlyId(); // bool
// Get the Vatly customer ID
$user->getVatlyId(); // string|null
Retrieving customer data
// Get the full customer object from the Vatly API
$customer = $user->asVatlyCustomer();
How it works
The vatly_id column on your billable model stores the Vatly customer identifier. When you call createAsVatlyCustomer(), it:
- Sends a
POSTrequest to the Vatly API to create a customer - Stores the returned customer ID in the
vatly_idcolumn - Returns the customer response
If the user already has a vatly_id, calling createAsVatlyCustomer() will throw a CustomerAlreadyBoundException.
Automatic customer creation
When a user starts a checkout without an existing Vatly customer ID, Vatly creates the customer automatically during the checkout flow. The customer ID is synced back to your application via webhooks.
This means you don't need to call createAsVatlyCustomer() before starting a checkout — just redirect the user directly.
Anonymous / guest checkouts
A common flow: a visitor buys before they sign up — checkout completes, Vatly fires order.paid / subscription.started webhooks, and your app has no User row to attribute the purchase to yet.
The package handles this without losing data:
- The
vatly_subscriptionsandvatly_orderstables both have a nullableowner_id/owner_typeplus acustomer_idcolumn. The webhook writes the row withowner_id = nullbut populatescustomer_idwith the Vatly customer id (cus_…). - When the visitor returns on the checkout-success redirect, call
claimVatlyCustomerFromReturn()to link those rows to the now-known user.
Learning the checkout on the redirect back: the {CHECKOUT_ID} placeholder
The one hard part of guest checkout is: how does your app know which Vatly customer to claim when the buyer lands back on your site? You don't have to plumb the cus_… through a session or cookie yourself (that approach breaks under multi-tab / double-click — the last write wins, and the buyer may finish the checkout that isn't in the session).
Instead, put the literal {CHECKOUT_ID} placeholder in your return URL. Vatly substitutes it with the checkout's id at creation:
$user->checkout()->create(
items: [['id' => 'product_abc123', 'quantity' => 1]],
redirectUrlSuccess: route('vatly.return').'?checkout_id={CHECKOUT_ID}',
redirectUrlCanceled: route('billing'),
);
On the return route, hand the request to claimVatlyCustomerFromReturn():
public function return(Request $request)
{
// Reads ?checkout_id=…, resolves that checkout's Vatly customer, and
// (if there is one) claims it for the authenticated user.
$request->user()->claimVatlyCustomerFromReturn($request);
return redirect()->route('dashboard');
}
It returns true when a claim happened and false for a missing / unknown checkout id or a checkout with no customer yet — so it's safe to call unconditionally. Pass a second argument to read a different query key:
$user->claimVatlyCustomerFromReturn($request, 'cid'); // reads ?cid=…
This is multi-tab safe by construction: each tab carries its own checkout id in its own redirect URL, so two checkouts in flight resolve independently — there is no shared carrier whose last write wins.
The checkout id travels in the URL, so it may also appear in the
Refererheader and your server logs. It is low-sensitivity — it only names a checkout the buyer just completed — but treat it like any URL parameter.
Under the hood claimVatlyCustomerFromReturn() resolves the checkout's customer id via the Vatly API and then runs claimVatlyCustomer(), which:
- Binds the Vatly customer id to this host entity via the configured
CustomerBindingRepository(default impl: writesvatly_idon the billable table). - Saves the model.
- Backfills
owner_type/owner_idon everyvatly_subscriptionsandvatly_ordersrow that carries the samecustomer_idbut had no owner yet.
The order.paid / subscription.started webhook usually lands before the buyer is redirected back, so those rows already exist (with owner_id = null) by the time you claim — step 3 simply backfills them. The reverse order works too: if the claim runs first, it binds the customer, and a later webhook for the same customer_id finds the binding and writes owner_id directly. Either way the purchase ends up attributed.
If you already hold the cus_… by other means, you can still call claimVatlyCustomer($vatlyCustomerId) directly; it returns the number of rows re-attributed.
Out of scope: buyers who never come back via the redirect link (closed the tab, switched device). Recovering those is the email-based recovery story — a separate concern this helper does not cover.
Need the email / name on the customer to surface a "would you like to claim purchase X?" prompt at signup? Fetch the customer via the Vatly API:
$customer = app(\Vatly\Fluent\Vatly::class)
->customers()
->findByVatlyCustomerId($vatlyCustomerId);
$customer->email;
$customer->name;