Adding Try-On to a Laravel Store
By the ZeDaq.ai team · · 4 min read
On this page
To add try-on to a Laravel store, create a route and controller that call the ZeDaq.ai API with Laravel's HTTP client, and keep your x-api-key in .env so it never reaches the browser. The product page then posts the shopper's photo to your route and shows the image your controller returns.
This guide assumes you already know the API basics from the virtual try-on API guide for developers. It works for a custom Laravel shop and for Laravel-based platforms such as Bagisto, as long as you can add a route and a view.
How should the request flow work?
The browser never talks to ZeDaq.ai directly. Your server sits in the middle.
- The shopper picks a photo on the product page.
- The page sends the photo and the product ID to your Laravel route.
- The controller loads the product image, calls
POST /api/v1/tryonand returns the result.
This keeps the key private. The API reference says to keep server keys on your server and lock keys to your domains.
How do you store the API key?
Add the key to .env, then read it through a config file so it works with config caching.
// .env
ZEDAQ_API_KEY=your-key-here
// config/services.php
'zedaq' => [
'key' => env('ZEDAQ_API_KEY'),
'base' => 'https://www.zedaq.ai',
],
Never call env() directly inside controllers. After you run php artisan config:cache, env() can return null outside config files.
What does the controller look like?
The controller below validates the upload, builds the JSON body and sends it with the Http facade. It assumes your product model has an image URL.
<?php
namespace App\Http\Controllers;
use App\Models\Product;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;
class TryOnController extends Controller
{
public function store(Request $request, Product $product)
{
$request->validate(['photo' => 'required|image|mimes:jpeg,png']);
$base = config('services.zedaq.base');
$headers = ['x-api-key' => config('services.zedaq.key')];
$personB64 = base64_encode(file_get_contents($request->file('photo')->getRealPath()));
$productB64 = base64_encode(Http::get($product->image_url)->body());
$response = Http::withHeaders($headers)->post($base . '/api/v1/tryon', [
'person_image_base64' => $personB64,
'product_image_base64' => $productB64,
'garment_type' => $product->garment_type, // upper_body, lower_body or dresses
]);
// 202 means the render is still running: poll until it finishes.
$tries = 0;
while ($response->status() === 202 && $tries++ < 30) {
sleep(2);
$response = Http::withHeaders($headers)->get($base . $response->json('poll_url'));
}
return response()->json($response->json(), $response->status());
}
}
Register the route in routes/web.php or routes/api.php, and protect it with Laravel's throttle middleware so one visitor can't use up your allowance.
Route::post('/products/{product}/tryon', [TryOnController::class, 'store'])
->middleware('throttle:10,1');
How do you handle the 202 response?
A 202 means the render is not finished. The API reference says to poll the poll_url every 2 seconds, which is what the loop above does.
On a busy store, holding a PHP request open for this long ties up a worker. A better pattern is to dispatch a queued job, return the job_id right away and let the page poll your own status route. For another PHP example of the same flow, see calling the API from plain PHP.
How do you call it from the product page?
Add a file input and a few lines of JavaScript to your Blade view. Send the photo as FormData, include the CSRF token, and show the returned image.
<input type="file" id="tryon-photo" accept="image/jpeg,image/png">
<img id="tryon-result" alt="You wearing this garment" hidden>
<script>
document.getElementById('tryon-photo').addEventListener('change', async (e) => {
const body = new FormData();
body.append('photo', e.target.files[0]);
const res = await fetch('{{ route("tryon.store", $product) }}', {
method: 'POST',
headers: { 'X-CSRF-TOKEN': '{{ csrf_token() }}' },
body,
});
const data = await res.json();
if (!res.ok) { alert(data.message); return; }
const img = document.getElementById('tryon-result');
img.src = `data:${data.mime_type};base64,${data.image_base64}`;
img.hidden = false;
});
</script>
Name the route tryon.store in your route file for this to work. Tell shoppers how their photo is used near the upload field. The API returns an error code and a message, so show the message and log the code.
What should you test before launch?
Work through this short list:
- A normal full-length photo returns an image.
- A wrong file type is rejected by validation.
- A missing product image gives a clear error, not a blank page.
- The throttle blocks rapid repeat requests.
- The key is absent from page source and network responses.
The same server pattern works in other stacks. If your front end is a JavaScript framework, the guide to Vue and Nuxt storefronts shows the browser side, and the Django version shows the same flow in Python. For background on where try-on fits in the wider shift, read about generative AI in fashion ecommerce.
Get an API key for your Laravel store
FAQs
Do I need a Laravel package for this?
No. The built-in Http facade is enough. Any backend that can send an HTTPS request with a header works.
Can I use the widget instead?
Yes. If you only need a button, the script-tag widget skips the controller. For a custom interface, use the REST API through your own route.
Does a failed render use up my allowance?
The API reference says usage is charged only when a render succeeds.
How big should the shopper's photo be?
The API reference suggests a full-length photo around 800px wide. Resizing in the browser keeps the upload small.
Try ZeDaq.ai free
Virtual try-on and Find my size for your store, plus client, order and invoicing tools for tailors, in one subscription.


