Merchant API Integration Guide
Build a Merchant API that connects your product to SubscriptionPro. Once integrated, customers can buy your product through SubscriptionPro and licenses are generated automatically.
Last updated: April 26, 2026
How It Works (Big Picture)
The full purchase-to-license flow between the customer, SubscriptionPro, and your Merchant API:
In simple terms:
- Customer clicks “Buy” on SubscriptionPro
- Customer is redirected to your website to authorize access
- After authorization, SubscriptionPro gets a token from your API
- After payment, SubscriptionPro calls your API to generate a license
- The license is shown to the customer in their order dashboard
What You Need to Build
Your API needs these 8 endpoints. Here's exactly what each one does.
| # | Method | Endpoint | Purpose |
|---|---|---|---|
| 1 | GET | /api/authorize | Show authorization page to customer |
| 2 | GET | /api/authorize/confirm | Customer clicks “Authorize”, redirect back with code |
| 3 | POST | /api/token | Exchange auth code for access token |
| 4 | POST | /api/license/activate | Generate a license after payment |
| 5 | POST | /api/license/validate | Check if a license key is valid |
| 6 | GET | /api/profile | Get customer profile |
| 7 | GET | /api/billing | Get billing history |
| 8 | POST | /api/cancel | Cancel subscription and revoke license |
/api/ can be customized. You'll configure them in the SubscriptionPro merchant dashboard when setting up your product.
Step 1 · Database Tables
You'll need these four tables in your database.
customers — Stores customer data from SubscriptionPro
CREATE TABLE customers (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(255),
email VARCHAR(255) UNIQUE,
access_token VARCHAR(255) UNIQUE NULL,
product_identifier VARCHAR(255) NULL,
sp_customer_id BIGINT UNSIGNED NULL COMMENT 'Customer ID from SubscriptionPro',
created_at TIMESTAMP NULL,
updated_at TIMESTAMP NULL
);
oauth_codes — Temporary authorization codes
CREATE TABLE oauth_codes (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
code VARCHAR(255),
client_id VARCHAR(255) DEFAULT 'subscriptionpro',
product_identifier VARCHAR(255) NULL,
customer_id BIGINT UNSIGNED NULL,
used BOOLEAN DEFAULT FALSE,
expires_at TIMESTAMP NULL,
created_at TIMESTAMP NULL,
updated_at TIMESTAMP NULL
);
licenses — License keys generated for customers
CREATE TABLE licenses (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
customer_id BIGINT UNSIGNED,
plan_name VARCHAR(255),
license_key VARCHAR(255) UNIQUE,
product_identifier VARCHAR(255),
status VARCHAR(50) DEFAULT 'active' COMMENT 'active, cancelled, expired, revoked',
activated_at TIMESTAMP NULL,
expires_at TIMESTAMP NULL,
created_at TIMESTAMP NULL,
updated_at TIMESTAMP NULL
);
billing_records — Payment history
CREATE TABLE billing_records (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
customer_id BIGINT UNSIGNED,
plan_name VARCHAR(255),
amount DECIMAL(10,2),
transaction_id VARCHAR(255),
status VARCHAR(50) DEFAULT 'paid',
billed_at TIMESTAMP NULL,
created_at TIMESTAMP NULL,
updated_at TIMESTAMP NULL
);
Step 2 · Build Your Endpoints
/api/authorize/confirm2.2 · Confirm Authorization
Called when the customer clicks “Authorize”. You generate a one-time code and redirect back to SubscriptionPro.
Query Parameters: Same as above (product_identifier, redirect_uri).
What to do
- Generate a random code (e.g. 40 characters)
- Save it to
oauth_codeswithused = falseandexpires_at = now + 10 minutes - Redirect the customer to:
{redirect_uri}?code={code}&product_identifier={product_identifier}
Example (PHP / Laravel)
public function authorizeConfirm(Request $request)
{
$code = Str::random(40);
OauthCode::create([
'code' => $code,
'product_identifier' => $request->product_identifier,
'used' => false,
'expires_at' => now()->addMinutes(10),
]);
$separator = str_contains($request->redirect_uri, '?') ? '&' : '?';
return redirect(
$request->redirect_uri . $separator
. "code={$code}&product_identifier={$request->product_identifier}"
);
}
/api/token2.3 · Token Exchange
SubscriptionPro's backend calls this to exchange the one-time code for a long-lived access token.
Request Body (JSON)
{
"code": "abc123xyz...",
"product_identifier": "your-product-id",
"customer_id": 42
}
What to do
- Look up the code in
oauth_codes— check it's not used, not expired - Mark the code as
used = true - Create or update a customer record with a new
access_token - Return the token
Response (JSON)
{
"access_token": "your_generated_token_here",
"refresh_token": "optional_refresh_token",
"token_type": "Bearer",
"customer_id": 1,
"expires_in": 31536000
}
access_token is stored by SubscriptionPro and used for all future API calls. Make it long-lived (1 year recommended).
/api/license/activateMost Important2.4 · License Activation
Called automatically by SubscriptionPro after a successful payment. This is where you generate the license key.
Request Body (JSON)
{
"access_token": "the_customers_token",
"plan_name": "Monthly Plan",
"product_identifier": "your-product-id",
"amount": 9.99
}
What to do
- Validate the
access_token— find the customer - Generate a unique license key
- Save it to
licenseswithstatus = active - Return the license details
Response (JSON)
{
"success": true,
"license_key": "ABCDE-FGHIJ-KLMNO-PQRST",
"plan_name": "Monthly Plan",
"status": "active",
"activated_at": "2026-04-26T12:00:00.000000Z",
"expires_at": "2027-04-26T12:00:00.000000Z"
}
Example (PHP / Laravel)
public function licenseActivate(Request $request)
{
$customer = Customer::where('access_token', $request->access_token)->first();
if (!$customer) {
return response()->json(['error' => 'Invalid access token'], 401);
}
// Generate a unique license key (format: XXXXX-XXXXX-XXXXX-XXXXX)
$licenseKey = strtoupper(implode('-', str_split(Str::random(20), 5)));
$license = License::create([
'customer_id' => $customer->id,
'plan_name' => $request->plan_name,
'license_key' => $licenseKey,
'product_identifier' => $request->product_identifier,
'status' => 'active',
'activated_at' => now(),
'expires_at' => now()->addYear(), // or addMonth() for monthly
]);
return response()->json([
'success' => true,
'license_key' => $licenseKey,
'plan_name' => $request->plan_name,
'status' => 'active',
'activated_at' => $license->activated_at->toISOString(),
'expires_at' => $license->expires_at->toISOString(),
]);
}
/api/license/validate2.5 · License Validation
Checks if a license key is valid. Can be called by anyone with the key (no auth required).
Request Body
{
"license_key": "ABCDE-FGHIJ-KLMNO-PQRST"
}
Response (valid)
{
"valid": true,
"license_key": "ABCDE-FGHIJ-KLMNO-PQRST",
"status": "active",
"plan_name": "Monthly Plan",
"product_identifier": "your-product-id",
"activated_at": "2026-04-26T12:00:00.000000Z",
"expires_at": "2027-04-26T12:00:00.000000Z",
"message": "License is valid and active."
}
Response (invalid)
{
"valid": false,
"message": "License key not found."
}
/api/profile2.6 · Customer Profile
Returns customer info and their active license. Pass access_token as a Bearer header or query parameter.
Response
{
"id": 1,
"name": "John Doe",
"email": "john@example.com",
"product_identifier": "your-product-id",
"sp_customer_id": 42,
"active_license": {
"license_key": "ABCDE-FGHIJ-KLMNO-PQRST",
"plan_name": "Monthly Plan",
"status": "active",
"expires_at": "2027-04-26T12:00:00.000000Z"
}
}
/api/billing2.7 · Billing History
Returns all billing records for the customer.
Response
{
"billing": [
{
"id": 1,
"plan_name": "Monthly Plan",
"amount": 9.99,
"transaction_id": "TXN_ABC123",
"status": "paid",
"billed_at": "2026-04-26T12:00:00.000000Z"
}
]
}
/api/cancel2.8 · Cancel Subscription
Called when a customer cancels their subscription on SubscriptionPro. You should revoke their active licenses.
Request Body
{
"access_token": "the_customers_token"
}
Response
{
"success": true,
"message": "Subscription cancelled successfully."
}
Step 3 · Configure Your Product in SubscriptionPro
When creating your product in the SubscriptionPro merchant dashboard, enable “API Integration” and fill in these URLs.
| Field | What to enter | Example |
|---|---|---|
| Webhook (Base URL) | Your API's base URL | https://api.yourproduct.com/api |
| Authorization URL | Path for OAuth page | authorize |
| License Activation URL | Path for license generation | license/activate |
| Payment URL | Path for payment recording | payment |
| Profile URL | Path for customer profile | profile |
| Billing URL | Path for billing history | billing |
| Cancel URL | Path for cancellation | cancel |
https://api.yourproduct.com/api and the license activation URL is license/activate, SubscriptionPro will call https://api.yourproduct.com/api/license/activate.
Step 4 · Test Your Integration
Quick Test with cURL
1 · Get a Token (simulate)
curl -X POST http://your-api.com/api/token \
-H "Content-Type: application/json" \
-d '{"code":"test123", "product_identifier":"your-product-id", "customer_id": 1}'
2 · Activate a License
curl -X POST http://your-api.com/api/license/activate \
-H "Content-Type: application/json" \
-d '{
"access_token": "TOKEN_FROM_STEP_1",
"plan_name": "Monthly Plan",
"product_identifier": "your-product-id",
"amount": 9.99
}'
3 · Validate the License
curl -X POST http://your-api.com/api/license/validate \
-H "Content-Type: application/json" \
-d '{"license_key": "LICENSE_KEY_FROM_STEP_2"}'
Error Handling
All endpoints should return proper HTTP status codes.
| Status | Meaning | When to use |
|---|---|---|
| 200 | Success | Request completed successfully |
| 400 | Bad Request | Missing or invalid parameters |
| 401 | Unauthorized | Invalid or missing access token |
| 404 | Not Found | Resource not found (e.g. invalid license key) |
| 500 | Server Error | Something went wrong on your end |
Error response format
{
"error": "Description of what went wrong"
}
Security Best Practices
- Always validate access tokens — never trust the token without checking your database
- Use HTTPS — all API calls should be over HTTPS in production
- Expire authorization codes quickly — 10 minutes maximum
- Mark codes as used — prevent code replay attacks
- Generate unique license keys — use cryptographically random strings
- Rate limit your endpoints to prevent abuse
Complete Laravel Starter
Want a head start? Scaffold the project, then drop in your routes.
# Create a new Laravel project
composer create-project laravel/laravel my-merchant-api
cd my-merchant-api
# Create your migrations
php artisan make:migration create_customers_table
php artisan make:migration create_oauth_codes_table
php artisan make:migration create_licenses_table
php artisan make:migration create_billing_records_table
# Create your models
php artisan make:model Customer
php artisan make:model OauthCode
php artisan make:model License
php artisan make:model BillingRecord
# Create the controller
php artisan make:controller MerchantController
Then add your routes in routes/api.php
use App\Http\Controllers\MerchantController;
Route::get('/authorize', [MerchantController::class, 'showAuthorizePage']);
Route::get('/authorize/confirm', [MerchantController::class, 'authorizeConfirm']);
Route::post('/token', [MerchantController::class, 'token']);
Route::post('/license/activate', [MerchantController::class, 'licenseActivate']);
Route::post('/license/validate', [MerchantController::class, 'licenseValidate']);
Route::get('/profile', [MerchantController::class, 'profile']);
Route::get('/billing', [MerchantController::class, 'billing']);
Route::post('/cancel', [MerchantController::class, 'cancel']);
Need Help?
Demo Merchant API
Check the demo source code for a complete working example.
Contact Support
Reach out to SubscriptionPro support for integration assistance.
Public Webhook
Make sure your Webhook base URL is accessible from the internet (not localhost).