Skip to content

Storefront API Key Management Guide

Overview

Bagisto uses storefront API keys to identify the application calling your /api/shop/* and /api/graphql endpoints. The key says which app is calling — it never says which person, so customer-specific work still needs a customer Bearer token on top of it.

Header Format:

X-STOREFRONT-KEY: pk_storefront_xxxxxxxxxxxxx

Quick Example:

bash
curl -X GET "https://your-domain.com/api/shop/products" \
  -H "X-STOREFRONT-KEY: pk_storefront_xxxxxxxxxxxxx"

Key Management Notes

  • Public by design — the key ships inside browser bundles and mobile apps, so treat it as identification rather than a secret. It is not a password.
  • Not read-only — besides the catalog, the key alone permits the storefront's open writes: contact-us, newsletter subscribe, customer registration, and cart creation. See Authentication.
  • Customer operations — use Bearer tokens (from customer login) for anything tied to an account.
  • Rate limited — each key carries its own limit, enforced hourly. See Understanding Rate Limits.
  • Never expires by default — a generated key stays valid until you rotate or deactivate it. Rotating issues a fresh key valid 12 months and leaves the old one working for a 7-day transition window.

Quick Reference

TaskCommand
Create new keyphp artisan bagisto-api:generate-key --name="My App"
Check key statusphp artisan bagisto-api:key:manage status --key="My App"
Rotate keyphp artisan bagisto-api:key:manage rotate --key="My App"
Deactivate keyphp artisan bagisto-api:key:manage deactivate --key="Old Key"
View all keysphp artisan bagisto-api:key:manage summary
Run maintenancephp artisan bagisto-api:key:maintain --all

Getting Started

Step 1: Create Your First API Key

Run this command in your terminal:

bash
php artisan bagisto-api:generate-key --name="My App"

You'll see:

Storefront key generated successfully!

Key Details:
  ID : 11
  Name : My App
  Key : pk_storefront_i7gWgB6A0TWNUV5s5eX2pOUV8JNOelrC
  Rate Limit : 100 requests/minute
  Status : Active

Keep this key secure! It will be used in X-STOREFRONT-KEY header.
Do not share this key publicly or commit it to version control.

Copy the key now — no command prints it again. status reports whether a key is active and when it was last used, but never its value.

The Rate Limit line is printed as "requests/minute", but the limit is applied per hour at request time. The number is right; the unit in that message is not. See Understanding Rate Limits.

Step 2: Store the Key Safely

Add it to your .env file:

bash
# .env file
BAGISTO_API_KEY=pk_storefront_xxxxxxxxxxxxx

Or store it in your secret manager (AWS Secrets Manager, HashiCorp Vault, etc.).

Step 3: Start Making API Requests

REST API Example:

bash
curl -X GET "https://your-domain.com/api/shop/products" \
  -H "X-STOREFRONT-KEY: pk_storefront_xxxxxxxxxxxxx"

GraphQL API Example:

bash
curl -X POST "https://your-domain.com/api/graphql" \
  -H "X-STOREFRONT-KEY: pk_storefront_xxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{"query": "{ products(first: 2) { edges { node { _id sku name } } } }"}'

JavaScript/Node.js Example:

javascript
const apiKey = process.env.BAGISTO_API_KEY;

const response = await fetch('https://your-domain.com/api/shop/products', {
  method: 'GET',
  headers: {
    'X-STOREFRONT-KEY': apiKey,
    'Content-Type': 'application/json'
  }
});

const products = await response.json();

The header is X-STOREFRONT-KEY — hyphens, and KEY not API. A wrong header name reads as no key at all and returns 401.


Complete Command Reference

Generate API Key

Create new API keys for different environments and applications.

bash
php artisan bagisto-api:generate-key {--name=} {--rate-limit=100} {--no-activation}

Parameters:

ParameterTypeDefaultDescription
--namestringpromptedDescriptive name for your key (e.g., "Mobile App", "Third Party"). Omit it and the command asks. Names must be unique — reusing one fails.
--rate-limitinteger100Requests per hour. Leave empty for unlimited; anything above 5,000 is capped at 5,000 with a warning.
--no-activationflagfalseCreate key in inactive state

Examples:

bash
# Basic key generation
php artisan bagisto-api:generate-key --name="Mobile App"

# Higher-traffic application
php artisan bagisto-api:generate-key --name="Partner API" --rate-limit=500

# Unlimited — pass an empty value
php artisan bagisto-api:generate-key --name="Premium Integration" --rate-limit=

# Create inactive key (for later activation)
php artisan bagisto-api:generate-key --name="Staging Environment" --no-activation

# Above the ceiling: capped at 5,000 with a warning
php artisan bagisto-api:generate-key --name="Max Throughput" --rate-limit=10000

Pass --rate-limit= with nothing after it for unlimited. Writing --rate-limit=null sends the literal string null, which converts to 0 — which also means unlimited, but by accident rather than intent, so prefer the empty form.

Tips:

  • Name your keys clearly — "Mobile App", "Website Frontend", "Partner Integration"
  • Match rate limits to your needs — start with 100/hour and raise it as traffic grows
  • Rotate quarterly — change keys every 3 months for security
  • Never commit to Git — use .env files with .gitignore
  • Use one key per application — a shared key cannot be revoked for one consumer without breaking the rest

Finding and Identifying Keys

Keys can be referenced by either their numeric ID or name in all management commands:

bash
# By ID (numeric)
php artisan bagisto-api:key:manage status --key=1

# By name (more convenient)
php artisan bagisto-api:key:manage status --key="Mobile App"

# Either approach works with any command:
php artisan bagisto-api:key:manage rotate --key="My Integration"
php artisan bagisto-api:key:manage deactivate --key=5

The system automatically detects whether you're using an ID or name and looks up the key accordingly.


Manage API Keys

Monitor, rotate, and control your API keys throughout their lifecycle.

bash
php artisan bagisto-api:key:manage {action} {--key=} {--reason=} {--days=7} {--unused=90}

Available Actions:

ActionPurposeExample
rotateIssue a replacement key and start the old one's transition periodrotate --key="Mobile App"
deactivateDisable a key immediatelydeactivate --key="Old Key" --reason="Compromised"
statusCheck a key's validity and last usestatus --key="Mobile App"
expiringList keys expiring soonexpiring --days=30
unusedFind keys not used recentlyunused --unused=90
cleanupSoft-delete expired keyscleanup
summaryRotation-policy compliance overviewsummary

Parameters:

ParameterTypeDefaultApplies toDescription
--keystringoptionalrotate, deactivate, statusKey ID or name
--reasonstringoptionaldeactivateReason for deactivation (logged for audit)
--daysinteger7expiring onlyDays threshold
--unusedinteger90unused onlyDays threshold

The two thresholds are not interchangeable. unused --days=30 silently keeps the 90-day default, because unused reads --unused; the command still prints its real threshold in the heading (Unused keys (> 90 days)), so check that line rather than assuming your flag applied.

Rotate and deactivate ask for confirmation. Both prompt Are you sure…? (yes/no) [no]. In a cron job or deploy script the default answer is no and the command cancels, so pipe in a yes or run them interactively.

Examples:

bash
# Rotate a key (security best practice)
php artisan bagisto-api:key:manage rotate --key="Mobile App"

# Deactivate a compromised key
php artisan bagisto-api:key:manage deactivate --key="Old Integration" \
  --reason="Service discontinued - replaced by new key"

# Check a key's status
php artisan bagisto-api:key:manage status --key="Mobile App"

# Find keys expiring within 30 days
php artisan bagisto-api:key:manage expiring --days=30

# Find keys unused for 90 days
php artisan bagisto-api:key:manage unused --unused=90

# View the compliance summary
php artisan bagisto-api:key:manage summary

# Soft-delete expired keys
php artisan bagisto-api:key:manage cleanup

Output Examples:

# Status output
Key Status: Mobile App

Active: ✅ Yes
Usable: ✅ Yes
Expired: ✅ No
Deprecated: ✅ No

Expires At: Never
Days Until Expiry: N/A
Last Used: 2026-08-14 12:41:02
# Summary output
API Key Rotation Policy Compliance Summary

Total Keys: 3
Valid Keys: 3
Expired Keys: 0
Deprecated Keys: 0
Keys Expiring Soon (7 days): 0
Unused Keys (90 days): 3
Recently Rotated (30 days): 0

Neither view reports per-key request counts. Last Used is the only usage signal, so track call volume from your own logs or the rate-limit headers if you need it.

What Rotation Actually Does

Rotation is a hand-over, not a kill switch. Running it:

✅ Key rotated successfully!
Old Key: My App
Old Key ID: 11
Deprecation Date: 2026-08-21 13:07:06

New Key: My App (rotated 2026-08-14 13:07)
New Key ID: 13
New Key Value: pk_storefront_ekH7nWFO8erYJ838rJTLSpBZRz3MJzCT
Expires At: 2027-08-14 13:07:06

Three consequences worth planning around:

  1. The old key keeps working. It is marked deprecated with a date 7 days out and stays valid through that window — deliberately, so clients have time to switch. It is disabled only when key:maintain --invalidate (or --all) runs after that date. If nothing runs that command, the old key never stops working. When you are rotating because a key leaked, deactivate it instead — that takes effect immediately.
  2. The new key is renamed. It becomes <old name> (rotated YYYY-MM-DD HH:MM), so --key="My App" afterwards still resolves to the old key. Use the new ID, or rename it, before scripting against it.
  3. The new key expires. A generated key never expires, but a rotated one is given a 12-month expiry. A key created once and rotated annually will eventually expire on its own — watch it with expiring.

Security Maintenance Schedule:

FrequencyActionCommand
MonthlyReview unused keysphp artisan bagisto-api:key:manage unused --unused=90
MonthlyCheck expiring keysphp artisan bagisto-api:key:manage expiring --days=30
QuarterlyRotate production keysphp artisan bagisto-api:key:manage rotate --key="Production"
ImmediatelyKill a compromised keyphp artisan bagisto-api:key:manage deactivate --key="Old Key" --reason="Compromised"

Automate Maintenance

Set up automatic cleanup, invalidation, and notifications.

bash
php artisan bagisto-api:key:maintain {--cleanup} {--invalidate} {--notify} {--all}

What Each Option Does:

OptionWhat It DoesWhen to Use
--cleanupSoft-deletes expired keys, keeping the row for auditScheduled daily maintenance
--invalidateDisables rotated keys whose transition period has passedRequired for rotation to ever take effect
--notifySends expiration warningsProactive team notifications
--allRuns all threeComprehensive maintenance

--invalidate is the part that finishes a rotation. Without it on a schedule, every key you have ever rotated is still accepting requests.

Examples:

bash
# Clean up expired keys
php artisan bagisto-api:key:maintain --cleanup

# Send expiration notifications
php artisan bagisto-api:key:maintain --notify

# Run complete maintenance (recommended)
php artisan bagisto-api:key:maintain --all

Output:

🔄 Starting API Key Maintenance...

🧹 Cleaning up expired keys...
   ℹ️ No expired keys to clean up
⚠️ Invalidating deprecated keys...
   ℹ️ No deprecated keys to invalidate
📧 Sending expiration notifications...
   ℹ️ No keys requiring notifications

✅ API Key Maintenance Complete

Recommended Scheduler Setup:

Bagisto 2.4 runs on Laravel 12, where scheduled commands live in routes/console.php:

php
use Illuminate\Support\Facades\Schedule;

// Daily maintenance: cleanup, invalidate, notify
Schedule::command('bagisto-api:key:maintain --all')
    ->daily()
    ->at('02:00')
    ->onOneServer();

// Weekly check for expiring keys
Schedule::command('bagisto-api:key:manage expiring --days=30')
    ->weeklyOn(1, '09:00');

// Monthly review of unused keys
Schedule::command('bagisto-api:key:manage unused --unused=90')
    ->monthlyOn(1, '10:00');

The scheduler itself must be running — php artisan schedule:work in development, or a cron entry calling schedule:run every minute in production.


Security Best Practices

Do This

  • Store in .env — keep keys out of your codebase

    bash
    BAGISTO_API_KEY=pk_storefront_xxxxxxxxxxxxx
  • Use environment-specific keys — different keys for dev/staging/production

    bash
    BAGISTO_API_KEY_DEV=pk_storefront_xxxxxxx
    BAGISTO_API_KEY_PROD=pk_storefront_yyyyyyy
  • Access via config — use Laravel's config system

    php
    $apiKey = config('services.bagisto.api_key');
  • Rotate quarterly, deactivate on compromise — rotation hands over gracefully; only deactivation stops a key now

    bash
    php artisan bagisto-api:key:manage rotate --key="Production"
  • Use secret managers — AWS Secrets Manager, HashiCorp Vault, etc.

  • Schedule key:maintain --all — otherwise rotated keys are never retired

Don't Do This

  • Hardcode keys in source files
  • Log API keys in error messages
  • Share keys via email or chat
  • Commit .env to Git
  • Use the same key for multiple environments
  • Assume rotation revoked the old key — it stays valid for the transition period
  • Rely on the key as a security boundary — it is public in any browser or mobile client

Troubleshooting

Key Rejected

The two rejections are different, and the status code tells you which:

StatusBodyMeaning
401{"error": "missing_key"}No X-STOREFRONT-KEY header arrived — usually a misspelled header name
403{"error": "invalid_key"}A key arrived but is unknown, deactivated, expired, or soft-deleted
bash
# Check whether the key exists and is usable
php artisan bagisto-api:key:manage status --key="Your Key Name"

Usable: ✅ Yes is the line that decides it. A key can show Deprecated: ❌ Yes and still be usable — that is a rotated key inside its transition window.

"Rate Limit Exceeded" Error

Problem: The key has used its hourly allowance.

Symptoms:

  • HTTP 429
  • Body {"message": "Rate limit exceeded", "error": "rate_limit_exceeded", "retry_after": 1421}
  • retry_after is seconds until the next hour boundary, so it can be far longer than a minute

The 429 response carries no X-RateLimit-* headers — those appear only on successful responses, so read the remaining count from those rather than expecting them on the rejection.

Fixes:

  1. Raise the limit by issuing a key with more headroom:
    bash
    php artisan bagisto-api:generate-key --name="High Volume App" --rate-limit=2000
  2. Or an unlimited key, for a trusted internal service:
    bash
    php artisan bagisto-api:generate-key --name="Internal Service" --rate-limit=
  3. Cache and batch — the catalog endpoints are cacheable, and a paginated request costs one call instead of many.

An existing key's limit cannot be changed from the CLI; issue a new key with the limit you want.

Lost or Exposed Key

Immediate action — deactivate, do not rotate. Deactivation takes effect on the next request; rotation leaves the leaked key alive for another 7 days.

bash
php artisan bagisto-api:key:manage deactivate --key="My App" \
  --reason="Suspected compromise - exposed in logs"

Then issue a replacement and update your clients:

bash
php artisan bagisto-api:generate-key --name="My App (New)"

Key Created But Not Working

If the key was created with --no-activation it exists but is inactive, and every request returns 403. status shows Active: ❌ No. There is no activate command — issue the key without the flag when you are ready to use it.

Requests Working Locally But Not in Production

Check these in order:

  1. Different key? — production and development should hold different keys; confirm which one is loaded.
  2. Environment variables? — verify .env is loaded and the value is not null.
  3. Header name?X-STOREFRONT-KEY, not X-STOREFRONT-API. A wrong name gives 401 missing_key.
  4. Inactive, expired, or deactivated key?status --key="Production Key Name", and read the Usable line.
  5. Hourly limit hit? — a 429 with a large retry_after means the hour's allowance is gone, not that you are calling too fast right now.
  6. Rotated recently? — the new key carries a (rotated …) suffix in its name, so a script looking the key up by the old name is inspecting the wrong row.

Understanding Rate Limits

Each key carries a limit that is applied per clock hour, despite the CLI describing it in minutes. A key created with --rate-limit=100 allows 100 requests within the current hour; the counter resets at the top of the next hour, not 60 seconds after your first call.

Rate Limit Fundamentals

  • Window — the fixed clock hour. A key exhausted at 10:05 recovers at 11:00, and retry_after counts the seconds until then.
  • Default — 100 requests per hour for a generated key.
  • Unlimited — an empty or zero limit disables counting entirely.
  • Ceiling — a requested limit above 5,000 is capped at 5,000 with a warning.
  • Exceeded — HTTP 429 with retry_after in the body.

Setting Rate Limits

bash
# Default: 100 requests/hour
php artisan bagisto-api:generate-key --name="My App"

# Custom limit
php artisan bagisto-api:generate-key --name="High-Volume App" --rate-limit=2500

# Unlimited
php artisan bagisto-api:generate-key --name="Premium Partner" --rate-limit=

# Above the ceiling — capped at 5,000
php artisan bagisto-api:generate-key --name="Max Throughput" --rate-limit=10000

Monitoring Rate Limit Usage

Successful responses carry three headers:

bash
curl -X GET 'https://your-domain.com/api/shop/products' \
  -H 'X-STOREFRONT-KEY: pk_storefront_xxxxx' \
  -i
HeaderWhat it actually reports
X-RateLimit-RemainingCalls left in the current hour for this key — the one to trust
X-RateLimit-ResetSeconds remaining until the window resets, not a Unix timestamp
X-RateLimit-LimitThe store-wide configured default, not this key's limit

X-RateLimit-Limit is the trap: a key limited to 3 still reports X-RateLimit-Limit: 100. Budget from X-RateLimit-Remaining, and treat the limit header as decoration.

Handling Rate Limit Errors

json
{
  "message": "Rate limit exceeded",
  "error": "rate_limit_exceeded",
  "retry_after": 1421
}

retry_after is already in seconds, so wait on it directly:

javascript
if (response.status === 429) {
  const { retry_after } = await response.json();
  await sleep(retry_after * 1000);
  // retry request
}

Since the window is hourly, a naive retry loop can burn a long wait. Cache catalog responses, raise the key's limit, or split traffic across per-application keys instead of retrying into the same exhausted bucket.

Rate Limiting Best Practices

Do This:

  • Monitor X-RateLimit-Remaining rather than X-RateLimit-Limit
  • Read retry_after from the body and honour it
  • Cache catalog responses — they are the bulk of storefront traffic and they are cacheable
  • Give each application its own key so one client's spike cannot exhaust another's budget
  • Use unlimited keys only for internal, server-side services

Don't Do This:

  • Retry immediately on a 429 — the window is an hour, not a minute
  • Assume the reset header is a timestamp
  • Set unlimited limits for external integrations
  • Use one high-limit key for every application

What's Next?

Released under the MIT License.