getcandy/candy-api
A Laravel package that adds a flexible REST API to GetCandy, exposing products, carts, orders, customers, and checkout through consistent JSON endpoints. Ideal for building headless storefronts and third-party integrations.
This is the HTTP/data layer of GetCandy v1: Eloquent models (GetCandy\Models\*), repositories/services (GetCandy\Api\Core\*), resources, middleware and the REST routes that the admin UI and any storefront talk to. You install it when you need an e-commerce backend API, not when you want a ready-made UI.
composer require getcandy/candy-api on a Laravel 8 app (PHP 7.4/8.0).php artisan vendor:publish --provider="GetCandy\Api\CandyApiServiceProvider"
php artisan migrate
php artisan db:seed # base currency, tax zones, shipping, sample catalogue
php artisan passport:install (or Sanctum if your fork has swapped it).php artisan route:list --path=api/v1 — you should see products, carts, orders, customers, addresses, checkout, etc.Where to look first
config/getcandy.php — prefix, middleware, auth guard, model bindings. Published config, not vendor files, is where you change behaviour.src/Http/Controllers and src/Http/Resources — the contract your frontend actually sees.src/Core/ — repositories like ProductRepository, CartRepository, OrderRepository; these are what you bind/inject on the PHP side.src/Models — Product, ProductVariant, Cart, CartLine, Order, Customer, Address, Price, Attribute.First use case — a minimum cart flow against the API:
POST /api/v1/carts # returns cart id
POST /api/v1/carts/{id}/lines # { "product_variant_id": 1, "quantity": 2 }
GET /api/v1/carts/{id} # totals in cart meta
That single flow forces you through the three things that matter in v1: variant/pricing resolution, cart recalculation, and the wrapped response envelope.
Decoupled storefront against /api/v1. This is the intended use. Point Nuxt/Next/React at the API with axios, keep the base URL in env, and write one response interceptor that unwraps { data, meta, links } — every endpoint uses the same envelope, so this is worth doing on day one.
Same-process usage instead of HTTP. If your Laravel app is the storefront, skip HTTP and resolve the repositories:
$products = app(\GetCandy\Api\Core\Products\ProductRepository::class)->getPaginated();
$cart = app(\GetCandy\Api\Core\Cart\CartManager::class)->resolve($cartId);
Same code path, no serialization overhead, and you can wrap it in your own Blade/Livewire layer.
Cart-as-session object. Persist the cart id in a cookie or the session, not the cart itself. Re-fetch on each request so totals, tax and shipping stay authoritative. After any line mutation, recalculate before reading totals — totals are stored, not computed on read.
Extend, don't edit vendor. Two supported seams:
App\Models\Product extends \GetCandy\Models\Product) so you can add casts, scopes, accessors. Add columns through your own migration; never touch package migrations.Core interface and rebind it in a service provider. This is how you swap in custom pricing logic, stock checks, or an external search index without forking.Checkout orchestration. The package gives you the primitives (cart → addresses → shipping method/price → tax → order), not a single /checkout call. Treat it as a state machine in your frontend: validate shipping address → select shipping option → select payment → POST /orders. Keep order creation in one transaction and treat payment as a separate concern.
Inclusion and sparse responses. Use the include query param (?include=variants.prices,assets) and ?filter[...] on listings rather than fetching per-item. The resources are built for this; abusing it is the main source of N+1s.
Integration surface for admin. getcandy/candy-admin is just a Vue SPA over this API — anything the admin can do, your own tooling can do. Useful when you need a custom back-office screen: replicate the request, don't add a bespoke controller.
Testing. RefreshDatabase, seed the minimal catalogue fixture, and assert against the JSON contract ($this->getJson('/api/v1/products')->assertJsonStructure([...])). Because the envelope and resources are stable, contract tests survive refactors well.
It is effectively EOL. Last release September 2021, Laravel 8 era. It will fight you on PHP 8.1+/Laravel 9+. Lunar (lunarphp/lunar) is the maintained successor and a direct conceptual port — if this is a new build, that's the decision to make before writing code, not after.
Passport keys. Auth routes 500 with an opaque error if passport:install hasn't run or storage/*.key is missing. Check this before debugging "login doesn't work".
Stale totals. Cart caches totals/tax/shipping. Mutating a line without triggering recalculation gives you a cart that "didn't update". Recalculate explicitly after line add/update/remove and after any shipping or discount change.
Currency and price nulls. Prices are per-currency rows. A cart with no currency set, or a variant with no price for that currency, returns null totals rather than an error — which then silently breaks checkout. Set currency on cart creation and validate prices at the product level.
Config before code. Prefix, middleware stack, and auth guard all come from the published config. Changing the prefix without updating frontend base URLs (and CORS) is the classic first-day foot-gun.
CORS. The package assumes a separate origin. If you're calling from a SPA, configure config/cors.php for /api/* early; otherwise you'll debug a 200-with-no-body.
Attributes are JSON, not columns. Flexible attribute storage is great for display and terrible for filtering/sorting. For anything you query on frequently, add a real indexed column via your own migration and denormalize on save.
Debugging failed requests. Many package exceptions are converted into ApiResponse payloads — the message is in the response, not the stack trace. When something is genuinely broken, DB::listen plus route:list will get you further than the logs.
Route/model namespacing. v1 straddles GetCandy\Models\* and GetCandy\Api\Core\*; imports drift between the two in examples. Confirm the class exists before copying a snippet from a blog post.
Don't rename resources. If you need a custom shape, add a resource, don't modify the package's — the admin SPA depends on the stock shapes and will break in ways that are hard to trace.
Migration path. If you ship on this, budget an exit: keep custom logic in your own models/repositories (as above) and you can port to Lunar by rewriting the bindings rather than the domain code.
How can I help you explore Laravel packages today?